It is sometimes useful to be able to test your R package on a big-endian architecture, like s390x.
TL;DR
Copy this workflow and adjust it to your needs.
The rest of the post shows how this workflow works.
Create the container
First, we need to create an s390x Docker container. This is mostly
straightforward, and not the part that this post focuses on. Nevertheless
the
latest version
of the Docker image is at ghcr.io/r-hub/containers/s390x
.
It is built from a Dockerfile in the same repository, using GitHub Actions, specifically this workflow.
The container image uses R 4.1 currently, to avoid having to compile R on s390x.
Only x86_64 containers are welcome on GHA
GitHub Actions has
tools to run multi-architecture Docker,
so this is relatively easy, with the one catch that it is not possible
to run non-x86_64
containers natively.
This means that you cannot run actions inside the s390x container, at least
not out of the box. Which is a pity, because I would really like to run
setup-r-dependencies
to install package dependencies, and
check-r-package
to run R CMD check
, on the s390x container.
The rest of this post shows how to create a remote Rscript
shell that
runs all R script, including the ones from actions, inside the s390x
container.
Get the container and start it
We’ll put all the code together into a composite action.
First we need to install qemu:
1 | - name: Install Qemu |
Specifying the image is important. By default the action uses an older qemu image, which crashes frequently, at least for s390x.
Then we pull the image from a registry and start a container.
1 | - name: Pull and start image |
The first couple of lines deal with setting R_LIBS_USER
to the runner’s
temporary library.
platform
will be an input parameter that we pass to the --platform
option of docker pull
and docker run
, unless it is empty.
We’ll set the name of the container, also an input, so we can refer to it later to run code on it.
In this example we use two bind mounts, one to the current working directory, that’s where the git tree of the current R project is checked out, typically. The second is for the R package library, and we do that so we can cache the library. Hopefully the bind mounts are fast enough with qemu, I haven’t actually checked this. Emulating s390x on x86_64 is pretty slow, anyway, so hopefully this does not matter too much.
We run an empty loop on the container. We’ll run the actual R commands
with docker exec
later.
Use a custom shell to run Rscript
in the container
The next thing we need is a custom Rscript
shell that runs an R script
inside the container, instead of running it on the runner machine.
This is slightly tricky, because we want the custom shell to be a shell
script, and this is not supported by GitHub Actions out of the box.
See the previous blog post on how
to do this. I’ll repeat the code needed here:
1 | - name: Setup up remote shell |
We also set the CTR_NAME
env var to the name of the container,
so we can refer to it later.
We can start writing the actual Rscript wrapper:
1 |
|
We’ll need some temporary files to copy data back from the container. We
create the files for ${GITHUB_ENV}
and ${GITHUB_OUTPUT}
on the
container, the so actions that will be “forwarded” to the container by our
remote shell can set
environment variables
and
output parameters.
Now we are ready to run the R script, passed in as the first argument $1
:
1 | docker exec -i -w /root -e"R_LIB_FOR_PAK=/usr/lib/R/library" \ |
-w
sets the working directory to /root
where we bind mounted the current
working directory.
-e
passes in a bunch of environment variables. GITHUB_ENV
and
GITHUB_OUTPUT
point to the temporary files we created above. The rest of
them are typically needed for R packages. You might need to pass more
GITHUB_*
env vars
or env vars for R CMD check
.
❗ Env vars that you set with env: will not be available on the container, unless you explicitly pass them with -e here! |
---|
Finally, we save the exit status of docker exec
, so the Rsctipt wrapper
will be able to return the same status.
Now all we left to do is copying back the GITHUB_ENV
and GITHUB_OUTPUT
files.
1 | docker cp ${CTR}:/tmp/${env_file} /tmp/${env_file} || true |
To test the remote shell, you could use a step like this:
1 | - name: Test R in container |
R package binaries for s390x
Emulating s390x on x86_64 is difficult, and thus quite slow, so it is important that we pre-build some binary R packages, so they don’t have to be compiled and built for every workflow run. Building duckdb for s390x takes more than 24 hours on my laptop!
I won’t go into the details here, but the repository with s390x binarires is updated daily on GitHub Actions. The binary packages themselves are stored at GitHub releases at the https://github.com/cran CRAN mirror. E.g. the binaries for cli 3.6.3 are here.
Important to note that this kind of “distributed” repository only works
with pak, so you won’t be able to use
install.packages()
to install these binaries. We pre-install pak on our
container image and to also set up the repos
option to point to the
binary packages.
1 | # Use R-hub repo |
Putting it all together
It is convenient to create an action that starts the container and sets up the remote shell. Here is an example.
An example workflow that uses this action could look like this:
1 | on: |
The first interesting part calls the action mentioned above, with the right parameters:
1 | - uses: r-hub/actions/ctr-start@main |
Then we use the usual r-lib/actions
actions, but we do need to set some extra parameters:
1 | - uses: r-lib/actions/setup-r-dependencies@v2 |
We need to set pak-version: none
here, so setup-r-dependencies
does not
try to install the pak package, which is already pre-installed on the
container. We also set cache-version
to have an s390x specific name for
the cache.
1 | - uses: r-lib/actions/check-r-package@v2 |
For check-r-package
we need to set upload-results
and
upload-snapshots
because uploading the artifacts doesn’t currently work
with the remote shell. Instead, we upload artifacts manually:
1 | - uses: actions/upload-artifact@v4 |
Is this a hack?
The dummy Rscript
shell, and the “remote shell” are hacks. The
rest is good, though.
Updates
2024-10-07: In the first version of this blog post I stated that the
custom shell must be a binary program, but that is not true, I just didn’t manage to put the #!
hash-bang at the first line of the script. I updated
the post accordingly.