fix: infer missing platform fields of optional dependencies from the package name (#12312)

* fix: infer missing platform fields of optional deps from the package name

Some registries strip the os/cpu/libc fields (or just libc) from the
version objects of the packuments they serve. Resolution then saw every
platform-specific optional dependency as platform-unrestricted, so pnpm
downloaded and installed the binaries of every platform regardless of
supportedArchitectures, and wrote lockfile entries without the platform
fields, which broke installs from that lockfile on every machine.

Platform-specific binary packages encode their platform in the package
name (e.g. @nx/nx-win32-arm64-msvc), so packageIsInstallable now fills
the missing platform fields of an optional dependency from the name's
tokens. Since every install path decides installability through that
check before fetching, foreign-platform binaries are skipped without
even downloading them, in fresh resolution and in headless installs
with both node linkers alike. A package that declares no platform
fields at all is treated as platform-specific only when an operating
system is recognized in its name, so a generic name segment (such as
'arm' on its own) never gets a package skipped.

Fixes https://github.com/pnpm/pnpm/issues/11702
Fixes https://github.com/pnpm/pnpm/issues/9940

* chore: add platform name tokens to the cspell dictionary

* fix(package-is-installable): infer missing platform fields of optional deps from the package name

Port of pnpm commit https://github.com/pnpm/pnpm/commit/34875b2d7c
(PR https://github.com/pnpm/pnpm/pull/12312). Some registries strip
the os/cpu/libc fields (or just libc) from the version objects of the
packuments they serve, and lockfile entries written from such
metadata lack the fields too, so every platform's binaries were
installed regardless of supportedArchitectures.

Platform-specific binary packages encode their platform in the
package name (e.g. @nx/nx-win32-arm64-msvc), so the installability
check now fills the missing platform fields of an optional dependency
from the name's tokens: infer_platform_from_package_name +
inferred_platform in pacquet-package-is-installable, applied inside
package_is_installable (hoisted linker) and in
compute_skipped_snapshots (isolated linker, with the check cache
keyed by the snapshot's optional flag since the verdicts can differ).
The any_installability_constraint fast path now also considers
optional snapshots whose names infer a platform their metadata row
does not declare, so the inference is reachable on lockfiles without
any declared constraint.

Same guard rails as upstream: declared fields always win (each field
is filled only when missing — a missing libc alone is inferred,
disambiguating -gnu vs -musl), and a package declaring no platform
fields at all engages the inference only when an operating-system
token is recognized in its name, so a generic name segment such as
'arm' on its own never gets a package skipped.

Fixes https://github.com/pnpm/pnpm/issues/11702
Fixes https://github.com/pnpm/pnpm/issues/9940

* test: shut the metadata-stripping proxy down cleanly and forward the request method
This commit is contained in:
Zoltan Kochan
2026-06-10 21:51:40 +02:00
committed by GitHub
parent d976edf4ec
commit 52be454d57
18 changed files with 923 additions and 28 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/config.package-is-installable": patch
"pnpm": patch
---
Platform-specific optional dependencies are now skipped even when their `os`/`cpu`/`libc` fields are missing from the registry metadata or the lockfile. Some registries strip these fields from the package metadata, which made pnpm download and install the binaries of every platform regardless of `supportedArchitectures`. The missing platform fields of an optional dependency are now inferred from its name (e.g. `@nx/nx-win32-arm64-msvc``os: win32`, `cpu: arm64`), so foreign-platform binaries are skipped without even downloading them [#11702](https://github.com/pnpm/pnpm/issues/11702).

View File

@@ -7,9 +7,11 @@ import type { SupportedArchitectures } from '@pnpm/types'
import { checkEngine, UnsupportedEngineError, type WantedEngine } from './checkEngine.js'
import { checkPlatform, UnsupportedPlatformError } from './checkPlatform.js'
import { inferPlatformFromPackageName } from './inferPlatformFromPackageName.js'
export type { Engine } from './checkEngine.js'
export type { Platform, WantedPlatform } from './checkPlatform.js'
export { inferPlatformFromPackageName } from './inferPlatformFromPackageName.js'
export {
UnsupportedEngineError,
@@ -36,7 +38,7 @@ export function packageIsInstallable (
supportedArchitectures?: SupportedArchitectures
}
): boolean | null {
const warn = checkPackage(pkgId, pkg, options)
const warn = checkPackage(pkgId, { engines: pkg.engines, ...effectivePlatform(pkg, options.optional) }, options)
if (warn == null) return true
@@ -65,6 +67,36 @@ export function packageIsInstallable (
return null
}
interface PlatformFields {
cpu?: string[]
os?: string[]
libc?: string[]
}
/**
* The platform fields of an optional dependency may be incomplete: some
* registries strip os/cpu/libc (or just libc) from the metadata they serve,
* and lockfile entries written from such metadata lack them too. For a
* platform-specific binary the package name carries the same information, so
* each missing field is filled from the name's tokens. A package that
* declares no platform fields at all is treated as platform-specific only
* when an operating system is recognized in its name — a generic name
* segment (e.g. `arm` on its own) never marks it as such.
* https://github.com/pnpm/pnpm/issues/11702
*/
function effectivePlatform (pkg: PlatformFields & { name: string }, optional: boolean): PlatformFields {
if (!optional || (pkg.os != null && pkg.cpu != null && pkg.libc != null)) return pkg
const inferred = inferPlatformFromPackageName(pkg.name)
if (inferred == null) return pkg
const pkgDeclaresPlatform = pkg.os != null || pkg.cpu != null || pkg.libc != null
if (!pkgDeclaresPlatform && inferred.os == null) return pkg
return {
os: pkg.os ?? inferred.os,
cpu: pkg.cpu ?? inferred.cpu,
libc: pkg.libc ?? inferred.libc,
}
}
export function checkPackage (
pkgId: string,
manifest: {

View File

@@ -0,0 +1,79 @@
const OS_BY_TOKEN = new Map([
['aix', 'aix'],
['android', 'android'],
['darwin', 'darwin'],
['macos', 'darwin'],
['osx', 'darwin'],
['freebsd', 'freebsd'],
['linux', 'linux'],
['netbsd', 'netbsd'],
['openbsd', 'openbsd'],
['openharmony', 'openharmony'],
['sunos', 'sunos'],
['win32', 'win32'],
['windows', 'win32'],
])
const CPU_BY_TOKEN = new Map([
['arm', 'arm'],
['armv6', 'arm'],
['armv7', 'arm'],
['arm64', 'arm64'],
['aarch64', 'arm64'],
['ia32', 'ia32'],
['loong64', 'loong64'],
['mips64el', 'mips64el'],
['ppc64', 'ppc64'],
['ppc64le', 'ppc64'],
['riscv64', 'riscv64'],
['s390x', 's390x'],
['x64', 'x64'],
['amd64', 'x64'],
['wasm32', 'wasm32'],
])
const LIBC_BY_TOKEN = new Map([
['glibc', 'glibc'],
['gnu', 'glibc'],
['gnueabihf', 'glibc'],
['musl', 'musl'],
['musleabihf', 'musl'],
])
export interface PlatformInferredFromPackageName {
os?: string[]
cpu?: string[]
libc?: string[]
}
/**
* Infers the supported platforms of a package from the tokens of its name,
* e.g. `@nx/nx-win32-arm64-msvc` → `{ os: ['win32'], cpu: ['arm64'] }`.
* Platform-specific binary packages follow this naming convention, which is
* the only platform signal left when their os/cpu/libc manifest fields are
* absent. Returns null when no platform token is recognized in the name.
*/
export function inferPlatformFromPackageName (name: string): PlatformInferredFromPackageName | null {
const nameWithoutScope = name.includes('/') ? name.slice(name.indexOf('/') + 1) : name
const tokens = nameWithoutScope.toLowerCase().split(/[-_.]/)
const os = pickTokenValues(tokens, OS_BY_TOKEN)
const cpu = pickTokenValues(tokens, CPU_BY_TOKEN)
const libc = pickTokenValues(tokens, LIBC_BY_TOKEN)
if (os == null && cpu == null && libc == null) return null
return {
...(os != null ? { os } : {}),
...(cpu != null ? { cpu } : {}),
...(libc != null ? { libc } : {}),
}
}
function pickTokenValues (tokens: string[], valueByToken: Map<string, string>): string[] | undefined {
const values = new Set<string>()
for (const token of tokens) {
const value = valueByToken.get(token)
if (value != null) {
values.add(value)
}
}
return values.size > 0 ? Array.from(values) : undefined
}

View File

@@ -0,0 +1,112 @@
import { expect, jest, test } from '@jest/globals'
import type * as DetectLibc from 'detect-libc'
jest.mock('detect-libc', () => {
const original = jest.requireActual<typeof DetectLibc>('detect-libc')
return {
...original,
familySync: () => 'glibc',
}
})
const { inferPlatformFromPackageName } = await import('../lib/inferPlatformFromPackageName.js')
const { packageIsInstallable } = await import('../lib/index.js')
test.each([
['@nx/nx-win32-arm64-msvc', { os: ['win32'], cpu: ['arm64'] }],
['@nx/nx-linux-arm-gnueabihf', { os: ['linux'], cpu: ['arm'], libc: ['glibc'] }],
['@nx/nx-linux-x64-gnu', { os: ['linux'], cpu: ['x64'], libc: ['glibc'] }],
['@esbuild/aix-ppc64', { os: ['aix'], cpu: ['ppc64'] }],
['@esbuild/openharmony-arm64', { os: ['openharmony'], cpu: ['arm64'] }],
['@biomejs/cli-linux-x64-musl', { os: ['linux'], cpu: ['x64'], libc: ['musl'] }],
['@typescript/native-preview-darwin-arm64', { os: ['darwin'], cpu: ['arm64'] }],
['turbo-windows-64', { os: ['win32'] }],
['esbuild-darwin-64', { os: ['darwin'] }],
['bun-linux-aarch64', { os: ['linux'], cpu: ['arm64'] }],
['sharp-linux-armv7', { os: ['linux'], cpu: ['arm'] }],
['is-arm', { cpu: ['arm'] }],
['fsevents', null],
['lodash', null],
['@pnpm.e2e/not-compatible-with-any-os', null],
])('inferPlatformFromPackageName(%s)', (name, inferred) => {
expect(inferPlatformFromPackageName(name)).toStrictEqual(inferred)
})
test('an optional dependency without platform fields is not installable when its name declares an unsupported platform', () => {
expect(packageIsInstallable('@nx/nx-win32-arm64-msvc@1.0.0', {
name: '@nx/nx-win32-arm64-msvc',
version: '1.0.0',
}, {
optional: true,
lockfileDir: process.cwd(),
supportedArchitectures: { os: ['linux'], cpu: ['x64'] },
})).toBe(false)
})
test('a missing libc field is taken from the package name even when the other platform fields are declared', () => {
const options = {
optional: true,
lockfileDir: process.cwd(),
supportedArchitectures: { os: ['linux'], cpu: ['x64'], libc: ['glibc'] },
}
expect(packageIsInstallable('@nx/nx-linux-x64-musl@1.0.0', {
name: '@nx/nx-linux-x64-musl',
version: '1.0.0',
os: ['linux'],
cpu: ['x64'],
}, options)).toBe(false)
expect(packageIsInstallable('@nx/nx-linux-x64-gnu@1.0.0', {
name: '@nx/nx-linux-x64-gnu',
version: '1.0.0',
os: ['linux'],
cpu: ['x64'],
}, options)).toBe(true)
})
test('a missing cpu field is taken from the name of a package that declares its platform', () => {
expect(packageIsInstallable('@pnpm.e2e/some-pkg-arm64@1.0.0', {
name: '@pnpm.e2e/some-pkg-arm64',
version: '1.0.0',
os: ['linux'],
}, {
optional: true,
lockfileDir: process.cwd(),
supportedArchitectures: { os: ['linux'], cpu: ['x64'] },
})).toBe(false)
})
test('the platform fields of the manifest take precedence over the package name', () => {
expect(packageIsInstallable('@pnpm.e2e/win32-binary@1.0.0', {
name: '@pnpm.e2e/win32-binary',
version: '1.0.0',
os: ['linux'],
cpu: ['x64'],
libc: ['glibc'],
}, {
optional: true,
lockfileDir: process.cwd(),
supportedArchitectures: { os: ['linux'], cpu: ['x64'], libc: ['glibc'] },
})).toBe(true)
})
test('a package without any declared platform field is not skipped when its name has no operating system token', () => {
expect(packageIsInstallable('is-arm@1.0.0', {
name: 'is-arm',
version: '1.0.0',
}, {
optional: true,
lockfileDir: process.cwd(),
supportedArchitectures: { os: ['linux'], cpu: ['x64'] },
})).toBe(true)
})
test('the platform is not inferred from the name of a non-optional dependency', () => {
expect(packageIsInstallable('@nx/nx-win32-arm64-msvc@1.0.0', {
name: '@nx/nx-win32-arm64-msvc',
version: '1.0.0',
}, {
optional: false,
lockfileDir: process.cwd(),
supportedArchitectures: { os: ['linux'], cpu: ['x64'] },
})).toBe(true)
})

View File

@@ -123,6 +123,7 @@
"gitea",
"globalconfig",
"globstar",
"gnueabihf",
"gpgsign",
"grault",
"gruntfile",
@@ -178,6 +179,7 @@
"logstream",
"longlink",
"longpaths",
"loong",
"luca",
"martensson",
"maxtimeout",
@@ -195,6 +197,7 @@
"msgpackr",
"msvc",
"msys",
"musleabihf",
"mycomp",
"mycompany",
"myorg",
@@ -223,6 +226,7 @@
"ofjergrg",
"onclickoutside",
"oomol",
"openharmony",
"openpgp",
"ossl",
"outfile",
@@ -309,6 +313,7 @@
"rescopes",
"rescoping",
"rimrafed",
"riscv",
"rmgr",
"rpmdevtools",
"rpmlint",

View File

@@ -1,7 +1,10 @@
import fs from 'node:fs'
import http from 'node:http'
import type { AddressInfo } from 'node:net'
import path from 'node:path'
import { describe, expect, jest, test } from '@jest/globals'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import {
addDependenciesToPackage,
install,
@@ -11,10 +14,12 @@ import {
} from '@pnpm/installing.deps-installer'
import type { LockfileFile } from '@pnpm/lockfile.fs'
import { prepareEmpty, preparePackages } from '@pnpm/prepare'
import { REGISTRY_MOCK_PORT } from '@pnpm/testing.registry-mock'
import type { ProjectRootDir } from '@pnpm/types'
import { rimrafSync } from '@zkochan/rimraf'
import deepRequireCwd from 'deep-require-cwd'
import { readYamlFileSync } from 'read-yaml-file'
import { writeYamlFileSync } from 'write-yaml-file'
import { testDefaults } from '../utils/index.js'
@@ -140,6 +145,80 @@ test('skip optional dependency that does not support the current OS', async () =
}
})
// Test case for https://github.com/pnpm/pnpm/issues/11702
test('skip optional dependencies whose names declare unsupported platforms when the registry metadata has no platform fields', async () => {
const project = prepareEmpty()
const server = createMetadataStrippingRegistryProxy()
await new Promise<void>((resolve) => {
server.listen(0, resolve)
})
const registryProxy = `http://localhost:${(server.address() as AddressInfo).port}/`
try {
await install({
dependencies: {
'@pnpm.e2e/has-many-optional-deps': '1.0.0',
},
}, testDefaults({
registries: { default: registryProxy },
supportedArchitectures: { os: ['darwin'], cpu: ['arm64'] },
}))
} finally {
server.closeAllConnections()
await new Promise<void>((resolve, reject) => {
server.close((err) => {
if (err == null) {
resolve()
} else {
reject(err)
}
})
})
}
expect(deepRequireCwd(['@pnpm.e2e/has-many-optional-deps', '@pnpm.e2e/darwin-arm64', './package.json']).version).toBe('1.0.0')
// The platforms of the other binaries are inferred from their names, so
// they are skipped without even downloading their tarballs.
project.storeHasNot('@pnpm.e2e/linux-x64', '1.0.0')
project.storeHasNot('@pnpm.e2e/windows-x64', '1.0.0')
expect(fs.existsSync(path.resolve('node_modules/.pnpm/@pnpm.e2e+linux-x64@1.0.0'))).toBeFalsy()
expect(fs.existsSync(path.resolve('node_modules/.pnpm/@pnpm.e2e+windows-x64@1.0.0'))).toBeFalsy()
const modulesInfo = readYamlFileSync<{ skipped: string[] }>(path.join('node_modules', '.modules.yaml'))
expect(modulesInfo.skipped).toContain('@pnpm.e2e/linux-x64@1.0.0')
expect(modulesInfo.skipped).toContain('@pnpm.e2e/windows-x64@1.0.0')
})
// Simulates a registry that strips os/cpu/libc from packument version objects
// (some registry proxies do this), forwarding everything else to the registry mock.
function createMetadataStrippingRegistryProxy (): http.Server {
return http.createServer((req, res) => {
(async () => {
const upstream = await fetch(`http://localhost:${REGISTRY_MOCK_PORT}${req.url}`, {
method: req.method,
headers: { accept: req.headers.accept ?? '*/*' },
})
const contentType = upstream.headers.get('content-type') ?? ''
if (contentType.includes('json')) {
const doc = await upstream.json() as { versions?: Record<string, Record<string, unknown>> }
for (const versionMeta of Object.values(doc.versions ?? {})) {
delete versionMeta.os
delete versionMeta.cpu
delete versionMeta.libc
}
res.writeHead(upstream.status, { 'content-type': 'application/json' })
res.end(JSON.stringify(doc))
} else {
res.writeHead(upstream.status, { 'content-type': contentType })
res.end(Buffer.from(await upstream.arrayBuffer()))
}
})().catch((err) => {
res.writeHead(500)
res.end(String(err))
})
})
}
test('skip optional dependency that does not support the current Node version', async () => {
const project = prepareEmpty()
const reporter = jest.fn()
@@ -615,6 +694,43 @@ describe('supported architectures', () => {
})
expect(deepRequireCwd(['@pnpm.e2e/has-many-optional-deps', '@pnpm.e2e/linux-x64', './package.json']).version).toBe('1.0.0')
})
// Test case for https://github.com/pnpm/pnpm/issues/11702
test.each(['isolated', 'hoisted'])('skip optional dependencies that do not support the target architecture when their lockfile entries have no platform fields (nodeLinker=%s)', async (nodeLinker) => {
const project = prepareEmpty()
const supportedArchitectures = { os: ['darwin'], cpu: ['arm64'] }
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/has-many-optional-deps@1.0.0'], {
...testDefaults({ nodeLinker }),
supportedArchitectures,
})
// Simulate a lockfile resolved from registry metadata that lacks
// the platform fields.
const lockfile = project.readLockfile()
for (const pkgSnapshot of Object.values(lockfile.packages)) {
delete pkgSnapshot.os
delete pkgSnapshot.cpu
delete pkgSnapshot.libc
}
writeYamlFileSync(WANTED_LOCKFILE, lockfile, { lineWidth: 1000 })
rimrafSync('node_modules')
await install(manifest, {
...testDefaults({ nodeLinker }),
frozenLockfile: true,
supportedArchitectures,
})
if (nodeLinker === 'hoisted') {
expect(fs.existsSync(path.resolve('node_modules/@pnpm.e2e/darwin-arm64'))).toBeTruthy()
expect(fs.existsSync(path.resolve('node_modules/@pnpm.e2e/darwin-x64'))).toBeFalsy()
expect(fs.existsSync(path.resolve('node_modules/@pnpm.e2e/linux-x64'))).toBeFalsy()
} else {
expect(deepRequireCwd(['@pnpm.e2e/has-many-optional-deps', '@pnpm.e2e/darwin-arm64', './package.json']).version).toBe('1.0.0')
expect(fs.existsSync(path.resolve('node_modules/.pnpm/@pnpm.e2e+darwin-x64@1.0.0'))).toBeFalsy()
expect(fs.existsSync(path.resolve('node_modules/.pnpm/@pnpm.e2e+linux-x64@1.0.0'))).toBeFalsy()
}
})
test('remove optional dependencies that are not used', async () => {
prepareEmpty()
const opts = testDefaults({ modulesCacheMaxAge: 0 })

View File

@@ -931,6 +931,43 @@ test('do not fetch an optional package that is not installable', async () => {
expect(pkgResponse.fetching).toBeFalsy()
})
// Test case for https://github.com/pnpm/pnpm/issues/11702
test('do not fetch an optional package whose name declares an unsupported platform when the registry metadata has no platform fields', async () => {
const storeDir = temporaryDirectory()
const cafs = createCafsStore(storeDir)
const resolveWithoutPlatformFields: typeof resolve = async (wantedDependency, opts) => {
const result = await resolve(wantedDependency, opts)
if (result.manifest != null) {
delete result.manifest.os
delete result.manifest.cpu
delete result.manifest.libc
}
return result
}
const requestPackage = createPackageRequester({
resolve: resolveWithoutPlatformFields,
fetchers,
cafs,
networkConcurrency: 1,
storeDir,
verifyStoreIntegrity: true,
virtualStoreDirMaxLength: 120,
})
const projectDir = temporaryDirectory()
const pkgResponse = await requestPackage({ alias: '@pnpm.e2e/linux-x64', optional: true, bareSpecifier: '*' }, {
downloadPriority: 0,
lockfileDir: projectDir,
preferredVersions: {},
projectDir,
supportedArchitectures: { os: ['darwin'], cpu: ['arm64'] },
})
expect(pkgResponse.body.isInstallable).toBe(false)
expect(pkgResponse.body.id).toBe('@pnpm.e2e/linux-x64@1.0.0')
expect(pkgResponse.fetching).toBeFalsy()
})
// Test case for https://github.com/pnpm/pnpm/issues/1866
test('fetch a git package without a package.json', async () => {
// a small Deno library with a 'denolib.json' instead of a 'package.json'

View File

@@ -290,6 +290,7 @@ fn is_compatible<Reporter: self::Reporter>(
return true;
}
let manifest = PackageInstallabilityManifest {
name: subdep.name.clone(),
engines: None,
cpu: subdep.cpu.clone(),
os: subdep.os.clone(),

View File

@@ -0,0 +1,107 @@
//! Port of `inferPlatformFromPackageName.ts` from
//! <https://github.com/pnpm/pnpm/blob/34875b2d7c/config/package-is-installable/src/inferPlatformFromPackageName.ts>.
use crate::check_platform::{WantedPlatform, WantedPlatformRef};
fn os_for_token(token: &str) -> Option<&'static str> {
match token {
"aix" => Some("aix"),
"android" => Some("android"),
"darwin" | "macos" | "osx" => Some("darwin"),
"freebsd" => Some("freebsd"),
"linux" => Some("linux"),
"netbsd" => Some("netbsd"),
"openbsd" => Some("openbsd"),
"openharmony" => Some("openharmony"),
"sunos" => Some("sunos"),
"win32" | "windows" => Some("win32"),
_ => None,
}
}
fn cpu_for_token(token: &str) -> Option<&'static str> {
match token {
"arm" | "armv6" | "armv7" => Some("arm"),
"arm64" | "aarch64" => Some("arm64"),
"ia32" => Some("ia32"),
"loong64" => Some("loong64"),
"mips64el" => Some("mips64el"),
"ppc64" | "ppc64le" => Some("ppc64"),
"riscv64" => Some("riscv64"),
"s390x" => Some("s390x"),
"x64" | "amd64" => Some("x64"),
"wasm32" => Some("wasm32"),
_ => None,
}
}
fn libc_for_token(token: &str) -> Option<&'static str> {
match token {
"glibc" | "gnu" | "gnueabihf" => Some("glibc"),
"musl" | "musleabihf" => Some("musl"),
_ => None,
}
}
/// Infers the supported platforms of a package from the tokens of its name,
/// e.g. `@nx/nx-win32-arm64-msvc` produces `os: ["win32"], cpu: ["arm64"]`.
/// Platform-specific binary packages follow this naming convention, which is
/// the only platform signal left when their os/cpu/libc manifest fields are
/// absent. Returns `None` when no platform token is recognized in the name.
pub fn infer_platform_from_package_name(name: &str) -> Option<WantedPlatform> {
let name_without_scope = name.find('/').map_or(name, |idx| &name[idx + 1..]);
let lowercase = name_without_scope.to_lowercase();
let tokens: Vec<&str> = lowercase.split(['-', '_', '.']).collect();
let os = pick_token_values(&tokens, os_for_token);
let cpu = pick_token_values(&tokens, cpu_for_token);
let libc = pick_token_values(&tokens, libc_for_token);
if os.is_none() && cpu.is_none() && libc.is_none() {
return None;
}
Some(WantedPlatform { os, cpu, libc })
}
fn pick_token_values(
tokens: &[&str],
value_for_token: fn(&str) -> Option<&'static str>,
) -> Option<Vec<String>> {
let mut values: Vec<String> = Vec::new();
for token in tokens {
if let Some(value) = value_for_token(token)
&& !values.iter().any(|seen| seen == value)
{
values.push(value.to_string());
}
}
(!values.is_empty()).then_some(values)
}
/// The platform fields of an optional dependency may be incomplete: some
/// registries strip os/cpu/libc (or just libc) from the metadata they serve,
/// and lockfile entries written from such metadata lack them too. For a
/// platform-specific binary the package name carries the same information, so
/// each missing field is filled from the name's tokens. A package that
/// declares no platform fields at all is treated as platform-specific only
/// when an operating system is recognized in its name — a generic name
/// segment (e.g. `arm` on its own) never marks it as such.
///
/// Returns `None` when the declared fields stand as-is. The `optional` gate
/// stays at the call site, mirroring upstream's `effectivePlatform` at
/// <https://github.com/pnpm/pnpm/blob/34875b2d7c/config/package-is-installable/src/index.ts#L70-L96>.
/// See <https://github.com/pnpm/pnpm/issues/11702>.
pub fn inferred_platform(name: &str, declared: WantedPlatformRef<'_>) -> Option<WantedPlatform> {
if declared.os.is_some() && declared.cpu.is_some() && declared.libc.is_some() {
return None;
}
let inferred = infer_platform_from_package_name(name)?;
let declares_platform =
declared.os.is_some() || declared.cpu.is_some() || declared.libc.is_some();
if !declares_platform && inferred.os.is_none() {
return None;
}
Some(WantedPlatform {
os: declared.os.map(<[String]>::to_vec).or(inferred.os),
cpu: declared.cpu.map(<[String]>::to_vec).or(inferred.cpu),
libc: declared.libc.map(<[String]>::to_vec).or(inferred.libc),
})
}

View File

@@ -18,6 +18,7 @@
mod check_engine;
mod check_platform;
mod infer_platform_from_package_name;
mod package_is_installable;
#[cfg(test)]
@@ -30,6 +31,7 @@ pub use check_platform::{
Platform, SupportedArchitectures, UnsupportedPlatformError, WantedPlatform, WantedPlatformRef,
check_platform,
};
pub use infer_platform_from_package_name::{infer_platform_from_package_name, inferred_platform};
pub use package_is_installable::{
InstallabilityError, InstallabilityOptions, InstallabilityVerdict,
PackageInstallabilityManifest, SkipReason, check_package, package_is_installable,

View File

@@ -8,6 +8,7 @@ use crate::{
check_platform::{
SupportedArchitectures, UnsupportedPlatformError, WantedPlatformRef, check_platform,
},
infer_platform_from_package_name::inferred_platform,
};
use derive_more::{Display, Error};
use miette::Diagnostic;
@@ -15,8 +16,13 @@ use serde::Serialize;
/// Inputs from a package manifest (or lockfile metadata row) that
/// drive the installability check.
///
/// `name` feeds the platform-from-name inference for optional
/// dependencies (see [`inferred_platform`]); an empty name disables
/// the inference and leaves only the declared fields.
#[derive(Debug, Default, Clone)]
pub struct PackageInstallabilityManifest {
pub name: String,
pub engines: Option<WantedEngine>,
pub cpu: Option<Vec<String>>,
pub os: Option<Vec<String>>,
@@ -202,6 +208,31 @@ pub fn package_is_installable(
manifest: &PackageInstallabilityManifest,
options: &InstallabilityOptions<'_>,
) -> Result<InstallabilityVerdict, Box<InstallabilityError>> {
// Mirrors upstream's `effectivePlatform(pkg, options.optional)` at
// <https://github.com/pnpm/pnpm/blob/34875b2d7c/config/package-is-installable/src/index.ts#L41>:
// an optional dependency with incomplete platform fields gets the
// missing ones filled from its name before the check runs.
let effective: PackageInstallabilityManifest;
let manifest = if options.optional
&& let Some(platform) = inferred_platform(
&manifest.name,
WantedPlatformRef {
os: manifest.os.as_deref(),
cpu: manifest.cpu.as_deref(),
libc: manifest.libc.as_deref(),
},
) {
effective = PackageInstallabilityManifest {
name: manifest.name.clone(),
engines: manifest.engines.clone(),
os: platform.os,
cpu: platform.cpu,
libc: platform.libc,
};
&effective
} else {
manifest
};
let warn = match check_package(package_id, manifest, options) {
Ok(maybe) => maybe,
Err(invalid_node) => {

View File

@@ -1,10 +1,12 @@
//! Ports of upstream's unit tests:
//! - `config/package-is-installable/test/checkPlatform.ts`
//! - `config/package-is-installable/test/checkEngine.ts`
//! - `config/package-is-installable/test/inferPlatformFromPackageName.ts`
//!
//! Both live under
//! <https://github.com/pnpm/pnpm/tree/94240bc046/config/package-is-installable/test>.
//! All live under
//! <https://github.com/pnpm/pnpm/tree/34875b2d7c/config/package-is-installable/test>.
mod check_engine;
mod check_platform;
mod infer_platform_from_package_name;
mod package_is_installable;

View File

@@ -0,0 +1,171 @@
//! Port of `config/package-is-installable/test/inferPlatformFromPackageName.ts`
//! from
//! <https://github.com/pnpm/pnpm/blob/34875b2d7c/config/package-is-installable/test/inferPlatformFromPackageName.ts>.
use crate::{
InstallabilityOptions, InstallabilityVerdict, PackageInstallabilityManifest,
SupportedArchitectures, WantedPlatform, infer_platform_from_package_name,
package_is_installable,
};
use pretty_assertions::assert_eq;
fn platform(
os: Option<&[&str]>,
cpu: Option<&[&str]>,
libc: Option<&[&str]>,
) -> Option<WantedPlatform> {
Some(WantedPlatform { os: owned(os), cpu: owned(cpu), libc: owned(libc) })
}
fn owned(values: Option<&[&str]>) -> Option<Vec<String>> {
values.map(|values| values.iter().map(|value| (*value).to_string()).collect())
}
#[test]
fn infers_platform_from_real_world_names() {
let cases: &[(&str, Option<WantedPlatform>)] = &[
("@nx/nx-win32-arm64-msvc", platform(Some(&["win32"]), Some(&["arm64"]), None)),
(
"@nx/nx-linux-arm-gnueabihf",
platform(Some(&["linux"]), Some(&["arm"]), Some(&["glibc"])),
),
("@nx/nx-linux-x64-gnu", platform(Some(&["linux"]), Some(&["x64"]), Some(&["glibc"]))),
("@esbuild/aix-ppc64", platform(Some(&["aix"]), Some(&["ppc64"]), None)),
("@esbuild/openharmony-arm64", platform(Some(&["openharmony"]), Some(&["arm64"]), None)),
(
"@biomejs/cli-linux-x64-musl",
platform(Some(&["linux"]), Some(&["x64"]), Some(&["musl"])),
),
(
"@typescript/native-preview-darwin-arm64",
platform(Some(&["darwin"]), Some(&["arm64"]), None),
),
("turbo-windows-64", platform(Some(&["win32"]), None, None)),
("esbuild-darwin-64", platform(Some(&["darwin"]), None, None)),
("bun-linux-aarch64", platform(Some(&["linux"]), Some(&["arm64"]), None)),
("sharp-linux-armv7", platform(Some(&["linux"]), Some(&["arm"]), None)),
("is-arm", platform(None, Some(&["arm"]), None)),
("fsevents", None),
("lodash", None),
("@pnpm.e2e/not-compatible-with-any-os", None),
];
for (name, expected) in cases {
assert_eq!(&infer_platform_from_package_name(name), expected, "name: {name}");
}
}
fn optional_on_linux_x64(supported: Option<&SupportedArchitectures>) -> InstallabilityOptions<'_> {
InstallabilityOptions {
engine_strict: false,
optional: true,
current_node_version: "20.10.0",
pnpm_version: None,
current_os: "linux",
current_cpu: "x64",
current_libc: "glibc",
supported_architectures: supported,
}
}
fn supported_linux_x64_glibc() -> SupportedArchitectures {
SupportedArchitectures {
os: Some(vec!["linux".to_string()]),
cpu: Some(vec!["x64".to_string()]),
libc: Some(vec!["glibc".to_string()]),
}
}
#[test]
fn optional_dependency_without_platform_fields_is_skipped_by_name() {
let manifest = PackageInstallabilityManifest {
name: "@nx/nx-win32-arm64-msvc".to_string(),
..Default::default()
};
let verdict = package_is_installable(
"@nx/nx-win32-arm64-msvc@1.0.0",
&manifest,
&optional_on_linux_x64(None),
)
.unwrap();
assert!(matches!(verdict, InstallabilityVerdict::SkipOptional { .. }), "got {verdict:?}");
}
#[test]
fn missing_libc_is_taken_from_the_name_when_other_fields_are_declared() {
let supported = supported_linux_x64_glibc();
let options = optional_on_linux_x64(Some(&supported));
let musl = PackageInstallabilityManifest {
name: "@nx/nx-linux-x64-musl".to_string(),
os: Some(vec!["linux".to_string()]),
cpu: Some(vec!["x64".to_string()]),
..Default::default()
};
let verdict = package_is_installable("@nx/nx-linux-x64-musl@1.0.0", &musl, &options).unwrap();
assert!(matches!(verdict, InstallabilityVerdict::SkipOptional { .. }), "got {verdict:?}");
let gnu = PackageInstallabilityManifest {
name: "@nx/nx-linux-x64-gnu".to_string(),
os: Some(vec!["linux".to_string()]),
cpu: Some(vec!["x64".to_string()]),
..Default::default()
};
let verdict = package_is_installable("@nx/nx-linux-x64-gnu@1.0.0", &gnu, &options).unwrap();
assert_eq!(verdict, InstallabilityVerdict::Installable);
}
#[test]
fn missing_cpu_is_taken_from_the_name_of_a_package_that_declares_its_platform() {
let manifest = PackageInstallabilityManifest {
name: "@pnpm.e2e/some-pkg-arm64".to_string(),
os: Some(vec!["linux".to_string()]),
..Default::default()
};
let verdict = package_is_installable(
"@pnpm.e2e/some-pkg-arm64@1.0.0",
&manifest,
&optional_on_linux_x64(None),
)
.unwrap();
assert!(matches!(verdict, InstallabilityVerdict::SkipOptional { .. }), "got {verdict:?}");
}
#[test]
fn declared_platform_fields_take_precedence_over_the_name() {
let manifest = PackageInstallabilityManifest {
name: "@pnpm.e2e/win32-binary".to_string(),
os: Some(vec!["linux".to_string()]),
cpu: Some(vec!["x64".to_string()]),
libc: Some(vec!["glibc".to_string()]),
..Default::default()
};
let supported = supported_linux_x64_glibc();
let verdict = package_is_installable(
"@pnpm.e2e/win32-binary@1.0.0",
&manifest,
&optional_on_linux_x64(Some(&supported)),
)
.unwrap();
assert_eq!(verdict, InstallabilityVerdict::Installable);
}
#[test]
fn package_without_declared_fields_is_not_skipped_without_an_os_token() {
let manifest =
PackageInstallabilityManifest { name: "is-arm".to_string(), ..Default::default() };
let verdict =
package_is_installable("is-arm@1.0.0", &manifest, &optional_on_linux_x64(None)).unwrap();
assert_eq!(verdict, InstallabilityVerdict::Installable);
}
#[test]
fn platform_is_not_inferred_for_a_non_optional_dependency() {
let manifest = PackageInstallabilityManifest {
name: "@nx/nx-win32-arm64-msvc".to_string(),
..Default::default()
};
let mut options = optional_on_linux_x64(None);
options.optional = false;
let verdict =
package_is_installable("@nx/nx-win32-arm64-msvc@1.0.0", &manifest, &options).unwrap();
assert_eq!(verdict, InstallabilityVerdict::Installable);
}

View File

@@ -664,7 +664,7 @@ fn walk_deps(
// an unsupported platform is silently added to `skipped`;
// a required dep takes the error path.
if !state.opts.force {
let manifest = manifest_for_installability(metadata);
let manifest = manifest_for_installability(&pkg_key, metadata);
let optional = snapshot.map(|s| s.optional).unwrap_or(false);
let install_opts = InstallabilityOptions {
engine_strict: state.opts.engine_strict,
@@ -771,6 +771,7 @@ fn lookup_package_metadata<'a>(
/// [lockfileToHoistedDepGraph.ts:192-199](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-restorer/src/lockfileToHoistedDepGraph.ts#L192-L199);
/// extracted here so the walker body stays small.
fn manifest_for_installability(
pkg_key: &PackageKey,
metadata: &pacquet_lockfile::PackageMetadata,
) -> PackageInstallabilityManifest {
let engines = metadata.engines.as_ref().map(|engines| WantedEngine {
@@ -778,6 +779,7 @@ fn manifest_for_installability(
pnpm: engines.get("pnpm").cloned(),
});
PackageInstallabilityManifest {
name: pkg_key.name.to_string(),
engines,
cpu: metadata.cpu.clone(),
os: metadata.os.clone(),

View File

@@ -329,7 +329,9 @@ where
// extraction (so `CreateVirtualStore` can suppress slots for
// skipped snapshots), and the spawn cost is unavoidable.
let needs_installability_check = match (snapshots, packages) {
(Some(snaps), Some(pkgs)) if !snaps.is_empty() => any_installability_constraint(pkgs),
(Some(snaps), Some(pkgs)) if !snaps.is_empty() => {
any_installability_constraint(snaps, pkgs)
}
_ => false,
};

View File

@@ -1465,11 +1465,11 @@ impl<'a, DependencyGroupList> InstallWithFreshLockfile<'a, DependencyGroupList>
// constraint, so the engine check resolves against a real version
// instead of erroring on an empty one. Skip the `node --version`
// probe entirely when nothing constrains it.
let host_node = if built_lockfile
.packages
.as_ref()
.is_some_and(crate::any_installability_constraint)
{
let host_node = if built_lockfile.packages.as_ref().is_some_and(|packages| {
built_lockfile.snapshots.as_ref().is_some_and(|snapshots| {
crate::any_installability_constraint(snapshots, packages)
})
}) {
tokio::task::spawn_blocking(crate::InstallabilityHost::detect)
.await
.ok()

View File

@@ -25,7 +25,7 @@ use std::collections::{HashMap, HashSet};
use pacquet_lockfile::{PackageKey, PackageMetadata, SnapshotEntry};
use pacquet_package_is_installable::{
InstallabilityError, InstallabilityOptions, PackageInstallabilityManifest, SkipReason,
SupportedArchitectures, WantedEngine, check_package,
SupportedArchitectures, WantedEngine, WantedPlatformRef, check_package, inferred_platform,
};
use pacquet_reporter::{
LogEvent, LogLevel, Reporter, SkippedOptionalDependencyLog, SkippedOptionalPackage,
@@ -316,7 +316,7 @@ pub fn compute_skipped_snapshots<Reporter: self::Reporter>(
// shape as the loop it short-circuits — but does at most four
// `Option::is_some` checks per row and short-circuits on the
// first declared constraint.
if !any_installability_constraint(packages) {
if !any_installability_constraint(snapshots, packages) {
return Ok(seed);
}
@@ -355,7 +355,12 @@ pub fn compute_skipped_snapshots<Reporter: self::Reporter>(
// `SkipOptional` details payload and the `ProceedWithWarning`
// message body, matching upstream's `warn.toString()` / `warn.message`
// at `index.ts:50` / `:44`).
let mut check_cache: HashMap<PackageKey, Option<InstallabilityError>> = HashMap::new();
//
// The key carries the snapshot's `optional` flag because the
// platform-from-name inference only runs for optional snapshots,
// so the verdict of an optional and a non-optional snapshot of
// the same metadata row can differ.
let mut check_cache: HashMap<(PackageKey, bool), Option<InstallabilityError>> = HashMap::new();
for (snapshot_key, snapshot) in snapshots {
// Seeded entries short-circuit the per-snapshot re-check.
@@ -378,14 +383,33 @@ pub fn compute_skipped_snapshots<Reporter: self::Reporter>(
// (small) and only happens on the first peer-variant of each
// package. Subsequent peer-variants land in the `else` arm
// and read back the cached verdict.
let warn = if let Some(cached) = check_cache.get(&metadata_key) {
let cache_key = (metadata_key.clone(), snapshot.optional);
let warn = if let Some(cached) = check_cache.get(&cache_key) {
cached.clone()
} else {
let manifest = manifest_from_metadata(metadata);
let mut manifest = manifest_from_metadata(&metadata_key, metadata);
// Mirrors upstream's `effectivePlatform(pkg, options.optional)` at
// <https://github.com/pnpm/pnpm/blob/34875b2d7c/config/package-is-installable/src/index.ts#L41>:
// an optional snapshot with incomplete platform fields gets
// the missing ones filled from the package name.
if snapshot.optional
&& let Some(platform) = inferred_platform(
&manifest.name,
WantedPlatformRef {
os: manifest.os.as_deref(),
cpu: manifest.cpu.as_deref(),
libc: manifest.libc.as_deref(),
},
)
{
manifest.os = platform.os;
manifest.cpu = platform.cpu;
manifest.libc = platform.libc;
}
let pkg_id = metadata_key.to_string();
let result = check_package(&pkg_id, &manifest, &base_options)
.map_err(|invalid| Box::new(InstallabilityError::InvalidNodeVersion(invalid)))?;
check_cache.insert(metadata_key.clone(), result.clone());
check_cache.insert(cache_key, result.clone());
result
};
@@ -427,18 +451,38 @@ pub fn compute_skipped_snapshots<Reporter: self::Reporter>(
/// True if any package metadata row in the lockfile declares an
/// `engines` / `cpu` / `os` / `libc` constraint pacquet would need
/// to evaluate. Short-circuits on the first hit. When this returns
/// false, both [`compute_skipped_snapshots`] and the caller can
/// short-circuit: no need to spawn `node --version` or build the
/// host context, because the verdict is unconditionally an empty
/// skip set.
/// to evaluate, or any optional snapshot's package name infers a
/// platform constraint its metadata row doesn't declare.
/// Short-circuits on the first hit. When this returns false, both
/// [`compute_skipped_snapshots`] and the caller can short-circuit:
/// no need to spawn `node --version` or build the host context,
/// because the verdict is unconditionally an empty skip set.
///
/// `pub` so `install_frozen_lockfile` can gate the host detection
/// on it — the spawn is otherwise on the critical path of
/// `CreateVirtualStore::run` and serializes ~100ms of node-binary
/// startup with extraction it used to overlap with.
pub fn any_installability_constraint(packages: &HashMap<PackageKey, PackageMetadata>) -> bool {
pub fn any_installability_constraint(
snapshots: &HashMap<PackageKey, SnapshotEntry>,
packages: &HashMap<PackageKey, PackageMetadata>,
) -> bool {
packages.values().any(metadata_has_meaningful_constraint)
|| snapshots.iter().any(|(snapshot_key, snapshot)| {
snapshot.optional && {
let metadata_key = snapshot_key.without_peer();
packages.get(&metadata_key).is_some_and(|metadata| {
inferred_platform(
&metadata_key.name.to_string(),
WantedPlatformRef {
os: metadata.os.as_deref(),
cpu: metadata.cpu.as_deref(),
libc: metadata.libc.as_deref(),
},
)
.is_some()
})
}
})
}
/// True if a single metadata row carries a constraint pacquet would
@@ -473,8 +517,12 @@ fn platform_axis_meaningful(axis: Option<&[String]>) -> bool {
}
}
fn manifest_from_metadata(metadata: &PackageMetadata) -> PackageInstallabilityManifest {
fn manifest_from_metadata(
metadata_key: &PackageKey,
metadata: &PackageMetadata,
) -> PackageInstallabilityManifest {
PackageInstallabilityManifest {
name: metadata_key.name.to_string(),
engines: metadata.engines.as_ref().map(|map| WantedEngine {
node: map.get("node").cloned(),
pnpm: map.get("pnpm").cloned(),

View File

@@ -263,7 +263,7 @@ fn engines_without_node_or_pnpm_does_not_count_as_constraint() {
let mut packages = HashMap::new();
packages.insert(key, synthetic_metadata(Some(&[("npm", ">=8")]), None, None, None));
assert!(
!any_installability_constraint(&packages),
!any_installability_constraint(&HashMap::new(), &packages),
"engines.npm alone should not block the fast path",
);
}
@@ -277,7 +277,7 @@ fn platform_any_sentinel_does_not_count_as_constraint() {
let mut packages = HashMap::new();
packages.insert(key, synthetic_metadata(None, Some(&["any"]), Some(&["any"]), Some(&["any"])));
assert!(
!any_installability_constraint(&packages),
!any_installability_constraint(&HashMap::new(), &packages),
r#"cpu/os/libc = ["any"] should not block the fast path"#,
);
}
@@ -291,7 +291,7 @@ fn empty_platform_lists_do_not_count_as_constraint() {
let mut packages = HashMap::new();
packages.insert(key, synthetic_metadata(None, Some(&[]), Some(&[]), Some(&[])));
assert!(
!any_installability_constraint(&packages),
!any_installability_constraint(&HashMap::new(), &packages),
"empty platform lists should not block the fast path",
);
}
@@ -303,7 +303,10 @@ fn meaningful_engines_node_triggers_slow_path() {
let key = snapshot_key("for-legacy-node@1.0.0");
let mut packages = HashMap::new();
packages.insert(key, synthetic_metadata(Some(&[("node", "0.10")]), None, None, None));
assert!(any_installability_constraint(&packages), "engines.node must trigger the slow path");
assert!(
any_installability_constraint(&HashMap::new(), &packages),
"engines.node must trigger the slow path",
);
}
/// A meaningful non-`any` platform value triggers the slow path.
@@ -312,7 +315,10 @@ fn meaningful_platform_value_triggers_slow_path() {
let key = snapshot_key("not-compatible-with-any-os@1.0.0");
let mut packages = HashMap::new();
packages.insert(key, synthetic_metadata(None, None, Some(&["this-os-does-not-exist"]), None));
assert!(any_installability_constraint(&packages), "non-any os must trigger the slow path");
assert!(
any_installability_constraint(&HashMap::new(), &packages),
"non-any os must trigger the slow path",
);
}
/// Peer-resolved variants of the same metadata row (e.g.
@@ -595,3 +601,139 @@ fn fetch_failed_and_optional_excluded_are_symmetric() {
assert_eq!(skipped_b.iter().count(), 1);
assert!(skipped_b.contains(&key));
}
/// An optional snapshot whose metadata row carries no platform
/// fields (some registries strip os/cpu/libc from the metadata they
/// serve, and lockfile entries written from such metadata lack them
/// too) is still skipped when its package name declares an
/// unsupported platform. Ports the headless half of upstream's
/// `optionalDependencies.ts` `skip optional dependencies that do not
/// support the target architecture when their lockfile entries have
/// no platform fields`.
#[test]
fn skip_optional_with_platform_inferred_from_name() {
reset_events();
let foreign = snapshot_key("@nx/nx-win32-arm64-msvc@1.0.0");
let matching = snapshot_key("@nx/nx-linux-x64-gnu@1.0.0");
let mut snapshots = HashMap::new();
snapshots.insert(foreign.clone(), SnapshotEntry { optional: true, ..Default::default() });
snapshots.insert(matching.clone(), SnapshotEntry { optional: true, ..Default::default() });
let mut packages = HashMap::new();
packages.insert(foreign.clone(), synthetic_metadata(None, None, None, None));
packages.insert(matching.clone(), synthetic_metadata(None, None, None, None));
let skipped = compute_skipped_snapshots::<RecordingReporter>(
&snapshots,
&packages,
&host("20.10.0", "linux", "x64"),
"/proj",
SkippedSnapshots::new(),
)
.unwrap();
assert_eq!(skipped.len(), 1);
assert!(skipped.contains(&foreign));
assert!(!skipped.contains(&matching));
let events = take_events();
let skipped_events_count = events
.iter()
.filter(|event| matches!(event, LogEvent::SkippedOptionalDependency(_)))
.count();
assert_eq!(skipped_events_count, 1);
}
/// The platform is not inferred from the name of a non-optional
/// snapshot: a regular dependency that happens to carry a platform
/// token in its name installs everywhere unless its metadata says
/// otherwise.
#[test]
fn name_inference_does_not_apply_to_non_optional_snapshots() {
reset_events();
let key = snapshot_key("@nx/nx-win32-arm64-msvc@1.0.0");
let mut snapshots = HashMap::new();
snapshots.insert(key.clone(), SnapshotEntry { optional: false, ..Default::default() });
let mut packages = HashMap::new();
packages.insert(key, synthetic_metadata(None, None, None, None));
let skipped = compute_skipped_snapshots::<RecordingReporter>(
&snapshots,
&packages,
&host("20.10.0", "linux", "x64"),
"/proj",
SkippedSnapshots::new(),
)
.unwrap();
assert!(skipped.is_empty());
}
/// A missing libc field alone is taken from the package name: with
/// `supportedArchitectures.libc = ['glibc']`, the `-musl` variant is
/// skipped and the `-gnu` variant stays, even though neither
/// metadata row declares `libc`.
#[test]
fn missing_libc_is_inferred_from_name() {
reset_events();
let musl = snapshot_key("@nx/nx-linux-x64-musl@1.0.0");
let gnu = snapshot_key("@nx/nx-linux-x64-gnu@1.0.0");
let mut snapshots = HashMap::new();
snapshots.insert(musl.clone(), SnapshotEntry { optional: true, ..Default::default() });
snapshots.insert(gnu.clone(), SnapshotEntry { optional: true, ..Default::default() });
let mut packages = HashMap::new();
packages.insert(musl.clone(), synthetic_metadata(None, Some(&["x64"]), Some(&["linux"]), None));
packages.insert(gnu.clone(), synthetic_metadata(None, Some(&["x64"]), Some(&["linux"]), None));
let mut host = host("20.10.0", "linux", "x64");
host.libc = "glibc";
host.supported_architectures = Some(pacquet_package_is_installable::SupportedArchitectures {
os: Some(vec!["linux".to_string()]),
cpu: Some(vec!["x64".to_string()]),
libc: Some(vec!["glibc".to_string()]),
});
let skipped = compute_skipped_snapshots::<RecordingReporter>(
&snapshots,
&packages,
&host,
"/proj",
SkippedSnapshots::new(),
)
.unwrap();
assert_eq!(skipped.len(), 1);
assert!(skipped.contains(&musl));
assert!(!skipped.contains(&gnu));
}
/// An optional snapshot whose name carries a platform token while
/// its metadata row declares no platform fields must block the
/// fast path — otherwise the inference never gets a chance to run.
#[test]
fn name_inferable_optional_snapshot_triggers_slow_path() {
let key = snapshot_key("@nx/nx-win32-arm64-msvc@1.0.0");
let mut snapshots = HashMap::new();
snapshots.insert(key.clone(), SnapshotEntry { optional: true, ..Default::default() });
let mut packages = HashMap::new();
packages.insert(key, synthetic_metadata(None, None, None, None));
assert!(
any_installability_constraint(&snapshots, &packages),
"a platform-named optional snapshot must trigger the slow path",
);
}
/// A name without an operating-system token never marks a package
/// that declares no platform fields as platform-specific, so it
/// must not block the fast path either.
#[test]
fn generic_name_does_not_trigger_slow_path() {
let key = snapshot_key("is-arm@1.0.0");
let mut snapshots = HashMap::new();
snapshots.insert(key.clone(), SnapshotEntry { optional: true, ..Default::default() });
let mut packages = HashMap::new();
packages.insert(key, synthetic_metadata(None, None, None, None));
assert!(
!any_installability_constraint(&snapshots, &packages),
"a generic name segment must not block the fast path",
);
}