Skip to content
Merged
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
56 changes: 56 additions & 0 deletions yarn-project/aztec/src/cli/cmds/utils/needs_recompile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,62 @@ bad_dep = { path = "not_a_dir" }
await expect(needsRecompile()).rejects.toThrow('which is not a directory');
});

it('traverses workspace members and their path dependencies', async () => {
// Workspace root with two members: a contract and a test lib.
// The test lib has a path dependency to an external lib outside the workspace.
const contractDir = join(tempDir, 'test_contract');
const testDir = join(tempDir, 'test_test');
const externalLib = join(tempDir, 'external_lib');

await mkdirp(join(contractDir, 'src'));
await mkdirp(join(testDir, 'src'));
await mkdirp(join(externalLib, 'src'));
await mkdirp('target');

// Workspace root Nargo.toml
const workspaceToml = `[workspace]
members = ["test_contract", "test_test"]
`;
await writeFile('Nargo.toml', workspaceToml);
await utimes('Nargo.toml', 1000, 1000);

// Contract member Nargo.toml
await writeFile(join(contractDir, 'Nargo.toml'), '[package]\nname = "test_contract"\ntype = "contract"\n');
await utimes(join(contractDir, 'Nargo.toml'), 1000, 1000);

// Test member Nargo.toml with a path dependency to external_lib and a git dep (ignored)
const testToml = `[package]
name = "test_test"
type = "lib"

[dependencies]
aztec = { git = "https://github.com/AztecProtocol/aztec-nr", tag = "v5.0.0" }
ext = { path = "../external_lib" }
test_contract = { path = "../test_contract" }
`;
Comment on lines +267 to +275
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it easier to read like this? And we could do the same with the other definitions above and below

Suggested change
const testToml = `[package]
name = "test_test"
type = "lib"
[dependencies]
aztec = { git = "https://github.com/AztecProtocol/aztec-nr", tag = "v5.0.0" }
ext = { path = "../external_lib" }
test_contract = { path = "../test_contract" }
`;
const testToml = `
[package]
name = "test_test"
type = "lib"
[dependencies]
aztec = { git = "https://github.com/AztecProtocol/aztec-nr", tag = "v5.0.0" }
ext = { path = "../external_lib" }
test_contract = { path = "../test_contract" }
`;

Copy link
Contributor Author

@benesjan benesjan Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the resulting Nargo.toml would start with a new line. I don't think this matters much as it's already not hard to read.

await writeFile(join(testDir, 'Nargo.toml'), testToml);
await utimes(join(testDir, 'Nargo.toml'), 1000, 1000);

// External lib Nargo.toml
await writeFile(join(externalLib, 'Nargo.toml'), '[package]\nname = "external_lib"\ntype = "lib"\n');
await utimes(join(externalLib, 'Nargo.toml'), 1000, 1000);

// All source files are old
await touch(join(contractDir, 'src', 'main.nr'), 1000);
await touch(join(testDir, 'src', 'test.nr'), 1000);
await touch(join(externalLib, 'src', 'lib.nr'), 1000);

// Artifact is newer than all sources
await touch(join('target', 'artifact.json'), 2000);

expect(await needsRecompile()).toBe(false);

// Now update a source file in the external lib (reachable via workspace member's path dep)
await utimes(join(externalLib, 'src', 'lib.nr'), 3000, 3000);

expect(await needsRecompile()).toBe(true);
});

it('does not follow circular path dependencies', async () => {
// Two projects that depend on each other via path.
const libDir = join(tempDir, 'lib');
Expand Down
33 changes: 22 additions & 11 deletions yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,30 @@ async function collectCrateDirs(startCrateDir: string): Promise<string[]> {
throw new Error(`Incorrectly defined dependency. Nargo.toml not found in ${absDir}`);
});

// We parse and iterate over the dependencies
const parsed = TOML.parse(content) as Record<string, any>;
const deps = (parsed.dependencies as Record<string, any>) ?? {};
for (const dep of Object.values(deps)) {
if (dep && typeof dep === 'object' && typeof dep.path === 'string') {
const depPath = resolve(absDir, dep.path);
const s = await stat(depPath);
if (!s.isDirectory()) {
throw new Error(
`Dependency path "${dep.path}" in ${tomlPath} resolves to ${depPath} which is not a directory`,
);

const members = (parsed.workspace as Record<string, any>)?.members as string[] | undefined;

if (Array.isArray(members)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious. Is it possible for a workspace root to have both members and dependencies? If not, could we clarify that in a comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workspace is a collection of crates and doesn't have its own source code.

I don't think this is the place to explain what is noir workspace.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? In this code you are assuming that the toml file either has members or dependencies. I'm not saying that you should write a full explanation of how Noir workspaces work, but a one line doc that explains that this is the actual behavior would benefit people not yet familiar with Noir

But feel free to ignore

// The crate is a workspace root and has members defined so we visit the members
for (const member of members) {
const memberPath = resolve(absDir, member);
await visit(memberPath);
}
} else {
// The crate is not a workspace root so we check for dependencies
const deps = (parsed.dependencies as Record<string, any>) ?? {};
for (const dep of Object.values(deps)) {
if (dep && typeof dep === 'object' && typeof dep.path === 'string') {
const depPath = resolve(absDir, dep.path);
const s = await stat(depPath);
if (!s.isDirectory()) {
throw new Error(
`Dependency path "${dep.path}" in ${tomlPath} resolves to ${depPath} which is not a directory`,
);
}
await visit(depPath);
}
await visit(depPath);
}
}
}
Expand Down
Loading