From b6a30801cf1d43965635658b532f5ada759e6cf5 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Sun, 13 Jul 2025 20:03:52 +0200 Subject: [PATCH] feat: wip --- env/node.fetcher/package.json | 1 + env/node.fetcher/src/index.ts | 43 ++++++++++++------ env/node.fetcher/tsconfig.json | 3 ++ env/plugin-commands-env/src/index.ts | 4 +- env/plugin-commands-env/src/node.ts | 65 ++++++++++++++++++++------- lockfile/types/src/index.ts | 2 + pkg-manager/core/src/install/index.ts | 5 +++ pnpm-lock.yaml | 3 ++ 8 files changed, 96 insertions(+), 30 deletions(-) diff --git a/env/node.fetcher/package.json b/env/node.fetcher/package.json index e1d005b8bb4..8de54c6e8ba 100644 --- a/env/node.fetcher/package.json +++ b/env/node.fetcher/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@pnpm/create-cafs-store": "workspace:*", + "@pnpm/crypto.hash": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/fetcher-base": "workspace:*", "@pnpm/fetching-types": "workspace:*", diff --git a/env/node.fetcher/src/index.ts b/env/node.fetcher/src/index.ts index dd34865fad4..7a74ba62d66 100644 --- a/env/node.fetcher/src/index.ts +++ b/env/node.fetcher/src/index.ts @@ -1,5 +1,6 @@ import fs from 'fs' import path from 'path' +import { createHash } from '@pnpm/crypto.hash' import { PnpmError } from '@pnpm/error' import { type FetchFromRegistry, @@ -26,6 +27,7 @@ export interface FetchNodeOptions { fetchTimeout?: number nodeMirrorBaseUrl?: string retry?: RetryTimeoutOptions + expectedIntegrity?: string } interface NodeArtifactInfo { @@ -33,6 +35,7 @@ interface NodeArtifactInfo { integrity: string isZip: boolean basename: string + versionIntegrity: string } /** @@ -49,18 +52,19 @@ export async function fetchNode ( version: string, targetDir: string, opts: FetchNodeOptions -): Promise { +): Promise { await validateSystemCompatibility() const nodeMirrorBaseUrl = opts.nodeMirrorBaseUrl ?? DEFAULT_NODE_MIRROR_BASE_URL - const artifactInfo = await getNodeArtifactInfo(fetch, version, nodeMirrorBaseUrl) + const artifactInfo = await getNodeArtifactInfo(fetch, version, { nodeMirrorBaseUrl, expectedVersionIntegrity: opts.expectedIntegrity }) if (artifactInfo.isZip) { await downloadAndUnpackZip(fetch, artifactInfo, targetDir) - return + } else { + await downloadAndUnpackTarball(fetch, artifactInfo, targetDir, opts) } - - await downloadAndUnpackTarball(fetch, artifactInfo, targetDir, opts) + await fs.promises.writeFile(path.join(targetDir, 'integrity'), artifactInfo.versionIntegrity, 'utf-8') + return artifactInfo.versionIntegrity } /** @@ -82,18 +86,20 @@ async function validateSystemCompatibility (): Promise { * * @param fetch - Function to fetch resources from registry * @param version - Node.js version - * @param nodeMirrorBaseUrl - Base URL for Node.js mirror * @returns Promise resolving to artifact information * @throws {PnpmError} When integrity file cannot be fetched or parsed */ async function getNodeArtifactInfo ( fetch: FetchFromRegistry, version: string, - nodeMirrorBaseUrl: string + opts: { + nodeMirrorBaseUrl: string + expectedVersionIntegrity?: string + } ): Promise { const tarball = getNodeArtifactAddress({ version, - baseUrl: nodeMirrorBaseUrl, + baseUrl: opts.nodeMirrorBaseUrl, platform: process.platform, arch: process.arch, }) @@ -102,13 +108,14 @@ async function getNodeArtifactInfo ( const shasumsFileUrl = `${tarball.dirname}/SHASUMS256.txt` const url = `${tarball.dirname}/${tarballFileName}` - const integrity = await loadArtifactIntegrity(fetch, shasumsFileUrl, tarballFileName) + const { artifactIntegrity, versionIntegrity } = await loadArtifactIntegrity(fetch, shasumsFileUrl, tarballFileName, opts) return { url, - integrity, + integrity: artifactIntegrity, isZip: tarball.extname === '.zip', basename: tarball.basename, + versionIntegrity, } } @@ -124,8 +131,11 @@ async function getNodeArtifactInfo ( async function loadArtifactIntegrity ( fetch: FetchFromRegistry, integritiesFileUrl: string, - fileName: string -): Promise { + fileName: string, + opts: { + expectedVersionIntegrity?: string + } +): Promise<{ versionIntegrity: string, artifactIntegrity: string }> { const res = await fetch(integritiesFileUrl) if (!res.ok) { throw new PnpmError( @@ -135,6 +145,10 @@ async function loadArtifactIntegrity ( } const body = await res.text() + const versionIntegrity = createHash(body) + if (opts.expectedVersionIntegrity && versionIntegrity !== opts.expectedVersionIntegrity) { + throw new PnpmError('NODE_VERSION_INTEGRITY_MISMATCH', `The integrity check of the ${integritiesFileUrl} file has failed. Expected ${opts.expectedVersionIntegrity}, got: ${versionIntegrity}`) + } const line = body.split('\n').find(line => line.trim().endsWith(` ${fileName}`)) if (!line) { @@ -154,7 +168,10 @@ async function loadArtifactIntegrity ( const buffer = Buffer.from(sha256, 'hex') const base64 = buffer.toString('base64') - return `sha256-${base64}` + return { + versionIntegrity, + artifactIntegrity: `sha256-${base64}`, + } } /** diff --git a/env/node.fetcher/tsconfig.json b/env/node.fetcher/tsconfig.json index d1490551a22..2802e1fe913 100644 --- a/env/node.fetcher/tsconfig.json +++ b/env/node.fetcher/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../../__utils__/prepare" }, + { + "path": "../../crypto/hash" + }, { "path": "../../fetching/fetcher-base" }, diff --git a/env/plugin-commands-env/src/index.ts b/env/plugin-commands-env/src/index.ts index 8c72507c86e..ad2c25428db 100644 --- a/env/plugin-commands-env/src/index.ts +++ b/env/plugin-commands-env/src/index.ts @@ -1,4 +1,4 @@ import * as env from './env' -import { prepareExecutionEnv } from './node' +import { prepareExecutionEnv, resolveRuntime } from './node' -export { env, prepareExecutionEnv } +export { env, prepareExecutionEnv, resolveRuntime } diff --git a/env/plugin-commands-env/src/node.ts b/env/plugin-commands-env/src/node.ts index ccdd8efb8b6..dd0f0cefdd1 100644 --- a/env/plugin-commands-env/src/node.ts +++ b/env/plugin-commands-env/src/node.ts @@ -12,6 +12,7 @@ import loadJsonFile from 'load-json-file' import writeJsonFile from 'write-json-file' import { getNodeMirror } from './getNodeMirror' import { isValidVersion, parseNodeSpecifier } from './parseNodeSpecifier' +import {version} from 'os' export type NvmNodeCommandOptions = Pick> = {} +const nodeFetchPromises: Record> = {} -export async function prepareExecutionEnv (config: NvmNodeCommandOptions, { extraBinPaths, executionEnv }: PrepareExecutionEnvOptions): Promise { +export async function prepareExecutionEnv ( + config: NvmNodeCommandOptions, + { extraBinPaths, executionEnv }: PrepareExecutionEnvOptions +): Promise { if (!executionEnv?.nodeVersion || `v${executionEnv.nodeVersion}` === getSystemNodeVersion()) { return { extraBinPaths: extraBinPaths ?? [] } } - let nodePathPromise = nodeFetchPromises[executionEnv.nodeVersion] + const { dir } = await resolveRuntime(config, executionEnv.nodeVersion) + return { + extraBinPaths: [dir, ...extraBinPaths ?? []], + } +} + +export interface GetNodeBinDir { + dir: string + integrity: string +} + +export async function resolveRuntime ( + config: NvmNodeCommandOptions, + nodeVersion: string, + opts?: { + expectedVersionIntegrity?: string + } +) { + let nodePathPromise = nodeFetchPromises[nodeVersion] if (!nodePathPromise) { nodePathPromise = getNodeBinDir({ ...config, - useNodeVersion: executionEnv.nodeVersion, + useNodeVersion: nodeVersion, }) - nodeFetchPromises[executionEnv.nodeVersion] = nodePathPromise + nodeFetchPromises[nodeVersion] = nodePathPromise } - return { - extraBinPaths: [await nodePathPromise, ...extraBinPaths ?? []], - } + return nodePathPromise } -export async function getNodeBinDir (opts: NvmNodeCommandOptions): Promise { +export async function getNodeBinDir (opts: NvmNodeCommandOptions): Promise { const fetch = createFetchFromRegistry(opts) const nodesDir = getNodeVersionsBaseDir(opts.pnpmHomeDir) const manifestNodeVersion = (await readNodeVersionsManifest(nodesDir))?.default @@ -84,30 +104,37 @@ export async function getNodeBinDir (opts: NvmNodeCommandOptions): Promise { +export async function getNodeDir ( + fetch: FetchFromRegistry, + opts: NvmNodeCommandOptions & { useNodeVersion: string, nodeMirrorBaseUrl: string } +): Promise<{ versionIntegrity: string, versionDir: string }> { const nodesDir = getNodeVersionsBaseDir(opts.pnpmHomeDir) await fs.promises.mkdir(nodesDir, { recursive: true }) const versionDir = path.join(nodesDir, opts.useNodeVersion) - if (!fs.existsSync(versionDir)) { + let versionIntegrity = await readIntegrityFile(versionDir) + if (versionIntegrity == null) { const storeDir = await getStorePath({ pkgRoot: process.cwd(), storePath: opts.storeDir, pnpmHomeDir: opts.pnpmHomeDir, }) globalInfo(`Fetching Node.js ${opts.useNodeVersion} ...`) - await fetchNode(fetch, opts.useNodeVersion, versionDir, { + versionIntegrity = await fetchNode(fetch, opts.useNodeVersion, versionDir, { ...opts, storeDir, retry: { @@ -118,7 +145,15 @@ export async function getNodeDir (fetch: FetchFromRegistry, opts: NvmNodeCommand }, }) } - return versionDir + return { versionIntegrity, versionDir } +} + +async function readIntegrityFile (versionDir: string): Promise { + try { + return fs.promises.readFile(path.join(versionDir, 'integrity'), 'utf8') + } catch { + return null + } } async function readNodeVersionsManifest (nodesDir: string): Promise<{ default?: string }> { diff --git a/lockfile/types/src/index.ts b/lockfile/types/src/index.ts index 805bf61aea9..f0b68566972 100644 --- a/lockfile/types/src/index.ts +++ b/lockfile/types/src/index.ts @@ -27,6 +27,7 @@ export interface LockfileBase { export interface LockfileObject extends LockfileBase { importers: Record packages?: PackageSnapshots + runtimes?: Record } export interface LockfilePackageSnapshot { @@ -73,6 +74,7 @@ export interface ProjectSnapshot extends ProjectSnapshotBase { dependencies?: ResolvedDependencies optionalDependencies?: ResolvedDependencies devDependencies?: ResolvedDependencies + runtimeDependencies?: Record } export type ResolvedDependenciesOfImporters = Record diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index 07040273447..30f1ae9f5f3 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -280,6 +280,11 @@ export async function mutateModules ( await safeReadProjectManifestOnly(opts.lockfileDir) let ctx = await getContext(opts) + for (const project of Object.values(ctx.projects)) { + if (project.manifest.pnpm?.executionEnv?.nodeVersion) { + const {} = await resolveRuntime() + } + } if (!opts.lockfileOnly && ctx.modulesFile != null) { const { purged } = await validateModules(ctx.modulesFile, Object.values(ctx.projects), { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 755716a7dd1..c3f0357869b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2051,6 +2051,9 @@ importers: '@pnpm/create-cafs-store': specifier: workspace:* version: link:../../store/create-cafs-store + '@pnpm/crypto.hash': + specifier: workspace:* + version: link:../../crypto/hash '@pnpm/error': specifier: workspace:* version: link:../../packages/error