From b342426bb4b0df7e2b45b04659d48660ee7d9afc Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 10 Mar 2026 06:28:27 +0000 Subject: [PATCH 1/2] fix: handle workspace members in needsRecompile crate collection When a Nargo.toml defines a [workspace] with members, collectCrateDirs now visits each member directory and follows their path-based dependencies. Previously only the root Nargo.toml's [dependencies] were checked, which meant workspace member deps were never discovered and source changes in them would not trigger recompilation. Co-Authored-By: Claude Opus 4.6 --- .../cli/cmds/utils/needs_recompile.test.ts | 56 +++++++++++++++++++ .../src/cli/cmds/utils/needs_recompile.ts | 33 +++++++---- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.test.ts b/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.test.ts index 9f2755898be6..19f87e414522 100644 --- a/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.test.ts +++ b/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.test.ts @@ -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" } +`; + 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'); diff --git a/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts b/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts index c66bfc5bee08..0036287ebbd5 100644 --- a/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts +++ b/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts @@ -73,19 +73,30 @@ async function collectCrateDirs(startCrateDir: string): Promise { 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; - const deps = (parsed.dependencies as Record) ?? {}; - 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)?.members as string[] | undefined; + + if (Array.isArray(members)) { + // The crate is a workspace root and has member 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) ?? {}; + 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); } } } From e79fc6163150bf9be111f78d3e49865373c9a8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Wed, 11 Mar 2026 03:37:14 +0100 Subject: [PATCH 2/2] Update yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts Co-authored-by: Nicolas Chamo --- yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts b/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts index 0036287ebbd5..f2a8096d9fc0 100644 --- a/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts +++ b/yarn-project/aztec/src/cli/cmds/utils/needs_recompile.ts @@ -78,7 +78,7 @@ async function collectCrateDirs(startCrateDir: string): Promise { const members = (parsed.workspace as Record)?.members as string[] | undefined; if (Array.isArray(members)) { - // The crate is a workspace root and has member defined so we visit the members + // 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);