From 8a75d88c5a127d9ee8e71a91e89f7480752e559c Mon Sep 17 00:00:00 2001 From: Tyler Rockwood Date: Thu, 24 Sep 2026 14:33:06 -0500 Subject: [PATCH] Rust: keep dependencies that share a name with a workspace target --- src/lib/languages.js | 28 ++++++++++++++++++++++++++++ test/cache.test.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/lib/languages.js b/src/lib/languages.js index 3d67f6f..93a0c16 100644 --- a/src/lib/languages.js +++ b/src/lib/languages.js @@ -104,12 +104,39 @@ function escape(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +// Names of the registry and git packages in a Cargo.lock. Path dependencies +// have no source line and are left out. +function lockedDependencies(root) { + let lockfile; + try { + lockfile = fs.readFileSync(path.join(root, 'Cargo.lock'), 'utf8'); + } catch { + return []; + } + const names = []; + for (const block of lockfile.split(/^\[\[package\]\]$/m).slice(1)) { + const name = /^name = "([^"]+)"$/m.exec(block); + if (name && /^source = /m.test(block)) names.push(name[1]); + } + return names; +} + // The workspace's own crates rebuild on every run, because checkout gives // their sources new mtimes. Their artifacts only make the archive bigger, so // only dependencies are kept, as Swatinem/rust-cache does. +// +// Artifacts are matched by name only, so a workspace target that shares a +// name with a dependency, such as a lib named `jobserver`, would also drop +// that dependency and rebuild everything downstream of it. Those names are +// kept, along with the workspace artifacts that happen to share them. function workspaceArtifacts(roots) { const names = new Set(); + const dependencies = new Set(); for (const root of roots) { + for (const name of lockedDependencies(root)) { + dependencies.add(name); + dependencies.add(name.replace(/-/g, '_')); + } const metadata = commandOutput('cargo', ['metadata', '--no-deps', '--format-version', '1', '--offline'], root); if (!metadata) continue; try { @@ -123,6 +150,7 @@ function workspaceArtifacts(roots) { // Unparseable metadata: keep everything. } } + for (const name of dependencies) names.delete(name); if (names.size === 0) return undefined; const alternatives = [...names].map(escape).join('|'); // deps/libapp-0123456789abcdef.rlib, .fingerprint/app-0123456789abcdef/, ... diff --git a/test/cache.test.js b/test/cache.test.js index 9e59864..1f6b21f 100644 --- a/test/cache.test.js +++ b/test/cache.test.js @@ -455,6 +455,40 @@ test('drops workspace crate artifacts but keeps dependencies', { skip: !hasCargo assert.ok(!skip(path.join(root, 'elsewhere'), 'my-app'), 'uplifted names only next to deps/'); }); +test('keeps a dependency that shares a name with a workspace target', { skip: !hasCargo && 'cargo is not installed' }, () => { + const root = path.join(sandbox, 'shared-name'); + write( + path.join(root, 'Cargo.toml'), + '[package]\nname = "my-server"\nversion = "0.1.0"\nedition = "2021"\n\n[lib]\nname = "jobserver"\n', + ); + write(path.join(root, 'src', 'lib.rs'), ''); + write( + path.join(root, 'Cargo.lock'), + [ + 'version = 4', + '', + '[[package]]', + 'name = "jobserver"', + 'version = "0.1.34"', + 'source = "registry+https://github.com/rust-lang/crates.io-index"', + '', + '[[package]]', + 'name = "my-server"', + 'version = "0.1.0"', + '', + ].join('\n'), + ); + const debug = path.join(root, 'target', 'debug'); + const deps = path.join(debug, 'deps'); + fs.mkdirSync(deps, { recursive: true }); + const skip = languages.workspaceArtifacts([root]); + assert.ok(skip, 'cargo metadata listed the workspace'); + const hash = '0123456789abcdef'; + assert.ok(!skip(deps, `libjobserver-${hash}.rlib`), 'dependency rlib'); + assert.ok(!skip(path.join(debug, '.fingerprint'), `jobserver-${hash}`), 'dependency fingerprint'); + assert.ok(skip(path.join(debug, '.fingerprint'), `my-server-${hash}`), 'workspace package'); +}); + test('keys on lockfiles, falling back to manifests', () => { const locked = ['/r/Cargo.lock', '/r/Cargo.toml', '/r/crates/a/Cargo.toml', '/r/rust-toolchain.toml']; assert.deepEqual(steps.keyFiles(locked), ['/r/Cargo.lock', '/r/rust-toolchain.toml']);