From 699af690b76fd37dcc56c55b5bab94743acdcdc8 Mon Sep 17 00:00:00 2001 From: Denis Frenademetz Date: Wed, 5 Jul 2023 16:42:58 +0200 Subject: [PATCH] fix(core): ensure external dependency hashes are resolved in a deterministic way (#17926) (cherry picked from commit 65adb94bf671117b9307ea22037440836b4da9e3) --- packages/nx/src/hasher/task-hasher.spec.ts | 138 +++++++++++++++++++++ packages/nx/src/hasher/task-hasher.ts | 51 +++++--- 2 files changed, 175 insertions(+), 14 deletions(-) diff --git a/packages/nx/src/hasher/task-hasher.spec.ts b/packages/nx/src/hasher/task-hasher.spec.ts index e108672d31c6a..6ba82a56e424a 100644 --- a/packages/nx/src/hasher/task-hasher.spec.ts +++ b/packages/nx/src/hasher/task-hasher.spec.ts @@ -1161,6 +1161,144 @@ describe('TaskHasher', () => { expect(hash.value).toContain('|5.0.0|'); }); + it('should hash entire subtree of dependencies', async () => { + const createHasher = () => + new InProcessTaskHasher( + {}, + allWorkspaceFiles, + { + nodes: { + appA: { + name: 'appA', + type: 'app', + data: { + root: 'apps/appA', + targets: { build: { executor: '@nx/webpack:webpack' } }, + }, + }, + appB: { + name: 'appB', + type: 'app', + data: { + root: 'apps/appB', + targets: { build: { executor: '@nx/webpack:webpack' } }, + }, + }, + }, + externalNodes: { + 'npm:packageA': { + name: 'npm:packageA', + type: 'npm', + data: { + packageName: 'packageA', + version: '0.0.0', + hash: '$packageA0.0.0$', + }, + }, + 'npm:packageB': { + name: 'npm:packageB', + type: 'npm', + data: { + packageName: 'packageB', + version: '0.0.0', + hash: '$packageB0.0.0$', + }, + }, + 'npm:packageC': { + name: 'npm:packageC', + type: 'npm', + data: { + packageName: 'packageC', + version: '0.0.0', + hash: '$packageC0.0.0$', + }, + }, + }, + dependencies: { + appA: [ + { + source: 'app', + target: 'npm:packageA', + type: DependencyType.static, + }, + { + source: 'app', + target: 'npm:packageB', + type: DependencyType.static, + }, + { + source: 'app', + target: 'npm:packageC', + type: DependencyType.static, + }, + ], + appB: [ + { + source: 'app', + target: 'npm:packageC', + type: DependencyType.static, + }, + ], + 'npm:packageC': [ + { + source: 'app', + target: 'npm:packageA', + type: DependencyType.static, + }, + { + source: 'app', + target: 'npm:packageB', + type: DependencyType.static, + }, + ], + 'npm:packageB': [ + { + source: 'app', + target: 'npm:packageA', + type: DependencyType.static, + }, + ], + }, + }, + { + roots: ['app-build'], + tasks: { + 'app-build': { + id: 'app-build', + target: { project: 'app', target: 'build' }, + overrides: {}, + }, + }, + dependencies: {}, + }, + {} as any, + {}, + fileHasher + ); + + const computeTaskHash = async (hasher, appName) => { + const hashAppA = await hasher.hashTask({ + target: { project: appName, target: 'build' }, + id: `${appName}-build`, + overrides: { prop: 'prop-value' }, + }); + + return hashAppA.value; + }; + + const hasher1 = createHasher(); + + await computeTaskHash(hasher1, 'appA'); + const hashAppB1 = await computeTaskHash(hasher1, 'appB'); + + const hasher2 = createHasher(); + + const hashAppB2 = await computeTaskHash(hasher2, 'appB'); + await computeTaskHash(hasher2, 'appA'); + + expect(hashAppB1).toEqual(hashAppB2); + }); + it('should not hash when nx:run-commands executor', async () => { const hasher = new InProcessTaskHasher( {}, diff --git a/packages/nx/src/hasher/task-hasher.ts b/packages/nx/src/hasher/task-hasher.ts index 05246b9ac559c..4b40e50a03337 100644 --- a/packages/nx/src/hasher/task-hasher.ts +++ b/packages/nx/src/hasher/task-hasher.ts @@ -332,7 +332,7 @@ class TaskHasherImpl { visited ); } else { - const hash = this.hashExternalDependency(d.target); + const hash = this.hashExternalDependency(d.source, d.target); return { value: hash, details: { @@ -408,16 +408,29 @@ class TaskHasherImpl { return partialHashes; } + private computeExternalDependencyIdentifier( + sourceProjectName: string, + targetProjectName: string + ): `${string}->${string}` { + return `${sourceProjectName}->${targetProjectName}`; + } + private hashExternalDependency( - projectName: string, + sourceProjectName: string, + targetProjectName: string, visited = new Set() ): string { // try to retrieve the hash from cache - if (this.externalDepsHashCache[projectName]) { - return this.externalDepsHashCache[projectName]; + if (this.externalDepsHashCache[targetProjectName]) { + return this.externalDepsHashCache[targetProjectName]; } - visited.add(projectName); - const node = this.projectGraph.externalNodes[projectName]; + visited.add( + this.computeExternalDependencyIdentifier( + sourceProjectName, + targetProjectName + ) + ); + const node = this.projectGraph.externalNodes[targetProjectName]; let partialHash: string; if (node) { const partialHashes: string[] = []; @@ -429,22 +442,32 @@ class TaskHasherImpl { partialHashes.push(node.data.version); } // we want to calculate the hash of the entire dependency tree - if (this.projectGraph.dependencies[projectName]) { - this.projectGraph.dependencies[projectName].forEach((d) => { - if (!visited.has(d.target)) { - partialHashes.push(this.hashExternalDependency(d.target, visited)); + if (this.projectGraph.dependencies[targetProjectName]) { + this.projectGraph.dependencies[targetProjectName].forEach((d) => { + if ( + !visited.has( + this.computeExternalDependencyIdentifier( + targetProjectName, + d.target + ) + ) + ) { + partialHashes.push( + this.hashExternalDependency(targetProjectName, d.target, visited) + ); } }); } + partialHash = hashArray(partialHashes); } else { // unknown dependency // this may occur if dependency is not an npm package // but rather symlinked in node_modules or it's pointing to a remote git repo // in this case we have no information about the versioning of the given package - partialHash = `__${projectName}__`; + partialHash = `__${targetProjectName}__`; } - this.externalDepsHashCache[projectName] = partialHash; + this.externalDepsHashCache[targetProjectName] = partialHash; return partialHash; } @@ -470,7 +493,7 @@ class TaskHasherImpl { const executorPackage = target.executor.split(':')[0]; const executorNodeName = this.findExternalDependencyNodeName(executorPackage); - hash = this.hashExternalDependency(executorNodeName); + hash = this.hashExternalDependency(projectName, executorNodeName); } else { // use command external dependencies if available to construct the hash const partialHashes: string[] = []; @@ -482,7 +505,7 @@ class TaskHasherImpl { const externalDependencies = input['externalDependencies']; for (let dep of externalDependencies) { dep = this.findExternalDependencyNodeName(dep); - partialHashes.push(this.hashExternalDependency(dep)); + partialHashes.push(this.hashExternalDependency(projectName, dep)); } } }