feat: add support to install different architectures (#7214)

close #5965

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Nacho Aldama
2023-10-24 14:50:40 +02:00
committed by GitHub
parent 269926db9f
commit 43ce9e4a6a
40 changed files with 477 additions and 118 deletions

View File

@@ -0,0 +1,55 @@
---
"@pnpm/plugin-commands-publishing": minor
"@pnpm/plugin-commands-script-runners": minor
"@pnpm/filter-workspace-packages": minor
"@pnpm/plugin-commands-licenses": minor
"@pnpm/plugin-commands-patching": minor
"@pnpm/resolve-dependencies": minor
"@pnpm/package-is-installable": minor
"@pnpm/package-requester": minor
"@pnpm/plugin-commands-rebuild": minor
"@pnpm/store-controller-types": minor
"@pnpm/plugin-commands-store": minor
"@pnpm/license-scanner": minor
"@pnpm/filter-lockfile": minor
"@pnpm/workspace.find-packages": minor
"@pnpm/headless": minor
"@pnpm/deps.graph-builder": minor
"@pnpm/core": minor
"@pnpm/types": minor
"@pnpm/cli-utils": minor
"@pnpm/config": minor
"pnpm": minor
---
Support for multiple architectures when installing dependencies [#5965](https://github.com/pnpm/pnpm/issues/5965).
You can now specify architectures for which you'd like to install optional dependencies, even if they don't match the architecture of the system running the install. Use the `supportedArchitectures` field in `package.json` to define your preferences.
For example, the following configuration tells pnpm to install optional dependencies for Windows x64:
```json
{
"pnpm": {
"supportedArchitectures": {
"os": ["win32"],
"cpu": ["x64"]
}
}
}
```
Whereas this configuration will have pnpm install optional dependencies for Windows, macOS, and the architecture of the system currently running the install. It includes artifacts for both x64 and arm64 CPUs:
```json
{
"pnpm": {
"supportedArchitectures": {
"os": ["win32", "darwin", "current"],
"cpu": ["x64", "arm64"]
}
}
}
```
Additionally, `supportedArchitectures` also supports specifying the `libc` of the system.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/build-modules": patch
---
`filesIndexFile` may be undefined.

View File

@@ -1,6 +1,7 @@
import { packageManager } from '@pnpm/cli-meta'
import { logger } from '@pnpm/logger'
import { checkPackage, UnsupportedEngineError, type WantedEngine } from '@pnpm/package-is-installable'
import { type SupportedArchitectures } from '@pnpm/types'
export function packageIsInstallable (
pkgPath: string,
@@ -13,6 +14,7 @@ export function packageIsInstallable (
opts: {
engineStrict?: boolean
nodeVersion?: string
supportedArchitectures?: SupportedArchitectures
}
) {
const pnpmVersion = packageManager.name === 'pnpm'
@@ -21,6 +23,11 @@ export function packageIsInstallable (
const err = checkPackage(pkgPath, pkg, {
nodeVersion: opts.nodeVersion,
pnpmVersion,
supportedArchitectures: opts.supportedArchitectures ?? {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
})
if (err === null) return
if (

View File

@@ -1,18 +1,26 @@
import * as utils from '@pnpm/read-project-manifest'
import { type ProjectManifest } from '@pnpm/types'
import { type SupportedArchitectures, type ProjectManifest } from '@pnpm/types'
import { packageIsInstallable } from './packageIsInstallable'
export interface ReadProjectManifestOpts {
engineStrict?: boolean
nodeVersion?: string
supportedArchitectures?: SupportedArchitectures
}
interface BaseReadProjectManifestResult {
fileName: string
writeProjectManifest: (manifest: ProjectManifest, force?: boolean) => Promise<void>
}
export interface ReadProjectManifestResult extends BaseReadProjectManifestResult {
manifest: ProjectManifest
}
export async function readProjectManifest (
projectDir: string,
opts: {
engineStrict?: boolean
nodeVersion?: string
}
): Promise<{
fileName: string
manifest: ProjectManifest
writeProjectManifest: (manifest: ProjectManifest, force?: boolean) => Promise<void>
}> {
opts: ReadProjectManifestOpts = {}
): Promise<ReadProjectManifestResult> {
const { fileName, manifest, writeProjectManifest } = await utils.readProjectManifest(projectDir)
packageIsInstallable(projectDir, manifest as any, opts) // eslint-disable-line @typescript-eslint/no-explicit-any
return { fileName, manifest, writeProjectManifest }
@@ -20,27 +28,21 @@ export async function readProjectManifest (
export async function readProjectManifestOnly (
projectDir: string,
opts: {
engineStrict?: boolean
nodeVersion?: string
} = {}
opts: ReadProjectManifestOpts = {}
): Promise<ProjectManifest> {
const manifest = await utils.readProjectManifestOnly(projectDir)
packageIsInstallable(projectDir, manifest as any, opts) // eslint-disable-line @typescript-eslint/no-explicit-any
return manifest
}
export interface TryReadProjectManifestResult extends BaseReadProjectManifestResult {
manifest: ProjectManifest | null
}
export async function tryReadProjectManifest (
projectDir: string,
opts: {
engineStrict?: boolean
nodeVersion?: string
}
): Promise<{
fileName: string
manifest: ProjectManifest | null
writeProjectManifest: (manifest: ProjectManifest, force?: boolean) => Promise<void>
}> {
opts: ReadProjectManifestOpts
): Promise<TryReadProjectManifestResult> {
const { fileName, manifest, writeProjectManifest } = await utils.tryReadProjectManifest(projectDir)
if (manifest == null) return { fileName, manifest, writeProjectManifest }
packageIsInstallable(projectDir, manifest as any, opts) // eslint-disable-line @typescript-eslint/no-explicit-any

View File

@@ -1,6 +1,7 @@
import path from 'path'
import { PnpmError } from '@pnpm/error'
import {
type SupportedArchitectures,
type AllowedDeprecatedVersions,
type PackageExtension,
type PeerDependencyRules,
@@ -18,6 +19,7 @@ export interface OptionsFromRootManifest {
packageExtensions?: Record<string, PackageExtension>
patchedDependencies?: Record<string, string>
peerDependencyRules?: PeerDependencyRules
supportedArchitectures?: SupportedArchitectures
}
export function getOptionsFromRootManifest (manifestDir: string, manifest: ProjectManifest): OptionsFromRootManifest {
@@ -46,6 +48,13 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
patchedDependencies[dep] = path.join(manifestDir, patchFile)
}
}
const supportedArchitectures = {
os: manifest.pnpm?.supportedArchitectures?.os ?? ['current'],
cpu: manifest.pnpm?.supportedArchitectures?.cpu ?? ['current'],
libc: manifest.pnpm?.supportedArchitectures?.libc ?? ['current'],
}
const settings: OptionsFromRootManifest = {
allowedDeprecatedVersions,
allowNonAppliedPatches,
@@ -54,6 +63,7 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
packageExtensions,
peerDependencyRules,
patchedDependencies,
supportedArchitectures,
}
if (onlyBuiltDependencies) {
settings.onlyBuiltDependencies = onlyBuiltDependencies

View File

@@ -1,4 +1,5 @@
import { PnpmError } from '@pnpm/error'
import { type SupportedArchitectures } from '@pnpm/types'
import { familySync as getLibcFamilySync } from 'detect-libc'
const currentLibc = getLibcFamilySync() ?? 'unknown'
@@ -16,19 +17,26 @@ export class UnsupportedPlatformError extends PnpmError {
export function checkPlatform (
packageId: string,
wantedPlatform: WantedPlatform
wantedPlatform: WantedPlatform,
supportedArchitectures?: SupportedArchitectures
) {
const current = {
os: dedupeCurrent(process.platform, supportedArchitectures?.os ?? ['current']),
cpu: dedupeCurrent(process.arch, supportedArchitectures?.cpu ?? ['current']),
libc: dedupeCurrent(currentLibc, supportedArchitectures?.libc ?? ['current']),
}
const { platform, arch } = process
let osOk = true; let cpuOk = true; let libcOk = true
if (wantedPlatform.os) {
osOk = checkList(platform, wantedPlatform.os)
osOk = checkList(current.os, wantedPlatform.os)
}
if (wantedPlatform.cpu) {
cpuOk = checkList(arch, wantedPlatform.cpu)
cpuOk = checkList(current.cpu, wantedPlatform.cpu)
}
if (wantedPlatform.libc && currentLibc !== 'unknown') {
libcOk = checkList(currentLibc, wantedPlatform.libc)
libcOk = checkList(current.libc, wantedPlatform.libc)
}
if (!osOk || !cpuOk || !libcOk) {
@@ -45,25 +53,35 @@ export interface Platform {
export type WantedPlatform = Partial<Platform>
function checkList (value: string, list: string | string[]) {
let tmp; let match = false; let blc = 0
function checkList (value: string | string[], list: string | string[]): boolean {
let tmp
let match = false
let blc = 0
if (typeof list === 'string') {
list = [list]
}
if (list.length === 1 && list[0] === 'any') {
return true
}
for (let i = 0; i < list.length; ++i) {
tmp = list[i]
if (tmp[0] === '!') {
tmp = tmp.slice(1)
if (tmp === value) {
return false
const values = Array.isArray(value) ? value : [value]
for (const value of values) {
for (let i = 0; i < list.length; ++i) {
tmp = list[i]
if (tmp[0] === '!') {
tmp = tmp.slice(1)
if (tmp === value) {
return false
}
++blc
} else {
match = match || tmp === value
}
++blc
} else {
match = match || tmp === value
}
}
return match || blc === list.length
}
function dedupeCurrent (current: string, supported: string[]) {
return supported.map((supported) => supported === 'current' ? current : supported)
}

View File

@@ -5,6 +5,7 @@ import {
import { checkEngine, UnsupportedEngineError, type WantedEngine } from './checkEngine'
import { checkPlatform, UnsupportedPlatformError } from './checkPlatform'
import { getSystemNodeVersion } from './getSystemNodeVersion'
import { type SupportedArchitectures } from '@pnpm/types'
export type { Engine } from './checkEngine'
export type { Platform, WantedPlatform } from './checkPlatform'
@@ -31,6 +32,7 @@ export function packageIsInstallable (
optional: boolean
pnpmVersion?: string
lockfileDir: string
supportedArchitectures?: SupportedArchitectures
}
): boolean | null {
const warn = checkPackage(pkgId, pkg, options)
@@ -73,13 +75,14 @@ export function checkPackage (
options: {
nodeVersion?: string
pnpmVersion?: string
supportedArchitectures?: SupportedArchitectures
}
): null | UnsupportedEngineError | UnsupportedPlatformError {
return checkPlatform(pkgId, {
cpu: manifest.cpu ?? ['any'],
os: manifest.os ?? ['any'],
libc: manifest.libc ?? ['any'],
}) ?? (
}, options.supportedArchitectures) ?? (
(manifest.engines == null)
? null
: checkEngine(pkgId, manifest.engines, {
@@ -87,4 +90,4 @@ export function checkPackage (
pnpm: options.pnpmVersion,
})
)
}
}

View File

@@ -79,3 +79,45 @@ test('os wrong (negation)', () => {
test('nothing wrong (negation)', () => {
expect(checkPlatform(packageId, { cpu: '!enten-cpu', os: '!enten-os', libc: '!enten-libc' })).toBe(null)
})
test('override OS', () => {
expect(checkPlatform(packageId, { cpu: 'any', os: 'win32', libc: 'any' }, {
os: ['win32'],
cpu: ['current'],
libc: ['current'],
})).toBe(null)
})
test('accept another CPU', () => {
expect(checkPlatform(packageId, { cpu: 'x64', os: 'any', libc: 'any' }, {
os: ['current'],
cpu: ['current', 'x64'],
libc: ['current'],
})).toBe(null)
})
test('fail when CPU is different', () => {
const err = checkPlatform(packageId, { cpu: 'x64', os: 'any', libc: 'any' }, {
os: ['current'],
cpu: ['arm64'],
libc: ['current'],
})
expect(err).toBeTruthy()
expect(err?.code).toBe('ERR_PNPM_UNSUPPORTED_PLATFORM')
})
test('override libc', () => {
expect(checkPlatform(packageId, { cpu: 'any', os: 'any', libc: 'glibc' }, {
os: ['current'],
cpu: ['current'],
libc: ['glibc'],
})).toBe(null)
})
test('accept another libc', () => {
expect(checkPlatform(packageId, { cpu: 'any', os: 'any', libc: 'glibc' }, {
os: ['current'],
cpu: ['current'],
libc: ['current', 'glibc'],
})).toBe(null)
})

View File

@@ -16,7 +16,7 @@ import {
import { logger } from '@pnpm/logger'
import { type IncludedDependencies } from '@pnpm/modules-yaml'
import { packageIsInstallable } from '@pnpm/package-is-installable'
import { type PatchFile, type Registries } from '@pnpm/types'
import { type SupportedArchitectures, type PatchFile, type Registries } from '@pnpm/types'
import {
type PkgRequestFetchResult,
type FetchPackageToStoreFunction,
@@ -25,6 +25,7 @@ import {
import * as dp from '@pnpm/dependency-path'
import pathExists from 'path-exists'
import equals from 'ramda/src/equals'
import isEmpty from 'ramda/src/isEmpty'
const brokenModulesLogger = logger('_broken_node_modules')
@@ -33,7 +34,7 @@ export interface DependenciesGraphNode {
hasBundledDependencies: boolean
modules: string
name: string
fetching: () => Promise<PkgRequestFetchResult>
fetching?: () => Promise<PkgRequestFetchResult>
dir: string
children: Record<string, string>
optionalDependencies: Set<string>
@@ -43,7 +44,7 @@ export interface DependenciesGraphNode {
requiresBuild: boolean
prepare: boolean
hasBin: boolean
filesIndexFile: string
filesIndexFile?: string
patchFile?: PatchFile
}
@@ -68,6 +69,7 @@ export interface LockfileToDepGraphOptions {
storeController: StoreController
storeDir: string
virtualStoreDir: string
supportedArchitectures?: SupportedArchitectures
}
export interface DirectDependenciesByImporterId {
@@ -121,16 +123,18 @@ export async function lockfileToDepGraph (
nodeVersion: opts.nodeVersion,
optional: pkgSnapshot.optional === true,
pnpmVersion: opts.pnpmVersion,
supportedArchitectures: opts.supportedArchitectures,
}) === false
) {
opts.skipped.add(depPath)
return
}
const dir = path.join(modules, pkgName)
const depIsPresent = !refIsLocalDirectory(depPath) &&
currentPackages[depPath] && equals(currentPackages[depPath].dependencies, lockfile.packages![depPath].dependencies)
if (
!refIsLocalDirectory(depPath) &&
currentPackages[depPath] && equals(currentPackages[depPath].dependencies, lockfile.packages![depPath].dependencies) &&
equals(currentPackages[depPath].optionalDependencies, lockfile.packages![depPath].optionalDependencies)
depIsPresent && isEmpty(currentPackages[depPath].optionalDependencies) &&
isEmpty(lockfile.packages![depPath].optionalDependencies)
) {
if (await pathExists(dir)) {
return
@@ -140,31 +144,42 @@ export async function lockfileToDepGraph (
missing: dir,
})
}
const resolution = pkgSnapshotToResolution(depPath, pkgSnapshot, opts.registries)
progressLogger.debug({
packageId,
requester: opts.lockfileDir,
status: 'resolved',
})
let fetchResponse!: ReturnType<FetchPackageToStoreFunction>
try {
fetchResponse = opts.storeController.fetchPackage({
force: false,
lockfileDir: opts.lockfileDir,
ignoreScripts: opts.ignoreScripts,
pkg: {
id: packageId,
resolution,
},
expectedPkg: {
name: pkgName,
version: pkgVersion,
},
}) as any // eslint-disable-line
if (fetchResponse instanceof Promise) fetchResponse = await fetchResponse
} catch (err: any) { // eslint-disable-line
if (pkgSnapshot.optional) return
throw err
let fetchResponse!: Partial<ReturnType<FetchPackageToStoreFunction>>
if (depIsPresent && equals(currentPackages[depPath].optionalDependencies, lockfile.packages![depPath].optionalDependencies)) {
if (await pathExists(dir)) {
fetchResponse = {}
} else {
brokenModulesLogger.debug({
missing: dir,
})
}
}
if (!fetchResponse) {
const resolution = pkgSnapshotToResolution(depPath, pkgSnapshot, opts.registries)
progressLogger.debug({
packageId,
requester: opts.lockfileDir,
status: 'resolved',
})
try {
fetchResponse = opts.storeController.fetchPackage({
force: false,
lockfileDir: opts.lockfileDir,
ignoreScripts: opts.ignoreScripts,
pkg: {
id: packageId,
resolution,
},
expectedPkg: {
name: pkgName,
version: pkgVersion,
},
}) as any // eslint-disable-line
if (fetchResponse instanceof Promise) fetchResponse = await fetchResponse
} catch (err: any) { // eslint-disable-line
if (pkgSnapshot.optional) return
throw err
}
}
graph[dir] = {
children: {},

View File

@@ -8,7 +8,7 @@ export interface DependenciesGraphNode {
name: string
dir: string
fetchingBundledManifest?: () => Promise<PackageManifest | undefined>
filesIndexFile: string
filesIndexFile?: string
hasBin: boolean
hasBundledDependencies: boolean
installable?: boolean

View File

@@ -135,10 +135,12 @@ async function buildDependency (
patchFileHash: depNode.patchFile?.hash,
isBuilt: hasSideEffects,
})
await opts.storeController.upload(depNode.dir, {
sideEffectsCacheKey,
filesIndexFile: depNode.filesIndexFile,
})
if (depNode.filesIndexFile) {
await opts.storeController.upload(depNode.dir, {
sideEffectsCacheKey,
filesIndexFile: depNode.filesIndexFile,
})
}
} catch (err: any) { // eslint-disable-line
if (err.statusCode === 403) {
logger.warn({

View File

@@ -47,4 +47,9 @@ export const DEFAULT_OPTS = {
useRunningStoreServer: false,
useStoreServer: false,
workspaceConcurrency: 4,
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
}

View File

@@ -50,6 +50,11 @@ export const DEFAULT_OPTS = {
useRunningStoreServer: false,
useStoreServer: false,
workspaceConcurrency: 4,
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
}
export const DLX_DEFAULT_OPTS = {
@@ -80,4 +85,9 @@ export const DLX_DEFAULT_OPTS = {
storeDir: path.join(tmp, 'store'),
userConfig: {},
workspaceConcurrency: 1,
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
}

View File

@@ -7,7 +7,7 @@ import {
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile-utils'
import { logger } from '@pnpm/logger'
import { packageIsInstallable } from '@pnpm/package-is-installable'
import { type DependenciesField } from '@pnpm/types'
import { type SupportedArchitectures, type DependenciesField } from '@pnpm/types'
import * as dp from '@pnpm/dependency-path'
import mapValues from 'ramda/src/map'
import pickBy from 'ramda/src/pickBy'
@@ -16,21 +16,32 @@ import { filterImporter } from './filterImporter'
const lockfileLogger = logger('lockfile')
export function filterLockfileByEngine (
lockfile: Lockfile,
opts: FilterLockfileOptions
) {
const importerIds = Object.keys(lockfile.importers)
return filterLockfileByImportersAndEngine(lockfile, importerIds, opts)
}
export interface FilterLockfileOptions {
currentEngine: {
nodeVersion: string
pnpmVersion: string
}
engineStrict: boolean
include: { [dependenciesField in DependenciesField]: boolean }
includeIncompatiblePackages?: boolean
failOnMissingDependencies: boolean
lockfileDir: string
skipped: Set<string>
supportedArchitectures?: SupportedArchitectures
}
export function filterLockfileByImportersAndEngine (
lockfile: Lockfile,
importerIds: string[],
opts: {
currentEngine: {
nodeVersion: string
pnpmVersion: string
}
engineStrict: boolean
include: { [dependenciesField in DependenciesField]: boolean }
includeIncompatiblePackages?: boolean
failOnMissingDependencies: boolean
lockfileDir: string
skipped: Set<string>
}
opts: FilterLockfileOptions
): { lockfile: Lockfile, selectedImporterIds: string[] } {
const importerIdSet = new Set(importerIds) as Set<string>
@@ -50,6 +61,7 @@ export function filterLockfileByImportersAndEngine (
opts.includeIncompatiblePackages === true,
lockfileDir: opts.lockfileDir,
skipped: opts.skipped,
supportedArchitectures: opts.supportedArchitectures,
})
: {}
@@ -89,6 +101,7 @@ function pickPkgsWithAllDeps (
includeIncompatiblePackages: boolean
lockfileDir: string
skipped: Set<string>
supportedArchitectures?: SupportedArchitectures
}
) {
const pickedPackages = {} as PackageSnapshots
@@ -115,6 +128,7 @@ function pkgAllDeps (
includeIncompatiblePackages: boolean
lockfileDir: string
skipped: Set<string>
supportedArchitectures?: SupportedArchitectures
}
) {
for (const depPath of depPaths) {
@@ -150,6 +164,7 @@ function pkgAllDeps (
nodeVersion: opts.currentEngine.nodeVersion,
optional: pkgSnapshot.optional === true,
pnpmVersion: opts.currentEngine.pnpmVersion,
supportedArchitectures: opts.supportedArchitectures,
}) !== false
if (!installable) {
if (!ctx.pickedPackages[depPath] && pkgSnapshot.optional === true) {

View File

@@ -1,3 +1,3 @@
export { filterLockfile } from './filterLockfile'
export { filterLockfileByImporters } from './filterLockfileByImporters'
export { filterLockfileByImportersAndEngine } from './filterLockfileByImportersAndEngine'
export { filterLockfileByImportersAndEngine, filterLockfileByEngine } from './filterLockfileByImportersAndEngine'

View File

@@ -116,6 +116,11 @@ test('filterByImportersAndEngine(): skip packages that are not installable', ()
},
lockfileDir: process.cwd(),
skipped: skippedPackages,
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
}
)
@@ -297,6 +302,11 @@ test('filterByImportersAndEngine(): filter the packages that set os and cpu', ()
},
lockfileDir: process.cwd(),
skipped: skippedPackages,
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
}
)
@@ -467,6 +477,11 @@ test('filterByImportersAndEngine(): filter the packages that set libc', () => {
},
lockfileDir: process.cwd(),
skipped: skippedPackages,
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
}
)
@@ -602,6 +617,11 @@ test('filterByImportersAndEngine(): includes linked packages', () => {
},
lockfileDir: process.cwd(),
skipped: new Set(),
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
}
)

View File

@@ -140,6 +140,7 @@ export type ProjectManifest = BaseManifest & {
ignoreCves?: string[]
}
requiredScripts?: string[]
supportedArchitectures?: SupportedArchitectures
}
private?: boolean
resolutions?: Record<string, string>
@@ -148,3 +149,9 @@ export type ProjectManifest = BaseManifest & {
export type PackageManifest = DependencyManifest & {
deprecated?: string
}
export interface SupportedArchitectures {
os?: string[]
cpu?: string[]
libc?: string[]
}

View File

@@ -48,4 +48,9 @@ export const DEFAULT_OPTS = {
useRunningStoreServer: false,
useStoreServer: false,
workspaceConcurrency: 4,
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
}

View File

@@ -20,6 +20,7 @@ export type ListMissingPeersOptions = Partial<GetContextOptions>
| 'useGitBranchLockfile'
| 'workspacePackages'
>
& Partial<Pick<InstallOptions, 'supportedArchitectures'>>
& Pick<GetContextOptions, 'autoInstallPeers' | 'excludeLinksFromLockfile' | 'storeDir'>
export async function getPeerDependencyIssues (
@@ -84,6 +85,7 @@ export async function getPeerDependencyIssues (
virtualStoreDir: ctx.virtualStoreDir,
wantedLockfile: ctx.wantedLockfile,
workspacePackages: opts.workspacePackages ?? {},
supportedArchitectures: opts.supportedArchitectures,
}
)

View File

@@ -9,6 +9,7 @@ import { normalizeRegistries, DEFAULT_REGISTRIES } from '@pnpm/normalize-registr
import { type WorkspacePackages } from '@pnpm/resolver-base'
import { type StoreController } from '@pnpm/store-controller-types'
import {
type SupportedArchitectures,
type AllowedDeprecatedVersions,
type PackageExtension,
type PeerDependencyRules,
@@ -136,6 +137,8 @@ export interface StrictInstallOptions {
* The option might be used in the future to improve performance.
*/
disableRelinkLocalDirDeps: boolean
supportedArchitectures?: SupportedArchitectures
}
export type InstallOptions =

View File

@@ -1038,6 +1038,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
patchedDependencies: opts.patchedDependencies,
lockfileIncludeTarballUrl: opts.lockfileIncludeTarballUrl,
resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot,
supportedArchitectures: opts.supportedArchitectures,
}
)
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {

View File

@@ -342,7 +342,9 @@ async function linkNewPackages (
for (const depPath of wantedRelDepPaths) {
if (currentLockfile.packages[depPath] &&
(!equals(currentLockfile.packages[depPath].dependencies, wantedLockfile.packages[depPath].dependencies) ||
!equals(currentLockfile.packages[depPath].optionalDependencies, wantedLockfile.packages[depPath].optionalDependencies))) {
!isEmpty(currentLockfile.packages[depPath].optionalDependencies) ||
!isEmpty(wantedLockfile.packages[depPath].optionalDependencies))
) {
// TODO: come up with a test that triggers the usecase of depGraph[depPath] undefined
// see related issue: https://github.com/pnpm/pnpm/issues/870
if (depGraph[depPath] && !newDepPathsSet.has(depPath)) {

View File

@@ -1,7 +1,9 @@
import fs from 'fs'
import path from 'path'
import { type Lockfile } from '@pnpm/lockfile-file'
import { prepareEmpty, preparePackages } from '@pnpm/prepare'
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import deepRequireCwd from 'deep-require-cwd'
import readYamlFile from 'read-yaml-file'
import {
addDependenciesToPackage,
@@ -13,7 +15,6 @@ import {
import rimraf from '@zkochan/rimraf'
import exists from 'path-exists'
import sinon from 'sinon'
import deepRequireCwd from 'deep-require-cwd'
import { testDefaults } from '../utils'
test('successfully install optional dependency with subdependencies', async () => {
@@ -575,3 +576,75 @@ test('fail on a package with failing postinstall if the package is both an optio
)
).rejects.toThrow()
})
describe('supported architectures', () => {
test.each(['isolated', 'hoisted'])('install optional dependency for the supported architecture set by the user (nodeLinker=%s)', async (nodeLinker) => {
prepareEmpty()
const opts = await testDefaults({ nodeLinker })
const manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/has-many-optional-deps@1.0.0'], {
...opts,
supportedArchitectures: { os: ['darwin'], cpu: ['arm64'] },
})
expect(deepRequireCwd(['@pnpm.e2e/has-many-optional-deps', '@pnpm.e2e/darwin-arm64', './package.json']).version).toBe('1.0.0')
await install(manifest, {
...opts,
preferFrozenLockfile: false,
supportedArchitectures: { os: ['darwin'], cpu: ['x64'] },
})
expect(deepRequireCwd(['@pnpm.e2e/has-many-optional-deps', '@pnpm.e2e/darwin-x64', './package.json']).version).toBe('1.0.0')
await install(manifest, {
...opts,
frozenLockfile: true,
supportedArchitectures: { os: ['linux'], cpu: ['x64'] },
})
expect(deepRequireCwd(['@pnpm.e2e/has-many-optional-deps', '@pnpm.e2e/linux-x64', './package.json']).version).toBe('1.0.0')
})
test('remove optional dependencies that are not used', async () => {
prepareEmpty()
const opts = await testDefaults({ modulesCacheMaxAge: 0 })
const manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/has-many-optional-deps@1.0.0'], {
...opts,
supportedArchitectures: { os: ['darwin', 'linux', 'win32'], cpu: ['arm64', 'x64'] },
})
await install(manifest, {
...opts,
supportedArchitectures: { os: ['darwin'], cpu: ['x64'] },
})
expect(fs.readdirSync('node_modules/.pnpm').length).toBe(3)
})
test('remove optional dependencies that are not used, when hoisted node linker is used', async () => {
prepareEmpty()
const opts = await testDefaults({ nodeLinker: 'hoisted' })
const manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/has-many-optional-deps@1.0.0'], {
...opts,
supportedArchitectures: { os: ['darwin', 'linux', 'win32'], cpu: ['arm64', 'x64'] },
})
await install(manifest, {
...opts,
supportedArchitectures: { os: ['darwin'], cpu: ['x64'] },
})
expect(fs.readdirSync('node_modules/@pnpm.e2e').sort()).toStrictEqual(['darwin-x64', 'has-many-optional-deps'])
})
test('remove optional dependencies if supported architectures have changed and a new dependency is added', async () => {
prepareEmpty()
const opts = await testDefaults({ modulesCacheMaxAge: 0 })
const manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/parent-of-has-many-optional-deps@1.0.0'], {
...opts,
supportedArchitectures: { os: ['darwin', 'linux', 'win32'], cpu: ['arm64', 'x64'] },
})
await addDependenciesToPackage(manifest, ['is-positive@1.0.0'], {
...opts,
supportedArchitectures: { os: ['darwin'], cpu: ['x64'] },
})
expect(fs.readdirSync('node_modules/.pnpm').length).toBe(5)
})
})

View File

@@ -15,6 +15,7 @@ import {
summaryLogger,
} from '@pnpm/core-loggers'
import {
filterLockfileByEngine,
filterLockfileByImportersAndEngine,
} from '@pnpm/filter-lockfile'
import { hoist } from '@pnpm/hoist'
@@ -55,7 +56,7 @@ import {
type StoreController,
} from '@pnpm/store-controller-types'
import { symlinkDependency } from '@pnpm/symlink-dependency'
import { type DependencyManifest, type HoistedDependencies, type ProjectManifest, type Registries, DEPENDENCIES_FIELDS } from '@pnpm/types'
import { type DependencyManifest, type HoistedDependencies, type ProjectManifest, type Registries, DEPENDENCIES_FIELDS, type SupportedArchitectures } from '@pnpm/types'
import * as dp from '@pnpm/dependency-path'
import { symlinkAllModules } from '@pnpm/worker'
import pLimit from 'p-limit'
@@ -159,6 +160,7 @@ export interface HeadlessOptions {
nodeLinker?: 'isolated' | 'hoisted' | 'pnp'
useGitBranchLockfile?: boolean
useLockfile?: boolean
supportedArchitectures?: SupportedArchitectures
}
export interface InstallationResultStats {
@@ -215,6 +217,17 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
}
const skipped = opts.skipped || new Set<string>()
const filterOpts = {
include: opts.include,
registries: opts.registries,
skipped,
currentEngine: opts.currentEngine,
engineStrict: opts.engineStrict,
failOnMissingDependencies: true,
includeIncompatiblePackages: opts.force,
lockfileDir,
supportedArchitectures: opts.supportedArchitectures,
}
let removed = 0
if (opts.nodeLinker !== 'hoisted') {
if (currentLockfile != null && !opts.ignorePackageManifest) {
@@ -234,7 +247,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
skipped,
storeController: opts.storeController,
virtualStoreDir,
wantedLockfile,
wantedLockfile: filterLockfileByEngine(wantedLockfile, filterOpts).lockfile,
}
)
removed = removedDepPaths.size
@@ -251,22 +264,10 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
stage: 'importing_started',
})
const filterOpts = {
include: opts.include,
registries: opts.registries,
skipped,
}
const initialImporterIds = (opts.ignorePackageManifest === true || opts.nodeLinker === 'hoisted')
? Object.keys(wantedLockfile.importers)
: selectedProjects.map(({ id }) => id)
const { lockfile: filteredLockfile, selectedImporterIds: importerIds } = filterLockfileByImportersAndEngine(wantedLockfile, initialImporterIds, {
...filterOpts,
currentEngine: opts.currentEngine,
engineStrict: opts.engineStrict,
failOnMissingDependencies: true,
includeIncompatiblePackages: opts.force,
lockfileDir,
})
const { lockfile: filteredLockfile, selectedImporterIds: importerIds } = filterLockfileByImportersAndEngine(wantedLockfile, initialImporterIds, filterOpts)
if (opts.excludeLinksFromLockfile) {
for (const { id, manifest, rootDir } of selectedProjects) {
if (filteredLockfile.importers[id]) {
@@ -305,6 +306,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
virtualStoreDir,
nodeVersion: opts.currentEngine.nodeVersion,
pnpmVersion: opts.currentEngine.pnpmVersion,
supportedArchitectures: opts.supportedArchitectures,
} as LockfileToDepGraphOptions
const {
directDependenciesByImporterId,
@@ -787,6 +789,7 @@ async function linkAllPkgs (
) {
return Promise.all(
depNodes.map(async (depNode) => {
if (!depNode.fetching) return
let filesResponse!: PackageFilesResponse
try {
filesResponse = (await depNode.fetching()).files

View File

@@ -13,7 +13,7 @@ import {
import { type IncludedDependencies } from '@pnpm/modules-yaml'
import { packageIsInstallable } from '@pnpm/package-is-installable'
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
import { type PatchFile, type Registries } from '@pnpm/types'
import { type SupportedArchitectures, type PatchFile, type Registries } from '@pnpm/types'
import {
type FetchPackageToStoreFunction,
type StoreController,
@@ -47,6 +47,7 @@ export interface LockfileToHoistedDepGraphOptions {
storeController: StoreController
storeDir: string
virtualStoreDir: string
supportedArchitectures?: SupportedArchitectures
}
export async function lockfileToHoistedDepGraph (
@@ -56,7 +57,11 @@ export async function lockfileToHoistedDepGraph (
): Promise<LockfileToDepGraphResult> {
let prevGraph!: DependenciesGraph
if (currentLockfile?.packages != null) {
prevGraph = (await _lockfileToHoistedDepGraph(currentLockfile, opts)).graph
prevGraph = (await _lockfileToHoistedDepGraph(currentLockfile, {
...opts,
force: true,
skipped: new Set(),
})).graph
} else {
prevGraph = {}
}
@@ -181,6 +186,7 @@ async function fetchDeps (
nodeVersion: opts.nodeVersion,
optional: pkgSnapshot.optional === true,
pnpmVersion: opts.pnpmVersion,
supportedArchitectures: opts.supportedArchitectures,
}) === false
) {
opts.skipped.add(depPath)

View File

@@ -231,6 +231,7 @@ async function resolveAndFetch (
nodeVersion: ctx.nodeVersion,
optional: wantedDependency.optional === true,
pnpmVersion: ctx.pnpmVersion,
supportedArchitectures: options.supportedArchitectures,
})
)
)

View File

@@ -30,6 +30,7 @@ import {
type StoreController,
} from '@pnpm/store-controller-types'
import {
type SupportedArchitectures,
type AllowedDeprecatedVersions,
type Dependencies,
type PackageManifest,
@@ -253,6 +254,7 @@ interface ResolvedDependenciesOptions {
updateMatching?: UpdateMatchingFunction
updateDepth: number
prefix: string
supportedArchitectures?: SupportedArchitectures
}
interface PostponedResolutionOpts {
@@ -674,6 +676,7 @@ async function resolveDependenciesOfDependency (
update,
updateDepth,
updateMatching: options.updateMatching,
supportedArchitectures: options.supportedArchitectures,
}
const resolveDependencyResult = await resolveDependency(extendedWantedDep.wantedDependency, ctx, resolveDependencyOpts)
@@ -709,6 +712,7 @@ async function resolveDependenciesOfDependency (
updateDepth,
prefix: options.prefix,
updateMatching: options.updateMatching,
supportedArchitectures: options.supportedArchitectures,
})
return {
resolveDependencyResult,
@@ -756,6 +760,7 @@ async function resolveChildren (
updateDepth,
updateMatching,
prefix,
supportedArchitectures,
}: {
parentPkg: PkgAddress
dependencyLockfile: PackageSnapshot | undefined
@@ -763,6 +768,7 @@ async function resolveChildren (
updateDepth: number
prefix: string
updateMatching?: UpdateMatchingFunction
supportedArchitectures?: SupportedArchitectures
},
{
parentPkgAliases,
@@ -808,6 +814,7 @@ async function resolveChildren (
resolvedDependencies,
updateDepth,
updateMatching,
supportedArchitectures,
}
)
ctx.childrenByParentDepPath[parentPkg.depPath] = pkgAddresses.map((child) => ({
@@ -1011,6 +1018,7 @@ interface ResolveDependencyOptions {
update: boolean
updateDepth: number
updateMatching?: UpdateMatchingFunction
supportedArchitectures?: SupportedArchitectures
}
type ResolveDependencyResult = PkgAddress | LinkedDependency | null
@@ -1083,6 +1091,7 @@ async function resolveDependency (
skipFetch: false,
update: options.update,
workspacePackages: ctx.workspacePackages,
supportedArchitectures: options.supportedArchitectures,
})
} catch (err: any) { // eslint-disable-line
if (wantedDependency.optional) {

View File

@@ -2,6 +2,7 @@ import { type Lockfile, type PatchFile } from '@pnpm/lockfile-types'
import { type PreferredVersions, type Resolution, type WorkspacePackages } from '@pnpm/resolver-base'
import { type StoreController } from '@pnpm/store-controller-types'
import {
type SupportedArchitectures,
type AllowedDeprecatedVersions,
type ProjectManifest,
type ReadPackageHook,
@@ -87,6 +88,7 @@ export interface ResolveDependenciesOptions {
virtualStoreDir: string
wantedLockfile: Lockfile
workspacePackages: WorkspacePackages
supportedArchitectures?: SupportedArchitectures
}
export async function resolveDependencyTree<T> (
@@ -153,6 +155,7 @@ export async function resolveDependencyTree<T> (
updateDepth: -1,
updateMatching: importer.updateMatching,
prefix: importer.rootDir,
supportedArchitectures: opts.supportedArchitectures,
}
return {
updatePackageManifest: importer.updatePackageManifest,

3
pnpm-lock.yaml generated
View File

@@ -6110,6 +6110,9 @@ importers:
'@pnpm/filter-workspace-packages':
specifier: workspace:*
version: 'link:'
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
'@types/is-windows':
specifier: ^1.0.0
version: 1.0.0

View File

@@ -31,7 +31,13 @@ export async function complete (
if (input.currentTypedWordType !== 'option') {
if (input.lastOption === '--filter') {
const workspaceDir = await findWorkspaceDir(process.cwd()) ?? process.cwd()
const allProjects = await findWorkspacePackages(workspaceDir, {})
const allProjects = await findWorkspacePackages(workspaceDir, {
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
})
return allProjects
.filter(({ manifest }) => manifest.name)
.map(({ manifest }) => ({ name: manifest.name }))

View File

@@ -146,7 +146,7 @@ async function packPkg (opts: {
projectDir,
embedReadme,
} = opts
const { manifest } = await readProjectManifest(projectDir, {})
const { manifest } = await readProjectManifest(projectDir)
const bins = [
...(await getBinsFromPackageManifest(manifest as DependencyManifest, projectDir)).map(({ path }) => path),
...(manifest.publishConfig?.executableFiles ?? [])

View File

@@ -1,6 +1,7 @@
import { PnpmError } from '@pnpm/error'
import { type Lockfile } from '@pnpm/lockfile-file'
import {
type SupportedArchitectures,
type DependenciesField,
type IncludedDependencies,
type ProjectManifest,
@@ -74,6 +75,7 @@ export async function findDependencyLicenses (opts: {
registries: Registries
wantedLockfile: Lockfile | null
includedImporterIds?: string[]
supportedArchitectures?: SupportedArchitectures
}): Promise<LicensePackage[]> {
if (opts.wantedLockfile == null) {
throw new PnpmError(
@@ -90,6 +92,7 @@ export async function findDependencyLicenses (opts: {
include: opts.include,
registries: opts.registries,
includedImporterIds: opts.includedImporterIds,
supportedArchitectures: opts.supportedArchitectures,
})
const licensePackages = new Map<string, LicensePackage>()

View File

@@ -5,7 +5,7 @@ import {
lockfileWalkerGroupImporterSteps,
type LockfileWalkerStep,
} from '@pnpm/lockfile-walker'
import { type DependenciesField, type Registries } from '@pnpm/types'
import { type SupportedArchitectures, type DependenciesField, type Registries } from '@pnpm/types'
import { getPkgInfo } from './getPkgInfo'
import mapValues from 'ramda/src/map'
@@ -36,6 +36,7 @@ export interface LicenseExtractOptions {
modulesDir?: string
dir: string
registries: Registries
supportedArchitectures?: SupportedArchitectures
}
export async function lockfileToLicenseNode (
@@ -56,6 +57,7 @@ export async function lockfileToLicenseNode (
}, {
optional: pkgSnapshot.optional ?? false,
lockfileDir: options.dir,
supportedArchitectures: options.supportedArchitectures,
})
// If the package is not installable on the given platform, we ignore the
@@ -137,6 +139,7 @@ export async function lockfileToLicenseNodeTree (
modulesDir: opts.modulesDir,
dir: opts.dir,
registries: opts.registries,
supportedArchitectures: opts.supportedArchitectures,
})
return [importerWalker.importerId, {
dependencies: importerDeps,

View File

@@ -1,5 +1,5 @@
import { readProjectManifestOnly } from '@pnpm/cli-utils'
import { type Config } from '@pnpm/config'
import { type Config, getOptionsFromRootManifest } from '@pnpm/config'
import { PnpmError } from '@pnpm/error'
import { getStorePath } from '@pnpm/store-path'
import { WANTED_LOCKFILE } from '@pnpm/constants'
@@ -47,7 +47,7 @@ export async function licensesList (opts: LicensesCommandOptions) {
optionalDependencies: opts.optional !== false,
}
const manifest = await readProjectManifestOnly(opts.dir, {})
const manifest = await readProjectManifestOnly(opts.dir)
const includedImporterIds = opts.selectedProjectsGraph
? Object.keys(opts.selectedProjectsGraph)
@@ -70,6 +70,7 @@ export async function licensesList (opts: LicensesCommandOptions) {
wantedLockfile: lockfile,
manifest,
includedImporterIds,
supportedArchitectures: getOptionsFromRootManifest(opts.rootProjectManifestDir, opts.rootProjectManifest ?? {}).supportedArchitectures,
})
if (licensePackages.length === 0)

View File

@@ -3,7 +3,7 @@ import { logger, globalInfo, streamParser } from '@pnpm/logger'
import { parseWantedDependency } from '@pnpm/parse-wanted-dependency'
import { pickRegistryForPackage } from '@pnpm/pick-registry-for-package'
import { type StoreController } from '@pnpm/store-controller-types'
import { type Registries } from '@pnpm/types'
import { type SupportedArchitectures, type Registries } from '@pnpm/types'
import { type ReporterFunction } from './types'
export async function storeAdd (
@@ -14,6 +14,7 @@ export async function storeAdd (
reporter?: ReporterFunction
storeController: StoreController
tag?: string
supportedArchitectures?: SupportedArchitectures
}
) {
const reporter = opts?.reporter
@@ -36,6 +37,7 @@ export async function storeAdd (
preferredVersions: {},
projectDir: prefix,
registry: (dep.alias && pickRegistryForPackage(registries, dep.alias)) ?? registries.default,
supportedArchitectures: opts.supportedArchitectures,
})
await pkgResponse.fetching!()
globalInfo(`+ ${pkgResponse.body.id}`)

View File

@@ -13,6 +13,7 @@ import {
type ResolvedFrom,
} from '@pnpm/cafs-types'
import {
type SupportedArchitectures,
type DependencyManifest,
type PackageManifest,
} from '@pnpm/types'
@@ -128,6 +129,7 @@ export interface RequestPackageOptions {
update?: boolean
workspacePackages?: WorkspacePackages
forceResolve?: boolean
supportedArchitectures?: SupportedArchitectures
}
export type BundledManifestFunction = () => Promise<BundledManifest | undefined>

View File

@@ -41,6 +41,7 @@
},
"devDependencies": {
"@pnpm/filter-workspace-packages": "workspace:*",
"@pnpm/types": "workspace:*",
"@types/is-windows": "^1.0.0",
"@types/micromatch": "^4.0.3",
"@types/ramda": "0.28.20",

View File

@@ -1,4 +1,5 @@
import { createMatcher } from '@pnpm/matcher'
import { type SupportedArchitectures } from '@pnpm/types'
import { findWorkspacePackages, type Project } from '@pnpm/workspace.find-packages'
import { createPkgGraph, type Package, type PackageNode } from '@pnpm/workspace.pkgs-graph'
import isSubdir from 'is-subdir'
@@ -42,9 +43,10 @@ export async function readProjects (
engineStrict?: boolean
linkWorkspacePackages?: boolean
changedFilesIgnorePattern?: string[]
supportedArchitectures?: SupportedArchitectures
}
): Promise<ReadProjectsResult> {
const allProjects = await findWorkspacePackages(workspaceDir, { engineStrict: opts?.engineStrict })
const allProjects = await findWorkspacePackages(workspaceDir, { engineStrict: opts?.engineStrict, supportedArchitectures: opts?.supportedArchitectures ?? { os: ['current'], cpu: ['current'], libc: ['current'] } })
const { allProjectsGraph, selectedProjectsGraph } = await filterPkgsBySelectorObjects(
allProjects,
pkgSelectors,
@@ -74,6 +76,7 @@ export async function filterPackagesFromDir (
engineStrict?: boolean
nodeVersion?: string
patterns: string[]
supportedArchitectures?: SupportedArchitectures
}
) {
const allProjects = await findWorkspacePackages(workspaceDir, {
@@ -81,6 +84,7 @@ export async function filterPackagesFromDir (
patterns: opts.patterns,
sharedWorkspaceLockfile: opts.sharedWorkspaceLockfile,
nodeVersion: opts.nodeVersion,
supportedArchitectures: opts.supportedArchitectures,
})
return {
allProjects,

View File

@@ -15,6 +15,9 @@
{
"path": "../../packages/error"
},
{
"path": "../../packages/types"
},
{
"path": "../find-packages"
},

View File

@@ -1,7 +1,7 @@
import path from 'path'
import { packageIsInstallable } from '@pnpm/cli-utils'
import { WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
import { type ProjectManifest, type Project } from '@pnpm/types'
import { type ProjectManifest, type Project, type SupportedArchitectures } from '@pnpm/types'
import { lexCompare } from '@pnpm/util.lex-comparator'
import { findPackages } from '@pnpm/fs.find-packages'
import { logger } from '@pnpm/logger'
@@ -16,11 +16,18 @@ export async function findWorkspacePackages (
nodeVersion?: string
patterns?: string[]
sharedWorkspaceLockfile?: boolean
supportedArchitectures?: SupportedArchitectures
}
): Promise<Project[]> {
const pkgs = await findWorkspacePackagesNoCheck(workspaceRoot, opts)
for (const pkg of pkgs) {
packageIsInstallable(pkg.dir, pkg.manifest, opts ?? {})
packageIsInstallable(pkg.dir, pkg.manifest, opts ?? {
supportedArchitectures: {
os: ['current'],
cpu: ['current'],
libc: ['current'],
},
})
// When setting shared-workspace-lockfile=false, `pnpm` can be set in sub-project's package.json.
if (opts?.sharedWorkspaceLockfile && pkg.dir !== workspaceRoot) {
checkNonRootProjectManifest(pkg)