Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
636 lines (579 loc) · 24.3 KB
/
Copy pathinstall.sh
File metadata and controls
636 lines (579 loc) · 24.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
#!/bin/sh
# From https://github.com/Homebrew/install/blob/master/install.sh
abort() {
printf "%s\n" "$@"
exit 1
}
# string formatters
if [ -t 1 ]; then
tty_escape() { printf "\033[%sm" "$1"; }
else
tty_escape() { :; }
fi
tty_mkbold() { tty_escape "1;$1"; }
tty_blue="$(tty_mkbold 34)"
tty_bold="$(tty_mkbold 39)"
tty_reset="$(tty_escape 0)"
ohai() {
printf "${tty_blue}==>${tty_bold} %s${tty_reset}\n" "$1"
}
# End from https://github.com/Homebrew/install/blob/master/install.sh
download() {
if command -v curl > /dev/null 2>&1; then
curl -fsSL "$1"
else
wget -qO- "$1"
fi
}
NPM_REGISTRY='https://registry.npmjs.org'
# npm's registry signing key, mirrored from
# https://registry.npmjs.org/-/npm/v1/keys.
#
# Pinning it here is what makes the check below worth running. The registry
# publishes both the tarball and the checksum, so a checksum fetched from it
# proves nothing on its own; the signature does, because the key that produced
# it is not one the download host can mint. Rotations are rare — when npm
# rotates, this value has to be updated here.
NPM_SIGNING_KEY_ID='SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U'
NPM_SIGNING_KEY='MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g=='
# The "dist-tags" object of a packument, one `tag:version` per line.
dist_tags() {
printf '%s' "$1" | tr -d ' \n' | sed 's/.*"dist-tags":{//; s/}.*//' | tr ',' '\n' | tr -d '"'
}
# The version a dist-tag points at, empty when there is no such tag.
dist_tag_version() {
dist_tags "$1" | grep "^$2:" | head -n 1 | sed "s/^$2://"
}
# Turn what PNPM_VERSION asks for — a dist-tag, a version, or a bare major — into
# a concrete version, left in RESOLVED_VERSION. `resolveVersion` in the
# `get-pnpm` package resolves the same three the same way.
#
# The result comes back in a variable rather than on stdout because the aborts
# here have to end the install: from a command substitution they would end only
# the subshell, and their message would be captured as the version.
resolve_version() {
local spec kind version_json version
spec="$1"
# A leading `v` is accepted on a version or a major, but not stripped from
# every spec: that would turn a dist-tag like `vnext` into a lookup for
# `next`.
case "$spec" in
v[0-9]*) spec="${spec#v}" ;;
esac
case "$spec" in
# Starts with a digit and carries something else: the version it says it
# is, with nothing to look up.
[0-9]*[!0-9]*) RESOLVED_VERSION="$spec"; return 0 ;;
# All digits: a bare major.
[0-9]*) kind='major' ;;
*) kind='tag' ;;
esac
# Tags come from `pnpm`: it is published for every release, whereas
# `@pnpm/exe` is on its way out and would go stale.
version_json="$(download "$NPM_REGISTRY/pnpm")" || abort "Download Error!"
if [ "$kind" = 'major' ]; then
# A bare major asks for that major's current release. pnpm publishes it as
# `latest-<major>` once the major is stable, and only as `next-<major>`
# before it is promoted — taking either is what makes a new major
# installable on the day it lands rather than once someone promotes it.
version="$(dist_tag_version "$version_json" "latest-$spec")"
[ -n "$version" ] || version="$(dist_tag_version "$version_json" "next-$spec")"
else
version="$(dist_tag_version "$version_json" "$spec")"
fi
[ -n "$version" ] || abort \
"Sorry! pnpm \"$1\" could not be found." \
"" \
"PNPM_VERSION takes a version, a major, or one of these tags:" \
"$(dist_tags "$version_json")"
RESOLVED_VERSION="$version"
}
# First "<name>":"<value>" pair in a JSON document. The fields read out of the
# registry metadata below (tarball, integrity, sig, keyid) each occur once.
json_string() {
printf '%s' "$1" | tr -d ' \n' | grep -o "\"$2\":\"[^\"]*\"" | head -n 1 | sed "s/\"$2\":\"//; s/\"\$//"
}
# Verify an npm registry signature over "<name>@<version>:<integrity>".
# Returns 2 when openssl is missing, so the caller can tell "cannot check" from
# "checked and wrong".
verify_npm_signature() {
local message signature dir
message="$1"
signature="$2"
dir="$3"
command -v openssl > /dev/null 2>&1 || return 2
# RFC 7468 wants the base64 body wrapped at 64 columns. OpenSSL tolerates one
# long line; not every parser does, and a rejected key would be reported here
# as a bad signature.
{
printf -- '-----BEGIN PUBLIC KEY-----\n'
printf '%s\n' "$NPM_SIGNING_KEY" | fold -w 64
printf -- '-----END PUBLIC KEY-----\n'
} > "$dir/npm-key.pem"
printf '%s' "$signature" | openssl base64 -d -A > "$dir/npm-sig.bin" 2>/dev/null || return 1
printf '%s' "$message" > "$dir/npm-msg"
openssl dgst -sha256 -verify "$dir/npm-key.pem" -signature "$dir/npm-sig.bin" "$dir/npm-msg" > /dev/null 2>&1
}
# Compare a file against a "sha512-<base64>" integrity string. Returns 2 when
# no usable digest tool is present.
verify_integrity() {
local file expected actual
file="$1"
expected="${2#sha512-}"
[ "$expected" != "$2" ] || return 2
if command -v openssl > /dev/null 2>&1; then
actual="$(openssl dgst -sha512 -binary "$file" | openssl base64 -A)"
else
# No openssl: compare in hex instead, which busybox and coreutils can do.
expected="$(printf '%s' "$expected" | base64 -d 2>/dev/null | od -An -v -tx1 | tr -d ' \n')"
if command -v sha512sum > /dev/null 2>&1; then
actual="$(sha512sum "$file" | cut -d' ' -f1)"
elif command -v shasum > /dev/null 2>&1; then
actual="$(shasum -a 512 "$file" | cut -d' ' -f1)"
else
return 2
fi
fi
[ -n "$expected" ] && [ "$actual" = "$expected" ]
}
# Download <pkg>@<version> from the npm registry into $2, checking the registry
# signature and the tarball checksum before anything is extracted.
fetch_verified_package() {
local pkg version dir meta tarball_url integrity signature keyid archive result
pkg="$1"
version="$2"
dir="$3"
meta="$(download "$NPM_REGISTRY/$pkg/$version")" || abort "Could not reach the npm registry for $pkg@$version."
tarball_url="$(json_string "$meta" tarball)"
integrity="$(json_string "$meta" integrity)"
signature="$(json_string "$meta" sig)"
keyid="$(json_string "$meta" keyid)"
[ -n "$tarball_url" ] || abort "The npm registry published no tarball for $pkg@$version."
[ -n "$integrity" ] || abort "The npm registry published no checksum for $pkg@$version."
if [ -z "$signature" ]; then
abort "$pkg@$version carries no npm registry signature, so it cannot be verified."
elif [ "$keyid" != "$NPM_SIGNING_KEY_ID" ]; then
abort "$pkg@$version is signed with an unexpected npm key ($keyid)." \
"" \
"If npm has rotated its signing key, this installer needs updating." \
"Until then, install pnpm another way: https://pnpm.io/installation"
fi
verify_npm_signature "$pkg@$version:$integrity" "$signature" "$dir"
result=$?
if [ "$result" -eq 1 ]; then
abort "The npm registry signature for $pkg@$version is not valid. Refusing to install."
elif [ "$result" -eq 2 ]; then
ohai "openssl was not found — checking the download against its checksum only, not the npm signature"
fi
archive="$dir/package.tgz"
download "$tarball_url" > "$archive" || abort "Download Error!"
verify_integrity "$archive" "$integrity"
result=$?
if [ "$result" -eq 1 ]; then
abort "$pkg@$version does not match the checksum the npm registry published for it. Refusing to install."
elif [ "$result" -eq 2 ]; then
abort "No usable SHA-512 tool was found, so the download cannot be verified."
fi
# Extract whole, then lift the wanted entry out of the package/ root: no
# reliance on --strip-components or member selection, which busybox tar and
# bsdtar disagree about.
rm -rf "$dir/unpacked"
mkdir -p "$dir/unpacked"
tar -xzf "$archive" -C "$dir/unpacked" || abort "Could not unpack $pkg@$version."
rm -f "$archive"
}
is_glibc_compatible() {
getconf GNU_LIBC_VERSION >/dev/null 2>&1 || ldd --version >/dev/null 2>&1 || return 1
}
# Detect the OS portion of the target triplet using `process.platform`-style
# names (`linux`, `darwin`, `win32`) — the scheme pnpm's own platform packages
# and release assets use from v11.0.0-rc.3 onward. See `legacy_asset_basename`
# below for the older `macos` / `win` / `linuxstatic` mapping used by earlier
# releases.
# Android reports itself as Linux, so `uname` alone cannot tell the two apart,
# and the wrong answer picks an asset that cannot run: the musl Linux binary
# resolves DNS through `/etc/resolv.conf`, which Android does not have. Only
# 64-bit Android is built for, and this is the loader those binaries name as
# their interpreter, so its absence means no asset would run here anyway.
is_android() {
[ -e /system/bin/linker64 ]
}
detect_platform() {
local platform
platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
case "${platform}" in
linux) if is_android; then platform="android"; else platform="linux"; fi ;;
darwin) platform="darwin" ;;
mingw*|msys*|cygwin*) platform="win32" ;;
windows*) platform="win32" ;;
esac
printf '%s' "${platform}"
}
detect_libc_suffix() {
case "$1-$2" in
linux-x64 | linux-arm64)
if ! is_glibc_compatible; then
printf -- '-musl'
fi
;;
esac
}
# The asset renaming shipped in pnpm v11.0.0-rc.3. Anything older than that
# release still has only the legacy asset names on its GitHub release page
# (`pnpm-macos-*`, `pnpm-win-*`, `pnpm-linuxstatic-*`), so the installer needs
# to know when to request which.
use_legacy_assets() {
local version="$1"
local major
major="$(echo "$version" | cut -d. -f1)"
if [ "$major" -lt 11 ] 2>/dev/null; then
return 0
fi
# Only v11.0.0-rc.1 and v11.0.0-rc.2 were published before the rename.
case "$version" in
11.0.0-rc.1|11.0.0-rc.2) return 0 ;;
*) return 1 ;;
esac
}
# Map the new-scheme target back to the legacy asset basename used by
# pre-rename pnpm releases. Arch is unchanged.
legacy_asset_basename() {
local platform arch libc_suffix
platform="$1"
arch="$2"
libc_suffix="$3"
case "${platform}:${libc_suffix}" in
'darwin:') printf 'pnpm-macos-%s' "$arch" ;;
'win32:') printf 'pnpm-win-%s' "$arch" ;;
'linux:-musl') printf 'pnpm-linuxstatic-%s' "$arch" ;;
*) printf 'pnpm-%s-%s%s' "$platform" "$arch" "$libc_suffix" ;;
esac
}
# Release-page asset basename (without extension) for the given target triplet
# and pnpm version.
asset_basename() {
local version platform arch libc_suffix
version="$1"
platform="$2"
arch="$3"
libc_suffix="$4"
if use_legacy_assets "$version"; then
legacy_asset_basename "$platform" "$arch" "$libc_suffix"
else
printf 'pnpm-%s-%s%s' "$platform" "$arch" "$libc_suffix"
fi
}
detect_arch() {
local arch
arch="$(uname -m | tr '[:upper:]' '[:lower:]')"
case "${arch}" in
x86_64 | amd64) arch="x64" ;;
armv*) arch="arm" ;;
arm64 | aarch64) arch="arm64" ;;
ppc64le) arch="ppc64" ;;
# Node names both POWER endiannesses ppc64; only little-endian is built.
ppc64) return 1 ;;
esac
# `uname -m` in some cases mis-reports 32-bit OS as 64-bit, so double check
if [ "${arch}" = "x64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
arch=i686
elif [ "${arch}" = "arm64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
arch=arm
fi
case "$arch" in
x64*) ;;
arm64*) ;;
ppc64 | riscv64 | s390x) ;;
*) return 1
esac
printf '%s' "${arch}"
}
assert_target_is_built() {
local platform arch major
platform="$1"
arch="$2"
major="$3"
case "${platform}-${arch}" in
android-arm64 | android-x64) ;;
freebsd-x64 | linux-ppc64 | linux-riscv64 | linux-s390x) ;;
darwin-x64 | darwin-arm64 | linux-x64 | linux-arm64 | win32-x64 | win32-arm64) return 0 ;;
*) abort "Sorry! pnpm does not provide a pre-built binary for ${platform}-${arch}." ;;
esac
if [ "$major" -lt 12 ]; then
abort \
"pnpm v${major} does not provide a pre-built binary for ${platform}-${arch}." \
"" \
"pnpm 12 does. Install it with:" \
" PNPM_VERSION=12 sh -c \"\$(curl -fsSL https://get.pnpm.io/install.sh)\""
fi
}
download_and_install() {
local platform arch libc_suffix version tmp_dir major_version asset_base
platform="$(detect_platform)"
arch="$(detect_arch)" || abort "Sorry! pnpm does not provide a pre-built binary for this architecture."
libc_suffix="$(detect_libc_suffix "$platform" "$arch")"
# PNPM_VERSION takes a dist-tag, a version, or a major, as it does in
# install.ps1.
resolve_version "${PNPM_VERSION:-latest}"
version="$RESOLVED_VERSION"
# Everything below builds URLs out of this value, so keep it to the shape of
# a version. Every pnpm release is `major.minor.patch`, optionally with a
# prerelease part; a partial version like `12.0` is not a version the registry
# holds, and without this test it reaches it as one and comes back as a 404
# rather than as the mistake it is.
case "$version" in
[0-9]*.[0-9]*.[0-9]*) ;;
*) abort "Invalid pnpm version: $version" \
"" \
"PNPM_VERSION takes a full version (12.0.0), a major (12), or a dist-tag (next-12)." ;;
esac
# Checked separately, because the test above admits a path: without this
# `PNPM_VERSION=12.0.0/../../@evil/pkg` passes the major-version test too and
# changes which path is requested.
case "$version" in
*[!0-9A-Za-z.+-]*) abort "Invalid pnpm version: $version" ;;
esac
# Compute the major version once. Strip an optional leading "v" so
# PNPM_VERSION=v11.0.0 normalizes the same as 11.0.0; the second sed
# captures only the leading digits so prereleases like 11.0.0-rc.1
# resolve to 11. A bare `cut -d. -f1` would leave a "v" or non-digit
# prefix in the value, fail the numeric -ge test silently (the existing
# `2>/dev/null` suppression hid the parse error), and skip every
# major-gated branch below.
major_version="$(printf '%s' "$version" | sed -E 's/^v//; s/^([0-9]+).*/\1/')"
# Caught here so the major-gated tests below don't fail with a shell error
# about integers if the resolved value is not a version after all.
case "$major_version" in
'' | *[!0-9]*) abort "Invalid pnpm version: $version" ;;
esac
assert_target_is_built "$platform" "$arch" "$major_version"
# Intel macOS isn't supported on pnpm v11 only: the SEA binary produced
# by Node.js for darwin-x64 segfaults at startup because of an upstream
# Node.js bug the Node.js team has decided not to fix (Intel macOS is
# being phased out). Without this guard the script would 404 on
# pnpm-darwin-x64.tar.gz and surface as a generic "Install Error!".
# v12 (the Rust port) ships darwin-x64 again, so the guard is v11-bound.
# See https://github.com/pnpm/pnpm/issues/11423 and
# https://github.com/nodejs/node/issues/62893.
if [ "${platform}" = "darwin" ] && [ "${arch}" = "x64" ] && [ "$major_version" -eq 11 ]; then
abort \
"pnpm v${version} does not provide a working binary for Intel macOS (darwin-x64) due to an upstream Node.js SEA bug." \
"" \
"Install pnpm a different way instead:" \
" npm install -g pnpm # uses your system Node.js" \
" brew install pnpm # via Homebrew" \
" corepack enable pnpm # bundled with Node.js" \
"" \
"More context: https://github.com/pnpm/pnpm/issues/11423"
fi
# install to PNPM_HOME, defaulting to ~/.pnpm
tmp_dir="$(mktemp -d)" || abort "Tmpdir Error!"
# Use double quotes with single-quoted variable to interpolate at trap setup time.
# This ensures the directory path is captured even if tmp_dir goes out of scope.
# shellcheck disable=SC2064
trap "rm -rf '$tmp_dir'" EXIT INT TERM HUP
if [ "$major_version" -ge 12 ]; then
# v12+ is published to the npm registry as well as to the GitHub release
# page, and the two carry the same executable byte for byte. Only the
# registry copy comes with a signature over its checksum, so that is the
# one worth downloading: see NPM_SIGNING_KEY above.
local executable platform_pkg
executable='pnpm'
if [ "${platform}" = 'win32' ]; then
executable='pnpm.exe'
fi
ohai "Downloading pnpm ${version}"
platform_pkg="@pnpm/exe.${platform}-${arch}${libc_suffix}"
fetch_verified_package "$platform_pkg" "$version" "$tmp_dir"
mv "$tmp_dir/unpacked/package/$executable" "$tmp_dir/$executable" || return 1
# The executable expects the `dist/` tree next to it. `pnpm` is where the
# registry publishes it, verified the same way. (`@pnpm/exe` currently
# carries identical content, but only `pnpm` is published going forward.)
fetch_verified_package 'pnpm' "$version" "$tmp_dir"
mv "$tmp_dir/unpacked/package/dist" "$tmp_dir/dist" || return 1
chmod +x "$tmp_dir/$executable"
SHELL="$SHELL" "$tmp_dir/$executable" setup --force || return 1
return 0
fi
ohai "Downloading pnpm binaries ${version}"
asset_base="$(asset_basename "$version" "$platform" "$arch" "$libc_suffix")"
if [ "$major_version" -ge 11 ]; then
# v11+: distributed as tarballs containing the binary and dist/ directory
if [ "${platform}" = "win32" ]; then
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}.zip" > "$tmp_dir/pnpm.zip" || return 1
unzip -q "$tmp_dir/pnpm.zip" -d "$tmp_dir" || return 1
SHELL="$SHELL" "$tmp_dir/pnpm.exe" setup --force || return 1
else
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}.tar.gz" > "$tmp_dir/pnpm.tar.gz" || return 1
tar -xzf "$tmp_dir/pnpm.tar.gz" -C "$tmp_dir" || return 1
chmod +x "$tmp_dir/pnpm"
SHELL="$SHELL" "$tmp_dir/pnpm" setup --force || return 1
fi
else
# older versions: distributed as a single executable binary
local archive_url
archive_url="https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}"
if [ "${platform}" = "win32" ]; then
archive_url="${archive_url}.exe"
fi
download "$archive_url" > "$tmp_dir/pnpm" || return 1
chmod +x "$tmp_dir/pnpm"
SHELL="$SHELL" "$tmp_dir/pnpm" setup --force || return 1
fi
}
# Where `pnpm setup` installs: PNPM_HOME, or pnpm's default for the platform,
# resolved the way pnpm resolves it (XDG_DATA_HOME first, then the platform
# directory).
pnpm_home_dir() {
if [ -n "${PNPM_HOME:-}" ]; then
printf '%s' "$PNPM_HOME"
elif [ -n "${XDG_DATA_HOME:-}" ]; then
printf '%s/pnpm' "$XDG_DATA_HOME"
elif [ "$(detect_platform)" = 'darwin' ]; then
printf '%s/Library/pnpm' "$HOME"
else
printf '%s/.local/share/pnpm' "$HOME"
fi
}
# The path with every symlink resolved, or the path itself where nothing can
# resolve it (`readlink -f` only reached macOS in 12.3). These functions run
# their utilities through `command -p`, on the system's default PATH, because
# the PATH being examined is the one that may hold a stray `pnpm`.
real_path() {
command -p realpath "$1" 2>/dev/null || command -p readlink -f "$1" 2>/dev/null || printf '%s' "$1"
}
# Whether "$1" is a directory `pnpm setup` links pnpm into: `$PNPM_HOME/bin`,
# or `$PNPM_HOME` itself, which the v10 layout linked into directly. Either
# side may reach the directory through a symlink (`/var` on macOS is one).
is_pnpm_home_bin() {
local dir pnpm_home
dir="${1%/}"
pnpm_home="${2%/}"
if [ "$dir" = "$pnpm_home/bin" ] || [ "$dir" = "$pnpm_home" ]; then
return 0
fi
dir="$(real_path "$dir")"
[ "$dir" = "$(real_path "$pnpm_home/bin")" ] || [ "$dir" = "$(real_path "$pnpm_home")" ]
}
# The first `pnpm` a PATH lookup finds when it is not the one `pnpm setup`
# installed under "$1", left in SHADOWING_PNPM; the exit status is 1 when the
# shell finds pnpm's own first, or none at all. SHADOWED_BIN_ON_PATH is `yes`
# when `$1/bin` is on PATH at all, behind SHADOWING_PNPM. Both come back in
# variables so the function can run in the shell that reads them, not in a
# command substitution's subshell.
#
# A `pnpm` from another installer (npm, Homebrew, Corepack, Volta) that sits
# ahead of `$1/bin` on PATH keeps running after this script reports success,
# so every version it installs looks like it never took. This is what makes
# the install script say so.
find_shadowing_pnpm() {
local pnpm_home dir candidate rest
pnpm_home="$1"
SHADOWING_PNPM=''
SHADOWED_BIN_ON_PATH=no
rest="$PATH"
while [ -n "$rest" ]; do
case "$rest" in
*:*) dir="${rest%%:*}"; rest="${rest#*:}" ;;
*) dir="$rest"; rest='' ;;
esac
[ -n "$dir" ] || dir='.'
if is_pnpm_home_bin "$dir" "$pnpm_home"; then
[ -n "$SHADOWING_PNPM" ] || return 1
SHADOWED_BIN_ON_PATH=yes
break
fi
[ -z "$SHADOWING_PNPM" ] || continue
candidate="$dir/pnpm"
[ -f "$candidate" ] && [ -x "$candidate" ] || continue
# A symlink into pnpm's own directory runs the pnpm just installed.
if is_pnpm_home_bin "$(command -p dirname "$(real_path "$candidate")")" "$pnpm_home"; then
return 1
fi
SHADOWING_PNPM="$candidate"
done
[ -n "$SHADOWING_PNPM" ]
}
# How the `pnpm` at "$1" got there, as far as its location and, for a shim
# script, its contents tell: npm, homebrew, corepack, volta, or unknown.
install_origin() {
local executable
for executable in "$(real_path "$1")" "$1"; do
case "$executable" in
*/Cellar/pnpm/*) printf 'homebrew'; return ;;
*corepack*) printf 'corepack'; return ;;
*/.volta/*) printf 'volta'; return ;;
*/node_modules/pnpm/*) printf 'npm'; return ;;
esac
done
# npm's and Corepack's shims name their target inside; a real executable
# is far larger than any shim, so only its head is read.
if command -p head -c 65536 "$1" 2>/dev/null | command -p grep -q 'corepack'; then
printf 'corepack'
elif command -p head -c 65536 "$1" 2>/dev/null | command -p grep -q 'node_modules/pnpm/'; then
printf 'npm'
else
printf 'unknown'
fi
}
# The warning for a `pnpm` at "$1" of origin "$2" that shadows the one
# installed under "$3". `SHADOWED_BIN_ON_PATH` tells which of the two
# situations it describes.
shadowing_pnpm_warning() {
local executable origin pnpm_home described removal reorder fix
executable="$1"
origin="$2"
pnpm_home="$3"
case "$origin" in
npm) described='installed with npm'; removal='npm uninstall -g pnpm' ;;
homebrew) described='installed with Homebrew'; removal='brew uninstall pnpm' ;;
corepack) described='a Corepack shim'; removal='corepack disable pnpm' ;;
volta) described='installed with Volta'; removal='volta uninstall pnpm' ;;
*) described='not installed by pnpm'; removal='' ;;
esac
reorder="move $pnpm_home/bin ahead of $(command -p dirname "$executable") in PATH"
if [ -n "$removal" ]; then
fix="run \"$removal\" or $reorder"
else
fix="$reorder"
fi
if [ "$SHADOWED_BIN_ON_PATH" = 'yes' ]; then
printf '%s\n' \
"Warning: \"pnpm\" on PATH is $executable ($described), which comes before $pnpm_home/bin." \
"Your shell keeps running that pnpm, not the one pnpm installed to $pnpm_home/bin." \
"To finish switching, $fix."
else
printf '%s\n' \
"Warning: \"pnpm\" on PATH is $executable ($described), and $pnpm_home/bin is not on PATH yet." \
"Once a new shell adds it, it has to come first: $fix."
fi
}
# Say so when the pnpm just installed is not the one PATH resolves, or the
# success above is the last thing the person reads before `pnpm --version`
# prints the old version again. Windows paths take another shape under
# MSYS, so the lookup is left to `pnpm setup` there.
warn_if_pnpm_is_shadowed() {
local pnpm_home
[ "$(detect_platform)" != 'win32' ] || return 0
pnpm_home="$(pnpm_home_dir)"
find_shadowing_pnpm "$pnpm_home" || return 0
shadowing_pnpm_warning "$SHADOWING_PNPM" "$(install_origin "$SHADOWING_PNPM")" "$pnpm_home" >&2
}
# How to take back what `pnpm setup` just did. Setup names the shell config
# file it changed and prints the lines it added, so this points back at that
# output rather than guessing the file again. On Windows, setup edits the user
# environment instead.
print_undo_hint() {
if [ "$(detect_platform)" = 'win32' ]; then
printf '%s\n' \
"To uninstall, delete the PNPM_HOME directory, then remove the PNPM_HOME variable and the Path entry pnpm setup added from your user environment variables." \
"See https://pnpm.io/uninstall"
else
printf '%s\n' \
"To uninstall, delete $(pnpm_home_dir) and the lines pnpm setup added to the shell config file named above." \
"See https://pnpm.io/uninstall"
fi
}
download_and_install || abort "Install Error!"
print_undo_hint
warn_if_pnpm_is_shadowed