refactor(scripts): share npm os/cpu matching for pnpm pruning

Move the release verifier's npm platform check into scripts/npm-platform.ts and use it when pruning pnpm's bundled packages, so negations, 'any' and invalid fields follow npm. Skip non-directories and name the manifest on invalid fields.
This commit is contained in:
Turtle committed 2026-10-26 03:25:12 +08:00
1 parent 44494c0c83
commit daf7a0cc43
4 files changed
+56 -27

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
/** npm `os`/`cpu` package-field matching for build and release scripts. */
/**
* Test whether an npm `os` or `cpu` field admits a value, following npm's install checks.
* @param value - Raw manifest field; `undefined` admits every value.
* @param current - Target platform or architecture.
* @returns Whether the package may install for the target.
* @throws When the field is not an array of strings.
*/
export function acceptsNpmPlatform(value: unknown, current: string): boolean {
if (value === undefined) return true
if (!Array.isArray(value) || !value.every((tag): tag is string => typeof tag === 'string')) {
throw new Error('npm platform constraints must be arrays of strings')
}
let accepted = value.every(tag => tag.startsWith('!'))
for (const tag of value) {
if (tag === `!${current}`) return false
if (tag === current || (tag === 'any' && value.length === 1)) accepted = true
}
return accepted
}
+20 -5
View File
@@ -168,25 +168,40 @@ it('keeps carrier pnpm versions aligned with the shared payload build', () => {
expect(root.packageManager).toBe(`pnpm@${root.devDependencies.pnpm}`)
})
it('keeps only pnpm-bundled native packages that the target platform and architecture admit', async () => {
it.each([
['darwin', 'arm64', ['reflink', 'reflink-darwin-arm64'], ['.bin', '@reflink', 'any-cpu', 'not-windows', 'semver']],
['darwin', 'x64', ['reflink', 'reflink-darwin-x64'], ['.bin', '@reflink', 'any-cpu', 'not-windows', 'semver']],
['win32', 'x64', ['reflink', 'reflink-win32-x64-msvc'], ['.bin', '@reflink', 'any-cpu', 'not-darwin', 'semver']],
] as const)('keeps only pnpm-bundled packages that %s/%s admits', async (platform, arch, reflink, top) => {
const root = await mkdtemp(join(tmpdir(), 'primary-pnpm-natives-'))
try {
const modules = join(root, 'dist', 'node_modules')
const packages: Record<string, { os?: string[]; cpu?: string[] }> = {
'semver': {}, '@reflink/reflink': {},
'semver': {}, '@reflink/reflink': {}, 'any-cpu': { cpu: ['any'] },
'@reflink/reflink-darwin-arm64': { os: ['darwin'], cpu: ['arm64'] },
'@reflink/reflink-darwin-x64': { os: ['darwin'], cpu: ['x64'] },
'@reflink/reflink-win32-x64-msvc': { os: ['win32'], cpu: ['x64'] },
'not-windows': { os: ['!win32'] },
'not-darwin': { os: ['!darwin'] },
'contradictory': { os: ['darwin', 'win32', '!darwin', '!win32'] },
}
for (const [name, fields] of Object.entries(packages)) {
await mkdir(join(modules, name), { recursive: true })
await writeFile(join(modules, name, 'package.json'), JSON.stringify({ name, ...fields }))
}
await mkdir(join(modules, '.bin'))
prunePnpmForeignNativePackages(root, 'darwin', 'arm64')
expect((await readdir(modules)).sort()).toEqual(['.bin', '@reflink', 'not-windows', 'semver'])
expect((await readdir(join(modules, '@reflink'))).sort()).toEqual(['reflink', 'reflink-darwin-arm64'])
await writeFile(join(modules, '.modules.yaml'), '')
prunePnpmForeignNativePackages(root, platform, arch)
expect((await readdir(modules)).filter(name => name !== '.modules.yaml').sort()).toEqual(top)
expect((await readdir(join(modules, '@reflink'))).sort()).toEqual(reflink)
} finally { await rm(root, { recursive: true, force: true }) }
})
it('names the bundled manifest whose platform field is invalid', async () => {
const root = await mkdtemp(join(tmpdir(), 'primary-pnpm-invalid-'))
try {
await mkdir(join(root, 'dist', 'node_modules', 'invalid'), { recursive: true })
await writeFile(join(root, 'dist', 'node_modules', 'invalid', 'package.json'), JSON.stringify({ os: 'darwin' }))
expect(() => { prunePnpmForeignNativePackages(root, 'darwin', 'arm64') }).toThrow(/invalid.package\.json: npm platform constraints must be arrays of strings/u)
} finally { await rm(root, { recursive: true, force: true }) }
})
+13 -8
View File
@@ -6,11 +6,12 @@ import { cpSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, r
import { cp } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { basename, dirname, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import extractZip from 'extract-zip'
import { x as extractTar } from 'tar'
import { copyPrimaryRuntime, parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
import { acceptsNpmPlatform } from '../npm-platform.ts'
import lock from './lock.json' with { type: 'json' }
import { prunePrimaryRuntimePythonTests } from './prune-python-tests.ts'
@@ -149,21 +150,25 @@ export async function unpackPrimaryRuntimeWheel(archive: string, destination: st
}
/**
* Remove packages bundled under pnpm's dist/node_modules whose npm `os`/`cpu` fields exclude the target.
* Remove packages bundled directly under pnpm's dist/node_modules whose npm `os`/`cpu` fields exclude the target.
* The pnpm tarball carries every platform's optional native packages, such as `@reflink/reflink-darwin-x64`.
* Nested node_modules and the `libc` field are not inspected; pnpm 11 bundles neither nested native packages nor Linux variants.
* @param pnpm - Unpacked pnpm package directory.
* @param platform - Target Node.js platform.
* @param arch - Target Node.js architecture.
* @throws When a bundled package manifest is missing or has invalid `os`/`cpu` fields.
*/
export function prunePnpmForeignNativePackages(pnpm: string, platform: NodeJS.Platform, arch: NodeJS.Architecture): void {
const modules = join(pnpm, 'dist', 'node_modules')
const admits = (values: string[] | undefined, value: string): boolean => values === undefined
|| (values.some(entry => !entry.startsWith('!')) ? values.includes(value) : !values.includes(`!${value}`))
for (const name of readdirSync(modules).filter(entry => entry !== '.bin')) {
for (const directory of name.startsWith('@') ? readdirSync(join(modules, name)).map(child => join(modules, name, child)) : [join(modules, name)]) {
const { os, cpu } = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as { os?: string[]; cpu?: string[] }
if (!admits(os, platform) || !admits(cpu, arch)) rmSync(directory, { recursive: true })
const directories = (parent: string): string[] => readdirSync(parent, { withFileTypes: true })
.filter(entry => entry.isDirectory() && entry.name !== '.bin').map(entry => join(parent, entry.name))
for (const directory of directories(modules).flatMap(entry => basename(entry).startsWith('@') ? directories(entry) : [entry])) {
const { os, cpu } = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as { os?: unknown; cpu?: unknown }
let admitted: boolean
try { admitted = acceptsNpmPlatform(os, platform) && acceptsNpmPlatform(cpu, arch) } catch (error) {
throw new Error(`primary runtime: ${directory}/package.json: ${(error as Error).message}`, { cause: error })
}
if (!admitted) rmSync(directory, { recursive: true })
}
}
+2 -14
View File
@@ -22,6 +22,7 @@ import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { parseArgs } from 'node:util'
import { acceptsNpmPlatform } from '../npm-platform.ts'
import { releaseFamily } from './families.ts'
import { capture, isEntry } from './process.ts'
import { packedManifest } from './tarball.ts'
@@ -45,19 +46,6 @@ function consumerEnvironment(consumerRoot: string): NodeJS.ProcessEnv {
return environment
}
function acceptsPlatform(value: unknown, current: string): boolean {
if (value === undefined) return true
if (!Array.isArray(value) || !value.every((tag): tag is string => typeof tag === 'string')) {
throw new Error('packed platform constraints must be arrays of strings')
}
let accepted = value.every(tag => tag.startsWith('!'))
for (const tag of value) {
if (tag === `!${current}`) return false
if (tag === current || (tag === 'any' && value.length === 1)) accepted = true
}
return accepted
}
/**
* Packed tarballs and their compatibility with the verification host, as `file:` dependency entries.
*
@@ -77,7 +65,7 @@ function packedDependencies(directories: readonly string[]): Map<string, { url:
const manifest = packedManifest(tarball)
const { name, version } = manifest
if (typeof name !== 'string' || typeof version !== 'string') throw new Error(`${tarball} manifest lacks name/version`)
const compatible = acceptsPlatform(manifest.os, process.platform) && acceptsPlatform(manifest.cpu, process.arch)
const compatible = acceptsNpmPlatform(manifest.os, process.platform) && acceptsNpmPlatform(manifest.cpu, process.arch)
dependencies.set(name, { url: pathToFileURL(tarball).href, version, compatible })
}
}