@pnpm/exe 12.0.0-rc.4 → 12.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +42 -0
- package/bin/pnpm.mjs +141 -0
- package/bin/pnpx.mjs +9 -0
- package/dist/node_modules/.bin/get-pnpm +50 -0
- package/dist/node_modules/.package-map.json +1 -1
- package/dist/node_modules/.pnpm-workspace-state-v1.json +5 -4
- package/dist/node_modules/get-pnpm/bin/get-pnpm.js +11 -0
- package/dist/node_modules/get-pnpm/lib/downloadExecutable.js +99 -0
- package/dist/node_modules/get-pnpm/lib/extractTarball.js +23 -0
- package/dist/node_modules/get-pnpm/lib/extractTarballMember.js +128 -0
- package/dist/node_modules/get-pnpm/lib/index.js +208 -0
- package/dist/node_modules/get-pnpm/lib/npmSigningKeys.js +23 -0
- package/dist/node_modules/get-pnpm/lib/platformPackageName.js +60 -0
- package/dist/node_modules/get-pnpm/lib/registry.js +120 -0
- package/dist/node_modules/get-pnpm/lib/resolveVersion.js +36 -0
- package/dist/node_modules/get-pnpm/lib/sameFileContents.js +45 -0
- package/dist/node_modules/get-pnpm/lib/verifySignature.js +51 -0
- package/dist/node_modules/get-pnpm/package.json +45 -0
- package/dist/node_modules/node-gyp/.release-please-manifest.json +1 -1
- package/dist/node_modules/node-gyp/gyp/.release-please-manifest.json +1 -1
- package/dist/node_modules/node-gyp/gyp/pyproject.toml +4 -4
- package/dist/node_modules/node-gyp/lib/download.js +3 -3
- package/dist/node_modules/node-gyp/package.json +1 -1
- package/install.js +20 -97
- package/native-binary.mjs +116 -0
- package/package.json +13 -10
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { extractTarball } from './extractTarball.js';
|
|
6
|
+
import { isMusl, platformPackageName } from './platformPackageName.js';
|
|
7
|
+
import { downloadTarball, fetchPackument, fetchVersionMeta, registryFromEnv } from './registry.js';
|
|
8
|
+
import { majorVersion, resolveVersion } from './resolveVersion.js';
|
|
9
|
+
import { verifyRegistrySignature } from './verifySignature.js';
|
|
10
|
+
export { downloadPnpmExecutable } from './downloadExecutable.js';
|
|
11
|
+
export { extractTarballMember } from './extractTarballMember.js';
|
|
12
|
+
export { isMusl, platformPackageName } from './platformPackageName.js';
|
|
13
|
+
export { DEFAULT_REGISTRY, registryFromEnv } from './registry.js';
|
|
14
|
+
export { majorVersion, resolveVersion } from './resolveVersion.js';
|
|
15
|
+
export { verifyRegistrySignature } from './verifySignature.js';
|
|
16
|
+
/**
|
|
17
|
+
* The package whose dist-tags name every pnpm release. `@pnpm/exe` carries the
|
|
18
|
+
* same versions today, but only `pnpm` is published from v12 onward, so its
|
|
19
|
+
* tags are the ones that cannot go stale.
|
|
20
|
+
*/
|
|
21
|
+
const CLI_PKG_NAME = 'pnpm';
|
|
22
|
+
/** Holds the unpacked tarballs while the installation is assembled beside it. */
|
|
23
|
+
const UNPACK_DIR = '.unpack';
|
|
24
|
+
/** Where the `dist/` tree that ships beside the executable is published. */
|
|
25
|
+
function wrapperPackageName(major) {
|
|
26
|
+
return major >= 12 ? CLI_PKG_NAME : '@pnpm/exe';
|
|
27
|
+
}
|
|
28
|
+
const USAGE = `Usage: npx get-pnpm [version]
|
|
29
|
+
|
|
30
|
+
Installs pnpm as a standalone executable and adds it to your PATH.
|
|
31
|
+
|
|
32
|
+
Arguments:
|
|
33
|
+
version An exact version (11.20.0), a major (12), or a dist-tag
|
|
34
|
+
(latest, next-12). Defaults to $PNPM_VERSION, then "latest".
|
|
35
|
+
|
|
36
|
+
Environment variables:
|
|
37
|
+
PNPM_VERSION Version to install when no argument is given.
|
|
38
|
+
PNPM_HOME Directory to install pnpm into.
|
|
39
|
+
npm_config_registry Registry to download pnpm from.
|
|
40
|
+
`;
|
|
41
|
+
export async function runCli(argv) {
|
|
42
|
+
const positional = [];
|
|
43
|
+
for (const arg of argv) {
|
|
44
|
+
if (arg === '--help' || arg === '-h') {
|
|
45
|
+
console.log(USAGE);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
if (arg.startsWith('-')) {
|
|
49
|
+
throw new Error(`Unknown option "${arg}".\n\n${USAGE}`);
|
|
50
|
+
}
|
|
51
|
+
positional.push(arg);
|
|
52
|
+
}
|
|
53
|
+
if (positional.length > 1) {
|
|
54
|
+
throw new Error(`Expected at most one version, got ${positional.length}.\n\n${USAGE}`);
|
|
55
|
+
}
|
|
56
|
+
return installPnpm({
|
|
57
|
+
versionSpec: positional[0] ?? process.env.PNPM_VERSION ?? 'latest',
|
|
58
|
+
registry: registryFromEnv(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Downloads the pnpm executable and hands over to `pnpm setup`, which installs
|
|
63
|
+
* it globally and puts it on the PATH.
|
|
64
|
+
*
|
|
65
|
+
* Every download is checked against the checksum the registry published for it,
|
|
66
|
+
* and that checksum against npm's signature — see `verifyRegistrySignature`.
|
|
67
|
+
*
|
|
68
|
+
* The temporary directory is assembled to look like the release tarball that
|
|
69
|
+
* https://get.pnpm.io/install.sh downloads — the executable next to its `dist/`
|
|
70
|
+
* tree — because `pnpm setup` installs that directory as-is.
|
|
71
|
+
*
|
|
72
|
+
* @returns the exit code of `pnpm setup`.
|
|
73
|
+
*/
|
|
74
|
+
export async function installPnpm(opts) {
|
|
75
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-install-'));
|
|
76
|
+
const removeTmpDir = () => {
|
|
77
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
78
|
+
};
|
|
79
|
+
// Registered for the duration of the call and no longer: this is also a
|
|
80
|
+
// library function, and handlers left behind would accumulate and outlive the
|
|
81
|
+
// directory they exist to clean up.
|
|
82
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
83
|
+
const onSignal = () => {
|
|
84
|
+
removeTmpDir();
|
|
85
|
+
process.exit(1);
|
|
86
|
+
};
|
|
87
|
+
for (const signal of signals) {
|
|
88
|
+
process.once(signal, onSignal);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const { binPath } = await downloadPnpm({ ...opts, dest: tmpDir });
|
|
92
|
+
const { error, status } = spawnSync(binPath, ['setup', '--force'], { stdio: 'inherit' });
|
|
93
|
+
if (error != null)
|
|
94
|
+
throw error;
|
|
95
|
+
return status ?? 1;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
for (const signal of signals) {
|
|
99
|
+
process.off(signal, onSignal);
|
|
100
|
+
}
|
|
101
|
+
removeTmpDir();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Downloads the pnpm executable into `dest`, laid out the way the release
|
|
106
|
+
* tarball lays it out — the executable next to the `dist/` tree it loads.
|
|
107
|
+
*
|
|
108
|
+
* Every download is checked against the checksum the registry published for it,
|
|
109
|
+
* and that checksum against npm's signature; see `verifyRegistrySignature`.
|
|
110
|
+
* Nothing outside `dest` is touched, so a caller that manages its own PATH — a
|
|
111
|
+
* CI action, say — can use this without the global install `installPnpm` does.
|
|
112
|
+
*
|
|
113
|
+
* @returns the version installed and the path to the executable.
|
|
114
|
+
*/
|
|
115
|
+
export async function downloadPnpm(opts) {
|
|
116
|
+
const packument = await fetchPackument(opts.registry, CLI_PKG_NAME);
|
|
117
|
+
const version = resolveVersion(packument, opts.versionSpec);
|
|
118
|
+
const major = majorVersion(version);
|
|
119
|
+
const platformPkgName = platformPackageName({
|
|
120
|
+
major,
|
|
121
|
+
platform: process.platform,
|
|
122
|
+
arch: process.arch,
|
|
123
|
+
musl: isMusl(),
|
|
124
|
+
});
|
|
125
|
+
const { dest } = opts;
|
|
126
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
127
|
+
try {
|
|
128
|
+
console.log(`==> Downloading pnpm ${version}`);
|
|
129
|
+
const executable = process.platform === 'win32' ? 'pnpm.exe' : 'pnpm';
|
|
130
|
+
const fetchPackage = verifiedPackageFetcher({ dir: dest, registry: opts.registry, version, keys: opts.keys });
|
|
131
|
+
// Settled, not `all`: a rejection there would leave the other fetch writing
|
|
132
|
+
// into the directory the `finally` below is about to remove.
|
|
133
|
+
const [platformResult, wrapperResult] = await Promise.allSettled([
|
|
134
|
+
fetchPackage(platformPkgName),
|
|
135
|
+
// v11 was the first release to keep files next to the executable; up to
|
|
136
|
+
// v10 the executable is self-contained and ships no `dist/`.
|
|
137
|
+
major >= 11 ? fetchPackage(wrapperPackageName(major)) : Promise.resolve(undefined),
|
|
138
|
+
]);
|
|
139
|
+
if (platformResult.status === 'rejected')
|
|
140
|
+
throw platformResult.reason;
|
|
141
|
+
if (wrapperResult.status === 'rejected')
|
|
142
|
+
throw wrapperResult.reason;
|
|
143
|
+
const platformPkg = platformResult.value;
|
|
144
|
+
const wrapperPkg = wrapperResult.value;
|
|
145
|
+
const binPath = path.join(dest, executable);
|
|
146
|
+
fs.renameSync(path.join(platformPkg.dir, executable), binPath);
|
|
147
|
+
fs.chmodSync(binPath, 0o755);
|
|
148
|
+
if (wrapperPkg != null) {
|
|
149
|
+
fs.rmSync(path.join(dest, 'dist'), { recursive: true, force: true });
|
|
150
|
+
fs.renameSync(path.join(wrapperPkg.dir, 'dist'), path.join(dest, 'dist'));
|
|
151
|
+
writeManifest({ dest, executable, version, wrapperDir: wrapperPkg.dir, major });
|
|
152
|
+
}
|
|
153
|
+
return { version, binPath };
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
// `pnpm setup` installs this directory as a package, so nothing may be left
|
|
157
|
+
// in it that does not belong in the installation.
|
|
158
|
+
fs.rmSync(path.join(dest, UNPACK_DIR), { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* `pnpm setup` installs the directory as a package, writing a minimal manifest
|
|
163
|
+
* when there is none — which is what the release tarball relies on. That
|
|
164
|
+
* tarball bundles the runtime dependencies inside `dist/`; the registry copy
|
|
165
|
+
* declares them instead, so up to v11 they have to be declared here or the
|
|
166
|
+
* install silently loses them (`@reflink/reflink`, and with it copy-on-write
|
|
167
|
+
* cloning). From v12 the `dist/` tree is self-contained again, so the manifest
|
|
168
|
+
* `setup` writes is left to it.
|
|
169
|
+
*/
|
|
170
|
+
function writeManifest(opts) {
|
|
171
|
+
if (opts.major >= 12)
|
|
172
|
+
return;
|
|
173
|
+
const wrapper = JSON.parse(fs.readFileSync(path.join(opts.wrapperDir, 'package.json'), 'utf8'));
|
|
174
|
+
if (wrapper.dependencies == null)
|
|
175
|
+
return;
|
|
176
|
+
fs.writeFileSync(path.join(opts.dest, 'package.json'), JSON.stringify({
|
|
177
|
+
name: '@pnpm/exe',
|
|
178
|
+
version: opts.version,
|
|
179
|
+
type: 'module',
|
|
180
|
+
bin: { pnpm: opts.executable, pn: opts.executable },
|
|
181
|
+
dependencies: wrapper.dependencies,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
/** Downloads packages of one version into one directory, verifying each. */
|
|
185
|
+
function verifiedPackageFetcher(opts) {
|
|
186
|
+
return async function fetchPackage(pkgName) {
|
|
187
|
+
const meta = await fetchVersionMeta(opts.registry, pkgName, opts.version);
|
|
188
|
+
if (!meta.dist.integrity) {
|
|
189
|
+
throw new Error(`The npm registry published no checksum for ${pkgName}@${opts.version}, so it cannot be verified.`);
|
|
190
|
+
}
|
|
191
|
+
verifyRegistrySignature({
|
|
192
|
+
name: pkgName,
|
|
193
|
+
version: opts.version,
|
|
194
|
+
integrity: meta.dist.integrity,
|
|
195
|
+
signatures: meta.dist.signatures,
|
|
196
|
+
keys: opts.keys,
|
|
197
|
+
});
|
|
198
|
+
// Unpack away from the directory being assembled: `pnpm` is both a package
|
|
199
|
+
// name and the name of the executable that ends up beside it.
|
|
200
|
+
const unpackDir = path.join(opts.dir, UNPACK_DIR, pkgName.replaceAll('/', '-'));
|
|
201
|
+
const tarball = `${unpackDir}.tgz`;
|
|
202
|
+
fs.mkdirSync(path.dirname(tarball), { recursive: true });
|
|
203
|
+
await downloadTarball(meta, tarball, { registry: opts.registry });
|
|
204
|
+
extractTarball(tarball, unpackDir);
|
|
205
|
+
fs.rmSync(tarball);
|
|
206
|
+
return { dir: path.join(unpackDir, 'package') };
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// GENERATED — npm's public registry signing keys, mirrored from
|
|
3
|
+
// https://registry.npmjs.org/-/npm/v1/keys
|
|
4
|
+
//
|
|
5
|
+
// Refresh with: node scripts/update-npm-keys.mjs --update
|
|
6
|
+
// A scheduled workflow runs the check, so a rotation arrives as a pull request
|
|
7
|
+
// rather than as a failed install.
|
|
8
|
+
export const NPM_SIGNING_KEYS = [
|
|
9
|
+
{
|
|
10
|
+
"expires": null,
|
|
11
|
+
"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
|
|
12
|
+
"keytype": "ecdsa-sha2-nistp256",
|
|
13
|
+
"scheme": "ecdsa-sha2-nistp256",
|
|
14
|
+
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g=="
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"expires": "2025-01-29T00:00:00.000Z",
|
|
18
|
+
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
|
|
19
|
+
"keytype": "ecdsa-sha2-nistp256",
|
|
20
|
+
"scheme": "ecdsa-sha2-nistp256",
|
|
21
|
+
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg=="
|
|
22
|
+
}
|
|
23
|
+
];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name of the npm package that carries the pnpm executable for `target`.
|
|
3
|
+
*
|
|
4
|
+
* pnpm ships the binary in a per-host package that `@pnpm/exe` lists as an
|
|
5
|
+
* optional dependency. The naming scheme changed with the Rust rewrite: v12
|
|
6
|
+
* publishes `@pnpm/exe.<process.platform>-<arch>[-musl]`, while v11 and older
|
|
7
|
+
* publish `@pnpm/<macos|win|linux|linuxstatic>-<arch>`.
|
|
8
|
+
*
|
|
9
|
+
* @throws if pnpm publishes no binary for the host — either because the
|
|
10
|
+
* architecture was never supported, or because of the v11-only Intel macOS gap.
|
|
11
|
+
*/
|
|
12
|
+
export function platformPackageName({ major, platform, arch, musl }) {
|
|
13
|
+
if (arch !== 'x64' && arch !== 'arm64') {
|
|
14
|
+
throw new Error('Sorry! pnpm currently only provides pre-built binaries for x86_64/arm64 architectures.');
|
|
15
|
+
}
|
|
16
|
+
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
|
|
17
|
+
throw new Error(`Sorry! pnpm does not provide a pre-built binary for ${platform}.`);
|
|
18
|
+
}
|
|
19
|
+
if (platform === 'darwin' && arch === 'x64' && major === 11) {
|
|
20
|
+
throw new Error(`pnpm v11 does not provide a working binary for Intel macOS (darwin-x64) due to an upstream Node.js SEA bug.
|
|
21
|
+
|
|
22
|
+
Install pnpm a different way instead:
|
|
23
|
+
npx get-pnpm 12 # pnpm v12 ships an Intel macOS binary
|
|
24
|
+
npm install -g pnpm # uses your system Node.js
|
|
25
|
+
brew install pnpm # via Homebrew
|
|
26
|
+
|
|
27
|
+
More context: https://github.com/pnpm/pnpm/issues/11423`);
|
|
28
|
+
}
|
|
29
|
+
const linuxMusl = platform === 'linux' && musl;
|
|
30
|
+
if (major >= 12) {
|
|
31
|
+
return `@pnpm/exe.${platform}-${arch}${linuxMusl ? '-musl' : ''}`;
|
|
32
|
+
}
|
|
33
|
+
return `@pnpm/${legacyOsSegment(platform, linuxMusl)}-${arch}`;
|
|
34
|
+
}
|
|
35
|
+
function legacyOsSegment(platform, isMusl) {
|
|
36
|
+
switch (platform) {
|
|
37
|
+
case 'darwin': return 'macos';
|
|
38
|
+
case 'win32': return 'win';
|
|
39
|
+
default: return isMusl ? 'linuxstatic' : 'linux';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether this host's libc is musl rather than glibc.
|
|
44
|
+
*
|
|
45
|
+
* Probed the way pnpm's own wrappers do it rather than through a dependency:
|
|
46
|
+
* glibc builds report a `glibcVersionRuntime`, musl builds leave it unset.
|
|
47
|
+
* Keeping this package dependency-free means `npx` fetches one thing before
|
|
48
|
+
* pnpm exists.
|
|
49
|
+
*/
|
|
50
|
+
export function isMusl() {
|
|
51
|
+
if (process.platform !== 'linux')
|
|
52
|
+
return false;
|
|
53
|
+
try {
|
|
54
|
+
const report = process.report?.getReport();
|
|
55
|
+
return report?.header != null && !report.header.glibcVersionRuntime;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
const ABBREVIATED_PACKUMENT = 'application/vnd.npm.install-v1+json';
|
|
5
|
+
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
|
|
6
|
+
/** The registry npx/npm is configured with, so a mirror stays a mirror. */
|
|
7
|
+
export function registryFromEnv() {
|
|
8
|
+
return normalizeRegistry(process.env.npm_config_registry ?? process.env.NPM_CONFIG_REGISTRY ?? DEFAULT_REGISTRY);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A registry URL that can be used as a base for a relative path.
|
|
12
|
+
*
|
|
13
|
+
* `new URL('pkg', 'https://mirror.example.com/npm')` drops the last segment,
|
|
14
|
+
* so a registry served from a subpath needs its trailing slash to survive.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeRegistry(registry) {
|
|
17
|
+
return registry.endsWith('/') ? registry : `${registry}/`;
|
|
18
|
+
}
|
|
19
|
+
// Node's fetch applies no timeout of its own: a registry that accepts the
|
|
20
|
+
// connection and then stalls would hang the install with no output. Metadata is
|
|
21
|
+
// small and quick; the tarball budget has to survive a slow connection carrying
|
|
22
|
+
// a ~150 MB download.
|
|
23
|
+
const METADATA_TIMEOUT_MS = 30_000;
|
|
24
|
+
const TARBALL_TIMEOUT_MS = 15 * 60_000;
|
|
25
|
+
export async function fetchPackument(registry, pkgName, headers) {
|
|
26
|
+
return fetchJson(new URL(pkgName, registry), ABBREVIATED_PACKUMENT, headers);
|
|
27
|
+
}
|
|
28
|
+
export async function fetchVersionMeta(registry, pkgName, version, headers) {
|
|
29
|
+
return fetchJson(new URL(`${pkgName}/${version}`, registry), 'application/json', headers);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Streams `meta.dist.tarball` to `dest`, verifying the checksum the registry
|
|
33
|
+
* published for it. A mismatch removes nothing — the caller discards the whole
|
|
34
|
+
* temporary directory.
|
|
35
|
+
*
|
|
36
|
+
* `registry` re-hosts a tarball URL that points at npm onto that registry, so a
|
|
37
|
+
* mirror that answered the metadata request serves the download too; `headers`
|
|
38
|
+
* travel only to the registry's own origin, never to a download host it names.
|
|
39
|
+
*/
|
|
40
|
+
export async function downloadTarball(meta, dest, opts = {}) {
|
|
41
|
+
const url = tarballUrl(meta, opts.registry);
|
|
42
|
+
const response = await request(url, undefined, TARBALL_TIMEOUT_MS, headersFor(url, opts));
|
|
43
|
+
const [algorithm, expected] = checksum(meta);
|
|
44
|
+
const hash = createHash(algorithm);
|
|
45
|
+
const body = response.body;
|
|
46
|
+
await pipeline(async function* () {
|
|
47
|
+
for await (const chunk of body) {
|
|
48
|
+
hash.update(chunk);
|
|
49
|
+
yield chunk;
|
|
50
|
+
}
|
|
51
|
+
}, createWriteStream(dest));
|
|
52
|
+
const actual = hash.digest('base64');
|
|
53
|
+
if (actual !== expected) {
|
|
54
|
+
throw new Error(`The download from ${url.href} does not match the checksum the npm registry published for it. Refusing to install.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Where to download `meta`'s tarball from.
|
|
59
|
+
*
|
|
60
|
+
* Registries that proxy npm hand back npm's own URL. Following it would leave
|
|
61
|
+
* the mirror the metadata came from — for an air-gapped one, it would not
|
|
62
|
+
* resolve at all — so the path is re-hosted onto `registry`. Matched by origin,
|
|
63
|
+
* so a host that merely starts with npm's is left alone.
|
|
64
|
+
*/
|
|
65
|
+
export function tarballUrl(meta, registry) {
|
|
66
|
+
const url = new URL(meta.dist.tarball);
|
|
67
|
+
if (registry == null || url.origin !== new URL(DEFAULT_REGISTRY).origin)
|
|
68
|
+
return url;
|
|
69
|
+
return new URL(`${url.pathname.replace(/^\//, '')}${url.search}`, normalizeRegistry(registry));
|
|
70
|
+
}
|
|
71
|
+
function headersFor(url, opts) {
|
|
72
|
+
if (opts.headers == null || opts.registry == null)
|
|
73
|
+
return undefined;
|
|
74
|
+
return url.origin === new URL(opts.registry).origin ? opts.headers : undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The algorithm and digest to check a tarball against.
|
|
78
|
+
*
|
|
79
|
+
* `integrity` is an SRI string, which may hold several space-separated entries;
|
|
80
|
+
* the first is used. Only the digest that the registry signature covers is
|
|
81
|
+
* accepted — `shasum` is SHA-1, and a package without `integrity` has already
|
|
82
|
+
* been refused before any download starts.
|
|
83
|
+
*/
|
|
84
|
+
function checksum(meta) {
|
|
85
|
+
const entry = meta.dist.integrity?.trim().split(/\s+/)[0];
|
|
86
|
+
if (!entry) {
|
|
87
|
+
throw new Error(`The registry published no checksum for ${meta.dist.tarball}, so it cannot be verified.`);
|
|
88
|
+
}
|
|
89
|
+
const separator = entry.indexOf('-');
|
|
90
|
+
if (separator === -1) {
|
|
91
|
+
throw new Error(`The registry published an unreadable checksum for ${meta.dist.tarball}: ${entry}`);
|
|
92
|
+
}
|
|
93
|
+
return [entry.slice(0, separator), entry.slice(separator + 1)];
|
|
94
|
+
}
|
|
95
|
+
async function fetchJson(url, accept, headers) {
|
|
96
|
+
const response = await request(url, accept, METADATA_TIMEOUT_MS, headers);
|
|
97
|
+
return await response.json();
|
|
98
|
+
}
|
|
99
|
+
async function request(url, accept, timeoutMs, headers) {
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetch(url, {
|
|
103
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
104
|
+
headers: { ...headers, ...(accept ? { accept } : {}) },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const reason = err.name === 'TimeoutError'
|
|
109
|
+
? `timed out after ${Math.round(timeoutMs / 1000)}s`
|
|
110
|
+
: err.message;
|
|
111
|
+
throw new Error(`Could not reach ${url.href}: ${reason}`, { cause: err });
|
|
112
|
+
}
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`Could not download ${url.href}: ${response.status} ${response.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
if (response.body == null) {
|
|
117
|
+
throw new Error(`Empty response from ${url.href}`);
|
|
118
|
+
}
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns what the user asked for into a concrete pnpm version.
|
|
3
|
+
*
|
|
4
|
+
* `spec` may be a dist-tag (`latest`, `next-12`), an exact version (`11.20.0`,
|
|
5
|
+
* with an optional leading `v`), or a bare major (`12`), which picks that
|
|
6
|
+
* major's stable release and falls back to its prerelease lane.
|
|
7
|
+
*
|
|
8
|
+
* Dist-tags win over exact versions, matching `install.ps1`.
|
|
9
|
+
*
|
|
10
|
+
* @throws if `spec` matches neither a dist-tag nor a published version.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveVersion(packument, spec) {
|
|
13
|
+
const distTags = packument['dist-tags'];
|
|
14
|
+
if (distTags[spec])
|
|
15
|
+
return distTags[spec];
|
|
16
|
+
const version = spec.startsWith('v') ? spec.slice(1) : spec;
|
|
17
|
+
if (packument.versions[version])
|
|
18
|
+
return version;
|
|
19
|
+
if (/^\d+$/.test(version)) {
|
|
20
|
+
const majorTag = distTags[`latest-${version}`] ?? distTags[`next-${version}`];
|
|
21
|
+
if (majorTag)
|
|
22
|
+
return majorTag;
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Sorry! pnpm version "${spec}" could not be found. Available tags: ${Object.keys(distTags).sort().join(', ')}`);
|
|
25
|
+
}
|
|
26
|
+
/** The major of an exact version, as the package layout depends on it. */
|
|
27
|
+
export function majorVersion(version) {
|
|
28
|
+
const field = version.split('.')[0] ?? '';
|
|
29
|
+
// Digits only: `Number` reads '' as 0 and '0x10' as 16, and a caller-supplied
|
|
30
|
+
// version that means neither should say so here rather than fail later as a
|
|
31
|
+
// package name nobody publishes.
|
|
32
|
+
if (!/^\d+$/.test(field)) {
|
|
33
|
+
throw new Error(`Could not read a major version from "${version}".`);
|
|
34
|
+
}
|
|
35
|
+
return Number(field);
|
|
36
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
const CHUNK_SIZE = 64 * 1024;
|
|
3
|
+
/**
|
|
4
|
+
* Whether two paths hold the same bytes.
|
|
5
|
+
*
|
|
6
|
+
* Compared rather than hashed: the answer is usually "no" at the first
|
|
7
|
+
* differing byte, and there is nothing to gain from reading further.
|
|
8
|
+
*
|
|
9
|
+
* `b` is the untrusted side — a path something else may hold — so anything
|
|
10
|
+
* other than a readable regular file of the same size is simply not the same
|
|
11
|
+
* file, rather than an error.
|
|
12
|
+
*/
|
|
13
|
+
export function sameFileContents(a, b) {
|
|
14
|
+
const sizeA = fs.statSync(a).size;
|
|
15
|
+
const statB = fs.lstatSync(b, { throwIfNoEntry: false });
|
|
16
|
+
if (statB?.isFile() !== true || statB.size !== sizeA)
|
|
17
|
+
return false;
|
|
18
|
+
let fdA;
|
|
19
|
+
let fdB;
|
|
20
|
+
try {
|
|
21
|
+
fdA = fs.openSync(a, 'r');
|
|
22
|
+
fdB = fs.openSync(b, 'r');
|
|
23
|
+
const bufferA = Buffer.alloc(CHUNK_SIZE);
|
|
24
|
+
const bufferB = Buffer.alloc(CHUNK_SIZE);
|
|
25
|
+
while (true) {
|
|
26
|
+
const readA = fs.readSync(fdA, bufferA, 0, CHUNK_SIZE, null);
|
|
27
|
+
const readB = fs.readSync(fdB, bufferB, 0, CHUNK_SIZE, null);
|
|
28
|
+
if (readA !== readB)
|
|
29
|
+
return false;
|
|
30
|
+
if (readA === 0)
|
|
31
|
+
return true;
|
|
32
|
+
if (!bufferA.subarray(0, readA).equals(bufferB.subarray(0, readB)))
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (fdA !== undefined)
|
|
41
|
+
fs.closeSync(fdA);
|
|
42
|
+
if (fdB !== undefined)
|
|
43
|
+
fs.closeSync(fdB);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createVerify } from 'node:crypto';
|
|
2
|
+
import { NPM_SIGNING_KEYS } from './npmSigningKeys.js';
|
|
3
|
+
/**
|
|
4
|
+
* Checks the registry's signature over a package's identity and checksum.
|
|
5
|
+
*
|
|
6
|
+
* The registry serves both the tarball and the checksum, so a checksum taken
|
|
7
|
+
* from it proves nothing on its own. This is what makes it worth anything: the
|
|
8
|
+
* signature is made with a key the registry publishes but the download host
|
|
9
|
+
* cannot mint, and the trusted copy of that key ships inside this package.
|
|
10
|
+
*
|
|
11
|
+
* @throws if the package is unsigned, signed with a key that isn't trusted or
|
|
12
|
+
* has expired, or the signature does not verify.
|
|
13
|
+
*/
|
|
14
|
+
export function verifyRegistrySignature(opts) {
|
|
15
|
+
const pkg = `${opts.name}@${opts.version}`;
|
|
16
|
+
const signatures = opts.signatures ?? [];
|
|
17
|
+
if (signatures.length === 0) {
|
|
18
|
+
throw new Error(`${pkg} carries no npm registry signature, so it cannot be verified.`);
|
|
19
|
+
}
|
|
20
|
+
// Pick the signature by key rather than by position. A package can carry
|
|
21
|
+
// several, in no guaranteed order, and across a rotation one of them can be
|
|
22
|
+
// from a key this installer does not pin — which is not a reason to refuse a
|
|
23
|
+
// package that another, pinned key also signed.
|
|
24
|
+
const keys = opts.keys ?? NPM_SIGNING_KEYS;
|
|
25
|
+
const match = signatures
|
|
26
|
+
.map((signature) => ({ signature, key: keys.find(({ keyid }) => keyid === signature.keyid) }))
|
|
27
|
+
.find((candidate) => candidate.key != null);
|
|
28
|
+
if (match?.key == null) {
|
|
29
|
+
throw new Error(`${pkg} is signed with an unexpected npm key (${signatures.map(({ keyid }) => keyid).join(', ')}).
|
|
30
|
+
|
|
31
|
+
If npm has rotated its signing key, this installer needs updating.
|
|
32
|
+
Until then, install pnpm another way: https://pnpm.io/installation`);
|
|
33
|
+
}
|
|
34
|
+
const { signature, key } = match;
|
|
35
|
+
if (key.expires != null) {
|
|
36
|
+
const expires = new Date(key.expires).getTime();
|
|
37
|
+
// An unreadable date must not read as "never expires".
|
|
38
|
+
if (Number.isNaN(expires)) {
|
|
39
|
+
throw new Error(`${pkg} is signed with an npm key whose expiry date cannot be read (${key.expires}).`);
|
|
40
|
+
}
|
|
41
|
+
if (expires < (opts.now ?? new Date()).getTime()) {
|
|
42
|
+
throw new Error(`${pkg} is signed with an npm key that expired on ${key.expires}.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Registry signatures cover the package identity and its content hash.
|
|
46
|
+
const message = `${pkg}:${opts.integrity}`;
|
|
47
|
+
const publicKey = `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`;
|
|
48
|
+
if (!createVerify('SHA256').update(message).verify(publicKey, signature.sig, 'base64')) {
|
|
49
|
+
throw new Error(`The npm registry signature for ${pkg} is not valid. Refusing to install.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "get-pnpm",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Installs pnpm as a standalone executable",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pnpm",
|
|
7
|
+
"install",
|
|
8
|
+
"installer"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"funding": "https://opencollective.com/pnpm",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/pnpm/get.pnpm.io/tree/main/get-pnpm"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/pnpm/get.pnpm.io/tree/main/get-pnpm#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/pnpm/get.pnpm.io/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "lib/index.js",
|
|
22
|
+
"types": "lib/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./lib/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"lib",
|
|
28
|
+
"!*.map",
|
|
29
|
+
"bin"
|
|
30
|
+
],
|
|
31
|
+
"bin": {
|
|
32
|
+
"get-pnpm": "bin/get-pnpm.js"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^24.10.1",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.13"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"test": "tsc && node --test test/*.test.ts"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -4,20 +4,20 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gyp-next"
|
|
7
|
-
version = "0.22.
|
|
7
|
+
version = "0.22.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Node.js contributors", email="ryzokuken@disroot.org" },
|
|
10
10
|
]
|
|
11
11
|
description = "A fork of the GYP build system for use in the Node.js projects"
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
license = "
|
|
14
|
-
license-files = ["LICENSE"]
|
|
13
|
+
license = { file="LICENSE" }
|
|
15
14
|
requires-python = ">=3.9"
|
|
16
|
-
dependencies = ["packaging>=24.0", "setuptools>=
|
|
15
|
+
dependencies = ["packaging>=24.0", "setuptools>=69.5.1"]
|
|
17
16
|
classifiers = [
|
|
18
17
|
"Development Status :: 3 - Alpha",
|
|
19
18
|
"Environment :: Console",
|
|
20
19
|
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: BSD License",
|
|
21
21
|
"Natural Language :: English",
|
|
22
22
|
"Programming Language :: Python",
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { Readable } = require('stream')
|
|
2
|
-
const {
|
|
2
|
+
const { EnvHttpProxyAgent } = require('undici')
|
|
3
3
|
const { promises: fs } = require('graceful-fs')
|
|
4
4
|
const log = require('./log')
|
|
5
5
|
|
|
@@ -48,7 +48,7 @@ async function createDispatcher (gyp) {
|
|
|
48
48
|
const env = process.env
|
|
49
49
|
const hasProxyEnv = env.http_proxy || env.HTTP_PROXY || env.https_proxy || env.HTTPS_PROXY
|
|
50
50
|
if (!gyp.opts.proxy && !gyp.opts.cafile && !hasProxyEnv) {
|
|
51
|
-
return
|
|
51
|
+
return undefined
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const opts = {}
|
|
@@ -69,7 +69,7 @@ async function createDispatcher (gyp) {
|
|
|
69
69
|
if (gyp.opts.noproxy) {
|
|
70
70
|
opts.noProxy = gyp.opts.noproxy
|
|
71
71
|
}
|
|
72
|
-
return new
|
|
72
|
+
return new EnvHttpProxyAgent(opts)
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
async function readCAFile (filename) {
|