diff --git a/tools/release/public-consumer-smoke.mts b/tools/release/public-consumer-smoke.mts index 2f712db58..13e6f2f33 100644 --- a/tools/release/public-consumer-smoke.mts +++ b/tools/release/public-consumer-smoke.mts @@ -13,9 +13,11 @@ import { } from 'node:fs'; import path from 'node:path'; import process from 'node:process'; +import { EXTENSION_PORTABLE_TARGET } from '../../src/wasix/runtime/tools/wasix-cargo-artifact-contract.mts'; import { DEFAULT_PUBLICATION_LOCK, loadPublicationLock } from './publication-lock.mts'; import { registryRetryDelaySeconds, registryStatusRetryable } from './registry-http-retry.mts'; import { validateRegistryReceiptEvidence } from './registry-integrity.mts'; +import { DESKTOP_TARGETS } from './release-artifact-targets.mts'; import { compareText, loadProducts, ROOT } from './release-graph.mts'; import { validateGithubAttestationReceipt } from './verify_github_release_attestations.mts'; @@ -915,6 +917,28 @@ export function validateNpmResolution(packageLock, carriers, requiredEntryIds, n return { resolved, installedCarrierIds }; } +function npmEntryPlatform(carrier) { + if ( + carrier.target == null || + carrier.target === 'portable' || + carrier.target === EXTENSION_PORTABLE_TARGET + ) + return null; + const target = + { 'darwin-arm64': 'macos-arm64', 'win32-x64-msvc': 'windows-x64-msvc' }[carrier.target] ?? + carrier.target; + const profile = DESKTOP_TARGETS[target]; + if (!profile?.npmOs || !profile.npmCpu) + throw error( + `${carrier.id} has unsupported npm consumer target ${JSON.stringify(carrier.target)}`, + ); + return { + os: profile.npmOs, + cpu: profile.npmCpu, + ...(profile.npmLibc ? { libc: profile.npmLibc } : {}), + }; +} + function npmSurface({ lock, surface, root }, prepare) { const directory = path.join(root, 'npm'); const home = path.join(root, 'npm-home'); @@ -934,13 +958,25 @@ function npmSurface({ lock, surface, root }, prepare) { NPM_CONFIG_USERCONFIG: userConfig, }); const entries = []; + const entryPlatforms = []; const rows = []; const installed = new Set(); for (const [index, entryCarrierId] of surface.entryCarrierIds.entries()) { const carrier = byId.get(entryCarrierId); + const platform = npmEntryPlatform(carrier); + entryPlatforms.push({ entryCarrierId, platform }); const consumer = path.join(directory, `entry-${String(index).padStart(3, '0')}`); if (prepare) { mkdirSync(consumer, { recursive: true }); + // npm honors platform overrides for optional dependencies. The exact + // entry must still be installed below; --ignore-scripts avoids execution. + if (platform !== null) + writeFileSync( + path.join(consumer, '.npmrc'), + Object.entries(platform) + .map(([key, value]) => `${key}=${value}\n`) + .join(''), + ); writeFileSync( path.join(consumer, 'package.json'), `${JSON.stringify( @@ -948,7 +984,9 @@ function npmSurface({ lock, surface, root }, prepare) { name: `oliphaunt-public-consumer-smoke-${String(index).padStart(3, '0')}`, version: '0.0.0', private: true, - dependencies: { [carrier.name]: carrier.version }, + [platform === null ? 'dependencies' : 'optionalDependencies']: { + [carrier.name]: carrier.version, + }, }, null, 2, @@ -965,9 +1003,10 @@ function npmSurface({ lock, surface, root }, prepare) { if (prepare) return env; return { surface: 'npm', - mode: 'anonymous-public-independent-entry-host-install-and-lock-resolution', + mode: 'anonymous-public-independent-entry-platform-install-and-lock-resolution', registry: 'https://registry.npmjs.org', host: `${process.platform}-${process.arch}`, + entryPlatforms, ...resolvedSurfaceCoverage(surface, entries, rows), installedCarrierIds: [...installed].sort(compareText), receiptCoveredNotHostInstalledCarrierIds: surface.carrierIds @@ -1268,6 +1307,13 @@ export function validatePublicConsumerEvidence(evidence, lock, plan) { } } if (surface.ecosystem === 'npm') { + const byId = new Map(lock.carriers.map((carrier) => [carrier.id, carrier])); + const platforms = surface.entryCarrierIds.map((entryCarrierId) => ({ + entryCarrierId, + platform: npmEntryPlatform(byId.get(entryCarrierId)), + })); + if (stableJson(observed.entryPlatforms) !== stableJson(platforms)) + throw error('npm entry platforms differ from the frozen carrier targets'); const installed = sortedUniqueStrings( observed?.installedCarrierIds ?? [], 'npm installedCarrierIds', diff --git a/tools/release/public-consumer-smoke.test.mts b/tools/release/public-consumer-smoke.test.mts index f4913a113..2b076fa60 100644 --- a/tools/release/public-consumer-smoke.test.mts +++ b/tools/release/public-consumer-smoke.test.mts @@ -61,6 +61,18 @@ function graph(products) { }; } +const npmPlatformFixtures = [ + { target: null, platform: null }, + { target: 'portable', platform: null }, + { target: 'linux-arm64-gnu', platform: { os: 'linux', cpu: 'arm64', libc: 'glibc' } }, + { target: 'linux-x64-gnu', platform: { os: 'linux', cpu: 'x64', libc: 'glibc' } }, + { target: 'macos-arm64', platform: { os: 'darwin', cpu: 'arm64' } }, + { target: 'darwin-arm64', platform: { os: 'darwin', cpu: 'arm64' } }, + { target: 'windows-x64-msvc', platform: { os: 'win32', cpu: 'x64' } }, + { target: 'win32-x64-msvc', platform: { os: 'win32', cpu: 'x64' } }, + { target: 'wasix-portable', platform: null }, +]; + const [fixtureMode, fixtureRoot, scenario] = process.argv.slice(2); if (fixtureMode === 'prepare-cargo') { const environment = publicCargoEnvironment(fixtureRoot, { @@ -94,7 +106,17 @@ if (fixtureMode === 'prepare-cargo') { } if (fixtureMode === 'prepare-npm') { const products = [product('sdk', ['npm'])]; - const frozen = lock(products, [carrier('npm:@example/sdk', 'sdk', 0)]); + const carriers = + scenario === 'platforms' + ? npmPlatformFixtures.map(({ target }, index) => ({ + ...carrier(`npm:@example/platform-${index}`, 'sdk', index), + target, + })) + : scenario === 'missing-entry' + ? [{ ...carrier('npm:@example/platform-5', 'sdk', 0), target: 'darwin-arm64' }] + : [carrier('npm:@example/sdk', 'sdk', 0)]; + if (scenario === 'unknown-platform') carriers[0].target = 'unknown-platform'; + const frozen = lock(products, carriers); writeFileSync( path.join(fixtureRoot, 'context.json'), JSON.stringify({ @@ -107,9 +129,23 @@ if (fixtureMode === 'prepare-npm') { } if (fixtureMode === 'install-npm') { const manifest = JSON.parse(readFileSync('package.json', 'utf8')); - const [[name, version]] = Object.entries(manifest.dependencies); - mkdirSync(`node_modules/${name}`, { recursive: true }); - writeFileSync(`node_modules/${name}/package.json`, JSON.stringify({ name, version })); + const [[name, version]] = Object.entries(manifest.dependencies ?? manifest.optionalDependencies); + if (name.startsWith('@example/platform-')) { + const { platform } = npmPlatformFixtures[Number(name.split('-').at(-1))]; + assert.equal(manifest.optionalDependencies !== undefined, platform !== null); + if (platform === null) assert.equal(existsSync('.npmrc'), false); + else + assert.equal( + readFileSync('.npmrc', 'utf8'), + Object.entries(platform) + .map(([key, value]) => `${key}=${value}\n`) + .join(''), + ); + } + if (!process.env.OMIT_PUBLIC_PROBE) { + mkdirSync(`node_modules/${name}`, { recursive: true }); + writeFileSync(`node_modules/${name}/package.json`, JSON.stringify({ name, version })); + } writeFileSync( 'package-lock.json', JSON.stringify({ @@ -126,12 +162,43 @@ if (fixtureMode === 'install-npm') { process.exit(0); } if (fixtureMode === 'assert-npm') { - if (scenario === 'success') { + if (scenario === 'success' || scenario === 'platforms') { const result = JSON.parse(readFileSync(path.join(fixtureRoot, 'npm.json'), 'utf8')); - assert.deepEqual(result.installedCarrierIds, ['npm:@example/sdk']); + const context = JSON.parse(readFileSync(path.join(fixtureRoot, 'context.json'), 'utf8')); + const ids = context.lock.carriers.map(({ id }) => id); + assert.equal( + result.mode, + 'anonymous-public-independent-entry-platform-install-and-lock-resolution', + ); + assert.deepEqual(result.installedCarrierIds, ids); assert.deepEqual( result.resolved.map(({ id }) => id), - ['npm:@example/sdk'], + ids, + ); + assert.deepEqual( + result.entryPlatforms, + ids.map((entryCarrierId, index) => ({ + entryCarrierId, + platform: scenario === 'platforms' ? npmPlatformFixtures[index].platform : null, + })), + ); + const evidence = publicConsumerEvidence({ + ...context, + registryReceiptSha256: 'e'.repeat(64), + githubReceiptDigest: 'f'.repeat(64), + surfaces: [ + result, + { surface: 'github', productTags: context.plan.github.productTags, swift: null }, + ], + }); + validatePublicConsumerEvidence(evidence, context.lock, context.plan); + evidence.surfaces.find(({ surface }) => surface === 'npm').entryPlatforms[0].platform = { + os: 'linux', + cpu: 'x64', + }; + assert.throws( + () => validatePublicConsumerEvidence(evidence, context.lock, context.plan), + /npm entry platforms differ from the frozen carrier targets/u, ); } else assert.equal(existsSync(path.join(fixtureRoot, 'npm.json')), false); process.exit(0); @@ -667,10 +734,11 @@ test('builds canonical lock/receipt-bound evidence and writes it immutably', () const surfaces = [ { surface: 'npm', - mode: 'anonymous-public-independent-entry-host-install-and-lock-resolution', + mode: 'anonymous-public-independent-entry-platform-install-and-lock-resolution', carrierIds: ['npm:@example/alpha'], dependencyScopes: ['optional', 'peer', 'runtime'], entryCarrierIds: ['npm:@example/alpha'], + entryPlatforms: [{ entryCarrierId: 'npm:@example/alpha', platform: null }], plannedEntryClosures: [ { entryCarrierId: 'npm:@example/alpha', carrierIds: ['npm:@example/alpha'] }, ], @@ -735,10 +803,11 @@ test('cannot silently relabel a frozen entry dependency as receipt-only', () => surfaces: [ { surface: 'npm', - mode: 'anonymous-public-independent-entry-host-install-and-lock-resolution', + mode: 'anonymous-public-independent-entry-platform-install-and-lock-resolution', carrierIds: ['npm:@example/alpha', 'npm:@example/leaf'], dependencyScopes: ['optional', 'peer', 'runtime'], entryCarrierIds: ['npm:@example/alpha'], + entryPlatforms: [{ entryCarrierId: 'npm:@example/alpha', platform: null }], plannedEntryClosures: [ { entryCarrierId: 'npm:@example/alpha', diff --git a/tools/release/public-consumer-smoke.test.sh b/tools/release/public-consumer-smoke.test.sh index a703b46ac..d9c8f9342 100644 --- a/tools/release/public-consumer-smoke.test.sh +++ b/tools/release/public-consumer-smoke.test.sh @@ -41,21 +41,25 @@ SH chmod +x "$scratch/bin/npm" export PATH="$scratch/bin:$PATH" SENSITIVE_TOKEN=must-not-survive CARGO_REGISTRY_TOKEN=must-not-survive export PUBLIC_PROBE_COUNTER="$scratch/attempts" PUBLIC_PROBE_FIXTURE="$source_root/tools/release/public-consumer-smoke.test.mts" -for mode in success fail timeout expired; do +for mode in success platforms unknown-platform missing-entry fail timeout expired; do mkdir "$scratch/$mode" bun tools/release/public-consumer-smoke.test.mts prepare-npm "$scratch/$mode" "$mode" - unset FAIL_PUBLIC_PROBE HANG_PUBLIC_PROBE PUBLIC_PROBE_CHILD + unset FAIL_PUBLIC_PROBE HANG_PUBLIC_PROBE OMIT_PUBLIC_PROBE PUBLIC_PROBE_CHILD expected=0 if [[ "$mode" == expired ]]; then expected=1; fi + if [[ "$mode" == unknown-platform ]]; then expected=1; fi + if [[ "$mode" == missing-entry ]]; then export OMIT_PUBLIC_PROBE=1; expected=1; fi if [[ "$mode" == fail ]]; then export FAIL_PUBLIC_PROBE=1; expected=7; fi if [[ "$mode" == timeout ]]; then export HANG_PUBLIC_PROBE=1 PUBLIC_PROBE_CHILD="$scratch/child-pid"; expected=124; fi status=0 bash tools/release/public-consumer-smoke.sh --surface "$scratch/$mode" npm > "$scratch/$mode/output" 2>&1 || status=$? if [[ "$status" != "$expected" ]]; then cat "$scratch/$mode/output" >&2; exit 1; fi if [[ "$mode" == expired ]]; then grep -q "shared public-consumer deadline reached" "$scratch/$mode/output"; fi + if [[ "$mode" == unknown-platform ]]; then grep -q "unsupported npm consumer target" "$scratch/$mode/output"; fi + if [[ "$mode" == missing-entry ]]; then grep -q "entry package was not installed from the public registry" "$scratch/$mode/output"; fi bun tools/release/public-consumer-smoke.test.mts assert-npm "$scratch/$mode" "$mode" done -[[ "$(wc -l < "$scratch/attempts")" -eq 3 ]] +[[ "$(wc -l < "$scratch/attempts")" -eq 13 ]] pid="$(cat "$scratch/child-pid")" status=0 state="$(ps -o stat= -p "$pid")" || status=$?