@runuai/host 0.9.13 → 0.9.42
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/README.md +22 -5
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +80 -30
- package/images/standard/Dockerfile +36 -10
- package/images/standard/README.md +63 -18
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +308 -38
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/lib/agent-cli.ts +69 -4
- package/lib/agent.ts +46 -7
- package/lib/agents/claude.ts +13 -8
- package/lib/agents/codex.ts +11 -6
- package/lib/agents/cursor.ts +39 -29
- package/lib/agents/durable-proc.ts +20 -27
- package/lib/agents/factory.ts +9 -25
- package/lib/agents/grok.ts +43 -30
- package/lib/agents/kimi.ts +44 -29
- package/lib/agents/opencode.ts +43 -31
- package/lib/agents/proc.ts +149 -114
- package/lib/agents/transport.ts +62 -50
- package/lib/agents/types.ts +6 -4
- package/lib/apple-runtime-recycle.ts +236 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/browser-testing.ts +233 -93
- package/lib/codex-auth.ts +40 -6
- package/lib/command-db.ts +20 -0
- package/lib/container-runtime.ts +1338 -0
- package/lib/db.ts +1 -0
- package/lib/docker-exec.ts +87 -5
- package/lib/engine-accounts.ts +68 -5
- package/lib/engine-login.ts +1952 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +155 -0
- package/lib/env.ts +4 -0
- package/lib/git-diff.ts +98 -32
- package/lib/git-identity.ts +199 -87
- package/lib/github-tokens.ts +202 -91
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-logs.ts +962 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +49 -0
- package/lib/managed-runtime.ts +3644 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-gateway.ts +450 -23
- package/lib/orchestrator.ts +3070 -223
- package/lib/preview-sidecar.ts +57 -13
- package/lib/release-manifest.ts +708 -0
- package/lib/release-trust.ts +28 -0
- package/lib/runtime-activation-tail.ts +232 -0
- package/lib/runtime-archive.ts +1086 -0
- package/lib/runtime-authority.ts +79 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-state.ts +232 -12
- package/lib/skills.ts +24 -3
- package/lib/ssh.ts +18 -0
- package/lib/standard-image.ts +1104 -141
- package/lib/stopped-task-status-queue.ts +44 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-diff.ts +66 -46
- package/lib/task-environment/apple-container.ts +757 -0
- package/lib/task-environment/docker.ts +945 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +443 -0
- package/lib/task-environment/registry.ts +58 -0
- package/lib/task-environment/types.ts +408 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +585 -0
- package/lib/tunnel-registry.ts +135 -19
- package/lib/tunnel-runtime.ts +235 -0
- package/package.json +1 -1
- package/scripts/agent/_common.sh +123 -3
- package/scripts/agent/task-down.sh +146 -38
- package/scripts/agent/task-status.sh +19 -3
- package/scripts/agent/task-up.sh +1463 -109
- package/scripts/install/darwin.ts +848 -50
- package/scripts/install/linux.ts +838 -35
- package/scripts/install/types.ts +43 -0
- package/scripts/install/util.ts +215 -8
- package/scripts/install/win.ts +12 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/cli.ts +1464 -72
- package/src/event-outbox.ts +83 -4
- package/src/index.ts +871 -50
- package/src/main.ts +1398 -255
- package/src/paths.ts +17 -1
- package/src/protocol.ts +695 -1
- package/src/runtime-bootstrap.ts +165 -0
- package/src/ui/server.ts +46 -10
- package/src/ui/types.ts +37 -0
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Materialize project-declared runtimes into /opt/asdf-data.
|
|
3
|
+
#
|
|
4
|
+
# Host contract: run as `node` with /opt/asdf-data writable. On Docker that
|
|
5
|
+
# is the shared host-wide volume, mounted read-write in a dedicated
|
|
6
|
+
# maintenance container with /workspace read-only. On the Apple runtime
|
|
7
|
+
# (ADR-108) this runs INSIDE the app container, where /opt/asdf-data is the
|
|
8
|
+
# task's in-guest overlay (shared read-only lower + task-private upper) —
|
|
9
|
+
# installs land in the task's own layer and the shared volume is never
|
|
10
|
+
# written on the task path. The host supplies the total operation timeout;
|
|
11
|
+
# this executable supplies the bounded volume lock (cross-process on Docker,
|
|
12
|
+
# per-task on the overlay, which is exactly the exclusion each case needs).
|
|
13
|
+
#
|
|
14
|
+
# UAI_MATERIALIZE_VERIFY_ONLY=1 (ADR-106): the same validation and positive
|
|
15
|
+
# execution proofs with ZERO volume mutations — no install, no repair, no
|
|
16
|
+
# reshim, no Corepack preparation — against /opt/asdf-data mounted READ-ONLY.
|
|
17
|
+
# Apple block volumes refuse a writable attach while any task container holds
|
|
18
|
+
# them, so a task starting next to running tasks can only verify; the host
|
|
19
|
+
# escalates to the writable pass when this one reports something missing.
|
|
20
|
+
# Every mutation below is individually gated so a verify pass that would need
|
|
21
|
+
# one fails loudly instead of writing.
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
readonly WORKSPACE=/workspace
|
|
26
|
+
readonly PROJECT_ALLOWLIST=/run/uai/runtime-projects
|
|
27
|
+
readonly COREPACK_VERSION_FILE=/usr/local/share/uai/corepack-version
|
|
28
|
+
readonly ASDF_SH=/opt/asdf/asdf.sh
|
|
29
|
+
readonly ASDF_DATA=/opt/asdf-data
|
|
30
|
+
readonly LOCK_PATH="$ASDF_DATA/.uai-maintenance.lock"
|
|
31
|
+
readonly CONTROL_PATH=/opt/asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
32
|
+
readonly APP_PATH="$ASDF_DATA/shims:$CONTROL_PATH"
|
|
33
|
+
readonly FLOCK_BIN=/usr/bin/flock
|
|
34
|
+
readonly REALPATH_BIN=/usr/bin/realpath
|
|
35
|
+
readonly RM_BIN=/usr/bin/rm
|
|
36
|
+
readonly LOCK_WAIT_SECONDS=300
|
|
37
|
+
readonly PNPM_FALLBACK_VERSION=10.15.0
|
|
38
|
+
readonly YARN_FALLBACK_VERSION=4.9.2
|
|
39
|
+
readonly NPM_REGISTRY=https://registry.npmjs.org/
|
|
40
|
+
readonly NPM_USERCONFIG=/dev/null
|
|
41
|
+
readonly NPM_GLOBALCONFIG=/dev/uai-empty-npmrc
|
|
42
|
+
readonly MAX_ALLOWLIST_BYTES=1048576
|
|
43
|
+
readonly MAX_MANIFEST_BYTES=65536
|
|
44
|
+
|
|
45
|
+
log() {
|
|
46
|
+
printf 'uai-materialize-runtimes: %s\n' "$*" >&2
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fail() {
|
|
50
|
+
log "error: $*"
|
|
51
|
+
exit 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if [ "$#" -ne 0 ]; then
|
|
55
|
+
fail "this host-controlled executable accepts no arguments"
|
|
56
|
+
fi
|
|
57
|
+
readonly VERIFY_ONLY="${UAI_MATERIALIZE_VERIFY_ONLY:-0}"
|
|
58
|
+
case "$VERIFY_ONLY" in
|
|
59
|
+
0|1) ;;
|
|
60
|
+
*) fail "UAI_MATERIALIZE_VERIFY_ONLY must be 0 or 1" ;;
|
|
61
|
+
esac
|
|
62
|
+
export HOME=/home/node
|
|
63
|
+
export ASDF_DIR=/opt/asdf
|
|
64
|
+
export ASDF_DATA_DIR="$ASDF_DATA"
|
|
65
|
+
export ASDF_CONFIG_FILE=/dev/null
|
|
66
|
+
export ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions
|
|
67
|
+
export PATH="$CONTROL_PATH"
|
|
68
|
+
export COREPACK_DEFAULT_TO_LATEST=0
|
|
69
|
+
export COREPACK_ENABLE_AUTO_PIN=0
|
|
70
|
+
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
|
71
|
+
export COREPACK_ENV_FILE=0
|
|
72
|
+
export COREPACK_ENABLE_PROJECT_SPEC=1
|
|
73
|
+
export COREPACK_ENABLE_STRICT=1
|
|
74
|
+
export COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=0
|
|
75
|
+
export COREPACK_INTEGRITY_CHECK=1
|
|
76
|
+
export COREPACK_NPM_REGISTRY=https://registry.npmjs.org
|
|
77
|
+
unset NODE_OPTIONS NPM_CONFIG_PREFIX NPM_CONFIG_REGISTRY \
|
|
78
|
+
NPM_CONFIG_USERCONFIG NPM_CONFIG_GLOBALCONFIG \
|
|
79
|
+
npm_config_prefix npm_config_registry npm_config_userconfig \
|
|
80
|
+
npm_config_globalconfig
|
|
81
|
+
unset COREPACK_ENABLE_NETWORK
|
|
82
|
+
if [ -L "$COREPACK_VERSION_FILE" ] || [ ! -f "$COREPACK_VERSION_FILE" ]; then
|
|
83
|
+
fail "$COREPACK_VERSION_FILE must be a regular, non-symbolic-link host asset"
|
|
84
|
+
fi
|
|
85
|
+
corepack_version=$(sed -n '1p' "$COREPACK_VERSION_FILE")
|
|
86
|
+
[[ "$corepack_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|
|
87
|
+
|| fail "$COREPACK_VERSION_FILE does not contain a trusted exact version"
|
|
88
|
+
if ! printf '%s\n' "$corepack_version" \
|
|
89
|
+
| cmp -s - "$COREPACK_VERSION_FILE"; then
|
|
90
|
+
fail "$COREPACK_VERSION_FILE must contain exactly one canonical version line"
|
|
91
|
+
fi
|
|
92
|
+
readonly COREPACK_VERSION=$corepack_version
|
|
93
|
+
unset corepack_version
|
|
94
|
+
if [ "$(id -un)" != node ]; then
|
|
95
|
+
fail "must run as the node user"
|
|
96
|
+
fi
|
|
97
|
+
if [ ! -d "$WORKSPACE" ]; then
|
|
98
|
+
fail "$WORKSPACE is missing (the host must mount the task workspace read-only)"
|
|
99
|
+
fi
|
|
100
|
+
if [ -e "$WORKSPACE/.tool-versions" ] || [ -L "$WORKSPACE/.tool-versions" ]; then
|
|
101
|
+
fail "$WORKSPACE/.tool-versions is forbidden because it is outside the selected-project authority"
|
|
102
|
+
fi
|
|
103
|
+
if [ ! -r "$ASDF_SH" ]; then
|
|
104
|
+
fail "asdf is unavailable at $ASDF_SH"
|
|
105
|
+
fi
|
|
106
|
+
if [ "$VERIFY_ONLY" = 1 ]; then
|
|
107
|
+
if [ ! -d "$ASDF_DATA" ] || [ ! -r "$ASDF_DATA" ]; then
|
|
108
|
+
fail "$ASDF_DATA is not a readable shared asdf volume"
|
|
109
|
+
fi
|
|
110
|
+
elif [ ! -d "$ASDF_DATA" ] || [ ! -w "$ASDF_DATA" ]; then
|
|
111
|
+
fail "$ASDF_DATA is not a writable shared asdf volume"
|
|
112
|
+
fi
|
|
113
|
+
[ ! -e "$NPM_GLOBALCONFIG" ] \
|
|
114
|
+
|| fail "trusted empty npm config path $NPM_GLOBALCONFIG unexpectedly exists"
|
|
115
|
+
[ -x "$FLOCK_BIN" ] || fail "flock is unavailable at $FLOCK_BIN"
|
|
116
|
+
[ -x "$REALPATH_BIN" ] || fail "realpath is unavailable at $REALPATH_BIN"
|
|
117
|
+
[ -x "$RM_BIN" ] || fail "rm is unavailable at $RM_BIN"
|
|
118
|
+
|
|
119
|
+
declare -a projects=()
|
|
120
|
+
declare -a manifests=()
|
|
121
|
+
# `/workspace` also carries control/shared directories such as `.uai` and
|
|
122
|
+
# `files`, so filesystem discovery cannot prove the complete selected-project
|
|
123
|
+
# set. The host binds its persisted selection as a fixed, read-only allowlist.
|
|
124
|
+
# Snapshot a bounded prefix before parsing so a read error cannot look like a
|
|
125
|
+
# clean EOF or silently produce a partial proof set.
|
|
126
|
+
if [ -L "$PROJECT_ALLOWLIST" ] || [ ! -f "$PROJECT_ALLOWLIST" ]; then
|
|
127
|
+
fail "$PROJECT_ALLOWLIST must be a regular, non-symbolic-link host allowlist"
|
|
128
|
+
fi
|
|
129
|
+
allowlist_snapshot=$(mktemp)
|
|
130
|
+
cleanup() {
|
|
131
|
+
rm -f -- "$allowlist_snapshot"
|
|
132
|
+
}
|
|
133
|
+
trap cleanup EXIT
|
|
134
|
+
if ! dd if="$PROJECT_ALLOWLIST" of="$allowlist_snapshot" \
|
|
135
|
+
bs=$((MAX_ALLOWLIST_BYTES + 1)) count=1 status=none; then
|
|
136
|
+
fail "could not read $PROJECT_ALLOWLIST"
|
|
137
|
+
fi
|
|
138
|
+
allowlist_bytes=$(wc -c < "$allowlist_snapshot")
|
|
139
|
+
[ "$allowlist_bytes" -gt 0 ] \
|
|
140
|
+
|| fail "$PROJECT_ALLOWLIST must name at least one selected project"
|
|
141
|
+
[ "$allowlist_bytes" -le "$MAX_ALLOWLIST_BYTES" ] \
|
|
142
|
+
|| fail "$PROJECT_ALLOWLIST exceeds the ${MAX_ALLOWLIST_BYTES}-byte limit"
|
|
143
|
+
if LC_ALL=C grep -q '[[:cntrl:]]' "$allowlist_snapshot"; then
|
|
144
|
+
fail "$PROJECT_ALLOWLIST contains a control character"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
declare -A seen_projects=()
|
|
148
|
+
while IFS= read -r slug || [ -n "$slug" ]; do
|
|
149
|
+
[[ "$slug" =~ ^[a-z0-9-]{1,64}$ ]] \
|
|
150
|
+
|| fail "$PROJECT_ALLOWLIST contains an invalid project slug"
|
|
151
|
+
[ -z "${seen_projects[$slug]:-}" ] \
|
|
152
|
+
|| fail "$PROJECT_ALLOWLIST contains duplicate project slug '$slug'"
|
|
153
|
+
seen_projects[$slug]=1
|
|
154
|
+
entry="$WORKSPACE/$slug"
|
|
155
|
+
if [ -L "$entry" ] || [ ! -d "$entry" ]; then
|
|
156
|
+
fail "selected project '$slug' is not a real one-level workspace directory"
|
|
157
|
+
fi
|
|
158
|
+
if [ -L "$entry/.git" ] \
|
|
159
|
+
|| { [ ! -f "$entry/.git" ] && [ ! -d "$entry/.git" ]; }; then
|
|
160
|
+
fail "selected project '$slug' lacks a regular, non-symbolic-link Git marker"
|
|
161
|
+
fi
|
|
162
|
+
projects+=("$entry")
|
|
163
|
+
done < "$allowlist_snapshot"
|
|
164
|
+
[ "${#projects[@]}" -gt 0 ] \
|
|
165
|
+
|| fail "$PROJECT_ALLOWLIST did not contain a selected project"
|
|
166
|
+
|
|
167
|
+
for project in "${projects[@]}"; do
|
|
168
|
+
manifest="$project/.tool-versions"
|
|
169
|
+
[ -e "$manifest" ] || [ -L "$manifest" ] || continue
|
|
170
|
+
if [ -L "$manifest" ] || [ ! -f "$manifest" ]; then
|
|
171
|
+
fail "$manifest must be a regular, non-symbolic-link .tool-versions file"
|
|
172
|
+
fi
|
|
173
|
+
manifests+=("$manifest")
|
|
174
|
+
done
|
|
175
|
+
|
|
176
|
+
trim_spaces() {
|
|
177
|
+
local value=$1
|
|
178
|
+
value="${value#"${value%%[! ]*}"}"
|
|
179
|
+
value="${value%"${value##*[! ]}"}"
|
|
180
|
+
printf '%s' "$value"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
reject_dot_segments() {
|
|
184
|
+
local value=$1 manifest=$2 line_number=$3
|
|
185
|
+
case "/$value/" in
|
|
186
|
+
*'/../'*|*'/./'*|*'//'*)
|
|
187
|
+
fail "$manifest:$line_number contains an unsafe version path segment"
|
|
188
|
+
;;
|
|
189
|
+
esac
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
validate_version() {
|
|
193
|
+
local version=$1 project=$2 manifest=$3 line_number=$4
|
|
194
|
+
local suffix resolved project_resolved
|
|
195
|
+
|
|
196
|
+
case "$version" in
|
|
197
|
+
system)
|
|
198
|
+
return
|
|
199
|
+
;;
|
|
200
|
+
path:*)
|
|
201
|
+
suffix=${version#path:}
|
|
202
|
+
[ -n "$suffix" ] || fail "$manifest:$line_number has an empty path: version"
|
|
203
|
+
case "$suffix" in
|
|
204
|
+
/*|'~'*)
|
|
205
|
+
fail "$manifest:$line_number path: versions must stay inside their project"
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
208
|
+
resolved=$(cd "$project" && "$REALPATH_BIN" -e -- "$suffix" 2>/dev/null) \
|
|
209
|
+
|| fail "$manifest:$line_number path:$suffix does not resolve"
|
|
210
|
+
project_resolved=$("$REALPATH_BIN" -e -- "$project") \
|
|
211
|
+
|| fail "$manifest:$line_number project directory does not resolve"
|
|
212
|
+
[ -d "$resolved" ] \
|
|
213
|
+
|| fail "$manifest:$line_number path:$suffix is not a directory"
|
|
214
|
+
case "$resolved" in
|
|
215
|
+
"$project_resolved"|"$project_resolved"/*) ;;
|
|
216
|
+
*) fail "$manifest:$line_number path:$suffix escapes its project" ;;
|
|
217
|
+
esac
|
|
218
|
+
return
|
|
219
|
+
;;
|
|
220
|
+
ref:*)
|
|
221
|
+
suffix=${version#ref:}
|
|
222
|
+
[ -n "$suffix" ] || fail "$manifest:$line_number has an empty ref: version"
|
|
223
|
+
case "$suffix" in
|
|
224
|
+
*/*)
|
|
225
|
+
fail "$manifest:$line_number ref: versions must use one cache path segment"
|
|
226
|
+
;;
|
|
227
|
+
esac
|
|
228
|
+
[[ "$suffix" =~ ^[[:alnum:]][[:alnum:].+_@/-]*$ ]] \
|
|
229
|
+
|| fail "$manifest:$line_number has a malformed ref: version"
|
|
230
|
+
reject_dot_segments "$suffix" "$manifest" "$line_number"
|
|
231
|
+
return
|
|
232
|
+
;;
|
|
233
|
+
esac
|
|
234
|
+
|
|
235
|
+
case "$version" in
|
|
236
|
+
ref-*)
|
|
237
|
+
# asdf 0.14.1 stores `ref:foo` at `ref-foo`; accepting a normal
|
|
238
|
+
# `ref-foo` selector would alias the same destructive repair target.
|
|
239
|
+
fail "$manifest:$line_number normal versions may not use the reserved ref- prefix"
|
|
240
|
+
;;
|
|
241
|
+
*/*)
|
|
242
|
+
# `foo` and `foo/bar` would be ancestor/descendant cache entries; a
|
|
243
|
+
# repair of the former could delete the latter.
|
|
244
|
+
fail "$manifest:$line_number managed versions must use one cache path segment"
|
|
245
|
+
;;
|
|
246
|
+
esac
|
|
247
|
+
[[ "$version" =~ ^[[:alnum:]][[:alnum:].+_@/-]*$ ]] \
|
|
248
|
+
|| fail "$manifest:$line_number has a malformed version '$version'"
|
|
249
|
+
reject_dot_segments "$version" "$manifest" "$line_number"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Parse and validate the complete manifest before passing any field to asdf.
|
|
253
|
+
# ASDF remains the resolution authority; this parser only rejects shapes that
|
|
254
|
+
# are ambiguous or could be interpreted as command/path injection.
|
|
255
|
+
validate_manifest() {
|
|
256
|
+
local manifest=$1 project line line_number=0 tool version bytes
|
|
257
|
+
local -a fields=()
|
|
258
|
+
local -A seen_tools=()
|
|
259
|
+
|
|
260
|
+
project=$(dirname "$manifest")
|
|
261
|
+
bytes=$(wc -c < "$manifest")
|
|
262
|
+
[ "$bytes" -le "$MAX_MANIFEST_BYTES" ] \
|
|
263
|
+
|| fail "$manifest exceeds the ${MAX_MANIFEST_BYTES}-byte limit"
|
|
264
|
+
if LC_ALL=C grep -q '[[:cntrl:]]' "$manifest"; then
|
|
265
|
+
fail "$manifest contains a control character (tabs and CRLF are not accepted)"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
269
|
+
line_number=$((line_number + 1))
|
|
270
|
+
line=${line%%#*}
|
|
271
|
+
line=$(trim_spaces "$line")
|
|
272
|
+
[ -n "$line" ] || continue
|
|
273
|
+
read -r -a fields <<< "$line"
|
|
274
|
+
[ "${#fields[@]}" -ge 2 ] \
|
|
275
|
+
|| fail "$manifest:$line_number must declare a tool and at least one version"
|
|
276
|
+
tool=${fields[0]}
|
|
277
|
+
[[ "$tool" =~ ^[a-z0-9][a-z0-9_-]*$ ]] \
|
|
278
|
+
|| fail "$manifest:$line_number has a malformed tool name '$tool'"
|
|
279
|
+
case "$tool" in
|
|
280
|
+
nodejs|python|golang|ruby|rust) ;;
|
|
281
|
+
pnpm|yarn)
|
|
282
|
+
# Not asdf plugins here: package managers are Corepack's domain
|
|
283
|
+
# (live 2026-08-17 — the asdf-pnpm convention is common enough that
|
|
284
|
+
# refusing it broke a real monorepo). One plain pinned version;
|
|
285
|
+
# project_package_manager consumes it below.
|
|
286
|
+
[ "${#fields[@]}" -eq 2 ] \
|
|
287
|
+
|| fail "$manifest:$line_number must pin exactly one $tool version"
|
|
288
|
+
[[ "${fields[1]}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] \
|
|
289
|
+
|| fail "$manifest:$line_number must pin an exact $tool version (Corepack manages package managers)"
|
|
290
|
+
;;
|
|
291
|
+
*) fail "$manifest:$line_number requests unsupported asdf plugin '$tool'" ;;
|
|
292
|
+
esac
|
|
293
|
+
[ -z "${seen_tools[$tool]:-}" ] \
|
|
294
|
+
|| fail "$manifest:$line_number declares '$tool' more than once"
|
|
295
|
+
seen_tools[$tool]=1
|
|
296
|
+
case "$tool" in
|
|
297
|
+
pnpm|yarn) continue ;;
|
|
298
|
+
esac
|
|
299
|
+
for version in "${fields[@]:1}"; do
|
|
300
|
+
validate_version "$version" "$project" "$manifest" "$line_number"
|
|
301
|
+
done
|
|
302
|
+
done < "$manifest"
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# The pnpm/yarn pin from a project's .tool-versions, if any — the asdf-world
|
|
306
|
+
# spelling of what package.json's packageManager states. Corepack authority
|
|
307
|
+
# order: packageManager field, then this, then lockfile fallback.
|
|
308
|
+
tool_versions_package_manager() {
|
|
309
|
+
local project=$1 line line_number=0
|
|
310
|
+
local -a fields=()
|
|
311
|
+
TOOL_VERSIONS_PM_MANAGER=""
|
|
312
|
+
TOOL_VERSIONS_PM_VERSION=""
|
|
313
|
+
[ -f "$project/.tool-versions" ] || return 0
|
|
314
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
315
|
+
line_number=$((line_number + 1))
|
|
316
|
+
line=${line%%#*}
|
|
317
|
+
line=$(trim_spaces "$line")
|
|
318
|
+
[ -n "$line" ] || continue
|
|
319
|
+
read -r -a fields <<< "$line"
|
|
320
|
+
case "${fields[0]}" in
|
|
321
|
+
pnpm|yarn)
|
|
322
|
+
TOOL_VERSIONS_PM_MANAGER=${fields[0]}
|
|
323
|
+
TOOL_VERSIONS_PM_VERSION=${fields[1]:-}
|
|
324
|
+
return 0
|
|
325
|
+
;;
|
|
326
|
+
esac
|
|
327
|
+
done < "$project/.tool-versions"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for manifest in "${manifests[@]}"; do
|
|
331
|
+
validate_manifest "$manifest"
|
|
332
|
+
done
|
|
333
|
+
|
|
334
|
+
# Do not inherit a derived image's ASDF_<TOOL>_VERSION overrides: the checked
|
|
335
|
+
# manifest mounted by the host is the authority for this operation.
|
|
336
|
+
unset_tool_override() {
|
|
337
|
+
local tool=$1 variable
|
|
338
|
+
variable="ASDF_$(printf '%s' "$tool" | tr '[:lower:]-' '[:upper:]_')_VERSION"
|
|
339
|
+
unset "$variable"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for baked_tool in nodejs python golang ruby rust; do
|
|
343
|
+
unset_tool_override "$baked_tool"
|
|
344
|
+
done
|
|
345
|
+
|
|
346
|
+
project_package_manager() {
|
|
347
|
+
local project=$1 spec="" manager="" is_fallback=0
|
|
348
|
+
local has_dev_engines_manager=0 has_supported_package_manager=0
|
|
349
|
+
PROJECT_PACKAGE_MANAGER=""
|
|
350
|
+
PROJECT_PACKAGE_MANAGER_SPEC=""
|
|
351
|
+
PROJECT_PACKAGE_MANAGER_IS_FALLBACK=0
|
|
352
|
+
|
|
353
|
+
if [ -f "$project/package.json" ]; then
|
|
354
|
+
jq -e 'type == "object"' "$project/package.json" >/dev/null 2>&1 \
|
|
355
|
+
|| fail "$(basename "$project") has an invalid package.json"
|
|
356
|
+
spec=$(jq -r \
|
|
357
|
+
'if (.packageManager | type) == "string" then .packageManager else "" end' \
|
|
358
|
+
"$project/package.json")
|
|
359
|
+
manager=${spec%%@*}
|
|
360
|
+
case "$manager" in
|
|
361
|
+
pnpm|yarn) has_supported_package_manager=1 ;;
|
|
362
|
+
esac
|
|
363
|
+
if jq -e \
|
|
364
|
+
'(.devEngines? | type) == "object" and (.devEngines | has("packageManager"))' \
|
|
365
|
+
"$project/package.json" >/dev/null; then
|
|
366
|
+
has_dev_engines_manager=1
|
|
367
|
+
fi
|
|
368
|
+
if [ "$has_dev_engines_manager" -eq 1 ] \
|
|
369
|
+
&& [ "$has_supported_package_manager" -ne 1 ]; then
|
|
370
|
+
fail "$(basename "$project") devEngines.packageManager requires an exact safe pnpm/yarn packageManager field"
|
|
371
|
+
fi
|
|
372
|
+
fi
|
|
373
|
+
if [ -z "$manager" ]; then
|
|
374
|
+
# The asdf-world pin (`pnpm 9.12.0` in .tool-versions) beats the generic
|
|
375
|
+
# lockfile fallback: it names an exact version the project chose.
|
|
376
|
+
tool_versions_package_manager "$project"
|
|
377
|
+
if [ -n "$TOOL_VERSIONS_PM_MANAGER" ]; then
|
|
378
|
+
manager=$TOOL_VERSIONS_PM_MANAGER
|
|
379
|
+
spec="$manager@$TOOL_VERSIONS_PM_VERSION"
|
|
380
|
+
is_fallback=1
|
|
381
|
+
fi
|
|
382
|
+
else
|
|
383
|
+
tool_versions_package_manager "$project"
|
|
384
|
+
if [ -n "$TOOL_VERSIONS_PM_MANAGER" ] \
|
|
385
|
+
&& [ "${spec%%+*}" != "$TOOL_VERSIONS_PM_MANAGER@$TOOL_VERSIONS_PM_VERSION" ]; then
|
|
386
|
+
log "$(basename "$project"): package.json packageManager ($spec) wins over .tool-versions ($TOOL_VERSIONS_PM_MANAGER $TOOL_VERSIONS_PM_VERSION)"
|
|
387
|
+
fi
|
|
388
|
+
fi
|
|
389
|
+
if [ -z "$manager" ]; then
|
|
390
|
+
if [ -f "$project/pnpm-lock.yaml" ]; then
|
|
391
|
+
manager=pnpm
|
|
392
|
+
spec="pnpm@$PNPM_FALLBACK_VERSION"
|
|
393
|
+
is_fallback=1
|
|
394
|
+
elif [ -f "$project/yarn.lock" ]; then
|
|
395
|
+
manager=yarn
|
|
396
|
+
spec="yarn@$YARN_FALLBACK_VERSION"
|
|
397
|
+
is_fallback=1
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
case "$manager" in
|
|
401
|
+
pnpm|yarn)
|
|
402
|
+
if ! [[ "$spec" =~ ^(pnpm|yarn)@[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+sha(224|256|384|512)\.[0-9A-Za-z_+/=-]+)?$ ]]; then
|
|
403
|
+
fail "$(basename "$project") packageManager must pin an exact safe $manager version"
|
|
404
|
+
fi
|
|
405
|
+
PROJECT_PACKAGE_MANAGER=$manager
|
|
406
|
+
PROJECT_PACKAGE_MANAGER_SPEC=$spec
|
|
407
|
+
PROJECT_PACKAGE_MANAGER_IS_FALLBACK=$is_fallback
|
|
408
|
+
;;
|
|
409
|
+
esac
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
tool_executable() {
|
|
413
|
+
case "$1" in
|
|
414
|
+
nodejs) printf node ;;
|
|
415
|
+
python) printf python ;;
|
|
416
|
+
golang) printf go ;;
|
|
417
|
+
ruby) printf ruby ;;
|
|
418
|
+
rust) printf rustc ;;
|
|
419
|
+
*) return 1 ;;
|
|
420
|
+
esac
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
selected_executable() {
|
|
424
|
+
local tool=$1 version=$2 project=$3 executable=$4 variable
|
|
425
|
+
SELECTED_EXECUTABLE=""
|
|
426
|
+
case "$version" in
|
|
427
|
+
system)
|
|
428
|
+
SELECTED_EXECUTABLE=$(PATH="$CONTROL_PATH" command -v "$executable" 2>/dev/null) \
|
|
429
|
+
|| return 1
|
|
430
|
+
;;
|
|
431
|
+
*)
|
|
432
|
+
# The plugin, not Uai, owns executable layout (golang includes both
|
|
433
|
+
# `bin` and `go/bin`). Path selections are explicitly reshimmed under the
|
|
434
|
+
# shared lock, so ASDF can resolve that plugin-aware layout without
|
|
435
|
+
# writing to the external tree itself.
|
|
436
|
+
variable="ASDF_$(printf '%s' "$tool" | tr '[:lower:]-' '[:upper:]_')_VERSION"
|
|
437
|
+
SELECTED_EXECUTABLE=$(cd "$project" && env "$variable=$version" \
|
|
438
|
+
asdf which "$executable" 2>/dev/null) \
|
|
439
|
+
|| return 1
|
|
440
|
+
;;
|
|
441
|
+
esac
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
run_tool_version_probe() {
|
|
445
|
+
local tool=$1 executable=$2 project=$3 output
|
|
446
|
+
TOOL_VERSION_OUTPUT=""
|
|
447
|
+
case "$tool" in
|
|
448
|
+
golang)
|
|
449
|
+
output=$(cd "$project" && PATH="$CONTROL_PATH" \
|
|
450
|
+
"$executable" version 2>&1) || return 1
|
|
451
|
+
;;
|
|
452
|
+
*)
|
|
453
|
+
output=$(cd "$project" && PATH="$CONTROL_PATH" \
|
|
454
|
+
"$executable" --version 2>&1) || return 1
|
|
455
|
+
;;
|
|
456
|
+
esac
|
|
457
|
+
TOOL_VERSION_OUTPUT=${output%%$'\n'*}
|
|
458
|
+
[ -n "$TOOL_VERSION_OUTPUT" ]
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
resolve_app_shim() {
|
|
462
|
+
local executable=$1 resolved
|
|
463
|
+
APP_SHIM=""
|
|
464
|
+
resolved=$(PATH="$APP_PATH" command -v "$executable" 2>/dev/null) \
|
|
465
|
+
|| return 1
|
|
466
|
+
[ "$resolved" = "$ASDF_DATA/shims/$executable" ] \
|
|
467
|
+
|| return 1
|
|
468
|
+
[ ! -L "$resolved" ] && [ -f "$resolved" ] && [ -x "$resolved" ] \
|
|
469
|
+
|| return 1
|
|
470
|
+
APP_SHIM=$resolved
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
run_tool_shim_version_probe() {
|
|
474
|
+
local tool=$1 executable=$2 project=$3 output
|
|
475
|
+
TOOL_VERSION_OUTPUT=""
|
|
476
|
+
resolve_app_shim "$executable" || return 1
|
|
477
|
+
# ASDF shims use `#!/usr/bin/env bash`; execute the already-resolved shim
|
|
478
|
+
# with the control PATH so another shared `bash` shim cannot intercept it.
|
|
479
|
+
case "$tool" in
|
|
480
|
+
golang)
|
|
481
|
+
output=$(cd "$project" && PATH="$CONTROL_PATH" \
|
|
482
|
+
"$APP_SHIM" version 2>&1) || return 1
|
|
483
|
+
;;
|
|
484
|
+
*)
|
|
485
|
+
output=$(cd "$project" && PATH="$CONTROL_PATH" \
|
|
486
|
+
"$APP_SHIM" --version 2>&1) || return 1
|
|
487
|
+
;;
|
|
488
|
+
esac
|
|
489
|
+
TOOL_VERSION_OUTPUT=${output%%$'\n'*}
|
|
490
|
+
[ -n "$TOOL_VERSION_OUTPUT" ]
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
run_node_shim_probe() {
|
|
494
|
+
local project=$1 node_version=$2 executable=$3
|
|
495
|
+
shift 3
|
|
496
|
+
resolve_app_shim "$executable" || return 1
|
|
497
|
+
(cd "$project" && PATH="$CONTROL_PATH" \
|
|
498
|
+
ASDF_NODEJS_VERSION="$node_version" "$APP_SHIM" "$@")
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
# BEGIN UAI_MANAGED_VERSION_PROOF
|
|
502
|
+
managed_executable_is_contained() {
|
|
503
|
+
local version=$1 executable=$2 real_executable
|
|
504
|
+
case "$version" in
|
|
505
|
+
system|path:*) return 0 ;;
|
|
506
|
+
esac
|
|
507
|
+
[ -n "${MANAGED_INSTALL_PATH:-}" ] || return 1
|
|
508
|
+
[ -f "$executable" ] && [ -x "$executable" ] || return 1
|
|
509
|
+
real_executable=$("$REALPATH_BIN" -e -- "$executable" 2>/dev/null) || return 1
|
|
510
|
+
case "$real_executable" in
|
|
511
|
+
"$MANAGED_INSTALL_PATH"/*) return 0 ;;
|
|
512
|
+
*) return 1 ;;
|
|
513
|
+
esac
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
normalize_tool_version_output() {
|
|
517
|
+
local tool=$1 output=$2
|
|
518
|
+
NORMALIZED_TOOL_VERSION=""
|
|
519
|
+
case "$tool" in
|
|
520
|
+
nodejs)
|
|
521
|
+
[[ "$output" =~ ^v([^[:space:]]+) ]] || return 1
|
|
522
|
+
NORMALIZED_TOOL_VERSION=${BASH_REMATCH[1]}
|
|
523
|
+
;;
|
|
524
|
+
python)
|
|
525
|
+
[[ "$output" =~ ^Python[[:space:]]+([^[:space:]]+) ]] || return 1
|
|
526
|
+
NORMALIZED_TOOL_VERSION=${BASH_REMATCH[1]}
|
|
527
|
+
;;
|
|
528
|
+
golang)
|
|
529
|
+
[[ "$output" =~ ^go[[:space:]]+version[[:space:]]+go([^[:space:]]+) ]] \
|
|
530
|
+
|| return 1
|
|
531
|
+
NORMALIZED_TOOL_VERSION=${BASH_REMATCH[1]}
|
|
532
|
+
;;
|
|
533
|
+
ruby)
|
|
534
|
+
[[ "$output" =~ ^ruby[[:space:]]+([^[:space:]]+) ]] || return 1
|
|
535
|
+
NORMALIZED_TOOL_VERSION=${BASH_REMATCH[1]}
|
|
536
|
+
if [[ "$NORMALIZED_TOOL_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)p[0-9]+$ ]]; then
|
|
537
|
+
NORMALIZED_TOOL_VERSION=${BASH_REMATCH[1]}
|
|
538
|
+
fi
|
|
539
|
+
;;
|
|
540
|
+
rust)
|
|
541
|
+
[[ "$output" =~ ^rustc[[:space:]]+([^[:space:]]+) ]] || return 1
|
|
542
|
+
NORMALIZED_TOOL_VERSION=${BASH_REMATCH[1]}
|
|
543
|
+
;;
|
|
544
|
+
*) return 1 ;;
|
|
545
|
+
esac
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
tool_selection_requires_exact_output() {
|
|
549
|
+
local tool=$1 version=$2 numeric='[0-9]+\.[0-9]+\.[0-9]+'
|
|
550
|
+
case "$version" in
|
|
551
|
+
system|path:*|ref:*) return 1 ;;
|
|
552
|
+
esac
|
|
553
|
+
case "$tool" in
|
|
554
|
+
nodejs)
|
|
555
|
+
[[ "$version" =~ ^${numeric}(-rc\.[0-9]+)?$ ]]
|
|
556
|
+
;;
|
|
557
|
+
python)
|
|
558
|
+
[[ "$version" =~ ^${numeric}((a|b|rc)[0-9]+)?$ ]]
|
|
559
|
+
;;
|
|
560
|
+
ruby)
|
|
561
|
+
[[ "$version" =~ ^${numeric}(-(preview|rc)[0-9]+)?$ ]]
|
|
562
|
+
;;
|
|
563
|
+
golang|rust)
|
|
564
|
+
[[ "$version" =~ ^${numeric}$ ]]
|
|
565
|
+
;;
|
|
566
|
+
*) return 1 ;;
|
|
567
|
+
esac
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
tool_output_matches_selection() {
|
|
571
|
+
local tool=$1 version=$2 output=$3 expected
|
|
572
|
+
# Moving/plugin-specific aliases (for example Python's free-threaded `t`
|
|
573
|
+
# suffix or Ruby `-dev`) remain execution+containment proved. Plain releases
|
|
574
|
+
# and the deliberately enumerated prerelease forms must report exactly.
|
|
575
|
+
tool_selection_requires_exact_output "$tool" "$version" || return 0
|
|
576
|
+
normalize_tool_version_output "$tool" "$output" || return 1
|
|
577
|
+
expected=$version
|
|
578
|
+
if [ "$tool" = ruby ]; then
|
|
579
|
+
expected=${expected/-preview/preview}
|
|
580
|
+
expected=${expected/-rc/rc}
|
|
581
|
+
fi
|
|
582
|
+
[ "$NORMALIZED_TOOL_VERSION" = "$expected" ]
|
|
583
|
+
}
|
|
584
|
+
# END UAI_MANAGED_VERSION_PROOF
|
|
585
|
+
|
|
586
|
+
selection_is_proven() {
|
|
587
|
+
local tool=$1 version=$2 project=$3 executable variable reported npm_candidate npm_report
|
|
588
|
+
SELECTION_PROOF_ERROR=""
|
|
589
|
+
executable=$(tool_executable "$tool") \
|
|
590
|
+
|| {
|
|
591
|
+
SELECTION_PROOF_ERROR="no executable proof is defined for supported plugin $tool"
|
|
592
|
+
return 1
|
|
593
|
+
}
|
|
594
|
+
variable="ASDF_$(printf '%s' "$tool" | tr '[:lower:]-' '[:upper:]_')_VERSION"
|
|
595
|
+
export "$variable=$version"
|
|
596
|
+
if ! (cd "$project" && asdf current "$tool" >/dev/null); then
|
|
597
|
+
SELECTION_PROOF_ERROR="ASDF could not resolve $tool $version in $(basename "$project")"
|
|
598
|
+
unset "$variable"
|
|
599
|
+
return 1
|
|
600
|
+
fi
|
|
601
|
+
if ! selected_executable "$tool" "$version" "$project" "$executable"; then
|
|
602
|
+
SELECTION_PROOF_ERROR="$tool $version did not resolve executable '$executable'"
|
|
603
|
+
unset "$variable"
|
|
604
|
+
return 1
|
|
605
|
+
fi
|
|
606
|
+
if ! managed_executable_is_contained "$version" "$SELECTED_EXECUTABLE"; then
|
|
607
|
+
SELECTION_PROOF_ERROR="$tool $version executable escapes its exact managed install directory"
|
|
608
|
+
unset "$variable"
|
|
609
|
+
return 1
|
|
610
|
+
fi
|
|
611
|
+
if ! run_tool_version_probe "$tool" "$SELECTED_EXECUTABLE" "$project"; then
|
|
612
|
+
SELECTION_PROOF_ERROR="$tool $version executable '$executable' failed its version probe"
|
|
613
|
+
unset "$variable"
|
|
614
|
+
return 1
|
|
615
|
+
fi
|
|
616
|
+
reported=$TOOL_VERSION_OUTPUT
|
|
617
|
+
if ! tool_output_matches_selection "$tool" "$version" "$reported"; then
|
|
618
|
+
SELECTION_PROOF_ERROR="$tool $version executable reported '$reported'"
|
|
619
|
+
unset "$variable"
|
|
620
|
+
return 1
|
|
621
|
+
fi
|
|
622
|
+
if [ "$tool" = nodejs ]; then
|
|
623
|
+
case "$version" in
|
|
624
|
+
system|path:*) ;;
|
|
625
|
+
*)
|
|
626
|
+
npm_candidate="$MANAGED_INSTALL_PATH/bin/npm"
|
|
627
|
+
if ! managed_executable_is_contained "$version" "$npm_candidate" \
|
|
628
|
+
|| ! npm_report=$(cd "$project" \
|
|
629
|
+
&& PATH="$MANAGED_INSTALL_PATH/bin:$CONTROL_PATH" \
|
|
630
|
+
"$npm_candidate" --version 2>&1) \
|
|
631
|
+
|| [ -z "$npm_report" ]; then
|
|
632
|
+
SELECTION_PROOF_ERROR="nodejs $version has no contained runnable npm"
|
|
633
|
+
unset "$variable"
|
|
634
|
+
return 1
|
|
635
|
+
fi
|
|
636
|
+
;;
|
|
637
|
+
esac
|
|
638
|
+
fi
|
|
639
|
+
# Execute through the same shim-first PATH the app receives. This catches a
|
|
640
|
+
# direct `asdf which` success whose shim was lost by a later global reshim.
|
|
641
|
+
if ! run_tool_shim_version_probe "$tool" "$executable" "$project"; then
|
|
642
|
+
SELECTION_PROOF_ERROR="$tool $version shim '$executable' failed its version probe"
|
|
643
|
+
unset "$variable"
|
|
644
|
+
return 1
|
|
645
|
+
fi
|
|
646
|
+
reported=$TOOL_VERSION_OUTPUT
|
|
647
|
+
if ! tool_output_matches_selection "$tool" "$version" "$reported"; then
|
|
648
|
+
SELECTION_PROOF_ERROR="$tool $version shim reported '$reported'"
|
|
649
|
+
unset "$variable"
|
|
650
|
+
return 1
|
|
651
|
+
fi
|
|
652
|
+
unset "$variable"
|
|
653
|
+
return 0
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
prove_selection() {
|
|
657
|
+
local tool=$1 version=$2 project=$3 source=$4
|
|
658
|
+
case "$version" in
|
|
659
|
+
system|path:*) ;;
|
|
660
|
+
*) managed_install_path "$tool" "$version" "$project" "$source" ;;
|
|
661
|
+
esac
|
|
662
|
+
selection_is_proven "$tool" "$version" "$project" \
|
|
663
|
+
|| fail "$SELECTION_PROOF_ERROR"
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
# BEGIN UAI_MANAGED_INSTALL_REPAIR
|
|
667
|
+
# asdf 0.14.1 considers any existing install directory complete, even when a
|
|
668
|
+
# daemon/container death left it empty. A managed selection that fails the
|
|
669
|
+
# positive executable proof gets one exact-directory repair under the global
|
|
670
|
+
# maintenance lock. system/path selections never enter this code.
|
|
671
|
+
managed_install_location() {
|
|
672
|
+
local tool=$1 version=$2 project=$3 source=$4 suffix component
|
|
673
|
+
local installs_root lexical_installs_root real_installs_root
|
|
674
|
+
local root lexical_root real_root candidate lexical_candidate real_candidate
|
|
675
|
+
|
|
676
|
+
case "$tool" in
|
|
677
|
+
nodejs|python|golang|ruby|rust) ;;
|
|
678
|
+
*) fail "refusing to repair unsupported asdf plugin '$tool'" ;;
|
|
679
|
+
esac
|
|
680
|
+
case "$version" in
|
|
681
|
+
system|path:*) fail "refusing to repair external $tool $version" ;;
|
|
682
|
+
esac
|
|
683
|
+
# Revalidate at the destructive boundary instead of relying only on the
|
|
684
|
+
# earlier parser pass.
|
|
685
|
+
validate_version "$version" "$project" "$source" 0
|
|
686
|
+
case "$version" in
|
|
687
|
+
ref:*)
|
|
688
|
+
suffix=${version#ref:}
|
|
689
|
+
component="ref-$suffix"
|
|
690
|
+
;;
|
|
691
|
+
*) component=$version ;;
|
|
692
|
+
esac
|
|
693
|
+
|
|
694
|
+
installs_root="$ASDF_DATA/installs"
|
|
695
|
+
if [ -L "$installs_root" ] || [ ! -d "$installs_root" ]; then
|
|
696
|
+
fail "managed installs root $installs_root is not a real directory"
|
|
697
|
+
fi
|
|
698
|
+
lexical_installs_root=$("$REALPATH_BIN" -m -s -- "$installs_root") \
|
|
699
|
+
|| fail "could not normalize managed installs root $installs_root"
|
|
700
|
+
real_installs_root=$("$REALPATH_BIN" -e -- "$installs_root") \
|
|
701
|
+
|| fail "could not resolve managed installs root $installs_root"
|
|
702
|
+
[ "$real_installs_root" = "$lexical_installs_root" ] \
|
|
703
|
+
|| fail "managed installs root $installs_root traverses a symbolic link"
|
|
704
|
+
|
|
705
|
+
root="$installs_root/$tool"
|
|
706
|
+
lexical_root=$("$REALPATH_BIN" -m -s -- "$root") \
|
|
707
|
+
|| fail "could not normalize managed install root $root"
|
|
708
|
+
case "$lexical_root" in
|
|
709
|
+
"$lexical_installs_root"/*) ;;
|
|
710
|
+
*) fail "managed install root $root escapes $lexical_installs_root" ;;
|
|
711
|
+
esac
|
|
712
|
+
if [ -e "$root" ] || [ -L "$root" ]; then
|
|
713
|
+
if [ -L "$root" ] || [ ! -d "$root" ]; then
|
|
714
|
+
fail "managed install root $root is not a real directory"
|
|
715
|
+
fi
|
|
716
|
+
real_root=$("$REALPATH_BIN" -e -- "$root") \
|
|
717
|
+
|| fail "could not resolve managed install root $root"
|
|
718
|
+
[ "$real_root" = "$lexical_root" ] \
|
|
719
|
+
|| fail "managed install root $root traverses a symbolic link"
|
|
720
|
+
fi
|
|
721
|
+
|
|
722
|
+
candidate="$root/$component"
|
|
723
|
+
lexical_candidate=$("$REALPATH_BIN" -m -s -- "$candidate") \
|
|
724
|
+
|| fail "could not normalize managed install path for $tool $version"
|
|
725
|
+
case "$lexical_candidate" in
|
|
726
|
+
"$lexical_root"/*) ;;
|
|
727
|
+
*) fail "managed install path for $tool $version escapes $lexical_root" ;;
|
|
728
|
+
esac
|
|
729
|
+
MANAGED_INSTALL_EXISTS=0
|
|
730
|
+
if [ -e "$candidate" ] || [ -L "$candidate" ]; then
|
|
731
|
+
if [ -L "$candidate" ] || [ ! -d "$candidate" ]; then
|
|
732
|
+
fail "managed install path for $tool $version is not a real directory"
|
|
733
|
+
fi
|
|
734
|
+
real_candidate=$("$REALPATH_BIN" -e -- "$candidate") \
|
|
735
|
+
|| fail "could not resolve managed install path for $tool $version"
|
|
736
|
+
[ "$real_candidate" = "$lexical_candidate" ] \
|
|
737
|
+
|| fail "managed install path for $tool $version traverses a symbolic link"
|
|
738
|
+
MANAGED_INSTALL_EXISTS=1
|
|
739
|
+
fi
|
|
740
|
+
|
|
741
|
+
MANAGED_INSTALL_PATH=$lexical_candidate
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
managed_install_path() {
|
|
745
|
+
managed_install_location "$@"
|
|
746
|
+
[ "$MANAGED_INSTALL_EXISTS" -eq 1 ] \
|
|
747
|
+
|| fail "failed $1 $2 proof but its exact managed install directory is missing"
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
is_default_node_version() {
|
|
751
|
+
local requested=$1 line version
|
|
752
|
+
local -a fields=()
|
|
753
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
754
|
+
line=${line%%#*}
|
|
755
|
+
line=$(trim_spaces "$line")
|
|
756
|
+
case "$line" in
|
|
757
|
+
nodejs' '*)
|
|
758
|
+
read -r -a fields <<< "$line"
|
|
759
|
+
for version in "${fields[@]:1}"; do
|
|
760
|
+
[ "$version" != "$requested" ] || return 0
|
|
761
|
+
done
|
|
762
|
+
;;
|
|
763
|
+
esac
|
|
764
|
+
done < /home/node/.tool-versions
|
|
765
|
+
return 1
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
prove_or_repair_managed_selection() {
|
|
769
|
+
local tool=$1 version=$2 project=$3 source=$4 initial_error failed_path
|
|
770
|
+
case "$version" in
|
|
771
|
+
system|path:*)
|
|
772
|
+
selection_is_proven "$tool" "$version" "$project" \
|
|
773
|
+
|| fail "$SELECTION_PROOF_ERROR"
|
|
774
|
+
return 0
|
|
775
|
+
;;
|
|
776
|
+
esac
|
|
777
|
+
|
|
778
|
+
# Validate even a healthy-looking cache entry before ASDF resolution: asdf
|
|
779
|
+
# follows an install-directory symlink and can otherwise prove the wrong
|
|
780
|
+
# runtime version under the requested selector.
|
|
781
|
+
managed_install_path "$tool" "$version" "$project" "$source"
|
|
782
|
+
if selection_is_proven "$tool" "$version" "$project"; then
|
|
783
|
+
return 0
|
|
784
|
+
fi
|
|
785
|
+
initial_error=$SELECTION_PROOF_ERROR
|
|
786
|
+
# ${VERIFY_ONLY:-0}: this block is extracted into a standalone test harness
|
|
787
|
+
# that runs it without the script preamble.
|
|
788
|
+
if [ "${VERIFY_ONLY:-0}" = 1 ]; then
|
|
789
|
+
fail "$initial_error (verify-only pass makes no repairs)"
|
|
790
|
+
fi
|
|
791
|
+
failed_path=$MANAGED_INSTALL_PATH
|
|
792
|
+
if [ "$tool" = nodejs ] && is_default_node_version "$version"; then
|
|
793
|
+
fail "$initial_error; refusing to remove the host-default Node candidate because boot maintenance owns its CLI/Corepack invariant"
|
|
794
|
+
fi
|
|
795
|
+
# Candidate-controlled probes ran after the initial path check. Re-resolve
|
|
796
|
+
# every parent immediately at the destructive boundary and require the same
|
|
797
|
+
# exact target before removing anything.
|
|
798
|
+
managed_install_path "$tool" "$version" "$project" "$source"
|
|
799
|
+
[ "$MANAGED_INSTALL_PATH" = "$failed_path" ] \
|
|
800
|
+
|| fail "managed install path for $tool $version changed during proof"
|
|
801
|
+
log "$initial_error; removing incomplete exact cache entry $MANAGED_INSTALL_PATH and retrying once"
|
|
802
|
+
rm -rf --one-file-system -- "${MANAGED_INSTALL_PATH:?}" \
|
|
803
|
+
|| fail "could not remove incomplete $tool $version install"
|
|
804
|
+
if [ -e "$MANAGED_INSTALL_PATH" ] || [ -L "$MANAGED_INSTALL_PATH" ]; then
|
|
805
|
+
fail "incomplete $tool $version install still exists after repair removal"
|
|
806
|
+
fi
|
|
807
|
+
(cd "$project" && run_asdf_install "$tool" "$version") \
|
|
808
|
+
|| fail "asdf reinstall failed for $tool $version in $(basename "$project")"
|
|
809
|
+
managed_install_path "$tool" "$version" "$project" "$source"
|
|
810
|
+
(cd "$project" && run_asdf_reshim "$tool" "$version") \
|
|
811
|
+
|| fail "asdf reshim failed after repairing $tool $version"
|
|
812
|
+
managed_install_path "$tool" "$version" "$project" "$source"
|
|
813
|
+
selection_is_proven "$tool" "$version" "$project" \
|
|
814
|
+
|| fail "$SELECTION_PROOF_ERROR after one exact-directory repair"
|
|
815
|
+
}
|
|
816
|
+
# END UAI_MANAGED_INSTALL_REPAIR
|
|
817
|
+
|
|
818
|
+
resolve_node_context_executable() {
|
|
819
|
+
local project=$1 node_version=$2 source=$3 executable=$4 candidate
|
|
820
|
+
NODE_CONTEXT_EXECUTABLE=""
|
|
821
|
+
case "$node_version" in
|
|
822
|
+
system|path:*)
|
|
823
|
+
selected_executable nodejs "$node_version" "$project" "$executable" \
|
|
824
|
+
|| return 1
|
|
825
|
+
NODE_CONTEXT_EXECUTABLE=$SELECTED_EXECUTABLE
|
|
826
|
+
return 0
|
|
827
|
+
;;
|
|
828
|
+
esac
|
|
829
|
+
|
|
830
|
+
managed_install_path nodejs "$node_version" "$project" "$source"
|
|
831
|
+
if [ "$executable" = npm ]; then
|
|
832
|
+
# asdf-nodejs intentionally reports a plugin-owned wrapper for `asdf which
|
|
833
|
+
# npm`; maintenance must execute the selected runtime's own npm instead.
|
|
834
|
+
candidate="$MANAGED_INSTALL_PATH/bin/npm"
|
|
835
|
+
managed_executable_is_contained "$node_version" "$candidate" || return 1
|
|
836
|
+
NODE_CONTEXT_EXECUTABLE=$candidate
|
|
837
|
+
return 0
|
|
838
|
+
fi
|
|
839
|
+
selected_executable nodejs "$node_version" "$project" "$executable" \
|
|
840
|
+
|| return 1
|
|
841
|
+
managed_executable_is_contained "$node_version" "$SELECTED_EXECUTABLE" \
|
|
842
|
+
|| return 1
|
|
843
|
+
NODE_CONTEXT_EXECUTABLE=$SELECTED_EXECUTABLE
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
# BEGIN UAI_MANAGED_NODE_PROCESS_CONTEXT
|
|
847
|
+
managed_node_process_context_is_selected() {
|
|
848
|
+
local project=$1 node_version=$2 report reported_version reported_executable
|
|
849
|
+
report=$(cd "$project" \
|
|
850
|
+
&& PATH="$MANAGED_INSTALL_PATH/bin:$CONTROL_PATH" \
|
|
851
|
+
/usr/bin/env node -p 'process.version + "\t" + process.execPath' 2>/dev/null) \
|
|
852
|
+
|| return 1
|
|
853
|
+
case "$report" in
|
|
854
|
+
*$'\t'*) ;;
|
|
855
|
+
*) return 1 ;;
|
|
856
|
+
esac
|
|
857
|
+
reported_version=${report%%$'\t'*}
|
|
858
|
+
reported_executable=${report#*$'\t'}
|
|
859
|
+
managed_executable_is_contained "$node_version" "$reported_executable" \
|
|
860
|
+
|| return 1
|
|
861
|
+
tool_output_matches_selection nodejs "$node_version" "$reported_version"
|
|
862
|
+
}
|
|
863
|
+
# END UAI_MANAGED_NODE_PROCESS_CONTEXT
|
|
864
|
+
|
|
865
|
+
execute_in_node_context() {
|
|
866
|
+
local executable=$1 project=$2 node_version=$3 suffix resolved
|
|
867
|
+
shift 3
|
|
868
|
+
case "$node_version" in
|
|
869
|
+
system)
|
|
870
|
+
(cd "$project" && PATH="$CONTROL_PATH" "$executable" "$@")
|
|
871
|
+
;;
|
|
872
|
+
path:*)
|
|
873
|
+
suffix=${node_version#path:}
|
|
874
|
+
resolved=$(cd "$project" && "$REALPATH_BIN" -e -- "$suffix") || return 1
|
|
875
|
+
(cd "$project" && PATH="$resolved/bin:$CONTROL_PATH" "$executable" "$@")
|
|
876
|
+
;;
|
|
877
|
+
*)
|
|
878
|
+
managed_node_process_context_is_selected "$project" "$node_version" \
|
|
879
|
+
|| return 1
|
|
880
|
+
(cd "$project" && PATH="$MANAGED_INSTALL_PATH/bin:$CONTROL_PATH" \
|
|
881
|
+
ASDF_NODEJS_VERSION="$node_version" \
|
|
882
|
+
"$executable" "$@")
|
|
883
|
+
;;
|
|
884
|
+
esac
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
corepack_version_is_usable() {
|
|
888
|
+
local corepack_bin=$1 project=$2 node_version=$3 version
|
|
889
|
+
version=$(execute_in_node_context \
|
|
890
|
+
"$corepack_bin" "$project" "$node_version" --version 2>/dev/null) \
|
|
891
|
+
|| return 1
|
|
892
|
+
[ "$version" = "$COREPACK_VERSION" ]
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
# BEGIN UAI_MANAGED_COREPACK_REPAIR
|
|
896
|
+
reinstall_managed_corepack() {
|
|
897
|
+
local project=$1 node_version=$2 source_manifest=$3 manager=$4
|
|
898
|
+
local npm_bin corepack_bin
|
|
899
|
+
resolve_node_context_executable \
|
|
900
|
+
"$project" "$node_version" "$source_manifest" npm \
|
|
901
|
+
|| fail "nodejs $node_version has no contained npm for Corepack repair"
|
|
902
|
+
npm_bin=$NODE_CONTEXT_EXECUTABLE
|
|
903
|
+
log "reinstalling corepack@$COREPACK_VERSION inside nodejs $node_version"
|
|
904
|
+
(cd / && PATH="$MANAGED_INSTALL_PATH/bin:$CONTROL_PATH" \
|
|
905
|
+
ASDF_NODEJS_VERSION="$node_version" \
|
|
906
|
+
"$npm_bin" install -g --force "corepack@$COREPACK_VERSION" \
|
|
907
|
+
--no-audit --no-fund \
|
|
908
|
+
--registry="$NPM_REGISTRY" \
|
|
909
|
+
--userconfig="$NPM_USERCONFIG" \
|
|
910
|
+
--globalconfig="$NPM_GLOBALCONFIG") \
|
|
911
|
+
|| fail "could not reinstall corepack@$COREPACK_VERSION for nodejs $node_version"
|
|
912
|
+
managed_install_path nodejs "$node_version" "$project" "$source_manifest"
|
|
913
|
+
run_asdf_reshim nodejs "$node_version" \
|
|
914
|
+
|| fail "could not reshim nodejs $node_version after Corepack reinstall"
|
|
915
|
+
managed_install_path nodejs "$node_version" "$project" "$source_manifest"
|
|
916
|
+
resolve_node_context_executable \
|
|
917
|
+
"$project" "$node_version" "$source_manifest" corepack \
|
|
918
|
+
|| fail "reinstalled Corepack escaped nodejs $node_version"
|
|
919
|
+
corepack_bin=$NODE_CONTEXT_EXECUTABLE
|
|
920
|
+
corepack_version_is_usable "$corepack_bin" "$project" "$node_version" \
|
|
921
|
+
|| fail "reinstalled Corepack failed proof for nodejs $node_version"
|
|
922
|
+
execute_in_node_context \
|
|
923
|
+
"$corepack_bin" "$project" "$node_version" disable "$manager" \
|
|
924
|
+
|| fail "reinstalled Corepack could not clear the stale $manager launcher"
|
|
925
|
+
managed_install_path nodejs "$node_version" "$project" "$source_manifest"
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
prepare_managed_package_manager_once() {
|
|
929
|
+
local project=$1 node_version=$2 manager=$3 spec=$4 is_fallback=$5
|
|
930
|
+
local source_manifest=$6 corepack_bin manager_bin actual expected
|
|
931
|
+
COREPACK_PREPARATION_ERROR=""
|
|
932
|
+
|
|
933
|
+
if ! resolve_node_context_executable \
|
|
934
|
+
"$project" "$node_version" "$source_manifest" corepack; then
|
|
935
|
+
COREPACK_PREPARATION_ERROR="Corepack is not contained in nodejs $node_version"
|
|
936
|
+
return 1
|
|
937
|
+
fi
|
|
938
|
+
corepack_bin=$NODE_CONTEXT_EXECUTABLE
|
|
939
|
+
if ! corepack_version_is_usable "$corepack_bin" "$project" "$node_version"; then
|
|
940
|
+
COREPACK_PREPARATION_ERROR="Corepack version proof failed in nodejs $node_version"
|
|
941
|
+
return 1
|
|
942
|
+
fi
|
|
943
|
+
if [ "$is_fallback" -eq 1 ]; then
|
|
944
|
+
if ! execute_in_node_context \
|
|
945
|
+
"$corepack_bin" "$project" "$node_version" install --global "$spec"; then
|
|
946
|
+
COREPACK_PREPARATION_ERROR="Corepack could not install fallback $spec"
|
|
947
|
+
return 1
|
|
948
|
+
fi
|
|
949
|
+
elif ! execute_in_node_context \
|
|
950
|
+
"$corepack_bin" "$project" "$node_version" \
|
|
951
|
+
install --global --cache-only "$spec"; then
|
|
952
|
+
COREPACK_PREPARATION_ERROR="Corepack could not cache exact $spec"
|
|
953
|
+
return 1
|
|
954
|
+
fi
|
|
955
|
+
|
|
956
|
+
if ! resolve_node_context_executable \
|
|
957
|
+
"$project" "$node_version" "$source_manifest" corepack; then
|
|
958
|
+
COREPACK_PREPARATION_ERROR="Corepack escaped nodejs $node_version after cache preparation"
|
|
959
|
+
return 1
|
|
960
|
+
fi
|
|
961
|
+
corepack_bin=$NODE_CONTEXT_EXECUTABLE
|
|
962
|
+
if ! execute_in_node_context \
|
|
963
|
+
"$corepack_bin" "$project" "$node_version" enable "$manager"; then
|
|
964
|
+
COREPACK_PREPARATION_ERROR="Corepack could not enable $manager"
|
|
965
|
+
return 1
|
|
966
|
+
fi
|
|
967
|
+
managed_install_path nodejs "$node_version" "$project" "$source_manifest"
|
|
968
|
+
if ! run_asdf_reshim nodejs "$node_version"; then
|
|
969
|
+
COREPACK_PREPARATION_ERROR="asdf could not reshim $manager for nodejs $node_version"
|
|
970
|
+
return 1
|
|
971
|
+
fi
|
|
972
|
+
managed_install_path nodejs "$node_version" "$project" "$source_manifest"
|
|
973
|
+
if ! resolve_node_context_executable \
|
|
974
|
+
"$project" "$node_version" "$source_manifest" corepack; then
|
|
975
|
+
COREPACK_PREPARATION_ERROR="Corepack escaped nodejs $node_version after final reshim"
|
|
976
|
+
return 1
|
|
977
|
+
fi
|
|
978
|
+
corepack_bin=$NODE_CONTEXT_EXECUTABLE
|
|
979
|
+
if ! corepack_version_is_usable "$corepack_bin" "$project" "$node_version"; then
|
|
980
|
+
COREPACK_PREPARATION_ERROR="Corepack failed final version proof in nodejs $node_version"
|
|
981
|
+
return 1
|
|
982
|
+
fi
|
|
983
|
+
if ! resolve_node_context_executable \
|
|
984
|
+
"$project" "$node_version" "$source_manifest" "$manager"; then
|
|
985
|
+
COREPACK_PREPARATION_ERROR="$manager is not contained in nodejs $node_version"
|
|
986
|
+
return 1
|
|
987
|
+
fi
|
|
988
|
+
manager_bin=$NODE_CONTEXT_EXECUTABLE
|
|
989
|
+
if ! actual=$(execute_in_node_context \
|
|
990
|
+
"$manager_bin" "$project" "$node_version" --version); then
|
|
991
|
+
COREPACK_PREPARATION_ERROR="$manager is not executable in nodejs $node_version"
|
|
992
|
+
return 1
|
|
993
|
+
fi
|
|
994
|
+
expected=${spec#"$manager"@}
|
|
995
|
+
expected=${expected%%+*}
|
|
996
|
+
if [ "$actual" != "$expected" ]; then
|
|
997
|
+
COREPACK_PREPARATION_ERROR="$manager resolved $actual instead of $expected"
|
|
998
|
+
return 1
|
|
999
|
+
fi
|
|
1000
|
+
return 0
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
prepare_package_manager() {
|
|
1004
|
+
local project=$1 node_version=$2 manager=$3 spec=$4 is_fallback=$5
|
|
1005
|
+
local source_manifest=$6
|
|
1006
|
+
local corepack_bin manager_bin actual expected initial_error
|
|
1007
|
+
local corepack_repaired=0
|
|
1008
|
+
|
|
1009
|
+
case "$node_version" in
|
|
1010
|
+
system|path:*)
|
|
1011
|
+
resolve_node_context_executable \
|
|
1012
|
+
"$project" "$node_version" "$source_manifest" corepack \
|
|
1013
|
+
|| fail "external nodejs $node_version must already provide Corepack for $manager"
|
|
1014
|
+
corepack_bin=$NODE_CONTEXT_EXECUTABLE
|
|
1015
|
+
corepack_version_is_usable "$corepack_bin" "$project" "$node_version" || fail \
|
|
1016
|
+
"external nodejs $node_version has obsolete Corepack; Uai will not mutate external runtimes"
|
|
1017
|
+
resolve_node_context_executable \
|
|
1018
|
+
"$project" "$node_version" "$source_manifest" "$manager" \
|
|
1019
|
+
|| fail "external nodejs $node_version must already provide $manager; Uai will not mutate it"
|
|
1020
|
+
manager_bin=$NODE_CONTEXT_EXECUTABLE
|
|
1021
|
+
actual=$(execute_in_node_context \
|
|
1022
|
+
"$manager_bin" "$project" "$node_version" --version) \
|
|
1023
|
+
|| fail "$manager is not executable for external nodejs $node_version"
|
|
1024
|
+
expected=${spec#"$manager"@}
|
|
1025
|
+
expected=${expected%%+*}
|
|
1026
|
+
[ "$actual" = "$expected" ] || fail \
|
|
1027
|
+
"$manager resolved $actual instead of packageManager version $expected for $(basename "$project")"
|
|
1028
|
+
return
|
|
1029
|
+
;;
|
|
1030
|
+
esac
|
|
1031
|
+
|
|
1032
|
+
resolve_node_context_executable \
|
|
1033
|
+
"$project" "$node_version" "$source_manifest" npm \
|
|
1034
|
+
|| fail "nodejs $node_version installed without a contained runnable npm"
|
|
1035
|
+
if resolve_node_context_executable \
|
|
1036
|
+
"$project" "$node_version" "$source_manifest" corepack; then
|
|
1037
|
+
corepack_bin=$NODE_CONTEXT_EXECUTABLE
|
|
1038
|
+
else
|
|
1039
|
+
corepack_bin=""
|
|
1040
|
+
fi
|
|
1041
|
+
if [ -z "$corepack_bin" ] \
|
|
1042
|
+
|| ! corepack_version_is_usable "$corepack_bin" "$project" "$node_version"; then
|
|
1043
|
+
log "Corepack is missing or failed its exact proof in nodejs $node_version; performing one exact Corepack repair"
|
|
1044
|
+
reinstall_managed_corepack \
|
|
1045
|
+
"$project" "$node_version" "$source_manifest" "$manager"
|
|
1046
|
+
corepack_repaired=1
|
|
1047
|
+
fi
|
|
1048
|
+
|
|
1049
|
+
log "enabling Corepack $manager for nodejs $node_version"
|
|
1050
|
+
if prepare_managed_package_manager_once \
|
|
1051
|
+
"$project" "$node_version" "$manager" "$spec" "$is_fallback" \
|
|
1052
|
+
"$source_manifest"; then
|
|
1053
|
+
return 0
|
|
1054
|
+
fi
|
|
1055
|
+
initial_error=$COREPACK_PREPARATION_ERROR
|
|
1056
|
+
if [ "$corepack_repaired" -eq 1 ]; then
|
|
1057
|
+
fail "$initial_error after one exact Corepack repair"
|
|
1058
|
+
fi
|
|
1059
|
+
log "$initial_error; performing one exact Corepack repair and retry"
|
|
1060
|
+
reinstall_managed_corepack \
|
|
1061
|
+
"$project" "$node_version" "$source_manifest" "$manager"
|
|
1062
|
+
prepare_managed_package_manager_once \
|
|
1063
|
+
"$project" "$node_version" "$manager" "$spec" "$is_fallback" \
|
|
1064
|
+
"$source_manifest" \
|
|
1065
|
+
|| fail "$COREPACK_PREPARATION_ERROR after one exact Corepack repair (initial: $initial_error)"
|
|
1066
|
+
}
|
|
1067
|
+
# END UAI_MANAGED_COREPACK_REPAIR
|
|
1068
|
+
|
|
1069
|
+
materialize_manifest() {
|
|
1070
|
+
local manifest=$1 project line tool version line_number=0
|
|
1071
|
+
local -a fields=()
|
|
1072
|
+
|
|
1073
|
+
project=$(dirname "$manifest")
|
|
1074
|
+
project_package_manager "$project"
|
|
1075
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1076
|
+
line_number=$((line_number + 1))
|
|
1077
|
+
line=${line%%#*}
|
|
1078
|
+
line=$(trim_spaces "$line")
|
|
1079
|
+
[ -n "$line" ] || continue
|
|
1080
|
+
read -r -a fields <<< "$line"
|
|
1081
|
+
tool=${fields[0]}
|
|
1082
|
+
case "$tool" in
|
|
1083
|
+
pnpm|yarn) continue ;; # Corepack-managed; see project_package_manager.
|
|
1084
|
+
esac
|
|
1085
|
+
unset_tool_override "$tool"
|
|
1086
|
+
asdf plugin list | grep -Fqx -- "$tool" \
|
|
1087
|
+
|| fail "$manifest:$line_number requests unsupported asdf plugin '$tool'"
|
|
1088
|
+
|
|
1089
|
+
for version in "${fields[@]:1}"; do
|
|
1090
|
+
case "$version" in
|
|
1091
|
+
system) ;;
|
|
1092
|
+
path:*)
|
|
1093
|
+
if [ "$VERIFY_ONLY" = 1 ]; then
|
|
1094
|
+
# Shims either already exist (the later shim probe proves them) or
|
|
1095
|
+
# the verify pass fails and the host escalates to the writable one.
|
|
1096
|
+
log "verify-only: skipping shim creation for external $tool $version"
|
|
1097
|
+
else
|
|
1098
|
+
(cd "$project" && run_asdf_reshim "$tool" "$version") \
|
|
1099
|
+
|| fail "could not create ASDF shims for external $tool $version"
|
|
1100
|
+
fi
|
|
1101
|
+
;;
|
|
1102
|
+
*)
|
|
1103
|
+
if [ "$tool" = nodejs ] && is_default_node_version "$version"; then
|
|
1104
|
+
# Boot maintenance exclusively owns the default Node candidate and
|
|
1105
|
+
# its globally installed agent CLIs/Corepack. Do not create or
|
|
1106
|
+
# alter it after a failed boot transaction.
|
|
1107
|
+
managed_install_location \
|
|
1108
|
+
"$tool" "$version" "$project" "$manifest"
|
|
1109
|
+
[ "$MANAGED_INSTALL_EXISTS" -eq 1 ] \
|
|
1110
|
+
|| fail "host-default nodejs $version is missing; standard-image boot maintenance must repair it before task runtime materialization"
|
|
1111
|
+
log "proving host-default $tool $version for $(basename "$project")"
|
|
1112
|
+
elif [ "$VERIFY_ONLY" = 1 ]; then
|
|
1113
|
+
managed_install_location \
|
|
1114
|
+
"$tool" "$version" "$project" "$manifest"
|
|
1115
|
+
[ "$MANAGED_INSTALL_EXISTS" -eq 1 ] \
|
|
1116
|
+
|| fail "$tool $version is not installed on the shared runtime volume (verify-only pass makes no installs)"
|
|
1117
|
+
log "verifying $tool $version for $(basename "$project")"
|
|
1118
|
+
else
|
|
1119
|
+
managed_install_location \
|
|
1120
|
+
"$tool" "$version" "$project" "$manifest"
|
|
1121
|
+
log "installing $tool $version for $(basename "$project")"
|
|
1122
|
+
(cd "$project" && run_asdf_install "$tool" "$version") \
|
|
1123
|
+
|| fail "asdf install failed for $tool $version in $(basename "$project")"
|
|
1124
|
+
fi
|
|
1125
|
+
;;
|
|
1126
|
+
esac
|
|
1127
|
+
done
|
|
1128
|
+
for version in "${fields[@]:1}"; do
|
|
1129
|
+
prove_or_repair_managed_selection \
|
|
1130
|
+
"$tool" "$version" "$project" "$manifest"
|
|
1131
|
+
done
|
|
1132
|
+
|
|
1133
|
+
done < "$manifest"
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
manifest_declares_tool() {
|
|
1137
|
+
local manifest=$1 tool=$2 line
|
|
1138
|
+
[ -f "$manifest" ] || return 1
|
|
1139
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1140
|
+
line=${line%%#*}
|
|
1141
|
+
line=$(trim_spaces "$line")
|
|
1142
|
+
case "$line" in
|
|
1143
|
+
"$tool"' '*) return 0 ;;
|
|
1144
|
+
esac
|
|
1145
|
+
done < "$manifest"
|
|
1146
|
+
return 1
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
materialize_fallback_runtimes() {
|
|
1150
|
+
local project=$1 defaults=/home/node/.tool-versions manifest line tool version
|
|
1151
|
+
local -a fields=()
|
|
1152
|
+
manifest="$project/.tool-versions"
|
|
1153
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1154
|
+
line=${line%%#*}
|
|
1155
|
+
line=$(trim_spaces "$line")
|
|
1156
|
+
[ -n "$line" ] || continue
|
|
1157
|
+
read -r -a fields <<< "$line"
|
|
1158
|
+
tool=${fields[0]}
|
|
1159
|
+
if manifest_declares_tool "$manifest" "$tool"; then
|
|
1160
|
+
continue
|
|
1161
|
+
fi
|
|
1162
|
+
asdf plugin list | grep -Fqx -- "$tool" \
|
|
1163
|
+
|| fail "$defaults requests unavailable fallback plugin '$tool'"
|
|
1164
|
+
for version in "${fields[@]:1}"; do
|
|
1165
|
+
case "$version" in
|
|
1166
|
+
system) ;;
|
|
1167
|
+
path:*)
|
|
1168
|
+
if [ "$VERIFY_ONLY" = 1 ]; then
|
|
1169
|
+
log "verify-only: skipping shim creation for fallback external $tool $version"
|
|
1170
|
+
else
|
|
1171
|
+
(cd "$project" && run_asdf_reshim "$tool" "$version") \
|
|
1172
|
+
|| fail "could not create ASDF shims for fallback external $tool $version"
|
|
1173
|
+
fi
|
|
1174
|
+
;;
|
|
1175
|
+
*)
|
|
1176
|
+
if [ "$tool" = nodejs ] && is_default_node_version "$version"; then
|
|
1177
|
+
# The standard-image transaction must restore Node/npm plus its
|
|
1178
|
+
# CLI/Corepack invariant as one crash-retryable unit.
|
|
1179
|
+
managed_install_location \
|
|
1180
|
+
"$tool" "$version" "$project" "$defaults"
|
|
1181
|
+
[ "$MANAGED_INSTALL_EXISTS" -eq 1 ] \
|
|
1182
|
+
|| fail "fallback host-default nodejs $version is missing; standard-image boot maintenance must repair it before task runtime materialization"
|
|
1183
|
+
log "proving fallback host-default $tool $version for $(basename "$project")"
|
|
1184
|
+
elif [ "$VERIFY_ONLY" = 1 ]; then
|
|
1185
|
+
managed_install_location \
|
|
1186
|
+
"$tool" "$version" "$project" "$defaults"
|
|
1187
|
+
[ "$MANAGED_INSTALL_EXISTS" -eq 1 ] \
|
|
1188
|
+
|| fail "fallback $tool $version is not installed on the shared runtime volume (verify-only pass makes no installs)"
|
|
1189
|
+
log "verifying fallback $tool $version for $(basename "$project")"
|
|
1190
|
+
else
|
|
1191
|
+
managed_install_location \
|
|
1192
|
+
"$tool" "$version" "$project" "$defaults"
|
|
1193
|
+
log "installing fallback $tool $version for $(basename "$project")"
|
|
1194
|
+
(cd "$project" && run_asdf_install "$tool" "$version") \
|
|
1195
|
+
|| fail "asdf install failed for fallback $tool $version in $(basename "$project")"
|
|
1196
|
+
fi
|
|
1197
|
+
;;
|
|
1198
|
+
esac
|
|
1199
|
+
prove_or_repair_managed_selection \
|
|
1200
|
+
"$tool" "$version" "$project" "$defaults"
|
|
1201
|
+
done
|
|
1202
|
+
done < "$defaults"
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
default_node_versions() {
|
|
1206
|
+
local defaults=/home/node/.tool-versions line
|
|
1207
|
+
local -a fields=()
|
|
1208
|
+
DEFAULT_NODE_VERSIONS=()
|
|
1209
|
+
if [ -L "$defaults" ] || [ ! -f "$defaults" ]; then
|
|
1210
|
+
fail "effective default Node manifest $defaults is not a regular file"
|
|
1211
|
+
fi
|
|
1212
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1213
|
+
line=${line%%#*}
|
|
1214
|
+
line=$(trim_spaces "$line")
|
|
1215
|
+
case "$line" in
|
|
1216
|
+
nodejs' '*)
|
|
1217
|
+
read -r -a fields <<< "$line"
|
|
1218
|
+
for version in "${fields[@]:1}"; do
|
|
1219
|
+
validate_version "$version" /home/node "$defaults" 1
|
|
1220
|
+
DEFAULT_NODE_VERSIONS+=("$version")
|
|
1221
|
+
done
|
|
1222
|
+
;;
|
|
1223
|
+
esac
|
|
1224
|
+
done < "$defaults"
|
|
1225
|
+
[ "${#DEFAULT_NODE_VERSIONS[@]}" -gt 0 ] \
|
|
1226
|
+
|| fail "$defaults does not declare an effective default Node runtime"
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
effective_node_versions() {
|
|
1230
|
+
local project=$1 manifest="$project/.tool-versions" line
|
|
1231
|
+
local -a fields=()
|
|
1232
|
+
EFFECTIVE_NODE_VERSIONS=()
|
|
1233
|
+
EFFECTIVE_NODE_SOURCE=""
|
|
1234
|
+
if manifest_declares_tool "$manifest" nodejs; then
|
|
1235
|
+
EFFECTIVE_NODE_SOURCE=$manifest
|
|
1236
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1237
|
+
line=${line%%#*}
|
|
1238
|
+
line=$(trim_spaces "$line")
|
|
1239
|
+
case "$line" in
|
|
1240
|
+
nodejs' '*)
|
|
1241
|
+
read -r -a fields <<< "$line"
|
|
1242
|
+
EFFECTIVE_NODE_VERSIONS=("${fields[@]:1}")
|
|
1243
|
+
break
|
|
1244
|
+
;;
|
|
1245
|
+
esac
|
|
1246
|
+
done < "$manifest"
|
|
1247
|
+
else
|
|
1248
|
+
default_node_versions
|
|
1249
|
+
EFFECTIVE_NODE_VERSIONS=("${DEFAULT_NODE_VERSIONS[@]}")
|
|
1250
|
+
EFFECTIVE_NODE_SOURCE=/home/node/.tool-versions
|
|
1251
|
+
fi
|
|
1252
|
+
[ "${#EFFECTIVE_NODE_VERSIONS[@]}" -gt 0 ] \
|
|
1253
|
+
|| fail "could not determine effective Node versions for $(basename "$project")"
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
project_uses_npm() {
|
|
1257
|
+
local project=$1 manager=""
|
|
1258
|
+
[ -f "$project/package.json" ] || return 1
|
|
1259
|
+
manager=$(jq -r '.packageManager // ""' "$project/package.json" \
|
|
1260
|
+
| sed 's/@.*//')
|
|
1261
|
+
if [ -z "$manager" ]; then
|
|
1262
|
+
if [ -f "$project/pnpm-lock.yaml" ]; then
|
|
1263
|
+
manager=pnpm
|
|
1264
|
+
elif [ -f "$project/yarn.lock" ]; then
|
|
1265
|
+
manager=yarn
|
|
1266
|
+
elif [ -f "$project/bun.lockb" ] || [ -f "$project/bun.lock" ]; then
|
|
1267
|
+
manager=bun
|
|
1268
|
+
else
|
|
1269
|
+
manager=npm
|
|
1270
|
+
fi
|
|
1271
|
+
fi
|
|
1272
|
+
case "$manager" in
|
|
1273
|
+
pnpm|yarn|bun) return 1 ;;
|
|
1274
|
+
*) return 0 ;;
|
|
1275
|
+
esac
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
prove_project_npm() {
|
|
1279
|
+
local project=$1 version npm_bin actual shim_actual
|
|
1280
|
+
project_uses_npm "$project" || return 0
|
|
1281
|
+
effective_node_versions "$project"
|
|
1282
|
+
for version in "${EFFECTIVE_NODE_VERSIONS[@]}"; do
|
|
1283
|
+
resolve_node_context_executable \
|
|
1284
|
+
"$project" "$version" "$EFFECTIVE_NODE_SOURCE" npm \
|
|
1285
|
+
|| fail "npm did not resolve inside effective nodejs $version in $(basename "$project")"
|
|
1286
|
+
npm_bin=$NODE_CONTEXT_EXECUTABLE
|
|
1287
|
+
actual=$(execute_in_node_context \
|
|
1288
|
+
"$npm_bin" "$project" "$version" --version) \
|
|
1289
|
+
|| fail "npm execution failed for effective nodejs $version in $(basename "$project")"
|
|
1290
|
+
[ -n "$actual" ] \
|
|
1291
|
+
|| fail "npm returned no version for effective nodejs $version in $(basename "$project")"
|
|
1292
|
+
shim_actual=$(run_node_shim_probe \
|
|
1293
|
+
"$project" "$version" npm --version) \
|
|
1294
|
+
|| fail "npm shim execution failed for effective nodejs $version in $(basename "$project")"
|
|
1295
|
+
[ -n "$shim_actual" ] \
|
|
1296
|
+
|| fail "npm shim returned no version for effective nodejs $version in $(basename "$project")"
|
|
1297
|
+
done
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
prepare_project_package_manager() {
|
|
1301
|
+
local project=$1 version
|
|
1302
|
+
project_package_manager "$project"
|
|
1303
|
+
[ -n "$PROJECT_PACKAGE_MANAGER" ] || return 0
|
|
1304
|
+
effective_node_versions "$project"
|
|
1305
|
+
for version in "${EFFECTIVE_NODE_VERSIONS[@]}"; do
|
|
1306
|
+
prepare_package_manager "$project" "$version" \
|
|
1307
|
+
"$PROJECT_PACKAGE_MANAGER" "$PROJECT_PACKAGE_MANAGER_SPEC" \
|
|
1308
|
+
"$PROJECT_PACKAGE_MANAGER_IS_FALLBACK" "$EFFECTIVE_NODE_SOURCE"
|
|
1309
|
+
done
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
prove_project_package_manager() {
|
|
1313
|
+
local project=$1 version manager_bin actual shim_actual expected
|
|
1314
|
+
project_package_manager "$project"
|
|
1315
|
+
[ -n "$PROJECT_PACKAGE_MANAGER" ] || return 0
|
|
1316
|
+
effective_node_versions "$project"
|
|
1317
|
+
expected=${PROJECT_PACKAGE_MANAGER_SPEC#"$PROJECT_PACKAGE_MANAGER"@}
|
|
1318
|
+
expected=${expected%%+*}
|
|
1319
|
+
for version in "${EFFECTIVE_NODE_VERSIONS[@]}"; do
|
|
1320
|
+
resolve_node_context_executable \
|
|
1321
|
+
"$project" "$version" "$EFFECTIVE_NODE_SOURCE" \
|
|
1322
|
+
"$PROJECT_PACKAGE_MANAGER" \
|
|
1323
|
+
|| fail "final $PROJECT_PACKAGE_MANAGER proof could not resolve inside nodejs $version"
|
|
1324
|
+
manager_bin=$NODE_CONTEXT_EXECUTABLE
|
|
1325
|
+
actual=$(execute_in_node_context \
|
|
1326
|
+
"$manager_bin" "$project" "$version" --version) \
|
|
1327
|
+
|| fail "final $PROJECT_PACKAGE_MANAGER execution proof failed for nodejs $version"
|
|
1328
|
+
[ "$actual" = "$expected" ] || fail \
|
|
1329
|
+
"$PROJECT_PACKAGE_MANAGER resolved $actual instead of $expected after final reshim"
|
|
1330
|
+
shim_actual=$(run_node_shim_probe \
|
|
1331
|
+
"$project" "$version" "$PROJECT_PACKAGE_MANAGER" --version) \
|
|
1332
|
+
|| fail "final $PROJECT_PACKAGE_MANAGER shim execution failed for nodejs $version"
|
|
1333
|
+
[ "$shim_actual" = "$expected" ] || fail \
|
|
1334
|
+
"$PROJECT_PACKAGE_MANAGER shim resolved $shim_actual instead of $expected"
|
|
1335
|
+
done
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
prove_fallback_runtimes() {
|
|
1339
|
+
local project=$1 defaults=/home/node/.tool-versions manifest line tool version
|
|
1340
|
+
local -a fields=()
|
|
1341
|
+
manifest="$project/.tool-versions"
|
|
1342
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1343
|
+
line=${line%%#*}
|
|
1344
|
+
line=$(trim_spaces "$line")
|
|
1345
|
+
[ -n "$line" ] || continue
|
|
1346
|
+
read -r -a fields <<< "$line"
|
|
1347
|
+
tool=${fields[0]}
|
|
1348
|
+
if manifest_declares_tool "$manifest" "$tool"; then
|
|
1349
|
+
continue
|
|
1350
|
+
fi
|
|
1351
|
+
for version in "${fields[@]:1}"; do
|
|
1352
|
+
prove_selection "$tool" "$version" "$project" "$defaults"
|
|
1353
|
+
done
|
|
1354
|
+
done < "$defaults"
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
# Validate fallback and package-manager declarations before any shared-volume
|
|
1358
|
+
# mutation. Every real project has effective home defaults even with no file.
|
|
1359
|
+
validate_manifest /home/node/.tool-versions
|
|
1360
|
+
for project in "${projects[@]}"; do
|
|
1361
|
+
project_package_manager "$project"
|
|
1362
|
+
done
|
|
1363
|
+
|
|
1364
|
+
# Fresh image-seeded volumes already carry this root-owned, node-writable
|
|
1365
|
+
# regular file. Legacy volumes are repaired by standard-image boot maintenance.
|
|
1366
|
+
if [ -L "$LOCK_PATH" ]; then
|
|
1367
|
+
fail "$LOCK_PATH is a symbolic link"
|
|
1368
|
+
fi
|
|
1369
|
+
if [ "$VERIFY_ONLY" = 1 ]; then
|
|
1370
|
+
# A verify pass cannot create the lock on a read-only volume. Take a SHARED
|
|
1371
|
+
# flock when the file exists (readers must not serialize behind each other,
|
|
1372
|
+
# and must still wait out a writer on runtimes where one is possible); a
|
|
1373
|
+
# missing lock file only occurs on a legacy volume, where the read-only
|
|
1374
|
+
# attachment itself excludes every Apple writer.
|
|
1375
|
+
if [ -e "$LOCK_PATH" ]; then
|
|
1376
|
+
[ -f "$LOCK_PATH" ] || fail "$LOCK_PATH is not a regular lock file"
|
|
1377
|
+
exec 9< "$LOCK_PATH" || fail "could not open $LOCK_PATH"
|
|
1378
|
+
if ! "$FLOCK_BIN" --shared --timeout "$LOCK_WAIT_SECONDS" 9; then
|
|
1379
|
+
fail "timed out after ${LOCK_WAIT_SECONDS}s waiting for shared asdf maintenance"
|
|
1380
|
+
fi
|
|
1381
|
+
else
|
|
1382
|
+
log "verify-only: $LOCK_PATH is absent on the read-only volume; proceeding without it"
|
|
1383
|
+
fi
|
|
1384
|
+
else
|
|
1385
|
+
if [ ! -e "$LOCK_PATH" ]; then
|
|
1386
|
+
old_umask=$(umask)
|
|
1387
|
+
umask 002
|
|
1388
|
+
: > "$LOCK_PATH" || fail "could not create $LOCK_PATH"
|
|
1389
|
+
umask "$old_umask"
|
|
1390
|
+
fi
|
|
1391
|
+
if [ ! -f "$LOCK_PATH" ]; then
|
|
1392
|
+
fail "$LOCK_PATH is not a regular lock file"
|
|
1393
|
+
fi
|
|
1394
|
+
exec 9< "$LOCK_PATH" || fail "could not open $LOCK_PATH"
|
|
1395
|
+
if ! "$FLOCK_BIN" --exclusive --timeout "$LOCK_WAIT_SECONDS" 9; then
|
|
1396
|
+
fail "timed out after ${LOCK_WAIT_SECONDS}s waiting for shared asdf maintenance"
|
|
1397
|
+
fi
|
|
1398
|
+
fi
|
|
1399
|
+
|
|
1400
|
+
# BEGIN UAI_FORBIDDEN_INTERPRETER_SHIM
|
|
1401
|
+
validate_shims_root() {
|
|
1402
|
+
local shims_root="$ASDF_DATA/shims" lexical resolved
|
|
1403
|
+
if [ -L "$shims_root" ] || [ ! -d "$shims_root" ]; then
|
|
1404
|
+
fail "shared asdf shims root is not a real directory"
|
|
1405
|
+
fi
|
|
1406
|
+
lexical=$("$REALPATH_BIN" -m -s -- "$shims_root") \
|
|
1407
|
+
|| fail "could not normalize shared asdf shims root"
|
|
1408
|
+
resolved=$("$REALPATH_BIN" -e -- "$shims_root") \
|
|
1409
|
+
|| fail "could not resolve shared asdf shims root"
|
|
1410
|
+
[ "$lexical" = "$resolved" ] \
|
|
1411
|
+
|| fail "shared asdf shims root traverses a symbolic link"
|
|
1412
|
+
SHIMS_ROOT=$lexical
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
clear_forbidden_interpreter_shim() {
|
|
1416
|
+
local fail_if_found=$1 candidate
|
|
1417
|
+
validate_shims_root
|
|
1418
|
+
candidate="$SHIMS_ROOT/bash"
|
|
1419
|
+
# Every asdf shim uses `#!/usr/bin/env bash`. A shared shim named `bash`
|
|
1420
|
+
# would recursively intercept every app command and previously could also
|
|
1421
|
+
# intercept maintenance itself. It is not an advertised runtime executable.
|
|
1422
|
+
if [ ! -e "$candidate" ] && [ ! -L "$candidate" ]; then
|
|
1423
|
+
return 0
|
|
1424
|
+
fi
|
|
1425
|
+
[ ! -d "$candidate" ] \
|
|
1426
|
+
|| fail "forbidden shared asdf bash shim is a directory; refusing recursive cleanup"
|
|
1427
|
+
"$RM_BIN" -f -- "$candidate" \
|
|
1428
|
+
|| fail "could not remove the exact forbidden shared asdf bash shim"
|
|
1429
|
+
[ ! -e "$candidate" ] && [ ! -L "$candidate" ] \
|
|
1430
|
+
|| fail "forbidden shared asdf bash shim survived exact cleanup"
|
|
1431
|
+
log "removed forbidden shared asdf bash shim"
|
|
1432
|
+
[ "$fail_if_found" -eq 0 ] || return 1
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
run_asdf_reshim() {
|
|
1436
|
+
local reshim_status=0 shim_status=0
|
|
1437
|
+
asdf reshim "$@" || reshim_status=$?
|
|
1438
|
+
clear_forbidden_interpreter_shim 1 || shim_status=$?
|
|
1439
|
+
[ "$shim_status" -eq 0 ] || return 1
|
|
1440
|
+
return "$reshim_status"
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
run_asdf_install() {
|
|
1444
|
+
local install_status=0 shim_status=0
|
|
1445
|
+
asdf install "$@" || install_status=$?
|
|
1446
|
+
# asdf 0.14.1 reshims internally after a successful install. Always inspect
|
|
1447
|
+
# and clean that side effect, including when the install later reports an
|
|
1448
|
+
# error, before returning control to an actionable caller failure.
|
|
1449
|
+
clear_forbidden_interpreter_shim 1 || shim_status=$?
|
|
1450
|
+
[ "$shim_status" -eq 0 ] || return 1
|
|
1451
|
+
return "$install_status"
|
|
1452
|
+
}
|
|
1453
|
+
# END UAI_FORBIDDEN_INTERPRETER_SHIM
|
|
1454
|
+
|
|
1455
|
+
# A prior killed run may have created this exact stale shim. Clean it while
|
|
1456
|
+
# holding the real volume lock, before sourcing or executing any ASDF shim.
|
|
1457
|
+
clear_forbidden_interpreter_shim 0
|
|
1458
|
+
|
|
1459
|
+
# Sourcing asdf prepends the shared shims directory. Do it only after the
|
|
1460
|
+
# volume lock is held, retain its shell function, then restore the trusted
|
|
1461
|
+
# control-plane PATH. App/shim parity is opted into only at explicit probes.
|
|
1462
|
+
# shellcheck disable=SC1091
|
|
1463
|
+
. "$ASDF_SH"
|
|
1464
|
+
export PATH="$CONTROL_PATH"
|
|
1465
|
+
command -v asdf >/dev/null 2>&1 || fail "asdf did not initialize"
|
|
1466
|
+
|
|
1467
|
+
export COREPACK_HOME="$ASDF_DATA/corepack"
|
|
1468
|
+
# A verify pass only reads the Corepack cache; a missing directory then fails
|
|
1469
|
+
# the package-manager proofs below, which is the correct escalation signal.
|
|
1470
|
+
[ "$VERIFY_ONLY" = 1 ] || mkdir -p "$COREPACK_HOME"
|
|
1471
|
+
for manifest in "${manifests[@]}"; do
|
|
1472
|
+
# Revalidate after acquiring the lock, then perform this caller's own full
|
|
1473
|
+
# install and proof. A prior concurrent materializer is never success for us.
|
|
1474
|
+
validate_manifest "$manifest"
|
|
1475
|
+
materialize_manifest "$manifest"
|
|
1476
|
+
done
|
|
1477
|
+
for project in "${projects[@]}"; do
|
|
1478
|
+
materialize_fallback_runtimes "$project"
|
|
1479
|
+
done
|
|
1480
|
+
|
|
1481
|
+
# Rebuild shared shims after runtime installs, then prepare package managers.
|
|
1482
|
+
# Corepack preparation may reshim selected Node versions itself; every runtime
|
|
1483
|
+
# and manager is therefore re-executed below only after the last mutation.
|
|
1484
|
+
if [ "$VERIFY_ONLY" = 1 ]; then
|
|
1485
|
+
# Nothing was mutated, so there is nothing to reshim or prepare; the proofs
|
|
1486
|
+
# below still execute every runtime and manager through the existing shims.
|
|
1487
|
+
log "verify-only: skipping reshim and package-manager preparation"
|
|
1488
|
+
else
|
|
1489
|
+
run_asdf_reshim || fail "final asdf reshim failed"
|
|
1490
|
+
for project in "${projects[@]}"; do
|
|
1491
|
+
prepare_project_package_manager "$project"
|
|
1492
|
+
done
|
|
1493
|
+
clear_forbidden_interpreter_shim 1 \
|
|
1494
|
+
|| fail "a runtime recreated the forbidden shared asdf bash shim"
|
|
1495
|
+
fi
|
|
1496
|
+
|
|
1497
|
+
# From here on, package-manager execution is a cache-only postcondition. The
|
|
1498
|
+
# app receives the same setting, so a missing exact/fallback artifact fails now
|
|
1499
|
+
# instead of downloading later after the shared cache becomes read-only.
|
|
1500
|
+
export COREPACK_ENABLE_NETWORK=0
|
|
1501
|
+
for manifest in "${manifests[@]}"; do
|
|
1502
|
+
project=$(dirname "$manifest")
|
|
1503
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1504
|
+
line=${line%%#*}
|
|
1505
|
+
line=$(trim_spaces "$line")
|
|
1506
|
+
[ -n "$line" ] || continue
|
|
1507
|
+
read -r -a fields <<< "$line"
|
|
1508
|
+
case "${fields[0]}" in
|
|
1509
|
+
pnpm|yarn) continue ;; # proven via prove_project_package_manager below
|
|
1510
|
+
esac
|
|
1511
|
+
unset_tool_override "${fields[0]}"
|
|
1512
|
+
for version in "${fields[@]:1}"; do
|
|
1513
|
+
prove_selection "${fields[0]}" "$version" "$project" "$manifest"
|
|
1514
|
+
done
|
|
1515
|
+
done < "$manifest"
|
|
1516
|
+
done
|
|
1517
|
+
for project in "${projects[@]}"; do
|
|
1518
|
+
prove_fallback_runtimes "$project"
|
|
1519
|
+
prove_project_npm "$project"
|
|
1520
|
+
prove_project_package_manager "$project"
|
|
1521
|
+
done
|
|
1522
|
+
|
|
1523
|
+
if [ "$VERIFY_ONLY" = 1 ]; then
|
|
1524
|
+
log "runtime verification complete for ${#projects[@]} project(s) (no volume changes)"
|
|
1525
|
+
else
|
|
1526
|
+
log "runtime materialization complete for ${#projects[@]} project(s)"
|
|
1527
|
+
fi
|