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:
1 parent
44494c0c83
commit
daf7a0cc43
4 files changed
+56
-27
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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 }) }
|
||||
})
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user