Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/lib/languages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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/, ...
Expand Down
34 changes: 34 additions & 0 deletions test/cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down