extendr is a project that provides an interface between R and Rust. In the series of posts, I explained how to use extendr, but, this time, let me pick a complementary topic. The CI setup is important to develop a package and it’s not difficult to tweak the existing GitHub Actions (GHA) settings to compile Rust code. By using GHA, you can even provide precompiled binaries via GitHub releases.
Setup Rust toolchain
A GitHub repository for R package development typically has such a YAML for testing:
https://github.com/r-lib/actions/blob/master/examples/check-standard.yaml
To test a package using extendr, it’s as easy as to just add the steps to setup Rust toolchain. Note that the runners might already have Rust toolchain installed, but these steps ensure the intended toolchain is used.
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
default: true
- name: Additional Rust set up for Windows
if: runner.os == 'Windows'
run: |
rustup target add i686-pc-windows-gnu rustup target add x86_64-pc-windows-gnu
If you want to run tests also on the nightly toolchain, you can include the channel in the build matrix like this1:
- {os: windows-latest, r: 'release', rust: 'stable'}
- {os: macOS-latest, r: 'release', rust: 'stable'}
- {os: ubuntu-20.04, r: 'release', rust: 'stable', rspm: "..."}
- {os: ubuntu-20.04, r: 'devel', rust: 'stable', rspm: "..."}
- {os: ubuntu-20.04, r: 'release', rust: 'nightly', rspm: "..."}
and specify the channel in the actions-rs/toolchain@v1
step:
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.config.rust }}
default: true
You might need more setups depending on the crates you use, but basically that’s all you need to do.
Provide precompiled binaries for Windows
As you might have already noticed, the setup instruction for Windows is a bit complex compared to other OSes (i.e., Linux and macOS). So, it might be worth considering providing the precompiled static libraries, just like rwinlib does for many C++ libraries.
Other motivation is that some of CRAN machines don’t have Rust toolchain. If the package author wants to submit their package to CRAN, such a mechanism is needed.
(edit: it seems macOS is also the case, but I don’t find what’s the best way to solve this. I’ll probably write another post for this.)
There probably isn’t a single standard way to achieve this, but let me share what I did for my package, string2path here. YMMV, of course.
I used softprops/action-gh-release
action to publish the binaries as a GitHub release. The setting would be like this:
# rename the binaries before uploading so that we can distinguish them easily.
- name: Tweak staticlib
if: runner.os == 'Windows'
run: |
mv ./check/string2path.Rcheck/00_pkg_src/string2path/src-i386/rust/target/i686-pc-windows-gnu/release/libstring2path.a \
i686-pc-windows-gnu-libstring2path.a
mv ./check/string2path.Rcheck/00_pkg_src/string2path/src-x64/rust/target/x86_64-pc-windows-gnu/release/libstring2path.a \
x86_64-pc-windows-gnu-libstring2path.a
- name: Release
uses: softprops/action-gh-release@v1
# only run this on a tag event
if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/')
with:
fail_on_unmatched_files: true
files: |
i686-pc-windows-gnu-libstring2path.a x86_64-pc-windows-gnu-libstring2path.a
With this setup, you can publish the binaries by pushing tags. For example, let’s create windows_20210801-3
tag and push it.
git tag windows_20210801-3
git push origin windows_20210801-3
Then, the GHA will publish the corresponding release like this:
Next, tweak src/Makefile.win
as follows to allow users to download the binaries when cargo
is not available. There might be more nicer code to choose the latest release automatically, but I think it’s safe to specify a fixed tag name, though it’s a bit tiresome to update this manually every time you update Rust code.
(edit: I found this violates the CRAN policy. A package is not allowed to write “anywhere else on the file system apart from the R session’s temporary directory. You need to set CARGO_HOME
envvar to some temporary directory to avoid this.)
CRATE = foo # your crate name
BASE_TAG = windows_20210801-3 # the tag you want to use
# c.f. https://stackoverflow.com/a/34756868
# Note that this assignment (`:=`) is not available on Solaris, so you need to
# add "GNU make" to SystemRequirements field on DESCRIPTION, even though this
# can never compile on Solaris anyway...
CARGO_EXISTS := $(shell cargo --version 2> /dev/null)
# ..snip...
$(STATLIB):
ifdef CARGO_EXISTS
$(TARGET) --lib --release --manifest-path=./rust/Cargo.toml
cargo build --target=else
$(LIBDIR)
mkdir -p $(STATLIB) https://github.com/yutannihilation/$(CRATE)/releases/download/$(BASE_TAG)/$(TARGET)-lib$(CRATE).a
curl -L -o endif
One caveat is that this won’t work when cargo
is installed but with the GNU toolchain (extendr requires the MSVC toolchain on Windows). I guess some friendlier check can be done in configure.win
, but this post won’t look into the details.
Other topics I couldn’t cover
sccache
: Builds can be faster by using sccache, a ccache-like compiler caching tool for Rust. A blog post describes how to use this on GHA, but I don’t think I understand it to the extent where I can explain it here in clear words, sorry…- How to run tests on Rust’s side?: This post doesn’t explain how to run Rust tests (i.e.,
cargo test
) or lints (i.e.,cargo fmt
, andcargo clippy
). I even don’t figure out what tests should live in R or in Rust.
Example
Here’s the real example of the settings on my repo:
https://github.com/yutannihilation/string2path/blob/main/.github/workflows/check-pak.yaml
Footnotes
On Windows, you need to use
'stalble-msvc'
, not'stable-gnu'
, but msvc should be the default so you can just specify'stable'
↩︎