From 151eeee51b94b648db7d5aa68ce6a3cad1ddea4e Mon Sep 17 00:00:00 2001 From: marc0246 <40955683+marc0246@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:56:12 +0100 Subject: [PATCH] Add support for running rust-cache commands from within a Nix shell (#290) --- .github/workflows/nix.yml | 33 ++++++++++++++++++++++++++++ README.md | 10 +++++++++ action.yml | 4 ++++ dist/restore/index.js | 41 ++++++++++++++++++++++++----------- dist/save/index.js | 45 ++++++++++++++++++++++++++------------- src/config.ts | 22 +++++++++++++++---- src/save.ts | 4 ++-- src/utils.ts | 7 +++--- src/workspace.ts | 13 +++++------ tests/flake.nix | 31 +++++++++++++++++++++++++++ 10 files changed, 167 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/nix.yml create mode 100644 tests/flake.nix diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..ee755eb --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,33 @@ +name: nix + +on: [push, pull_request] + +permissions: {} + +jobs: + nix: + if: github.repository == 'Swatinem/rust-cache' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + name: Test Nix on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + + - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + + - uses: ./ + with: + workspaces: tests + cmd-format: nix develop ./tests -c {0} + + - run: | + nix develop -c cargo check + nix develop -c cargo test + working-directory: tests diff --git a/README.md b/README.md index ca06613..280226b 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,16 @@ sensible defaults. # Determines whether to cache the ~/.cargo/bin directory. # default: "true" cache-bin: "" + + # A format string used to format commands to be run, i.e. `rustc` and `cargo`. + # Must contain exactly one occurance of `{0}`, which is the formatting fragment + # that will be replaced with the `rustc` or `cargo` command. This is necessary + # when using Nix or other setup that requires running these commands within a + # specific shell, otherwise the system `rustc` and `cargo` will be run. + # default: "{0}" + cmd-format: "" + # To run within a Nix shell (using the default dev shell of a flake in the repo root): + cmd-format: nix develop -c {0} ``` Further examples are available in the [.github/workflows](./.github/workflows/) directory. diff --git a/action.yml b/action.yml index a2e3b5c..53f9574 100644 --- a/action.yml +++ b/action.yml @@ -60,6 +60,10 @@ inputs: description: "Check if a cache entry exists without downloading the cache" required: false default: "false" + cmd-format: + description: "A format string used to format commands to be run, i.e. `rustc` and `cargo`." + required: false + default: "{0}" outputs: cache-hit: description: "A boolean value that indicates an exact match was found." diff --git a/dist/restore/index.js b/dist/restore/index.js index f52e524..b51c9a7 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -150675,11 +150675,12 @@ function reportError(e) { lib_core.error(`${e.stack}`); } } -async function getCmdOutput(cmd, args = [], options = {}) { +async function getCmdOutput(cmdFormat, cmd, options = {}) { + cmd = cmdFormat.replace("{0}", cmd); let stdout = ""; let stderr = ""; try { - await exec.exec(cmd, args, { + await exec.exec(cmd, [], { silent: true, listeners: { stdout(data) { @@ -150694,7 +150695,7 @@ async function getCmdOutput(cmd, args = [], options = {}) { } catch (e) { e.commandFailed = { - command: `${cmd} ${args.join(" ")}`, + command: cmd, stderr, }; throw e; @@ -150742,11 +150743,12 @@ class Workspace { this.root = root; this.target = target; } - async getPackages(filter, ...extraArgs) { + async getPackages(cmdFormat, filter, extraArgs) { + const cmd = "cargo metadata --all-features --format-version 1" + (extraArgs ? ` ${extraArgs}` : ""); let packages = []; try { lib_core.debug(`collecting metadata for "${this.root}"`); - const meta = JSON.parse(await getCmdOutput("cargo", ["metadata", "--all-features", "--format-version", "1", ...extraArgs], { + const meta = JSON.parse(await getCmdOutput(cmdFormat, cmd, { cwd: this.root, env: { ...process.env, "CARGO_ENCODED_RUSTFLAGS": "" }, })); @@ -150761,11 +150763,11 @@ class Workspace { } return packages; } - async getPackagesOutsideWorkspaceRoot() { - return await this.getPackages((pkg) => !pkg.manifest_path.startsWith(this.root)); + async getPackagesOutsideWorkspaceRoot(cmdFormat) { + return await this.getPackages(cmdFormat, (pkg) => !pkg.manifest_path.startsWith(this.root)); } - async getWorkspaceMembers() { - return await this.getPackages((_) => true, "--no-deps"); + async getWorkspaceMembers(cmdFormat) { + return await this.getPackages(cmdFormat, (_) => true, "--no-deps"); } } @@ -150787,6 +150789,8 @@ const STATE_CONFIG = "RUST_CACHE_CONFIG"; const HASH_LENGTH = 8; class CacheConfig { constructor() { + /** A format string for running commands */ + this.cmdFormat = ""; /** All the paths we want to cache */ this.cachePaths = []; /** The primary cache key */ @@ -150815,6 +150819,17 @@ class CacheConfig { */ static async new() { const self = new CacheConfig(); + let cmdFormat = lib_core.getInput("cmd-format"); + if (cmdFormat) { + const placeholderMatches = cmdFormat.match(/\{0\}/g); + if (!placeholderMatches || placeholderMatches.length !== 1) { + cmdFormat = "{0}"; + } + } + else { + cmdFormat = "{0}"; + } + self.cmdFormat = cmdFormat; // Construct key prefix: // This uses either the `shared-key` input, // or the `key` input combined with the `job` key. @@ -150845,7 +150860,7 @@ class CacheConfig { // The env vars are sorted, matched by prefix and hashed into the // resulting environment hash. let hasher = external_crypto_default().createHash("sha1"); - const rustVersion = await getRustVersion(); + const rustVersion = await getRustVersion(cmdFormat); let keyRust = `${rustVersion.release} ${rustVersion.host}`; hasher.update(keyRust); hasher.update(rustVersion["commit-hash"]); @@ -150895,7 +150910,7 @@ class CacheConfig { for (const workspace of workspaces) { const root = workspace.root; keyFiles.push(...(await globFiles(`${root}/**/.cargo/config.toml\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`))); - const workspaceMembers = await workspace.getWorkspaceMembers(); + const workspaceMembers = await workspace.getWorkspaceMembers(cmdFormat); const cargo_manifests = sort_and_uniq(workspaceMembers.map((member) => external_path_default().join(member.path, "Cargo.toml"))); for (const cargo_manifest of cargo_manifests) { try { @@ -151071,8 +151086,8 @@ function isCacheUpToDate() { function digest(hasher) { return hasher.digest("hex").substring(0, HASH_LENGTH); } -async function getRustVersion() { - const stdout = await getCmdOutput("rustc", ["-vV"]); +async function getRustVersion(cmdFormat) { + const stdout = await getCmdOutput(cmdFormat, "rustc -vV"); let splits = stdout .split(/[\n\r]+/) .filter(Boolean) diff --git a/dist/save/index.js b/dist/save/index.js index b5b932c..ffaf496 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -150675,11 +150675,12 @@ function reportError(e) { core.error(`${e.stack}`); } } -async function getCmdOutput(cmd, args = [], options = {}) { +async function getCmdOutput(cmdFormat, cmd, options = {}) { + cmd = cmdFormat.replace("{0}", cmd); let stdout = ""; let stderr = ""; try { - await exec.exec(cmd, args, { + await exec.exec(cmd, [], { silent: true, listeners: { stdout(data) { @@ -150694,7 +150695,7 @@ async function getCmdOutput(cmd, args = [], options = {}) { } catch (e) { e.commandFailed = { - command: `${cmd} ${args.join(" ")}`, + command: cmd, stderr, }; throw e; @@ -150742,11 +150743,12 @@ class Workspace { this.root = root; this.target = target; } - async getPackages(filter, ...extraArgs) { + async getPackages(cmdFormat, filter, extraArgs) { + const cmd = "cargo metadata --all-features --format-version 1" + (extraArgs ? ` ${extraArgs}` : ""); let packages = []; try { core.debug(`collecting metadata for "${this.root}"`); - const meta = JSON.parse(await getCmdOutput("cargo", ["metadata", "--all-features", "--format-version", "1", ...extraArgs], { + const meta = JSON.parse(await getCmdOutput(cmdFormat, cmd, { cwd: this.root, env: { ...process.env, "CARGO_ENCODED_RUSTFLAGS": "" }, })); @@ -150761,11 +150763,11 @@ class Workspace { } return packages; } - async getPackagesOutsideWorkspaceRoot() { - return await this.getPackages((pkg) => !pkg.manifest_path.startsWith(this.root)); + async getPackagesOutsideWorkspaceRoot(cmdFormat) { + return await this.getPackages(cmdFormat, (pkg) => !pkg.manifest_path.startsWith(this.root)); } - async getWorkspaceMembers() { - return await this.getPackages((_) => true, "--no-deps"); + async getWorkspaceMembers(cmdFormat) { + return await this.getPackages(cmdFormat, (_) => true, "--no-deps"); } } @@ -150787,6 +150789,8 @@ const STATE_CONFIG = "RUST_CACHE_CONFIG"; const HASH_LENGTH = 8; class CacheConfig { constructor() { + /** A format string for running commands */ + this.cmdFormat = ""; /** All the paths we want to cache */ this.cachePaths = []; /** The primary cache key */ @@ -150815,6 +150819,17 @@ class CacheConfig { */ static async new() { const self = new CacheConfig(); + let cmdFormat = core.getInput("cmd-format"); + if (cmdFormat) { + const placeholderMatches = cmdFormat.match(/\{0\}/g); + if (!placeholderMatches || placeholderMatches.length !== 1) { + cmdFormat = "{0}"; + } + } + else { + cmdFormat = "{0}"; + } + self.cmdFormat = cmdFormat; // Construct key prefix: // This uses either the `shared-key` input, // or the `key` input combined with the `job` key. @@ -150845,7 +150860,7 @@ class CacheConfig { // The env vars are sorted, matched by prefix and hashed into the // resulting environment hash. let hasher = external_crypto_default().createHash("sha1"); - const rustVersion = await getRustVersion(); + const rustVersion = await getRustVersion(cmdFormat); let keyRust = `${rustVersion.release} ${rustVersion.host}`; hasher.update(keyRust); hasher.update(rustVersion["commit-hash"]); @@ -150895,7 +150910,7 @@ class CacheConfig { for (const workspace of workspaces) { const root = workspace.root; keyFiles.push(...(await globFiles(`${root}/**/.cargo/config.toml\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`))); - const workspaceMembers = await workspace.getWorkspaceMembers(); + const workspaceMembers = await workspace.getWorkspaceMembers(cmdFormat); const cargo_manifests = sort_and_uniq(workspaceMembers.map((member) => external_path_default().join(member.path, "Cargo.toml"))); for (const cargo_manifest of cargo_manifests) { try { @@ -151071,8 +151086,8 @@ function isCacheUpToDate() { function digest(hasher) { return hasher.digest("hex").substring(0, HASH_LENGTH); } -async function getRustVersion() { - const stdout = await getCmdOutput("rustc", ["-vV"]); +async function getRustVersion(cmdFormat) { + const stdout = await getCmdOutput(cmdFormat, "rustc -vV"); let splits = stdout .split(/[\n\r]+/) .filter(Boolean) @@ -151428,9 +151443,9 @@ async function run() { const workspaceCrates = core.getInput("cache-workspace-crates").toLowerCase() || "false"; const allPackages = []; for (const workspace of config.workspaces) { - const packages = await workspace.getPackagesOutsideWorkspaceRoot(); + const packages = await workspace.getPackagesOutsideWorkspaceRoot(config.cmdFormat); if (workspaceCrates === "true") { - const wsMembers = await workspace.getWorkspaceMembers(); + const wsMembers = await workspace.getWorkspaceMembers(config.cmdFormat); packages.push(...wsMembers); } allPackages.push(...packages); diff --git a/src/config.ts b/src/config.ts index 1a79706..d81aa4f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,6 +18,9 @@ const STATE_CONFIG = "RUST_CACHE_CONFIG"; const HASH_LENGTH = 8; export class CacheConfig { + /** A format string for running commands */ + public cmdFormat: string = ""; + /** All the paths we want to cache */ public cachePaths: Array = []; /** The primary cache key */ @@ -53,6 +56,17 @@ export class CacheConfig { static async new(): Promise { const self = new CacheConfig(); + let cmdFormat = core.getInput("cmd-format"); + if (cmdFormat) { + const placeholderMatches = cmdFormat.match(/\{0\}/g); + if (!placeholderMatches || placeholderMatches.length !== 1) { + cmdFormat = "{0}"; + } + } else { + cmdFormat = "{0}"; + } + self.cmdFormat = cmdFormat; + // Construct key prefix: // This uses either the `shared-key` input, // or the `key` input combined with the `job` key. @@ -89,7 +103,7 @@ export class CacheConfig { // resulting environment hash. let hasher = crypto.createHash("sha1"); - const rustVersion = await getRustVersion(); + const rustVersion = await getRustVersion(cmdFormat); let keyRust = `${rustVersion.release} ${rustVersion.host}`; hasher.update(keyRust); @@ -158,7 +172,7 @@ export class CacheConfig { )), ); - const workspaceMembers = await workspace.getWorkspaceMembers(); + const workspaceMembers = await workspace.getWorkspaceMembers(cmdFormat); const cargo_manifests = sort_and_uniq(workspaceMembers.map((member) => path.join(member.path, "Cargo.toml"))); @@ -366,8 +380,8 @@ interface RustVersion { "commit-hash": string; } -async function getRustVersion(): Promise { - const stdout = await getCmdOutput("rustc", ["-vV"]); +async function getRustVersion(cmdFormat: string): Promise { + const stdout = await getCmdOutput(cmdFormat, "rustc -vV"); let splits = stdout .split(/[\n\r]+/) .filter(Boolean) diff --git a/src/save.ts b/src/save.ts index 10fe79d..1df7f1d 100644 --- a/src/save.ts +++ b/src/save.ts @@ -39,9 +39,9 @@ async function run() { const workspaceCrates = core.getInput("cache-workspace-crates").toLowerCase() || "false"; const allPackages = []; for (const workspace of config.workspaces) { - const packages = await workspace.getPackagesOutsideWorkspaceRoot(); + const packages = await workspace.getPackagesOutsideWorkspaceRoot(config.cmdFormat); if (workspaceCrates === "true") { - const wsMembers = await workspace.getWorkspaceMembers(); + const wsMembers = await workspace.getWorkspaceMembers(config.cmdFormat); packages.push(...wsMembers); } allPackages.push(...packages); diff --git a/src/utils.ts b/src/utils.ts index 409a120..f6aa700 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,14 +16,15 @@ export function reportError(e: any) { } export async function getCmdOutput( + cmdFormat: string, cmd: string, - args: Array = [], options: exec.ExecOptions = {}, ): Promise { + cmd = cmdFormat.replace("{0}", cmd); let stdout = ""; let stderr = ""; try { - await exec.exec(cmd, args, { + await exec.exec(cmd, [], { silent: true, listeners: { stdout(data) { @@ -37,7 +38,7 @@ export async function getCmdOutput( }); } catch (e) { (e as any).commandFailed = { - command: `${cmd} ${args.join(" ")}`, + command: cmd, stderr, }; throw e; diff --git a/src/workspace.ts b/src/workspace.ts index 48326e4..d7734f5 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -8,12 +8,13 @@ const SAVE_TARGETS = new Set(["lib", "proc-macro"]); export class Workspace { constructor(public root: string, public target: string) {} - async getPackages(filter: (p: Meta["packages"][0]) => boolean, ...extraArgs: string[]): Promise { + async getPackages(cmdFormat: string, filter: (p: Meta["packages"][0]) => boolean, extraArgs?: string): Promise { + const cmd = "cargo metadata --all-features --format-version 1" + (extraArgs ? ` ${extraArgs}` : ""); let packages: Packages = []; try { core.debug(`collecting metadata for "${this.root}"`); const meta: Meta = JSON.parse( - await getCmdOutput("cargo", ["metadata", "--all-features", "--format-version", "1", ...extraArgs], { + await getCmdOutput(cmdFormat, cmd, { cwd: this.root, env: { ...process.env, "CARGO_ENCODED_RUSTFLAGS": "" }, }), @@ -29,12 +30,12 @@ export class Workspace { return packages; } - public async getPackagesOutsideWorkspaceRoot(): Promise { - return await this.getPackages((pkg) => !pkg.manifest_path.startsWith(this.root)); + public async getPackagesOutsideWorkspaceRoot(cmdFormat: string): Promise { + return await this.getPackages(cmdFormat, (pkg) => !pkg.manifest_path.startsWith(this.root)); } - public async getWorkspaceMembers(): Promise { - return await this.getPackages((_) => true, "--no-deps"); + public async getWorkspaceMembers(cmdFormat: string): Promise { + return await this.getPackages(cmdFormat, (_) => true, "--no-deps"); } } diff --git a/tests/flake.nix b/tests/flake.nix new file mode 100644 index 0000000..905b2ea --- /dev/null +++ b/tests/flake.nix @@ -0,0 +1,31 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs @ { self, nixpkgs, flake-utils, rust-overlay, ... }: + flake-utils.lib.eachDefaultSystem ( + system: let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + in { + devShells.default = with pkgs; mkShell { + buildInputs = [ + autoconf + gcc + gnumake + openssl + pkg-config + rust-bin.stable.latest.minimal + ]; + CARGO_TERM_COLOR = "always"; + }; + } + ); +} +