From d8f9cc9894c8e9c77223d33dca28b7fdbca815f2 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 1 Aug 2026 00:28:29 -0400 Subject: [PATCH] fix(manifest): isolate the Maven extension build The maven-extension jar build invoked the bundled Maven wrapper with no repository override, so every run downloaded the plugin and dependency closure into the developer's ~/.m2/repository and the Maven distribution into ~/.m2/wrapper/dists. Point both at a stable directory under the OS temp dir instead. The path is stable rather than per-run so the closure stays cached between builds; SOCKET_CLI_MAVEN_HOME overrides it for a genuinely cold build. Resolve that directory to an absolute path before use. Maven runs from a subshell that cds into the extension directory, so a relative SOCKET_CLI_MAVEN_HOME or TMPDIR would have created one tree at the caller's cwd and then written a second one under the extension directory. --- .../scripts/maven-extension/build-jar.sh | 18 ++- .../maven-extension-build-jar.test.mts | 118 ++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 packages/cli/test/unit/scripts/maven-extension-build-jar.test.mts diff --git a/packages/cli/src/commands/manifest/scripts/maven-extension/build-jar.sh b/packages/cli/src/commands/manifest/scripts/maven-extension/build-jar.sh index eeaef2f672..298bffb9f4 100755 --- a/packages/cli/src/commands/manifest/scripts/maven-extension/build-jar.sh +++ b/packages/cli/src/commands/manifest/scripts/maven-extension/build-jar.sh @@ -5,6 +5,22 @@ set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -( cd "$here" && ./mvnw -q --batch-mode package ) + +# Keep the downloaded artifacts and the Maven distribution out of the developer's ~/.m2. The path is +# stable so the plugin closure stays cached between runs; point SOCKET_CLI_MAVEN_HOME elsewhere for a +# cold build. +tmp_root="${TMPDIR:-/tmp}" +maven_home="${SOCKET_CLI_MAVEN_HOME:-${tmp_root%/}/socket-cli-maven-home}" +mkdir -p "$maven_home" +# Absolute, because the Maven invocation below runs after a cd into the extension directory: a +# relative SOCKET_CLI_MAVEN_HOME or TMPDIR would otherwise create one tree here and write another +# under the extension directory. +maven_home="$(cd "$maven_home" && pwd)" + +( + cd "$here" + MAVEN_USER_HOME="$maven_home" ./mvnw -q --batch-mode \ + -Dmaven.repo.local="$maven_home/repository" package +) cp -f "$here/target/coana-maven-extension.jar" "$here/coana-maven-extension.jar" echo "Coana Maven extension jar: $here/coana-maven-extension.jar" diff --git a/packages/cli/test/unit/scripts/maven-extension-build-jar.test.mts b/packages/cli/test/unit/scripts/maven-extension-build-jar.test.mts new file mode 100644 index 0000000000..1cb716ea77 --- /dev/null +++ b/packages/cli/test/unit/scripts/maven-extension-build-jar.test.mts @@ -0,0 +1,118 @@ +/** + * Unit tests for the Maven extension build script's cache-root resolution. + * + * Purpose: build-jar.sh creates the Maven home up front, then runs Maven from + * inside the extension directory. Those two steps must agree on which + * directory they mean, so the resolved home has to be absolute before the cd. + * + * Related Files: + * - src/commands/manifest/scripts/maven-extension/build-jar.sh (implementation) + */ + +import { + mkdtempSync, + readFileSync, + realpathSync, + statSync, + writeFileSync, +} from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { safeDelete } from '@socketsecurity/lib-stable/fs/safe' +import { spawn } from '@socketsecurity/lib-stable/process/spawn/child' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const BUILD_JAR_PATH = path.join( + __dirname, + '../../../src/commands/manifest/scripts/maven-extension/build-jar.sh', +) + +/** + * The script's leading cache-root resolution, up to the subshell that invokes + * Maven. Running just this part exercises the shipped lines without needing a + * JDK, the Maven wrapper, or the network. + */ +function readResolutionPrelude(): string { + const lines = readFileSync(BUILD_JAR_PATH, 'utf8').split('\n') + const subshellAt = lines.indexOf('(') + if (subshellAt === -1) { + throw new Error('build-jar.sh no longer opens a subshell with a bare "("') + } + return lines.slice(0, subshellAt).join('\n') +} + +/** + * Run the prelude with `cwd` as the working directory and report the + * `maven_home` it settled on. + */ +async function resolveMavenHome( + cwd: string, + env: Record, +): Promise { + const preludePath = path.join(cwd, 'prelude.sh') + writeFileSync( + preludePath, + `${readResolutionPrelude()}\nprintf '%s\\n' "$maven_home"\n`, + ) + + const result = await spawn('bash', [preludePath], { + cwd, + env: { ...process.env, ...env }, + }) + return result.stdout.trim() +} + +describe('maven-extension build-jar.sh cache root', () => { + let workDir: string + + beforeEach(() => { + workDir = realpathSync( + mkdtempSync(path.join(os.tmpdir(), 'build-jar-test-')), + ) + }) + + afterEach(async () => { + await safeDelete(workDir) + }) + + it('anchors a relative SOCKET_CLI_MAVEN_HOME to the caller, not the extension directory', async () => { + const mavenHome = await resolveMavenHome(workDir, { + SOCKET_CLI_MAVEN_HOME: 'relative/maven-home', + }) + + // Absolute, so the cd into the extension directory cannot re-anchor it. + expect(path.isAbsolute(mavenHome)).toBe(true) + expect(realpathSync(mavenHome)).toBe( + path.join(workDir, 'relative/maven-home'), + ) + // The directory the script created is the one Maven will be pointed at. + expect(statSync(mavenHome).isDirectory()).toBe(true) + }) + + it('anchors a relative TMPDIR to the caller', async () => { + const mavenHome = await resolveMavenHome(workDir, { + SOCKET_CLI_MAVEN_HOME: undefined, + TMPDIR: 'relative-tmp', + }) + + expect(path.isAbsolute(mavenHome)).toBe(true) + expect(realpathSync(mavenHome)).toBe( + path.join(workDir, 'relative-tmp/socket-cli-maven-home'), + ) + }) + + it('leaves an absolute SOCKET_CLI_MAVEN_HOME alone', async () => { + const absoluteHome = path.join(workDir, 'absolute/maven-home') + + const mavenHome = await resolveMavenHome(workDir, { + SOCKET_CLI_MAVEN_HOME: absoluteHome, + }) + + expect(realpathSync(mavenHome)).toBe(absoluteHome) + }) +})