diff --git a/scripts/release-setup.mjs b/scripts/release-setup.mjs index 6b83cb7e..3b2ca3fc 100644 --- a/scripts/release-setup.mjs +++ b/scripts/release-setup.mjs @@ -72,6 +72,19 @@ for (const name of siblings) { // Installed, and from the lockfile now checked out: npm records what it // installed in node_modules/.package-lock.json, and a pull that changes the // lockfile leaves that record older than it. +// `npm ci` empties node_modules before it installs, and follows a symbolic +// link there: a worktree that links to another checkout's install would have +// that checkout's packages removed. Such a checkout is prepared by hand. +const refuseLinkedModules = (directory) => { + const modules = join(directory, 'node_modules') + let link + try { + link = lstatSync(modules).isSymbolicLink() ? readlinkSync(modules) : undefined + } catch { + return + } + if (link) throw new Error(`release setup: ${modules} is a link to ${link}; npm would install into that checkout`) +} const installed = (directory) => { const record = join(directory, 'node_modules', '.package-lock.json') const lock = join(directory, 'package-lock.json') @@ -84,11 +97,16 @@ for (const repository of [compiler, ...siblings.map((name) => join(workspace, na .filter((file) => /(^|\/)package-lock\.json$/.test(file)) for (const lock of locks) { const directory = join(repository, dirname(lock)) - if (reinstall || !installed(directory)) step(directory, ['npm', 'ci', '--no-audit', '--no-fund']) + if (reinstall || !installed(directory)) { + refuseLinkedModules(directory) + step(directory, ['npm', 'ci', '--no-audit', '--no-fund']) + } } // A package that locks nothing (parallel) installs its declared ranges. - if (!locks.length && existsSync(join(repository, 'package.json')) && (reinstall || !installed(repository))) + if (!locks.length && existsSync(join(repository, 'package.json')) && (reinstall || !installed(repository))) { + refuseLinkedModules(repository) step(repository, ['npm', 'install', '--no-package-lock', '--no-audit', '--no-fund']) + } } const windowsPackage = join(workspace, 'windows/packages/geastack-windows') diff --git a/scripts/release-suite.mjs b/scripts/release-suite.mjs index ecbe7874..c9c0395b 100644 --- a/scripts/release-suite.mjs +++ b/scripts/release-suite.mjs @@ -55,6 +55,17 @@ const onlyAt = process.argv.findIndex((argument) => argument === '--only' || arg const only = onlyAt < 0 ? undefined : (process.argv[onlyAt].slice(7) || process.argv[onlyAt + 1] || '').split(',') const wanted = (lane) => !only || only.includes(lane) +const buildHostReachable = (host) => { + if (process.env.GEA_PARALLEL_HOST) return true + try { + execFileSync('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', host, 'true'], { stdio: 'ignore' }) + return true + } catch { + console.log(`parallel: ${host} is unreachable, the native fixtures build on this host`) + return false + } +} + const checkout = (name, marker) => { const directory = join(workspace, name) if (!workspace || !existsSync(join(directory, marker))) @@ -185,6 +196,11 @@ if (wanted('core')) { // not skipped quietly: the summary names every one this host left unrun. if (process.platform !== 'darwin' && readFileSync(join(tests, file), 'utf8').includes('-framework')) hostOnly.push(`core: ${file} (links a macOS framework)`) + // The companion tests compile the generated C++ of a gea-companion macOS + // build, which they expect under apple's targets/macos/generated and no + // lane produces; the summary names them until one does. + else if (file.startsWith('run-gea-companion-')) + hostOnly.push(`core: ${file} (needs a gea-companion macOS build's generated C++, which no lane produces)`) else add('core', file, examples, ['bash', join(tests, file)], env) } } @@ -211,8 +227,11 @@ if (wanted('parallel')) { add('parallel', 'test', parallel, ['npm', 'test']) add('parallel', 'refusals', parallel, ['npm', 'run', 'test:refusals']) // Linux and Windows build and run the fixtures on this host; elsewhere the - // runner ships them to its Linux build machine. - add('parallel', 'native', parallel, ['node', 'test/run-native.mjs', ...(process.platform !== 'darwin' ? ['--host=local'] : [])]) + // runner ships them to its Linux build machine, GEA_PARALLEL_HOST or its + // default -- unless that machine cannot be reached, when this host, which + // has the sanitizers the fixtures build with, builds and runs them itself. + const local = process.platform !== 'darwin' || !buildHostReachable(process.env.GEA_PARALLEL_HOST ?? 'dashwin-geastack') + add('parallel', 'native', parallel, ['node', 'test/run-native.mjs', ...(local ? ['--host=local'] : [])]) if (process.platform === 'win32') hostOnly.push('parallel: the ThreadSanitizer builds of test/run-native.mjs (clang has no ThreadSanitizer on Windows)') } @@ -378,13 +397,18 @@ if (wanted('perf')) { // @geastack/parallel workloads (against Rayon and a C++ pool), compared with // the baseline benchmarks/ records for this machine. A machine without one // fails: an unmeasured release has not passed this lane. - const benchmarks = checkout('benchmarks', 'node/release-gate/perf-gate.mjs') if (process.platform === 'linux' && process.arch === 'x64') - last('perf', 'HTTP and parallel performance gate', benchmarks, ['node', 'node/release-gate/perf-gate.mjs', '--fix-loopback'], { - GEA_COMPILER_DIR: compiler, - GEA_NODE_COMPAT_DIR: join(workspace, 'node-compat'), - PARALLEL_ROOT: join(workspace, 'parallel') - }) + last( + 'perf', + 'HTTP and parallel performance gate', + checkout('benchmarks', 'node/release-gate/perf-gate.mjs'), + ['node', 'node/release-gate/perf-gate.mjs', '--fix-loopback'], + { + GEA_COMPILER_DIR: compiler, + GEA_NODE_COMPAT_DIR: join(workspace, 'node-compat'), + PARALLEL_ROOT: join(workspace, 'parallel') + } + ) else hostOnly.push('perf: the HTTP and parallel performance gate (needs Linux on x86-64)') } diff --git a/test/gea-commonjs.mjs b/test/gea-commonjs.mjs index 053395de..98e89a43 100644 --- a/test/gea-commonjs.mjs +++ b/test/gea-commonjs.mjs @@ -7,7 +7,7 @@ import { compile } from '../dist/compiler.js' import { adoptGeaPackage, geaCommonJsGlobals } from '../dist/plugins/gea/host.js' const root = resolve(import.meta.dirname, '..') -const plugin = resolve(root, '../core/packages/geatsc-plugin-gea') +const plugin = resolve(process.env.GEA_CORE_ROOT ?? resolve(root, '../core'), 'packages/geatsc-plugin-gea') adoptGeaPackage(resolve(plugin, 'dist/index.js')) assert.equal(geaCommonJsGlobals().size, 3) const result = compile({ diff --git a/test/gea-host-namespace-members.mjs b/test/gea-host-namespace-members.mjs index f7cae176..548a5a45 100644 --- a/test/gea-host-namespace-members.mjs +++ b/test/gea-host-namespace-members.mjs @@ -6,7 +6,7 @@ import { adoptGeaPackage } from '../dist/plugins/gea/host.js' const root = resolve(import.meta.dirname, '..') const fixture = resolve(root, 'test/fixtures/gea-host-namespace-members') -adoptGeaPackage(resolve(root, '../core/packages/geatsc-plugin-gea/dist/index.js')) +adoptGeaPackage(resolve(process.env.GEA_CORE_ROOT ?? resolve(root, '../core'), 'packages/geatsc-plugin-gea/dist/index.js')) // A namespace path's member call renders the host's free function; the path // itself is never a value, and no logical receiver crosses the call. diff --git a/test/native-rtc.mjs b/test/native-rtc.mjs index d3a31d40..7d40c180 100644 --- a/test/native-rtc.mjs +++ b/test/native-rtc.mjs @@ -11,7 +11,7 @@ const root = resolve(import.meta.dirname, '..') const output = resolve(root, 'measurements/cxx-native-rtc') mkdirSync(output, { recursive: true }) process.env.TMPDIR = output -const core = resolve(root, '../core') +const core = process.env.GEA_CORE_ROOT ?? resolve(root, '../core') adoptGeaPackage(resolve(core, 'packages/geatsc-plugin-gea/dist/index.js')) const flags = [ '-std=c++20',