1 unstable release
|0.1.0-beta.1||Jan 15, 2023|
#244 in WebAssembly
GitHub action for Rust, written in Rust and compiled to WebAssembly.
actions-rs, the de-facto default for Rust-related GitHub actions appears to be all but abandoned. This repository is an experiment in replacing those actions with ones written in Rust, but compiled down to WebAssembly. This should make them both portable across platforms and more easily maintainable by developers who only know Rust.
Like all GitHub actions, this action is used via directives in a GitHub
For practicality and implementation reasons, Ferrous Actions is structured as
a ‘mono-action’, meaning that all operations are implemented as sub-commands of
a single action rather than being separated. The
command parameter is always compulsory.
In all following examples,
should be replaced by the version of the action that this README is for. An
example of the usage of Ferrous actions in a real project can be found
Note that by default, GitHub will use the name of action as the name of a build
step in its user interface. This can be confusing since with a mono-action
these are always the same. Liberal use of the
name attribute is recommended
and is used in the examples below.
Caching Cargo home
Registry indices (e.g. the list of packages on
crates.io), crate files and
Git repositories downloaded by Cargo can all be cached between CI jobs.
File modification timestamps are used to detect if no changes to the cached items have occurred and avoid needlessly uploading them back to the cache.
- uses: FrancisRussellemail@example.com name: Cargo cache with: command: cache cache-only: indices min-recache-crates: 1m min-recache-git-repos: 12h min-recache-indices: 7d
The following options are also available:
cache-only(optional): a whitespace separated list of the token
indices. If provided, only these items will be cached. The default is to cache all items.
min-recache-crates(optional): minimum time before recaching crates.
min-recache-git-repos(optional): minimum time before recaching Git repositories.
min-recache-indices(optional): minimum time before recaching registry indices.
cross-platform-sharing(optional, experimental): Attempt to share Cargo home caches across all platforms (
all), only Unix-like platforms (
unix-like), or make all caches platform-specific (
none). The default is
All recaching intervals are specified in human
time. Specifying the recaching
interval makes it possible to avoid uploading a new version of a cached item
each time it changes. This is useful for registry indices which (in the case of
crates.io) can be large (hundreds of MiBs), often modified, but only with
small changes. At writing, the index minimum recache interval is 2 days and
none is specified for crate files or Git repositories.
Installing a Rust toolchain with Rustup
Ferrous actions can download Rustup and install a specified Rust toolchain.
- uses: FrancisRussellfirstname.lastname@example.org name: Install Rustup with: command: install-rustup toolchain: nightly target: wasm32-unknown-unknown profile: minimal default: true
The following options are also available:
toolchain(required): The toolchain to install
target(optional): A whitespace separated list of target architectures.
profile(optional): The Rustup profile (e.g.
complete). The default is
default(optional): Whether this toolchain should be set as the Rustup default compiler. This defaults to
true. This is different to actions-rs's behaviour.
override(optional): Whether a Rustup ‘override’ should be set for the current directory. Defaults to
Cargo commands can be invoked via Ferrous actions. The value for
this case is
cargo SUBCOMMAND where
SUBCOMMAND is a single token.
- uses: FrancisRussellemail@example.com name: Cargo build with: command: cargo build toolchain: stable args: --release
The following options are available whenever a Cargo subcommand is invoked:
toolchain(optional): A toolchain identifier that will be passed to
+toolchainsyntax (only supported by Rustup-installed toolchains).
args(optional): Command line flags passed to
cargo. These will be parsed using Unix-style shell quoting rules regardless of platform.
Installing a package with Cargo install
Ferrous actions will use GitHub's caching mechanism to improve the performance of installing binaries compared to compiling them from scratch each time. Note that Ferrous actions currently aims towards transparent tool caching - the cache should not result in you using a version of a binary you would not otherwise had you not used the cache.
This means that Ferrous actions caches the build artifacts folder rather than the built binaries themselves. The only way to only cache the latter would be to be completely certain of all dependencies prior to the build, Nix-style.
From the user-perspective this means:
- Tools will be recompiled from scratch when they are compiled with a previously unseen version of a Rust toolchain. If you track nightly, this might happen relatively frequently.
- Any changes to a tool (because it or its dependencies have been updated in a registry) will be immediately reflected in the result of an install action. The updated build artifacts will be pushed back to the GitHub cache when this happens.
- uses: FrancisRussellfirstname.lastname@example.org name: Install grcov with: command: cargo install args: grcov
The following options have additional constraints:
args(required): As above, but at least the binary name is required. Note that the command line is hashed to produce the cache key so changes will cause a tool to be rebuilt from scratch.
When invoked via Ferrous actions,
cargo install will execute in a different
directory to the current one. The aim here is to avoid either a
rust-toolchain.toml or a Rustup override changing the compiler used to
compile the binary.
Getting annotations from cargo build, check or clippy
clippy Cargo subcommands are run via Ferrous actions,
annotations are output which can be viewed via the GitHub UI.
- uses: FrancisRussellemail@example.com name: Cargo clippy with: command: cargo clippy annotations: true
The following options are also available:
annotations(optional): Can be set to
falsedepending on whether annotations are desired. Default is
cargo build via Ferrous actions can also be done in such a way
that the cross tool is used.
- uses: FrancisRussellfirstname.lastname@example.org name: Cargo build with: command: cargo build args: --target=x86_64-apple-darwin use-cross: true
use-cross is specified as
cross will be used for
compilation. If is supplied as
false or not at all then
cargo will be
invoked as normal. If an existing
cross binary is not available, then one
will be built and installed.
The monotonically increasing cache problem
One major issue with caching is how to ensure that a cache does not monotonically increase in size. There are two places where this can occur: the cached cargo home artifacts, and cached binary intermediate build artifacts. This problem is only partially solved.
On Linux and Apple systems, file access times are used to determine what entries in cached cargo home items were accessed and to prune items that weren't. This has been implemented such that it still works in the presence of ‘relatime’ semantics - when the file access timestamp is only updated if it is behind the modification timestamp.
File access times are typically disabled under Windows - Microsoft never
implemented an equivalent of relatime, meaning that they remain a significant
performance hit. On filesystems that do not update file access time-stamps,
Ferrous actions will incorporate the hash of all
Cargo.lock files under the
current folder into the cache key. This means that caches will be rebuilt from
scratch whenever a
Cargo.lock file changes.
This solution is far from ideal since it causes recaching more than necessary
and won't work if no
Cargo.lock files are committed to Git, or items are
added to the cache via other means (e.g. due to
No solution exists for the issue of the build artifact folder increasing in size. Rust is a fast moving language so it's expected that compiler bumps will cause the folder to be rebuilt from scratch anyway before this becomes an issue. The file access timestamp technique is unlikely to work here, since it's likely that necessary files are only having timestamps examined rather than their content read, which won't be reflected in the access time.
Concurrent CI jobs
Larger projects will be split into multiple CI jobs which have different dependencies. It's important that these jobs don't compete against each other. This can happen because they have different opinions on what dependencies are needed or not (causing cache items to be repeatedly evicted and restored), and also because they may all decide a cached item needs to be updated and each pushes out a new copy at the same time.
Each CI job is assigned a unique identifier (derived from the workflow, job ID and any matrix properties) for which a list of dependencies is recorded. Each one of these dependencies is called a ‘cache group‘ and has a name which incorporates a hash of its expected contents.
A cache group represents one or more dependencies (e.g. crates, git repositories) which are bundled together. The individual dependencies may be updated, but none are ever added or removed. This means any CI job is free to update any cache group it uses. Jobs which have differing dependencies will interact with different cache groups.
Making a cache group the finest granularity of caching possible might seem like a good idea. This is currently the case for cached indices and Git repositories, but not for crates. Many crates are quite small (tens of KiB) and projects typically depend on a large number of crates. It seems suboptimal and potentially irritating to users to construct a separate cache entry for each cached crate. Therefore crates are cached at the level of all crates used from a particular index.
To avoid concurrent CI jobs all pushing out similar updated cache groups, we use an API internal to the cache action to determine if any cache group we intend to update has had a new version pushed out since we downloaded it, just before we upload a new version. This reduces the window for a race from minutes down to a few seconds.
This code is pushed to
crates.io primarily as a proactive measure against
name-squatting and for maintaining a historical record. Consequently it may be
out of date, and the homepage should be consulted for the latest information.
Notes / Disclaimer
Ferrous actions is very much experimental and should not be relied upon in production environments or for business critical purposes. See LICENSE for additional details.
Ferrous actions is primarily intended for use for hobbyish-sized Rust projects. If you need a complex caching framework then it's time to look at setting up sccache backed by cloud storage and/or Nix.
This repository is based off the template created by Peter Evans (@peter-evans) here.