valise-sh 0.1.0 → 0.2.0
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/CHANGELOG.md +170 -9
- package/README.md +48 -24
- package/bin/valise +2453 -333
- package/docs/architecture.md +78 -9
- package/docs/contrib-modules.md +68 -15
- package/docs/installation.md +120 -33
- package/docs/module-abi.md +103 -39
- package/docs/release-policy.md +110 -6
- package/docs/shared-modules.md +124 -2
- package/docs/variants.md +318 -54
- package/examples/README.md +11 -0
- package/examples/e2e/README.md +7 -7
- package/examples/e2e/build-manifest.sh +45 -157
- package/examples/profile-project/README.md +181 -0
- package/examples/profile-project/shell/layers/demo/module.d/contrib_demo_greeting.meta +10 -0
- package/examples/profile-project/shell/layers/demo/modules/shared/contrib_demo_greeting.sh +4 -0
- package/examples/profile-project/shell/module.d/greeting.meta +10 -0
- package/examples/profile-project/shell/module.d/profile_main.meta +9 -0
- package/examples/profile-project/shell/modules/greeting.sh +5 -0
- package/examples/profile-project/shell/release/bundle-roots +9 -0
- package/examples/profile-project/shell/release/install-variants +3 -0
- package/examples/profile-project/shell/release/module-registry.tsv +4 -0
- package/examples/profile-project/shell/release/project.conf +3 -0
- package/examples/shared/README.md +2 -2
- package/lib/compiler.awk +341 -20
- package/lib/fish/loader.fish +94 -19
- package/lib/fish/metadata.fish +13 -14
- package/lib/loader.sh +737 -157
- package/lib/metadata.sh +27 -51
- package/package.json +2 -1
package/bin/valise
CHANGED
|
@@ -7,37 +7,215 @@
|
|
|
7
7
|
|
|
8
8
|
set -eu
|
|
9
9
|
|
|
10
|
-
VALISE_VERSION="0.
|
|
11
|
-
|
|
12
|
-
# XDG defaults per install-contracts.md
|
|
13
|
-
VALISE_XDG_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/valise"
|
|
14
|
-
VALISE_XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/valise"
|
|
15
|
-
VALISE_XDG_STATE="${XDG_STATE_HOME:-$HOME/.local/state}/valise"
|
|
16
|
-
VALISE_RELEASES_DIR="$VALISE_XDG_DATA/releases"
|
|
17
|
-
VALISE_ACTIVE_LINK="$VALISE_XDG_CONFIG/active"
|
|
18
|
-
VALISE_LOCK_DIR="$VALISE_XDG_STATE/lock"
|
|
10
|
+
VALISE_VERSION="0.2.0"
|
|
19
11
|
|
|
20
12
|
usage() {
|
|
21
13
|
printf '%s\n' \
|
|
22
14
|
"Usage: valise <command> [options]" \
|
|
23
15
|
"" \
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
" variant [
|
|
16
|
+
"Project commands (run in a valise project):" \
|
|
17
|
+
" new [dir] Scaffold a new valise project" \
|
|
18
|
+
" module <new|list|remove> Create, list, or remove project modules" \
|
|
19
|
+
" build [--variant=<name>] [--bundle=<name>] [out]" \
|
|
20
|
+
" Build a release tree (default: build/release)" \
|
|
21
|
+
" apply [--variant=<name>] [--bundle=<name>] [--no-integration]" \
|
|
22
|
+
" Build the project, install it, and integrate it" \
|
|
23
|
+
" status Show project, active release, integration, and drift" \
|
|
24
|
+
"" \
|
|
25
|
+
"Release commands:" \
|
|
26
|
+
" install [--no-integration] Install the release tree in the current directory" \
|
|
27
|
+
" release <list|use|prune|verify> Manage installed releases" \
|
|
28
|
+
" init [posix|fish] Print the shell integration snippet" \
|
|
29
|
+
" doctor [--pre-install] [--no-integration] Run environment diagnostics" \
|
|
30
|
+
"" \
|
|
31
|
+
"Plumbing:" \
|
|
32
|
+
" compile [--bundle=<name>] [--variant=<name>] <output-dir> Compile plans only" \
|
|
33
|
+
" registry [--check] [path] Generate or verify module-registry.tsv from metadata" \
|
|
30
34
|
" make-manifest <tree> [--output <file>] Write a release manifest for a tree" \
|
|
31
|
-
"
|
|
32
|
-
" explain-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
" version Print version" \
|
|
35
|
+
" explain-variant [name] Show variant details and per-shell impact" \
|
|
36
|
+
" explain-bundles [--bundle=<name>] Explain bundle roots and module closure" \
|
|
37
|
+
" layer-lint <name> Validate a contribution layer structure" \
|
|
38
|
+
" version Print version" \
|
|
36
39
|
"" \
|
|
37
40
|
"Environment:" \
|
|
38
41
|
" XDG_DATA_HOME Release storage root (default: ~/.local/share)" \
|
|
39
42
|
" XDG_CONFIG_HOME Active pointer symlink (default: ~/.config)" \
|
|
40
|
-
" XDG_STATE_HOME Lock and journal state (default: ~/.local/state)"
|
|
43
|
+
" XDG_STATE_HOME Lock and journal state (default: ~/.local/state)" \
|
|
44
|
+
"" \
|
|
45
|
+
"Run 'valise <command> --help' for command-specific usage."
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Per-command usage. Each command prints its own contract instead of the
|
|
49
|
+
# global list, so `valise compile --help` names the arguments that command
|
|
50
|
+
# actually accepts.
|
|
51
|
+
|
|
52
|
+
usage_init() {
|
|
53
|
+
printf '%s\n' \
|
|
54
|
+
"usage: valise init [posix|fish] [--source-tree]" \
|
|
55
|
+
"" \
|
|
56
|
+
"Print the shell integration snippet for the active release. valise install" \
|
|
57
|
+
"and valise apply write it for you; use this to wire a startup file by hand." \
|
|
58
|
+
"" \
|
|
59
|
+
" posix A managed block for any POSIX shell. It loads the plan for" \
|
|
60
|
+
" the running shell, or the loader when the release has none." \
|
|
61
|
+
" fish The Fish conf.d hook." \
|
|
62
|
+
" --source-tree Skip release verification in the Fish hook (development only)." \
|
|
63
|
+
"" \
|
|
64
|
+
"With no argument, fish when \$SHELL names fish, otherwise posix."
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
usage_install() {
|
|
68
|
+
printf '%s\n' \
|
|
69
|
+
"usage: valise install [--no-integration]" \
|
|
70
|
+
"" \
|
|
71
|
+
"Install the release tree in the current directory and make it active, then" \
|
|
72
|
+
"write the shell integration." \
|
|
73
|
+
"" \
|
|
74
|
+
"A managed block is written to ~/.profile; to the first of ~/.bash_profile and" \
|
|
75
|
+
"~/.bash_login that exists, because it shadows ~/.profile for bash; to ~/.bashrc" \
|
|
76
|
+
"and ~/.zshrc when that shell is \$SHELL or the file exists; and to ~/.zprofile" \
|
|
77
|
+
"when zsh is installed or the file exists. Symlinked files are left alone. A" \
|
|
78
|
+
"Fish conf.d hook is written when Fish is installed. Each install replaces the" \
|
|
79
|
+
"block in place." \
|
|
80
|
+
"" \
|
|
81
|
+
" --no-integration Write no startup file and no Fish hook."
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
usage_doctor() {
|
|
85
|
+
printf '%s\n' \
|
|
86
|
+
"usage: valise doctor [--pre-install] [--no-integration]" \
|
|
87
|
+
"" \
|
|
88
|
+
"Report the environment, the active release, and the shell integration." \
|
|
89
|
+
"" \
|
|
90
|
+
" --pre-install Skip the active-release and integration checks, for use" \
|
|
91
|
+
" before the first install." \
|
|
92
|
+
" --no-integration Check the environment and active release only."
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
usage_release() {
|
|
96
|
+
printf '%s\n' \
|
|
97
|
+
"usage: valise release <list|use|prune|verify> [options]" \
|
|
98
|
+
"" \
|
|
99
|
+
" list List installed releases and mark the active one." \
|
|
100
|
+
" use <sha256> Verify a release and make it active." \
|
|
101
|
+
" prune [--keep=N] Remove older releases, keeping the active one and the" \
|
|
102
|
+
" N newest by modification time. A release that fails" \
|
|
103
|
+
" verification is kept, not removed." \
|
|
104
|
+
" verify [--print-root] Verify the sealed active release and optionally" \
|
|
105
|
+
" print its root." \
|
|
106
|
+
"" \
|
|
107
|
+
"prune --keep=0 keeps only the active release."
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
usage_new() {
|
|
111
|
+
printf '%s\n' \
|
|
112
|
+
"usage: valise new [dir]" \
|
|
113
|
+
"" \
|
|
114
|
+
"Scaffold a new valise project in dir (default: the current directory)." \
|
|
115
|
+
"" \
|
|
116
|
+
"The directory must be empty or absent. The scaffold contains a seed module," \
|
|
117
|
+
"bundle roots, install variants, and project defaults."
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
usage_module() {
|
|
121
|
+
printf '%s\n' \
|
|
122
|
+
"usage: valise module <new|list|remove> [options]" \
|
|
123
|
+
"" \
|
|
124
|
+
" new <name> [--exports=<list>] Create a module and wire it into the seed." \
|
|
125
|
+
" list List the project's modules." \
|
|
126
|
+
" remove <name> Remove a module and its references in metadata," \
|
|
127
|
+
" bundle roots, and variant exclusions. Refuses" \
|
|
128
|
+
" if that would leave a bundle or module empty."
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
usage_build() {
|
|
132
|
+
printf '%s\n' \
|
|
133
|
+
"usage: valise build [--variant=<name>] [--bundle=<name>] [out]" \
|
|
134
|
+
"" \
|
|
135
|
+
"Build a release tree from the project in the current directory." \
|
|
136
|
+
"" \
|
|
137
|
+
" --variant=<name> Build variant whose exclusion list applies (default:" \
|
|
138
|
+
" default, or the project's project.conf)." \
|
|
139
|
+
" --bundle=<name> Bundle whose roots seed each plan (default: default," \
|
|
140
|
+
" or the project's project.conf)." \
|
|
141
|
+
" out Output directory (default: build/release). An existing" \
|
|
142
|
+
" default output is replaced after a successful build." \
|
|
143
|
+
" An explicit output must be empty or absent." \
|
|
144
|
+
"" \
|
|
145
|
+
"The tree carries generated/installed-modules.<shell> plans, the capability" \
|
|
146
|
+
"prelude, every module body, and shell/release."
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
usage_apply() {
|
|
150
|
+
printf '%s\n' \
|
|
151
|
+
"usage: valise apply [--variant=<name>] [--bundle=<name>] [--no-integration]" \
|
|
152
|
+
"" \
|
|
153
|
+
"Build the project, install the result, write the shell integration, and verify" \
|
|
154
|
+
"the active release. See 'valise install --help' for the files it writes." \
|
|
155
|
+
"" \
|
|
156
|
+
" --no-integration Write no startup file and no Fish hook."
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
usage_status() {
|
|
160
|
+
printf '%s\n' \
|
|
161
|
+
"usage: valise status" \
|
|
162
|
+
"" \
|
|
163
|
+
"Report the project, the active release, the integration, and build drift."
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
usage_compile() {
|
|
167
|
+
printf '%s\n' \
|
|
168
|
+
"usage: valise compile [--bundle=<name>] [--variant=<name>] <output-dir>" \
|
|
169
|
+
"" \
|
|
170
|
+
"Compile the project's modules into per-shell installed plans." \
|
|
171
|
+
"" \
|
|
172
|
+
" --bundle=<name> Bundle whose roots seed each plan (default: default)." \
|
|
173
|
+
" --variant=<name> Build variant whose exclusion list applies (default:" \
|
|
174
|
+
" default)." \
|
|
175
|
+
" output-dir Destination directory. It must be empty or absent." \
|
|
176
|
+
"" \
|
|
177
|
+
"Run from the consuming project root."
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
usage_registry() {
|
|
181
|
+
printf '%s\n' \
|
|
182
|
+
"usage: valise registry [--check] [registry-path]" \
|
|
183
|
+
"" \
|
|
184
|
+
"Generate the module registry from the project's metadata." \
|
|
185
|
+
"" \
|
|
186
|
+
" --check Compare the generated registry with the existing one and fail on" \
|
|
187
|
+
" drift instead of rewriting it." \
|
|
188
|
+
" registry-path Registry file (default: shell/release/module-registry.tsv)."
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
usage_explain_bundles() {
|
|
192
|
+
printf '%s\n' \
|
|
193
|
+
"usage: valise explain-bundles [--bundle=<name>]" \
|
|
194
|
+
"" \
|
|
195
|
+
"Show the roots and module closure each bundle resolves to, per shell." \
|
|
196
|
+
"" \
|
|
197
|
+
" --bundle=<name> Report one bundle instead of all of them."
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
usage_layer_lint() {
|
|
201
|
+
printf '%s\n' \
|
|
202
|
+
"usage: valise layer-lint <layer-name>" \
|
|
203
|
+
"" \
|
|
204
|
+
"Validate a contribution layer's structure, namespace, metadata, load paths," \
|
|
205
|
+
"file modes, and dependency closure." \
|
|
206
|
+
"" \
|
|
207
|
+
" layer-name Provider directory under shell/layers/, or a path to a layer" \
|
|
208
|
+
" tree containing module.d/."
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
usage_explain_variant() {
|
|
212
|
+
printf '%s\n' \
|
|
213
|
+
"usage: valise explain-variant [variant-name]" \
|
|
214
|
+
"" \
|
|
215
|
+
"Show a variant's description, excluded modules, and per-shell impact." \
|
|
216
|
+
"" \
|
|
217
|
+
" variant-name Variant from shell/release/install-variants" \
|
|
218
|
+
" (default: default)."
|
|
41
219
|
}
|
|
42
220
|
|
|
43
221
|
die() {
|
|
@@ -49,8 +227,84 @@ info() {
|
|
|
49
227
|
printf 'valise: %s\n' "$*"
|
|
50
228
|
}
|
|
51
229
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
230
|
+
# Verification failure reporting. The default mode aborts the command, which
|
|
231
|
+
# is what install and release use. Quiet mode returns a nonzero status
|
|
232
|
+
# instead, so a diagnostic command can report the failure and carry on. The
|
|
233
|
+
# message text is identical in both modes.
|
|
234
|
+
_verify_quiet=0
|
|
235
|
+
_verify_fail() {
|
|
236
|
+
if [ "$_verify_quiet" -eq 1 ]; then
|
|
237
|
+
printf 'valise: error: %s\n' "$*" >&2
|
|
238
|
+
return 1
|
|
239
|
+
fi
|
|
240
|
+
die "$*"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Lexically normalize an absolute path: collapse repeated slashes, drop "."
|
|
244
|
+
# segments, and resolve ".." against the preceding component. Relative paths
|
|
245
|
+
# are resolved against the physical working directory first.
|
|
246
|
+
path_normalize() {
|
|
247
|
+
_path_norm_value=$1
|
|
248
|
+
case "$_path_norm_value" in
|
|
249
|
+
/*) ;;
|
|
250
|
+
*) _path_norm_value=$(pwd -P)/$_path_norm_value ;;
|
|
251
|
+
esac
|
|
252
|
+
_path_norm_out=
|
|
253
|
+
if [ "${IFS+x}" = x ]; then
|
|
254
|
+
_path_norm_ifs_was_set=1
|
|
255
|
+
_path_norm_old_ifs=${IFS-}
|
|
256
|
+
else
|
|
257
|
+
_path_norm_ifs_was_set=0
|
|
258
|
+
_path_norm_old_ifs=
|
|
259
|
+
fi
|
|
260
|
+
case $- in
|
|
261
|
+
*f*) _path_norm_glob_was_set=1 ;;
|
|
262
|
+
*) _path_norm_glob_was_set=0 ;;
|
|
263
|
+
esac
|
|
264
|
+
set -f
|
|
265
|
+
IFS=/
|
|
266
|
+
for _path_norm_part in $_path_norm_value; do
|
|
267
|
+
case "$_path_norm_part" in
|
|
268
|
+
''|.) continue ;;
|
|
269
|
+
..)
|
|
270
|
+
case "$_path_norm_out" in
|
|
271
|
+
'') ;;
|
|
272
|
+
*/*) _path_norm_out=${_path_norm_out%/*} ;;
|
|
273
|
+
*) _path_norm_out= ;;
|
|
274
|
+
esac
|
|
275
|
+
continue
|
|
276
|
+
;;
|
|
277
|
+
esac
|
|
278
|
+
_path_norm_out=${_path_norm_out:+$_path_norm_out/}$_path_norm_part
|
|
279
|
+
done
|
|
280
|
+
if [ "$_path_norm_ifs_was_set" -eq 1 ]; then
|
|
281
|
+
IFS=$_path_norm_old_ifs
|
|
282
|
+
else
|
|
283
|
+
unset IFS
|
|
284
|
+
fi
|
|
285
|
+
if [ "$_path_norm_glob_was_set" -eq 0 ]; then
|
|
286
|
+
set +f
|
|
287
|
+
fi
|
|
288
|
+
printf '/%s\n' "$_path_norm_out"
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# XDG defaults per install-contracts.md.
|
|
292
|
+
#
|
|
293
|
+
# The roots are normalized to absolute paths. A relative XDG_DATA_HOME would
|
|
294
|
+
# otherwise be written into the active symlink verbatim, and a relative
|
|
295
|
+
# symlink target resolves against the directory holding the link rather
|
|
296
|
+
# than the caller's working directory. The pointer would then dangle, and
|
|
297
|
+
# prune would compare the active release by a path spelling that never
|
|
298
|
+
# matches the release directory.
|
|
299
|
+
VALISE_XDG_DATA=$(path_normalize "${XDG_DATA_HOME:-$HOME/.local/share}/valise")
|
|
300
|
+
VALISE_XDG_CONFIG=$(path_normalize "${XDG_CONFIG_HOME:-$HOME/.config}/valise")
|
|
301
|
+
VALISE_XDG_STATE=$(path_normalize "${XDG_STATE_HOME:-$HOME/.local/state}/valise")
|
|
302
|
+
VALISE_RELEASES_DIR="$VALISE_XDG_DATA/releases"
|
|
303
|
+
VALISE_ACTIVE_LINK="$VALISE_XDG_CONFIG/active"
|
|
304
|
+
VALISE_LOCK_DIR="$VALISE_XDG_STATE/lock"
|
|
305
|
+
|
|
306
|
+
# POSIX path parent extraction avoids ambient dirname resolution in manifest
|
|
307
|
+
# discovery and every subsequent source/destination validation path.
|
|
54
308
|
path_parent() {
|
|
55
309
|
_path_parent_value=$1
|
|
56
310
|
# Strip trailing slashes so "out/" and "out" share one parent (".").
|
|
@@ -71,6 +325,20 @@ path_parent() {
|
|
|
71
325
|
printf '%s\n' "$_path_parent_value"
|
|
72
326
|
}
|
|
73
327
|
|
|
328
|
+
# Directory of this script with symlinks resolved, so package installs that
|
|
329
|
+
# expose bin/valise through a symlink (bun, npm) still find ../lib.
|
|
330
|
+
valise_script_dir() {
|
|
331
|
+
_vsd_path=$0
|
|
332
|
+
while [ -L "$_vsd_path" ]; do
|
|
333
|
+
_vsd_target=$(readlink "$_vsd_path") || return 1
|
|
334
|
+
case "$_vsd_target" in
|
|
335
|
+
/*) _vsd_path=$_vsd_target ;;
|
|
336
|
+
*) _vsd_path=$(path_parent "$_vsd_path")/$_vsd_target ;;
|
|
337
|
+
esac
|
|
338
|
+
done
|
|
339
|
+
(cd "$(path_parent "$_vsd_path")" && pwd -P)
|
|
340
|
+
}
|
|
341
|
+
|
|
74
342
|
# Emit a single-quoted literal for generated POSIX shell code. The generated
|
|
75
343
|
# snippets must not interpolate user-controlled XDG paths into double quotes:
|
|
76
344
|
# `$()` and backticks would still execute there. POSIX single quotes preserve
|
|
@@ -162,6 +430,79 @@ readlink_target() {
|
|
|
162
430
|
"$_readlink_path" "$1"
|
|
163
431
|
}
|
|
164
432
|
|
|
433
|
+
# active_release_dir
|
|
434
|
+
# Print the physical directory the active pointer names, or fail. The result
|
|
435
|
+
# is canonical, so a trailing slash, a relative target, or an alias symlink
|
|
436
|
+
# never changes the release identity.
|
|
437
|
+
active_release_dir() {
|
|
438
|
+
[ -L "$VALISE_ACTIVE_LINK" ] || return 1
|
|
439
|
+
_ard_target=$(readlink_target "$VALISE_ACTIVE_LINK" 2>/dev/null) || return 1
|
|
440
|
+
[ -n "$_ard_target" ] || return 1
|
|
441
|
+
case "$_ard_target" in
|
|
442
|
+
/*) : ;;
|
|
443
|
+
*)
|
|
444
|
+
_ard_parent=$(cd "$(path_parent "$VALISE_ACTIVE_LINK")" 2>/dev/null && pwd -P) || return 1
|
|
445
|
+
_ard_target="$_ard_parent/$_ard_target"
|
|
446
|
+
;;
|
|
447
|
+
esac
|
|
448
|
+
_ard_target=$(path_normalize "$_ard_target") || return 1
|
|
449
|
+
(cd "$_ard_target" 2>/dev/null && pwd -P)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# active_release_id
|
|
453
|
+
# Print the active release's directory name. Identity is the name, not the
|
|
454
|
+
# path spelling.
|
|
455
|
+
active_release_id() {
|
|
456
|
+
_ari_dir=$(active_release_dir) || return 1
|
|
457
|
+
printf '%s\n' "${_ari_dir##*/}"
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# Resolve the consuming project root for the commands that read project
|
|
461
|
+
# files (compile, build, explain-*, layer-lint). The working directory
|
|
462
|
+
# wins over the engine's own checkout, so a package install cannot supply a
|
|
463
|
+
# different source root than the project the caller is standing in. A
|
|
464
|
+
# project is recognised by shell/release/module-registry.tsv or by a
|
|
465
|
+
# shell/module.d directory.
|
|
466
|
+
#
|
|
467
|
+
# Prints the canonical project root, or fails.
|
|
468
|
+
resolve_project_root() {
|
|
469
|
+
_rpr_script_dir=$(valise_script_dir) || return 1
|
|
470
|
+
for _rpr_candidate in "." "$_rpr_script_dir/.." "$_rpr_script_dir/../.." "$_rpr_script_dir/../../.."; do
|
|
471
|
+
if [ -f "$_rpr_candidate/shell/release/module-registry.tsv" ] ||
|
|
472
|
+
[ -d "$_rpr_candidate/shell/module.d" ]; then
|
|
473
|
+
(cd "$_rpr_candidate" && pwd -P)
|
|
474
|
+
return 0
|
|
475
|
+
fi
|
|
476
|
+
done
|
|
477
|
+
return 1
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
# Resolve the engine checkout that ships this CLI. Its modules live in
|
|
481
|
+
# module.d/ at the top level, not under shell/.
|
|
482
|
+
resolve_engine_root() {
|
|
483
|
+
_rer_script_dir=$(valise_script_dir) || return 1
|
|
484
|
+
for _rer_candidate in "$_rer_script_dir/.." "$_rer_script_dir/../.." "$_rer_script_dir/../../.."; do
|
|
485
|
+
if [ -d "$_rer_candidate/module.d" ] && [ -d "$_rer_candidate/lib" ]; then
|
|
486
|
+
(cd "$_rer_candidate" && pwd -P)
|
|
487
|
+
return 0
|
|
488
|
+
fi
|
|
489
|
+
done
|
|
490
|
+
return 1
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# Print the module names a layer is allowed to depend on: the engine's own
|
|
494
|
+
# modules, plus the primary-root modules of the consuming project. A layer
|
|
495
|
+
# may require a module in its own layer, or one of these; anything else is a
|
|
496
|
+
# dependency the layer does not own.
|
|
497
|
+
engine_module_names() {
|
|
498
|
+
_emn_root=$1
|
|
499
|
+
for _emn_meta in "$_emn_root"/module.d/*.meta "$_emn_root"/shell/module.d/*.meta; do
|
|
500
|
+
[ -f "$_emn_meta" ] || continue
|
|
501
|
+
_emn_name=${_emn_meta##*/}
|
|
502
|
+
printf '%s\n' "${_emn_name%.meta}"
|
|
503
|
+
done
|
|
504
|
+
}
|
|
505
|
+
|
|
165
506
|
# Return a file's three-digit permission mode in the manifest's octal form.
|
|
166
507
|
# BSD stat and GNU stat use different option spellings, so probe both.
|
|
167
508
|
file_mode() {
|
|
@@ -187,6 +528,25 @@ file_bytes() {
|
|
|
187
528
|
"$_wc_path" -c < "$1" | "$_tr_path" -d '[:space:]'
|
|
188
529
|
}
|
|
189
530
|
|
|
531
|
+
# Return a file's modification time in whole epoch seconds. BSD stat and GNU
|
|
532
|
+
# stat use different option spellings, so probe both and select the result by
|
|
533
|
+
# validating the output rather than trusting exit status alone: GNU stat
|
|
534
|
+
# accepts -f as a filesystem-status flag and can exit successfully while
|
|
535
|
+
# printing something that is not a timestamp.
|
|
536
|
+
file_mtime() {
|
|
537
|
+
_mtime_stat=$(trusted_utility_path stat) || return 1
|
|
538
|
+
_mtime_value=$("$_mtime_stat" -c %Y "$1" 2>/dev/null || :)
|
|
539
|
+
case "$_mtime_value" in
|
|
540
|
+
'' | *[!0-9]*) ;;
|
|
541
|
+
*) printf '%s\n' "$_mtime_value"; return 0 ;;
|
|
542
|
+
esac
|
|
543
|
+
_mtime_value=$("$_mtime_stat" -f %m "$1" 2>/dev/null || :)
|
|
544
|
+
case "$_mtime_value" in
|
|
545
|
+
'' | *[!0-9]*) return 1 ;;
|
|
546
|
+
*) printf '%s\n' "$_mtime_value" ;;
|
|
547
|
+
esac
|
|
548
|
+
}
|
|
549
|
+
|
|
190
550
|
file_sha256() {
|
|
191
551
|
_sha_path=$(trusted_utility_path sha256sum 2>/dev/null || :)
|
|
192
552
|
if [ -n "$_sha_path" ]; then
|
|
@@ -223,15 +583,13 @@ lower_hex() {
|
|
|
223
583
|
# participates in lock integrity is resolved by absolute path so PATH cannot
|
|
224
584
|
# forge ownership or suppress stale-lock cleanup.
|
|
225
585
|
_lock_owned=0
|
|
226
|
-
_active_tmp=""
|
|
227
|
-
_active_backup=""
|
|
228
586
|
_publish_tmp=""
|
|
229
587
|
_manifest_rows=""
|
|
230
588
|
_manifest_parse=""
|
|
231
589
|
_stage=""
|
|
232
590
|
_hook_tmp=""
|
|
233
591
|
_mm_tmp=""
|
|
234
|
-
|
|
592
|
+
_rr_tmp=""
|
|
235
593
|
_mv_path=""
|
|
236
594
|
_cleanup_failed=0
|
|
237
595
|
acquire_lock() {
|
|
@@ -316,20 +674,6 @@ EOF
|
|
|
316
674
|
release_lock() {
|
|
317
675
|
_release_rm_path=$(trusted_utility_path rm 2>/dev/null || :)
|
|
318
676
|
_release_rmdir_path=$(trusted_utility_path rmdir 2>/dev/null || :)
|
|
319
|
-
if [ -n "$_active_tmp" ]; then
|
|
320
|
-
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_active_tmp" 2>/dev/null || :
|
|
321
|
-
if [ -e "$_active_tmp" ] || [ -L "$_active_tmp" ]; then _cleanup_failed=1; fi
|
|
322
|
-
_active_tmp=""
|
|
323
|
-
fi
|
|
324
|
-
if [ -n "$_active_backup" ]; then
|
|
325
|
-
if [ -L "$_active_backup" ] && [ ! -e "$VALISE_ACTIVE_LINK" ] && [ ! -L "$VALISE_ACTIVE_LINK" ]; then
|
|
326
|
-
[ -n "$_mv_path" ] && "$_mv_path" "$_active_backup" "$VALISE_ACTIVE_LINK" 2>/dev/null || :
|
|
327
|
-
else
|
|
328
|
-
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_active_backup" 2>/dev/null || :
|
|
329
|
-
fi
|
|
330
|
-
if [ -e "$_active_backup" ] || [ -L "$_active_backup" ]; then _cleanup_failed=1; fi
|
|
331
|
-
_active_backup=""
|
|
332
|
-
fi
|
|
333
677
|
if [ -n "$_publish_tmp" ]; then
|
|
334
678
|
remove_tree "$_publish_tmp" || _cleanup_failed=1
|
|
335
679
|
_publish_tmp=""
|
|
@@ -344,6 +688,41 @@ release_lock() {
|
|
|
344
688
|
if [ -e "$_manifest_parse" ] || [ -L "$_manifest_parse" ]; then _cleanup_failed=1; fi
|
|
345
689
|
_manifest_parse=""
|
|
346
690
|
fi
|
|
691
|
+
if [ -n "${_pm_tmp:-}" ]; then
|
|
692
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_pm_tmp" 2>/dev/null || :
|
|
693
|
+
if [ -e "$_pm_tmp" ] || [ -L "$_pm_tmp" ]; then _cleanup_failed=1; fi
|
|
694
|
+
_pm_tmp=""
|
|
695
|
+
fi
|
|
696
|
+
if [ -n "${_rr_tmp:-}" ]; then
|
|
697
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_rr_tmp" 2>/dev/null || :
|
|
698
|
+
if [ -e "$_rr_tmp" ] || [ -L "$_rr_tmp" ]; then _cleanup_failed=1; fi
|
|
699
|
+
_rr_tmp=""
|
|
700
|
+
fi
|
|
701
|
+
if [ -n "${_pi_tmp:-}" ]; then
|
|
702
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_pi_tmp" 2>/dev/null || :
|
|
703
|
+
if [ -e "$_pi_tmp" ] || [ -L "$_pi_tmp" ]; then _cleanup_failed=1; fi
|
|
704
|
+
_pi_tmp=""
|
|
705
|
+
fi
|
|
706
|
+
if [ -n "${_eb_registry_tmp:-}" ]; then
|
|
707
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_eb_registry_tmp" 2>/dev/null || :
|
|
708
|
+
if [ -e "$_eb_registry_tmp" ] || [ -L "$_eb_registry_tmp" ]; then _cleanup_failed=1; fi
|
|
709
|
+
_eb_registry_tmp=""
|
|
710
|
+
fi
|
|
711
|
+
if [ -n "${_doctor_cand_file:-}" ]; then
|
|
712
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_doctor_cand_file" 2>/dev/null || :
|
|
713
|
+
if [ -e "$_doctor_cand_file" ] || [ -L "$_doctor_cand_file" ]; then _cleanup_failed=1; fi
|
|
714
|
+
_doctor_cand_file=""
|
|
715
|
+
fi
|
|
716
|
+
if [ -n "${_module_list_tmp:-}" ]; then
|
|
717
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_module_list_tmp" 2>/dev/null || :
|
|
718
|
+
if [ -e "$_module_list_tmp" ] || [ -L "$_module_list_tmp" ]; then _cleanup_failed=1; fi
|
|
719
|
+
_module_list_tmp=""
|
|
720
|
+
fi
|
|
721
|
+
if [ -n "${_rg_tmp:-}" ]; then
|
|
722
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_rg_tmp" 2>/dev/null || :
|
|
723
|
+
if [ -e "$_rg_tmp" ] || [ -L "$_rg_tmp" ]; then _cleanup_failed=1; fi
|
|
724
|
+
_rg_tmp=""
|
|
725
|
+
fi
|
|
347
726
|
if [ -n "$_stage" ]; then
|
|
348
727
|
remove_tree "$_stage" || _cleanup_failed=1
|
|
349
728
|
_stage=""
|
|
@@ -358,11 +737,6 @@ release_lock() {
|
|
|
358
737
|
if [ -e "$_mm_tmp" ] || [ -L "$_mm_tmp" ]; then _cleanup_failed=1; fi
|
|
359
738
|
_mm_tmp=""
|
|
360
739
|
fi
|
|
361
|
-
if [ -n "$_posix_tmp" ]; then
|
|
362
|
-
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_posix_tmp" 2>/dev/null || :
|
|
363
|
-
if [ -e "$_posix_tmp" ] || [ -L "$_posix_tmp" ]; then _cleanup_failed=1; fi
|
|
364
|
-
_posix_tmp=""
|
|
365
|
-
fi
|
|
366
740
|
[ "$_lock_owned" -eq 1 ] || { [ "$_cleanup_failed" -eq 0 ]; return; }
|
|
367
741
|
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$VALISE_LOCK_DIR/pid" 2>/dev/null || :
|
|
368
742
|
if [ -e "$VALISE_LOCK_DIR/pid" ] || [ -L "$VALISE_LOCK_DIR/pid" ]; then _cleanup_failed=1; fi
|
|
@@ -394,15 +768,23 @@ switch_active_link() {
|
|
|
394
768
|
if [ -e "$VALISE_ACTIVE_LINK" ] && [ ! -L "$VALISE_ACTIVE_LINK" ]; then
|
|
395
769
|
die "active path exists and is not a symlink: $VALISE_ACTIVE_LINK"
|
|
396
770
|
fi
|
|
397
|
-
# ln -sfn replaces the destination symlink.
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
#
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
771
|
+
# ln -sfn replaces the destination symlink. The -n flag prevents following a
|
|
772
|
+
# destination that is a symlink to a directory. If the swap fails after the
|
|
773
|
+
# old link was removed, restore the prior target so the pointer is never
|
|
774
|
+
# left missing.
|
|
775
|
+
_switch_old_target=
|
|
776
|
+
if [ -L "$VALISE_ACTIVE_LINK" ]; then
|
|
777
|
+
_switch_old_target=$(readlink_target "$VALISE_ACTIVE_LINK" 2>/dev/null || :)
|
|
778
|
+
fi
|
|
779
|
+
if ! "$_ln_path" -sfn "$1" "$VALISE_ACTIVE_LINK"; then
|
|
780
|
+
if [ -n "$_switch_old_target" ]; then
|
|
781
|
+
"$_ln_path" -sfn "$_switch_old_target" "$VALISE_ACTIVE_LINK" 2>/dev/null || :
|
|
782
|
+
fi
|
|
783
|
+
die "failed to update active symlink"
|
|
784
|
+
fi
|
|
404
785
|
[ -L "$VALISE_ACTIVE_LINK" ] || die "active symlink was not created"
|
|
405
786
|
[ "$($_readlink_path "$VALISE_ACTIVE_LINK" 2>/dev/null)" = "$1" ] || die "active symlink target mismatch"
|
|
787
|
+
unset _switch_old_target
|
|
406
788
|
}
|
|
407
789
|
|
|
408
790
|
source_path_is_physical() {
|
|
@@ -527,15 +909,10 @@ prepare_manifest_rows() {
|
|
|
527
909
|
}
|
|
528
910
|
|
|
529
911
|
locate_manifest() {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if [ -f "$_loc_candidate" ]; then
|
|
535
|
-
printf '%s\n' "$_loc_candidate"
|
|
536
|
-
return 0
|
|
537
|
-
fi
|
|
538
|
-
done
|
|
912
|
+
if [ -f "./shell/install/profile-release.manifest" ]; then
|
|
913
|
+
printf '%s\n' "./shell/install/profile-release.manifest"
|
|
914
|
+
return 0
|
|
915
|
+
fi
|
|
539
916
|
return 1
|
|
540
917
|
}
|
|
541
918
|
|
|
@@ -558,40 +935,40 @@ _manifest_has_descendant() {
|
|
|
558
935
|
verify_release_dir() {
|
|
559
936
|
_verify_dir=$1
|
|
560
937
|
_verify_manifest=$2
|
|
561
|
-
_verify_canonical=$(cd "$_verify_dir" 2>/dev/null && pwd -P) ||
|
|
562
|
-
[ "$(file_mode "$_verify_dir")" = "0555" ] ||
|
|
938
|
+
_verify_canonical=$(cd "$_verify_dir" 2>/dev/null && pwd -P) || _verify_fail "cannot resolve release root" || return 1
|
|
939
|
+
[ "$(file_mode "$_verify_dir")" = "0555" ] || _verify_fail "release root mode mismatch" || return 1
|
|
563
940
|
_verify_snapshot="$_verify_dir/.valise-manifest"
|
|
564
941
|
if [ -e "$_verify_snapshot" ] || [ -L "$_verify_snapshot" ]; then
|
|
565
|
-
[ -f "$_verify_snapshot" ] ||
|
|
566
|
-
[ ! -L "$_verify_snapshot" ] ||
|
|
567
|
-
[ "$(file_mode "$_verify_snapshot")" = "0444" ] ||
|
|
568
|
-
[ "$(file_bytes "$_verify_snapshot")" = "$(file_bytes "$_verify_manifest")" ] ||
|
|
569
|
-
_verify_snapshot_digest=$(file_sha256 "$_verify_snapshot") ||
|
|
570
|
-
_verify_manifest_digest=$(file_sha256 "$_verify_manifest") ||
|
|
571
|
-
[ "$_verify_snapshot_digest" = "$_verify_manifest_digest" ] ||
|
|
942
|
+
[ -f "$_verify_snapshot" ] || _verify_fail "release manifest snapshot is not a regular file" || return 1
|
|
943
|
+
[ ! -L "$_verify_snapshot" ] || _verify_fail "release manifest snapshot must not be a symlink" || return 1
|
|
944
|
+
[ "$(file_mode "$_verify_snapshot")" = "0444" ] || _verify_fail "release manifest snapshot mode mismatch" || return 1
|
|
945
|
+
[ "$(file_bytes "$_verify_snapshot")" = "$(file_bytes "$_verify_manifest")" ] || _verify_fail "release manifest snapshot byte count mismatch" || return 1
|
|
946
|
+
_verify_snapshot_digest=$(file_sha256 "$_verify_snapshot") || _verify_fail "no SHA-256 utility available" || return 1
|
|
947
|
+
_verify_manifest_digest=$(file_sha256 "$_verify_manifest") || _verify_fail "no SHA-256 utility available" || return 1
|
|
948
|
+
[ "$_verify_snapshot_digest" = "$_verify_manifest_digest" ] || _verify_fail "release manifest snapshot digest mismatch" || return 1
|
|
572
949
|
_verify_expected_id="${3:-}"
|
|
573
950
|
if [ -z "$_verify_expected_id" ]; then
|
|
574
951
|
_verify_expected_id=$(basename "$_verify_dir")
|
|
575
952
|
fi
|
|
576
|
-
[ "$_verify_snapshot_digest" = "$_verify_expected_id" ] ||
|
|
953
|
+
[ "$_verify_snapshot_digest" = "$_verify_expected_id" ] || _verify_fail "release manifest snapshot identity mismatch" || return 1
|
|
577
954
|
fi
|
|
578
955
|
while IFS="$(printf '\t')" read -r _dest _src _mode _imode _size _sha; do
|
|
579
956
|
[ -n "$_dest" ] || continue
|
|
580
957
|
case "$_dest" in shell-profile-release-manifest-v1|destination) continue ;; esac
|
|
581
958
|
_verify_file="$_verify_dir/$_dest"
|
|
582
|
-
[ -f "$_verify_file" ] ||
|
|
583
|
-
[ ! -L "$_verify_file" ] ||
|
|
584
|
-
_verify_parent=$(cd "$(path_parent "$_verify_file")" 2>/dev/null && pwd -P) ||
|
|
585
|
-
case "$_verify_parent/" in "$_verify_canonical/"*) : ;; *)
|
|
586
|
-
[ "$(file_mode "$_verify_file")" = "$_imode" ] ||
|
|
587
|
-
[ "$(file_bytes "$_verify_file")" = "$_size" ] ||
|
|
588
|
-
case "$_sha" in *[!0123456789abcdefABCDEF]*)
|
|
589
|
-
[ "${#_sha}" -eq 64 ] ||
|
|
590
|
-
_verify_actual=$(file_sha256 "$_verify_file") ||
|
|
591
|
-
_verify_expected=$(printf '%s' "$_sha" | lower_hex) ||
|
|
592
|
-
[ "$_verify_actual" = "$_verify_expected" ] ||
|
|
959
|
+
[ -f "$_verify_file" ] || _verify_fail "release file missing: $_dest" || return 1
|
|
960
|
+
[ ! -L "$_verify_file" ] || _verify_fail "release file must not be a symlink: $_dest" || return 1
|
|
961
|
+
_verify_parent=$(cd "$(path_parent "$_verify_file")" 2>/dev/null && pwd -P) || _verify_fail "release parent missing: $_dest" || return 1
|
|
962
|
+
case "$_verify_parent/" in "$_verify_canonical/"*) : ;; *) _verify_fail "release path escapes target: $_dest" || return 1 ;; esac
|
|
963
|
+
[ "$(file_mode "$_verify_file")" = "$_imode" ] || _verify_fail "release mode mismatch for $_dest" || return 1
|
|
964
|
+
[ "$(file_bytes "$_verify_file")" = "$_size" ] || _verify_fail "release byte count mismatch for $_dest" || return 1
|
|
965
|
+
case "$_sha" in *[!0123456789abcdefABCDEF]*) _verify_fail "invalid release digest for $_dest" || return 1 ;; esac
|
|
966
|
+
[ "${#_sha}" -eq 64 ] || _verify_fail "invalid release digest for $_dest" || return 1
|
|
967
|
+
_verify_actual=$(file_sha256 "$_verify_file") || _verify_fail "no trusted SHA-256 utility available" || return 1
|
|
968
|
+
_verify_expected=$(printf '%s' "$_sha" | lower_hex) || _verify_fail "no trusted tr utility available" || return 1
|
|
969
|
+
[ "$_verify_actual" = "$_verify_expected" ] || _verify_fail "release digest mismatch for $_dest" || return 1
|
|
593
970
|
done < "${_manifest_parse:-$_manifest_rows}"
|
|
594
|
-
_find_path=$(trusted_utility_path find) ||
|
|
971
|
+
_find_path=$(trusted_utility_path find) || _verify_fail "no trusted find utility available" || return 1
|
|
595
972
|
_verify_bad=$("$_find_path" "$_verify_dir" -print | while IFS= read -r _verify_path; do
|
|
596
973
|
[ "$_verify_path" = "$_verify_dir" ] && continue
|
|
597
974
|
if [ -L "$_verify_path" ]; then
|
|
@@ -612,7 +989,7 @@ verify_release_dir() {
|
|
|
612
989
|
break
|
|
613
990
|
fi
|
|
614
991
|
done)
|
|
615
|
-
[ -z "$_verify_bad" ] ||
|
|
992
|
+
[ -z "$_verify_bad" ] || _verify_fail "release contains unmanifested or mutable path: $_verify_bad" || return 1
|
|
616
993
|
}
|
|
617
994
|
|
|
618
995
|
# --- Subcommands ---
|
|
@@ -628,7 +1005,8 @@ print_fish_integration() {
|
|
|
628
1005
|
_fish_cli_literal=$(fish_quote "$_fish_cli_path") || return 1
|
|
629
1006
|
_fish_source_tree_mode=${_fish_source_tree_mode:-0}
|
|
630
1007
|
printf '%s\n' \
|
|
631
|
-
"# valise
|
|
1008
|
+
"# valise Fish integration. Do not edit." \
|
|
1009
|
+
"# This hook loads the active valise release for Fish sessions." \
|
|
632
1010
|
"begin" \
|
|
633
1011
|
"set -l _valise_fish_source_tree_mode $_fish_source_tree_mode" \
|
|
634
1012
|
"set -l _valise_fish_previous_root ''" \
|
|
@@ -1145,6 +1523,12 @@ print_fish_integration() {
|
|
|
1145
1523
|
" end" \
|
|
1146
1524
|
" builtin functions -e \"\$_valise_fish_persistence_verify_helper\"; or return 125" \
|
|
1147
1525
|
" end" \
|
|
1526
|
+
" if builtin set -q _valise_fish_owned_cleanup; and builtin functions -q \"\$_valise_fish_owned_cleanup\"" \
|
|
1527
|
+
" builtin functions -e \"\$_valise_fish_owned_cleanup\"; or return 125" \
|
|
1528
|
+
" end" \
|
|
1529
|
+
" if builtin set -q _valise_fish_persistence_path_physical_helper; and builtin functions -q \"\$_valise_fish_persistence_path_physical_helper\"" \
|
|
1530
|
+
" builtin functions -e \"\$_valise_fish_persistence_path_physical_helper\"; or return 125" \
|
|
1531
|
+
" end" \
|
|
1148
1532
|
" if builtin functions -q \"\$_valise_fish_root_restore_helper\"" \
|
|
1149
1533
|
" if not builtin test (builtin functions \"\$_valise_fish_root_restore_helper\" | string collect) = \"\$_valise_fish_root_helper_definition\"" \
|
|
1150
1534
|
" builtin printf '%s\\n' 'trusted root restore helper was modified' >&2" \
|
|
@@ -1292,7 +1676,7 @@ print_fish_integration() {
|
|
|
1292
1676
|
"if test \"\$_valise_fish_source_tree_mode\" = 1" \
|
|
1293
1677
|
" set _valise_fish_active_root (builtin path resolve -- ${_fish_active_literal})" \
|
|
1294
1678
|
"else" \
|
|
1295
|
-
" set _valise_fish_active_root (${_fish_cli_literal} verify
|
|
1679
|
+
" set _valise_fish_active_root (${_fish_cli_literal} release verify --print-root 2>/dev/null)" \
|
|
1296
1680
|
"end" \
|
|
1297
1681
|
"set -l _valise_fish_owned_cleanup __valise_fish_owned_cleanup_(builtin random)" \
|
|
1298
1682
|
"function \$_valise_fish_owned_cleanup --no-scope-shadowing" \
|
|
@@ -1426,7 +1810,7 @@ print_fish_integration() {
|
|
|
1426
1810
|
" return 1" \
|
|
1427
1811
|
"end" \
|
|
1428
1812
|
"if test \"\$_valise_fish_source_tree_mode\" != 1" \
|
|
1429
|
-
" set -l _valise_fish_reverified_root (${_fish_cli_literal} verify
|
|
1813
|
+
" set -l _valise_fish_reverified_root (${_fish_cli_literal} release verify --print-root 2>/dev/null)" \
|
|
1430
1814
|
" if not test -x ${_fish_cli_literal}; or not test \"\$_valise_fish_reverified_root\" = \"\$_valise_fish_active_root\"" \
|
|
1431
1815
|
" printf '%s\\n' 'valise Fish integration: active release authentication failed' >&2" \
|
|
1432
1816
|
" if not builtin functions -q \$_valise_fish_owned_cleanup; or not builtin test (builtin functions \$_valise_fish_owned_cleanup | string collect) = \"\$_valise_fish_owned_cleanup_definition\"" \
|
|
@@ -1524,7 +1908,7 @@ print_fish_integration() {
|
|
|
1524
1908
|
" set _entrypoint_status 1" \
|
|
1525
1909
|
" end" \
|
|
1526
1910
|
" end" \
|
|
1527
|
-
" # The entrypoint is selected only after verify
|
|
1911
|
+
" # The entrypoint is selected only after release verify authenticates its" \
|
|
1528
1912
|
" # immutable release. Its native metadata/loader state is therefore an" \
|
|
1529
1913
|
" # intentional result of authenticated activation, not caller mutation." \
|
|
1530
1914
|
" # Keep the exact caller-function and embedded-function checks above; those" \
|
|
@@ -2295,19 +2679,10 @@ print_fish_integration() {
|
|
|
2295
2679
|
" end" \
|
|
2296
2680
|
" \$_valise_fish_publish_active_helper; or builtin exit 125" \
|
|
2297
2681
|
" else" \
|
|
2298
|
-
"
|
|
2682
|
+
" # No Fish entrypoint, no generated Fish plan and no native Fish loader:" \
|
|
2683
|
+
" # the release has nothing this hook can load. Fall through to the shared" \
|
|
2684
|
+
" # success cleanup so the hook stays silent and leaves no helper behind." \
|
|
2299
2685
|
" builtin functions -e \$_valise_fish_legacy_runner 2>/dev/null" \
|
|
2300
|
-
" if not builtin functions -q \$_valise_fish_owned_cleanup; or not builtin test (builtin functions \$_valise_fish_owned_cleanup | string collect) = \"\$_valise_fish_owned_cleanup_definition\"" \
|
|
2301
|
-
" builtin printf '%s\\n' 'trusted ownership cleanup helper was modified' >&2" \
|
|
2302
|
-
" builtin exit 125" \
|
|
2303
|
-
" end" \
|
|
2304
|
-
" \$_valise_fish_owned_cleanup" \
|
|
2305
|
-
" if not builtin functions -q \$_valise_fish_root_restore_helper; or not builtin test (builtin functions \$_valise_fish_root_restore_helper | string collect) = \"\$_valise_fish_root_helper_definition\"" \
|
|
2306
|
-
" builtin printf '%s\\n' 'trusted root restore helper was modified' >&2" \
|
|
2307
|
-
" builtin exit 125" \
|
|
2308
|
-
" end" \
|
|
2309
|
-
" \$_valise_fish_root_restore_helper; or builtin exit 125" \
|
|
2310
|
-
" return 1" \
|
|
2311
2686
|
"end" \
|
|
2312
2687
|
" end" \
|
|
2313
2688
|
"end" \
|
|
@@ -2320,62 +2695,165 @@ print_fish_integration() {
|
|
|
2320
2695
|
"end"
|
|
2321
2696
|
}
|
|
2322
2697
|
|
|
2698
|
+
# Emit the POSIX integration block for the active release.
|
|
2699
|
+
#
|
|
2700
|
+
# It works in every POSIX shell and with both release shapes. A built release
|
|
2701
|
+
# carries generated/installed-modules.<shell> plans; an engine release carries
|
|
2702
|
+
# entrypoints/profile.sh and the loader. The plan for the running shell is tried
|
|
2703
|
+
# first, then the loader, so a startup file never silently loads nothing when
|
|
2704
|
+
# the release has a way to load. The guard makes several startup files safe: a
|
|
2705
|
+
# login shell that reads more than one of them loads the profile once.
|
|
2706
|
+
print_posix_integration() {
|
|
2707
|
+
_pix_active_literal=$(shell_quote "$VALISE_ACTIVE_LINK") || return 1
|
|
2708
|
+
printf '%s\n' \
|
|
2709
|
+
'# BEGIN valise managed block' \
|
|
2710
|
+
"_valise_active_link=$_pix_active_literal" \
|
|
2711
|
+
'if [ -z "${_valise_profile_loaded:-}" ] || [ "${VALISE_RELOAD:-}" = 1 ]; then' \
|
|
2712
|
+
' _valise_profile_loaded=1' \
|
|
2713
|
+
' if [ -n "${BASH_VERSION-}" ]; then _valise_plan=bash; elif [ -n "${ZSH_VERSION-}" ]; then _valise_plan=zsh; elif [ -n "${KSH_VERSION-}" ]; then _valise_plan=ksh; else _valise_plan=; fi' \
|
|
2714
|
+
' _valise_active=$(cd "$_valise_active_link" 2>/dev/null && pwd -P)' \
|
|
2715
|
+
' if [ -n "$_valise_active" ]; then' \
|
|
2716
|
+
' if [ -n "$_valise_plan" ] && [ -f "$_valise_active/generated/installed-modules.$_valise_plan" ]; then' \
|
|
2717
|
+
' VALISE_MODULE_ROOT="$_valise_active"' \
|
|
2718
|
+
' . "$_valise_active/generated/installed-modules.$_valise_plan"' \
|
|
2719
|
+
' unset VALISE_MODULE_ROOT' \
|
|
2720
|
+
' elif [ -f "$_valise_active/entrypoints/profile.sh" ]; then' \
|
|
2721
|
+
' VALISE_ROOT="$_valise_active"' \
|
|
2722
|
+
' . "$_valise_active/entrypoints/profile.sh"' \
|
|
2723
|
+
' fi' \
|
|
2724
|
+
' fi' \
|
|
2725
|
+
' unset _valise_active _valise_plan' \
|
|
2726
|
+
'fi' \
|
|
2727
|
+
'unset _valise_active_link' \
|
|
2728
|
+
'# END valise managed block'
|
|
2729
|
+
unset _pix_active_literal
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2323
2732
|
cmd_init() {
|
|
2324
|
-
_shell="
|
|
2325
|
-
if [ "$#" -gt 0 ]; then
|
|
2326
|
-
shift
|
|
2327
|
-
fi
|
|
2733
|
+
_shell=""
|
|
2328
2734
|
_fish_source_tree_mode=0
|
|
2329
2735
|
for _arg do
|
|
2330
2736
|
case "$_arg" in
|
|
2331
2737
|
--source-tree) _fish_source_tree_mode=1 ;;
|
|
2332
|
-
|
|
2738
|
+
--help|-h) usage_init; return 0 ;;
|
|
2739
|
+
-*) die "unknown init option: $_arg" ;;
|
|
2740
|
+
*)
|
|
2741
|
+
[ -z "$_shell" ] || die "usage: valise init [posix|fish] [--source-tree]"
|
|
2742
|
+
_shell=$_arg
|
|
2743
|
+
;;
|
|
2333
2744
|
esac
|
|
2334
2745
|
done
|
|
2335
2746
|
if [ -z "$_shell" ]; then
|
|
2336
|
-
# Detect current shell
|
|
2337
2747
|
case "${SHELL:-}" in
|
|
2338
|
-
*bash) _shell=bash ;;
|
|
2339
|
-
*zsh) _shell=zsh ;;
|
|
2340
|
-
*ksh) _shell=ksh ;;
|
|
2341
2748
|
*fish) _shell=fish ;;
|
|
2342
|
-
*) _shell=
|
|
2749
|
+
*) _shell=posix ;;
|
|
2343
2750
|
esac
|
|
2344
2751
|
fi
|
|
2345
|
-
|
|
2346
2752
|
case "$_shell" in
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
# into the generated snippet as the right-hand side of an
|
|
2351
|
-
# assignment to _valise_active_link. Every later use references
|
|
2352
|
-
# "$_valise_active_link" in double quotes — no visually-unquoted
|
|
2353
|
-
# shell_quote output remains in the emitted text.
|
|
2354
|
-
_shell_active_literal=$(shell_quote "$VALISE_ACTIVE_LINK") || return 1
|
|
2355
|
-
printf '%s\n' \
|
|
2356
|
-
"# valise shell integration" \
|
|
2357
|
-
"_valise_active_link=${_shell_active_literal}" \
|
|
2358
|
-
"if [ -L \"\$_valise_active_link/entrypoints/profile.sh\" ] || [ -f \"\$_valise_active_link/entrypoints/profile.sh\" ]; then" \
|
|
2359
|
-
" _valise_active=\$(cd \"\$_valise_active_link\" 2>/dev/null && pwd -P)" \
|
|
2360
|
-
" if [ -n \"\$_valise_active\" ]; then" \
|
|
2361
|
-
" VALISE_ROOT=\"\$_valise_active\"" \
|
|
2362
|
-
" . \"\$_valise_active/entrypoints/profile.sh\"" \
|
|
2363
|
-
" fi" \
|
|
2364
|
-
" unset _valise_active" \
|
|
2365
|
-
"fi" \
|
|
2366
|
-
"unset _valise_active_link"
|
|
2367
|
-
;;
|
|
2368
|
-
fish)
|
|
2369
|
-
print_fish_integration
|
|
2370
|
-
;;
|
|
2371
|
-
*)
|
|
2372
|
-
die "unsupported shell: $_shell (use bash, zsh, ksh, or fish)"
|
|
2373
|
-
;;
|
|
2753
|
+
posix) print_posix_integration || return 1 ;;
|
|
2754
|
+
fish) print_fish_integration ;;
|
|
2755
|
+
*) die "unsupported shell family: $_shell (use posix or fish)" ;;
|
|
2374
2756
|
esac
|
|
2375
2757
|
}
|
|
2376
2758
|
|
|
2759
|
+
# Report whether a startup file carries valise integration: the POSIX managed
|
|
2760
|
+
# block, or the header of the Fish hook, which valise owns as a whole file.
|
|
2761
|
+
integration_has_managed_block() {
|
|
2762
|
+
_ihb_awk=$(trusted_utility_path awk) || return 1
|
|
2763
|
+
"$_ihb_awk" '
|
|
2764
|
+
index($0, "# BEGIN valise managed block") { found = 1; exit }
|
|
2765
|
+
index($0, "# valise Fish integration") { found = 1; exit }
|
|
2766
|
+
END { exit !found }
|
|
2767
|
+
' "$1" 2>/dev/null
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
# The startup files a shell reads at login and for interactive shells. A login
|
|
2771
|
+
# bash reads the first of .bash_profile, .bash_login and .profile that exists.
|
|
2772
|
+
#
|
|
2773
|
+
# integration_shell_files <shell> <home>
|
|
2774
|
+
# prints one candidate path per line.
|
|
2775
|
+
integration_shell_files() {
|
|
2776
|
+
case "$1" in
|
|
2777
|
+
bash)
|
|
2778
|
+
if [ -e "$2/.bash_profile" ]; then
|
|
2779
|
+
printf '%s\n' "$2/.bash_profile"
|
|
2780
|
+
elif [ -e "$2/.bash_login" ]; then
|
|
2781
|
+
printf '%s\n' "$2/.bash_login"
|
|
2782
|
+
else
|
|
2783
|
+
printf '%s\n' "$2/.profile"
|
|
2784
|
+
fi
|
|
2785
|
+
printf '%s\n' "$2/.bashrc"
|
|
2786
|
+
;;
|
|
2787
|
+
zsh)
|
|
2788
|
+
printf '%s\n' "$2/.zprofile" "$2/.zshrc"
|
|
2789
|
+
;;
|
|
2790
|
+
fish)
|
|
2791
|
+
printf '%s\n' "${XDG_CONFIG_HOME:-$2/.config}/fish/conf.d/valise.fish"
|
|
2792
|
+
;;
|
|
2793
|
+
*)
|
|
2794
|
+
printf '%s\n' "$2/.profile"
|
|
2795
|
+
;;
|
|
2796
|
+
esac
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
# The startup files install manages. ~/.profile is always included: it serves
|
|
2800
|
+
# sh, ksh and login bash. Files that belong to another shell are only created
|
|
2801
|
+
# for the user's own shell, except ~/.zprofile, because zsh reads nothing else at
|
|
2802
|
+
# login. Existing files are always kept current.
|
|
2803
|
+
#
|
|
2804
|
+
# posix_startup_targets <home>
|
|
2805
|
+
# prints one path per line.
|
|
2806
|
+
posix_startup_targets() {
|
|
2807
|
+
printf '%s\n' "$1/.profile"
|
|
2808
|
+
if [ -e "$1/.bash_profile" ] || [ -L "$1/.bash_profile" ]; then
|
|
2809
|
+
printf '%s\n' "$1/.bash_profile"
|
|
2810
|
+
elif [ -e "$1/.bash_login" ] || [ -L "$1/.bash_login" ]; then
|
|
2811
|
+
printf '%s\n' "$1/.bash_login"
|
|
2812
|
+
fi
|
|
2813
|
+
case "${SHELL:-}" in
|
|
2814
|
+
*/bash | bash) printf '%s\n' "$1/.bashrc" ;;
|
|
2815
|
+
*) if [ -e "$1/.bashrc" ] || [ -L "$1/.bashrc" ]; then printf '%s\n' "$1/.bashrc"; fi ;;
|
|
2816
|
+
esac
|
|
2817
|
+
if [ -e "$1/.zprofile" ] || [ -L "$1/.zprofile" ] || command -v zsh >/dev/null 2>&1; then
|
|
2818
|
+
printf '%s\n' "$1/.zprofile"
|
|
2819
|
+
fi
|
|
2820
|
+
case "${SHELL:-}" in
|
|
2821
|
+
*/zsh | zsh) printf '%s\n' "$1/.zshrc" ;;
|
|
2822
|
+
*) if [ -e "$1/.zshrc" ] || [ -L "$1/.zshrc" ]; then printf '%s\n' "$1/.zshrc"; fi ;;
|
|
2823
|
+
esac
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
# Write the managed block to every startup file install manages.
|
|
2827
|
+
write_posix_integration() {
|
|
2828
|
+
if [ -z "${HOME:-}" ]; then
|
|
2829
|
+
info "warning: HOME is not set; no startup file was written"
|
|
2830
|
+
return 0
|
|
2831
|
+
fi
|
|
2832
|
+
_wpi_home=$(path_normalize "$HOME") || die "cannot resolve HOME"
|
|
2833
|
+
_wpi_snippet=$(print_posix_integration) || die "cannot generate integration block"
|
|
2834
|
+
_wpi_targets=$(posix_startup_targets "$_wpi_home")
|
|
2835
|
+
while IFS= read -r _wpi_target; do
|
|
2836
|
+
[ -n "$_wpi_target" ] || continue
|
|
2837
|
+
project_write_integration "$_wpi_target" "$_wpi_snippet"
|
|
2838
|
+
done <<EOF
|
|
2839
|
+
$_wpi_targets
|
|
2840
|
+
EOF
|
|
2841
|
+
info "to remove the integration, delete the valise managed block from these files"
|
|
2842
|
+
unset _wpi_home _wpi_snippet _wpi_targets _wpi_target
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2377
2845
|
cmd_doctor() {
|
|
2378
2846
|
_status=0
|
|
2847
|
+
_doctor_pre_install=0
|
|
2848
|
+
_doctor_check_integration=1
|
|
2849
|
+
for _doctor_arg do
|
|
2850
|
+
case "$_doctor_arg" in
|
|
2851
|
+
--pre-install) _doctor_pre_install=1; _doctor_check_integration=0 ;;
|
|
2852
|
+
--no-integration) _doctor_check_integration=0 ;;
|
|
2853
|
+
--help|-h) usage_doctor; return 0 ;;
|
|
2854
|
+
*) die "unknown doctor option: $_doctor_arg (usage: valise doctor [--pre-install] [--no-integration])" ;;
|
|
2855
|
+
esac
|
|
2856
|
+
done
|
|
2379
2857
|
|
|
2380
2858
|
info "checking environment..."
|
|
2381
2859
|
|
|
@@ -2412,22 +2890,115 @@ cmd_doctor() {
|
|
|
2412
2890
|
done
|
|
2413
2891
|
|
|
2414
2892
|
# Check active symlink
|
|
2893
|
+
_doctor_active_ok=0
|
|
2415
2894
|
if [ -L "$VALISE_ACTIVE_LINK" ]; then
|
|
2416
|
-
_target=$(
|
|
2417
|
-
if [ -
|
|
2895
|
+
_target=$(active_release_dir 2>/dev/null || :)
|
|
2896
|
+
if [ -n "$_target" ]; then
|
|
2418
2897
|
printf ' ok: active release: %s\n' "$_target"
|
|
2898
|
+
_doctor_active_target=$_target
|
|
2899
|
+
_doctor_active_ok=1
|
|
2419
2900
|
else
|
|
2420
|
-
printf ' FAIL: active symlink
|
|
2901
|
+
printf ' FAIL: active symlink does not resolve to a release directory\n'
|
|
2421
2902
|
_status=1
|
|
2422
2903
|
fi
|
|
2423
2904
|
elif [ -e "$VALISE_ACTIVE_LINK" ]; then
|
|
2424
2905
|
printf ' FAIL: active pointer is not a symlink: %s\n' "$VALISE_ACTIVE_LINK"
|
|
2425
2906
|
_status=1
|
|
2426
|
-
|
|
2907
|
+
elif [ "$_doctor_pre_install" -eq 1 ]; then
|
|
2427
2908
|
printf ' info: no active release configured\n'
|
|
2909
|
+
else
|
|
2910
|
+
# With no release there is nothing for a shell to load, so doctor reports
|
|
2911
|
+
# the missing release as a failure. --pre-install is the explicit
|
|
2912
|
+
# opt-out for a machine that has not installed a release yet.
|
|
2913
|
+
printf ' FAIL: no active release configured\n'
|
|
2914
|
+
_status=1
|
|
2915
|
+
fi
|
|
2916
|
+
|
|
2917
|
+
# The active release must verify against its own manifest. A release that
|
|
2918
|
+
# fails verification is not usable, so doctor reports it as a failure.
|
|
2919
|
+
if [ "$_doctor_active_ok" -eq 1 ] && [ "$_doctor_pre_install" -eq 0 ]; then
|
|
2920
|
+
_doctor_active_manifest="$_doctor_active_target/.valise-manifest"
|
|
2921
|
+
if [ -f "$_doctor_active_manifest" ] && [ ! -L "$_doctor_active_manifest" ]; then
|
|
2922
|
+
_doctor_active_id=${_doctor_active_target##*/}
|
|
2923
|
+
if prepare_manifest_rows "$_doctor_active_manifest" &&
|
|
2924
|
+
validate_manifest "$_manifest_rows"; then
|
|
2925
|
+
_verify_quiet=1
|
|
2926
|
+
if verify_release_dir "$_doctor_active_target" "$_manifest_rows" "$_doctor_active_id"; then
|
|
2927
|
+
_verify_quiet=0
|
|
2928
|
+
printf ' ok: active release verifies: %s\n' "$_doctor_active_id"
|
|
2929
|
+
else
|
|
2930
|
+
_verify_quiet=0
|
|
2931
|
+
printf ' FAIL: active release does not verify: %s\n' "$_doctor_active_id"
|
|
2932
|
+
_status=1
|
|
2933
|
+
fi
|
|
2934
|
+
else
|
|
2935
|
+
printf ' FAIL: active release manifest is invalid: %s\n' "$_doctor_active_manifest"
|
|
2936
|
+
_status=1
|
|
2937
|
+
fi
|
|
2938
|
+
else
|
|
2939
|
+
printf ' FAIL: active release manifest is missing: %s\n' "$_doctor_active_manifest"
|
|
2940
|
+
_status=1
|
|
2941
|
+
fi
|
|
2942
|
+
elif [ "$_doctor_pre_install" -eq 1 ]; then
|
|
2943
|
+
printf ' info: skipping the active release check (--pre-install)\n'
|
|
2944
|
+
fi
|
|
2945
|
+
|
|
2946
|
+
if [ -n "${HOME:-}" ]; then
|
|
2947
|
+
# Check the startup files the running shell actually reads. The shell is
|
|
2948
|
+
# taken from $SHELL, which is the same signal install uses to pick its
|
|
2949
|
+
# per-shell startup file.
|
|
2950
|
+
if [ "$_doctor_check_integration" -eq 1 ]; then
|
|
2951
|
+
_doctor_shell=""
|
|
2952
|
+
case "${SHELL:-}" in
|
|
2953
|
+
*/bash|bash) _doctor_shell=bash ;;
|
|
2954
|
+
*/zsh|zsh) _doctor_shell=zsh ;;
|
|
2955
|
+
*/ksh|ksh) _doctor_shell=ksh ;;
|
|
2956
|
+
*/fish|fish) _doctor_shell=fish ;;
|
|
2957
|
+
esac
|
|
2958
|
+
if [ -n "$_doctor_shell" ]; then
|
|
2959
|
+
printf ' integration shell: %s\n' "$_doctor_shell"
|
|
2960
|
+
else
|
|
2961
|
+
printf ' integration shell: unknown (using ~/.profile)\n'
|
|
2962
|
+
fi
|
|
2963
|
+
_doctor_found=0
|
|
2964
|
+
_doctor_candidates=""
|
|
2965
|
+
# The candidate paths are read one per line. Word splitting on $(...)
|
|
2966
|
+
# would break a home directory containing whitespace, so the list goes
|
|
2967
|
+
# through a staging file instead.
|
|
2968
|
+
_doctor_cand_file=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-doctor-candidates.XXXXXX") ||
|
|
2969
|
+
die "cannot create doctor candidate staging file"
|
|
2970
|
+
integration_shell_files "$_doctor_shell" "$HOME" >"$_doctor_cand_file"
|
|
2971
|
+
while IFS= read -r _doctor_candidate; do
|
|
2972
|
+
[ -n "$_doctor_candidate" ] || continue
|
|
2973
|
+
_doctor_candidates="$_doctor_candidates $_doctor_candidate"
|
|
2974
|
+
if [ -L "$_doctor_candidate" ]; then
|
|
2975
|
+
printf ' warn: startup file is a symlink: %s\n' "$_doctor_candidate"
|
|
2976
|
+
continue
|
|
2977
|
+
fi
|
|
2978
|
+
if [ ! -f "$_doctor_candidate" ]; then
|
|
2979
|
+
printf ' info: startup file is not present: %s\n' "$_doctor_candidate"
|
|
2980
|
+
continue
|
|
2981
|
+
fi
|
|
2982
|
+
if integration_has_managed_block "$_doctor_candidate"; then
|
|
2983
|
+
printf ' ok: startup file contains the valise managed block: %s\n' "$_doctor_candidate"
|
|
2984
|
+
_doctor_found=1
|
|
2985
|
+
else
|
|
2986
|
+
printf ' warn: startup file exists but has no valise managed block: %s\n' "$_doctor_candidate"
|
|
2987
|
+
fi
|
|
2988
|
+
done <"$_doctor_cand_file"
|
|
2989
|
+
_doctor_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
2990
|
+
"$_doctor_rm" -f "$_doctor_cand_file" || die "cannot remove doctor candidate staging file"
|
|
2991
|
+
if [ "$_doctor_found" -eq 0 ]; then
|
|
2992
|
+
printf ' FAIL: no valise integration found for %s (checked:%s)\n' \
|
|
2993
|
+
"${_doctor_shell:-the current shell}" "$_doctor_candidates"
|
|
2994
|
+
_status=1
|
|
2995
|
+
fi
|
|
2996
|
+
else
|
|
2997
|
+
printf ' info: skipping the shell integration check\n'
|
|
2998
|
+
fi
|
|
2428
2999
|
fi
|
|
2429
3000
|
|
|
2430
|
-
# Platform check (informational only; install and
|
|
3001
|
+
# Platform check (informational only; install and release use are portable
|
|
2431
3002
|
# POSIX and not gated on any particular uname).
|
|
2432
3003
|
_uname=$(uname -s 2>/dev/null || echo unknown)
|
|
2433
3004
|
printf ' platform: %s\n' "$_uname"
|
|
@@ -2471,7 +3042,8 @@ cmd_verify_active() {
|
|
|
2471
3042
|
for _verify_arg do
|
|
2472
3043
|
case "$_verify_arg" in
|
|
2473
3044
|
--print-root) _verify_print_root=1 ;;
|
|
2474
|
-
|
|
3045
|
+
--help|-h) usage_release; return 0 ;;
|
|
3046
|
+
*) die "unknown release verify option: $_verify_arg" ;;
|
|
2475
3047
|
esac
|
|
2476
3048
|
done
|
|
2477
3049
|
[ -L "$VALISE_ACTIVE_LINK" ] || die "active pointer is not a symlink"
|
|
@@ -2505,27 +3077,36 @@ cmd_verify_active() {
|
|
|
2505
3077
|
fi
|
|
2506
3078
|
return 0
|
|
2507
3079
|
}
|
|
2508
|
-
|
|
3080
|
+
cmd_release() {
|
|
2509
3081
|
_subcmd="${1:-list}"
|
|
2510
3082
|
case "$_subcmd" in
|
|
3083
|
+
--help|-h) usage_release; return 0 ;;
|
|
2511
3084
|
list)
|
|
3085
|
+
case "${2:-}" in
|
|
3086
|
+
--help|-h) usage_release; return 0 ;;
|
|
3087
|
+
esac
|
|
2512
3088
|
if [ -d "$VALISE_RELEASES_DIR" ]; then
|
|
3089
|
+
_list_active_id=$(active_release_id 2>/dev/null || :)
|
|
2513
3090
|
for _rel in "$VALISE_RELEASES_DIR"/*/; do
|
|
2514
3091
|
[ -d "$_rel" ] || continue
|
|
2515
3092
|
_name=$(basename "$_rel")
|
|
2516
|
-
if [ -
|
|
3093
|
+
if [ -n "$_list_active_id" ] && [ "$_name" = "$_list_active_id" ]; then
|
|
2517
3094
|
printf '* %s (active)\n' "$_name"
|
|
2518
3095
|
else
|
|
2519
3096
|
printf ' %s\n' "$_name"
|
|
2520
3097
|
fi
|
|
2521
3098
|
done
|
|
3099
|
+
unset _list_active_id
|
|
2522
3100
|
else
|
|
2523
3101
|
info "no releases installed"
|
|
2524
3102
|
fi
|
|
2525
3103
|
;;
|
|
2526
3104
|
use)
|
|
3105
|
+
case "${2:-}" in
|
|
3106
|
+
--help|-h) usage_release; return 0 ;;
|
|
3107
|
+
esac
|
|
2527
3108
|
_target="${2:-}"
|
|
2528
|
-
[ -n "$_target" ] || die "usage: valise
|
|
3109
|
+
[ -n "$_target" ] || die "usage: valise release use <sha256>"
|
|
2529
3110
|
case "$_target" in
|
|
2530
3111
|
*[!A-Za-z0-9_.-]*|.*|*-|.-*|*..*) die "invalid release name: $_target" ;;
|
|
2531
3112
|
esac
|
|
@@ -2545,7 +3126,8 @@ cmd_variant() {
|
|
|
2545
3126
|
[ ! -L "$_target_manifest" ] || die "release manifest snapshot must not be a symlink"
|
|
2546
3127
|
_manifest="$_target_manifest"
|
|
2547
3128
|
else
|
|
2548
|
-
_manifest=$(locate_manifest) ||
|
|
3129
|
+
_manifest=$(locate_manifest) ||
|
|
3130
|
+
die "cannot find shell/install/profile-release.manifest in the current directory"
|
|
2549
3131
|
fi
|
|
2550
3132
|
prepare_manifest_rows "$_manifest" || die "cannot snapshot profile-release.manifest"
|
|
2551
3133
|
_manifest="$_manifest_rows"
|
|
@@ -2561,21 +3143,139 @@ cmd_variant() {
|
|
|
2561
3143
|
release_lock
|
|
2562
3144
|
info "switched to release $_target"
|
|
2563
3145
|
;;
|
|
3146
|
+
prune)
|
|
3147
|
+
shift
|
|
3148
|
+
_prune_keep=3
|
|
3149
|
+
_prune_keep_set=0
|
|
3150
|
+
for _prune_arg do
|
|
3151
|
+
case "$_prune_arg" in
|
|
3152
|
+
--keep=*)
|
|
3153
|
+
_prune_keep_set=1
|
|
3154
|
+
_prune_keep=${_prune_arg#--keep=}
|
|
3155
|
+
case "$_prune_keep" in
|
|
3156
|
+
'' | *[!0-9]*) die "--keep requires a non-negative integer: $_prune_keep" ;;
|
|
3157
|
+
esac
|
|
3158
|
+
# Strip leading zeros so the shell does not read the value as
|
|
3159
|
+
# octal in the arithmetic comparison below, and so the digit
|
|
3160
|
+
# count measures the value rather than its spelling.
|
|
3161
|
+
while [ "$_prune_keep" != "0" ] && [ "${_prune_keep#0}" != "$_prune_keep" ]; do
|
|
3162
|
+
_prune_keep=${_prune_keep#0}
|
|
3163
|
+
done
|
|
3164
|
+
# A value beyond the shell's integer range makes the comparison
|
|
3165
|
+
# below fail, which would read as "keep nothing" and remove every
|
|
3166
|
+
# release. Reject it instead. Nine digits is the bound: every
|
|
3167
|
+
# nine-digit value fits in a signed 32-bit integer, so no
|
|
3168
|
+
# lexicographic comparison against the maximum is needed.
|
|
3169
|
+
if [ "${#_prune_keep}" -gt 9 ]; then
|
|
3170
|
+
die "--keep is too large: $_prune_keep (maximum is 999999999)"
|
|
3171
|
+
fi
|
|
3172
|
+
;;
|
|
3173
|
+
--help|-h) usage_release; return 0 ;;
|
|
3174
|
+
*) die "unknown release prune option: $_prune_arg" ;;
|
|
3175
|
+
esac
|
|
3176
|
+
done
|
|
3177
|
+
[ -d "$VALISE_RELEASES_DIR" ] || die "no releases installed"
|
|
3178
|
+
# The active release is identified by its canonical directory name, not
|
|
3179
|
+
# by a path spelling. A trailing slash, a relative target, or an alias
|
|
3180
|
+
# symlink must never make prune delete the release the pointer names.
|
|
3181
|
+
_prune_active_id=$(active_release_id 2>/dev/null || :)
|
|
3182
|
+
_prune_active=$_prune_active_id
|
|
3183
|
+
# Collect the candidate releases with their modification times. The
|
|
3184
|
+
# active release is excluded here and kept unconditionally below.
|
|
3185
|
+
_prune_sort=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-prune.XXXXXX") ||
|
|
3186
|
+
die "cannot create prune staging file"
|
|
3187
|
+
for _prune_rel in "$VALISE_RELEASES_DIR"/*/; do
|
|
3188
|
+
[ -d "$_prune_rel" ] || continue
|
|
3189
|
+
[ ! -L "$_prune_rel" ] || continue
|
|
3190
|
+
_prune_dir=${_prune_rel%/}
|
|
3191
|
+
_prune_name=${_prune_dir##*/}
|
|
3192
|
+
[ "$_prune_name" = "$_prune_active_id" ] && continue
|
|
3193
|
+
_prune_mtime=$(file_mtime "$_prune_dir") || _prune_mtime=0
|
|
3194
|
+
printf '%s\t%s\n' "$_prune_mtime" "$_prune_name" >>"$_prune_sort"
|
|
3195
|
+
done
|
|
3196
|
+
_prune_sort_path=$(trusted_utility_path sort) || die "no trusted sort utility available"
|
|
3197
|
+
_prune_sorted=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-prune-sorted.XXXXXX") ||
|
|
3198
|
+
die "cannot create prune ordering file"
|
|
3199
|
+
# Newest first; equal timestamps fall back to the release name so the
|
|
3200
|
+
# ordering is deterministic.
|
|
3201
|
+
"$_prune_sort_path" -k1,1nr -k2,2 "$_prune_sort" >"$_prune_sorted" ||
|
|
3202
|
+
die "cannot order releases for pruning"
|
|
3203
|
+
_prune_index=0
|
|
3204
|
+
_prune_removed=0
|
|
3205
|
+
_prune_kept=0
|
|
3206
|
+
while IFS="$(printf '\t')" read -r _prune_mtime _prune_name; do
|
|
3207
|
+
[ -n "$_prune_name" ] || continue
|
|
3208
|
+
_prune_index=$((_prune_index + 1))
|
|
3209
|
+
if [ "$_prune_index" -le "$_prune_keep" ]; then
|
|
3210
|
+
_prune_kept=$((_prune_kept + 1))
|
|
3211
|
+
continue
|
|
3212
|
+
fi
|
|
3213
|
+
_prune_target="$VALISE_RELEASES_DIR/$_prune_name"
|
|
3214
|
+
# A release is only removed after it verifies against its own
|
|
3215
|
+
# manifest. A corrupted release is kept, never deleted, so a damaged
|
|
3216
|
+
# installation can still be inspected and repaired.
|
|
3217
|
+
_prune_manifest="$_prune_target/.valise-manifest"
|
|
3218
|
+
if [ ! -f "$_prune_manifest" ] || [ -L "$_prune_manifest" ]; then
|
|
3219
|
+
info "keeping unverifiable release $_prune_name (no manifest snapshot)"
|
|
3220
|
+
_prune_kept=$((_prune_kept + 1))
|
|
3221
|
+
continue
|
|
3222
|
+
fi
|
|
3223
|
+
if ! prepare_manifest_rows "$_prune_manifest" ||
|
|
3224
|
+
! validate_manifest "$_manifest_rows"; then
|
|
3225
|
+
info "keeping unverifiable release $_prune_name (invalid manifest)"
|
|
3226
|
+
_prune_kept=$((_prune_kept + 1))
|
|
3227
|
+
continue
|
|
3228
|
+
fi
|
|
3229
|
+
_verify_quiet=1
|
|
3230
|
+
if ! verify_release_dir "$_prune_target" "$_manifest_rows" "$_prune_name"; then
|
|
3231
|
+
_verify_quiet=0
|
|
3232
|
+
info "keeping unverifiable release $_prune_name (failed verification)"
|
|
3233
|
+
_prune_kept=$((_prune_kept + 1))
|
|
3234
|
+
continue
|
|
3235
|
+
fi
|
|
3236
|
+
_verify_quiet=0
|
|
3237
|
+
acquire_lock
|
|
3238
|
+
if remove_tree "$_prune_target"; then
|
|
3239
|
+
_prune_removed=$((_prune_removed + 1))
|
|
3240
|
+
info "removed release $_prune_name"
|
|
3241
|
+
else
|
|
3242
|
+
info "keeping release $_prune_name (could not be removed)"
|
|
3243
|
+
_prune_kept=$((_prune_kept + 1))
|
|
3244
|
+
fi
|
|
3245
|
+
release_lock
|
|
3246
|
+
done <"$_prune_sorted"
|
|
3247
|
+
_prune_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
3248
|
+
"$_prune_rm" -f "$_prune_sort" "$_prune_sorted" || die "cannot remove prune staging files"
|
|
3249
|
+
if [ -n "$_prune_active" ]; then
|
|
3250
|
+
info "prune kept the active release plus $_prune_kept other release(s); removed $_prune_removed"
|
|
3251
|
+
else
|
|
3252
|
+
info "prune kept $_prune_kept release(s); removed $_prune_removed (no active release)"
|
|
3253
|
+
fi
|
|
3254
|
+
;;
|
|
3255
|
+
verify)
|
|
3256
|
+
if [ "$#" -gt 0 ]; then
|
|
3257
|
+
shift
|
|
3258
|
+
fi
|
|
3259
|
+
cmd_verify_active "$@"
|
|
3260
|
+
;;
|
|
2564
3261
|
*)
|
|
2565
|
-
die "unknown
|
|
3262
|
+
die "unknown release subcommand: $_subcmd (use list, use, prune, or verify)"
|
|
2566
3263
|
;;
|
|
2567
3264
|
esac
|
|
2568
3265
|
}
|
|
2569
3266
|
|
|
2570
3267
|
cmd_install() {
|
|
3268
|
+
_install_integration=1
|
|
2571
3269
|
for _arg do
|
|
2572
3270
|
case "$_arg" in
|
|
2573
|
-
--help|-h)
|
|
2574
|
-
|
|
3271
|
+
--help|-h) usage_install; return 0 ;;
|
|
3272
|
+
--no-integration) _install_integration=0 ;;
|
|
3273
|
+
*) die "unknown install option: $_arg (usage: valise install [--no-integration])" ;;
|
|
2575
3274
|
esac
|
|
2576
3275
|
done
|
|
2577
3276
|
|
|
2578
|
-
_manifest_source=$(locate_manifest) ||
|
|
3277
|
+
_manifest_source=$(locate_manifest) ||
|
|
3278
|
+
die "cannot find shell/install/profile-release.manifest in the current directory (run valise install from the release tree)"
|
|
2579
3279
|
_manifest_root=$(cd "$(path_parent "$_manifest_source")/../.." && pwd -P) || die "cannot resolve manifest source root"
|
|
2580
3280
|
prepare_manifest_rows "$_manifest_source" || die "cannot normalize profile-release.manifest"
|
|
2581
3281
|
_manifest="$_manifest_rows"
|
|
@@ -2712,21 +3412,18 @@ cmd_install() {
|
|
|
2712
3412
|
|
|
2713
3413
|
# Update active symlink atomically without clobbering a non-symlink path.
|
|
2714
3414
|
if [ ! -L "$VALISE_ACTIVE_LINK" ] || [ "$(readlink_target "$VALISE_ACTIVE_LINK" 2>/dev/null)" != "$_target_dir" ]; then
|
|
2715
|
-
acquire_lock
|
|
2716
3415
|
if [ -e "$VALISE_ACTIVE_LINK" ] && [ ! -L "$VALISE_ACTIVE_LINK" ]; then
|
|
2717
|
-
release_lock
|
|
2718
3416
|
die "active path exists and is not a symlink: $VALISE_ACTIVE_LINK"
|
|
2719
3417
|
fi
|
|
2720
3418
|
switch_active_link "$_target_dir"
|
|
2721
|
-
release_lock
|
|
2722
3419
|
info "active release set to $_release_id"
|
|
2723
3420
|
fi
|
|
2724
3421
|
|
|
2725
|
-
# Deploy the Fish conf.d hook when Fish is available.
|
|
2726
|
-
#
|
|
2727
|
-
#
|
|
3422
|
+
# Deploy the Fish conf.d hook when Fish is available. A built release has no
|
|
3423
|
+
# Fish surface; an engine release exposes the project Fish entrypoint, so the
|
|
3424
|
+
# emitted integration handles that shape.
|
|
2728
3425
|
_fish_path=$(trusted_utility_path fish 2>/dev/null || :)
|
|
2729
|
-
if [ -n "$_fish_path" ]; then
|
|
3426
|
+
if [ "$_install_integration" -eq 1 ] && [ -n "$_fish_path" ]; then
|
|
2730
3427
|
_fish_conf_dir="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d"
|
|
2731
3428
|
_fish_hook="$_fish_conf_dir/valise.fish"
|
|
2732
3429
|
_mkdir_path=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
@@ -2740,12 +3437,7 @@ cmd_install() {
|
|
|
2740
3437
|
# shell redirection below into an attacker-selected victim.
|
|
2741
3438
|
_hook_tmp=$(trusted_mktemp_at "$_fish_conf_dir" file "$_fish_conf_dir/.valise-hook.XXXXXX") || die "cannot create Fish hook staging file"
|
|
2742
3439
|
[ -f "$_hook_tmp" ] && [ ! -L "$_hook_tmp" ] || die "Fish hook staging path is unsafe"
|
|
2743
|
-
|
|
2744
|
-
printf '%s\n' \
|
|
2745
|
-
"# Managed by valise install — do not edit." \
|
|
2746
|
-
"# This hook loads the active valise release for Fish sessions."
|
|
2747
|
-
print_fish_integration
|
|
2748
|
-
} >"$_hook_tmp" || die "cannot write Fish hook"
|
|
3440
|
+
print_fish_integration >"$_hook_tmp" || die "cannot write Fish hook"
|
|
2749
3441
|
_chmod_path=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
2750
3442
|
"$_chmod_path" 0644 "$_hook_tmp" || die "cannot set Fish hook mode"
|
|
2751
3443
|
_cat_path=$(trusted_utility_path cat) || die "no trusted cat utility available"
|
|
@@ -2762,97 +3454,14 @@ cmd_install() {
|
|
|
2762
3454
|
_hook_actual=$("$_cat_path" "$_fish_hook") || die "cannot read published Fish hook"
|
|
2763
3455
|
[ "$_hook_actual" = "$_hook_expected" ] || die "published Fish hook contents mismatch"
|
|
2764
3456
|
info "Fish hook deployed: $_fish_hook"
|
|
3457
|
+
info "remove this file to uninstall the Fish integration"
|
|
2765
3458
|
fi
|
|
2766
3459
|
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
# so .bash_profile / .zprofile is sourced per terminal.
|
|
2770
|
-
# Linux: terminal emulators open non-login interactive shells, so
|
|
2771
|
-
# .bashrc / .zshrc / .kshrc is sourced per terminal.
|
|
2772
|
-
_posix_shell="${SHELL:-}"
|
|
2773
|
-
_posix_shell_name=""
|
|
2774
|
-
case "$_posix_shell" in
|
|
2775
|
-
*/bash|bash) _posix_shell_name=bash ;;
|
|
2776
|
-
*/zsh|zsh) _posix_shell_name=zsh ;;
|
|
2777
|
-
*/ksh|ksh) _posix_shell_name=ksh ;;
|
|
2778
|
-
esac
|
|
2779
|
-
if [ -n "$_posix_shell_name" ] && [ -n "${HOME:-}" ]; then
|
|
2780
|
-
_posix_platform=$(trusted_uname -s 2>/dev/null || echo unknown)
|
|
2781
|
-
_posix_profile=""
|
|
2782
|
-
case "$_posix_platform" in
|
|
2783
|
-
Darwin)
|
|
2784
|
-
case "$_posix_shell_name" in
|
|
2785
|
-
bash) _posix_profile="$HOME/.bash_profile" ;;
|
|
2786
|
-
zsh) _posix_profile="$HOME/.zprofile" ;;
|
|
2787
|
-
ksh) _posix_profile="$HOME/.kshrc" ;;
|
|
2788
|
-
esac
|
|
2789
|
-
;;
|
|
2790
|
-
*)
|
|
2791
|
-
case "$_posix_shell_name" in
|
|
2792
|
-
bash) _posix_profile="$HOME/.bashrc" ;;
|
|
2793
|
-
zsh) _posix_profile="$HOME/.zshrc" ;;
|
|
2794
|
-
ksh) _posix_profile="$HOME/.kshrc" ;;
|
|
2795
|
-
esac
|
|
2796
|
-
;;
|
|
2797
|
-
esac
|
|
2798
|
-
if [ -n "$_posix_profile" ]; then
|
|
2799
|
-
if [ -L "$_posix_profile" ]; then
|
|
2800
|
-
info "shell integration skipped: $_posix_profile is a symlink (managed externally)"
|
|
2801
|
-
else
|
|
2802
|
-
_posix_marker="managed by valise install"
|
|
2803
|
-
_posix_already=0
|
|
2804
|
-
if [ -f "$_posix_profile" ]; then
|
|
2805
|
-
_awk_path=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
2806
|
-
if "$_awk_path" -v m="$_posix_marker" 'index($0,m){found=1;exit} END{exit !found}' "$_posix_profile" 2>/dev/null; then
|
|
2807
|
-
_posix_already=1
|
|
2808
|
-
fi
|
|
2809
|
-
fi
|
|
2810
|
-
if [ "$_posix_already" -eq 1 ]; then
|
|
2811
|
-
info "shell integration already present: $_posix_profile"
|
|
2812
|
-
else
|
|
2813
|
-
_posix_active_literal=$(shell_quote "$VALISE_ACTIVE_LINK") || die "cannot quote active link path"
|
|
2814
|
-
_posix_profile_dir=$(path_parent "$_posix_profile")
|
|
2815
|
-
if [ ! -d "$_posix_profile_dir" ]; then
|
|
2816
|
-
_mkdir_path=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
2817
|
-
"$_mkdir_path" -p "$_posix_profile_dir" || die "cannot create profile directory: $_posix_profile_dir"
|
|
2818
|
-
fi
|
|
2819
|
-
_posix_tmp=$(trusted_mktemp_at "$_posix_profile_dir" file "$_posix_profile_dir/.valise-profile.XXXXXX") || die "cannot create profile staging file"
|
|
2820
|
-
[ -f "$_posix_tmp" ] && [ ! -L "$_posix_tmp" ] || die "profile staging path is unsafe"
|
|
2821
|
-
if [ -f "$_posix_profile" ]; then
|
|
2822
|
-
_cat_path=$(trusted_utility_path cat) || die "no trusted cat utility available"
|
|
2823
|
-
"$_cat_path" "$_posix_profile" > "$_posix_tmp" || die "cannot copy existing profile"
|
|
2824
|
-
printf '\n' >> "$_posix_tmp"
|
|
2825
|
-
fi
|
|
2826
|
-
{
|
|
2827
|
-
printf '%s\n' "# valise shell integration — $_posix_marker"
|
|
2828
|
-
printf '%s\n' "_valise_active_link=${_posix_active_literal}"
|
|
2829
|
-
printf '%s\n' "if [ -L \"\$_valise_active_link/entrypoints/profile.sh\" ] || [ -f \"\$_valise_active_link/entrypoints/profile.sh\" ]; then"
|
|
2830
|
-
printf '%s\n' " _valise_active=\$(cd \"\$_valise_active_link\" 2>/dev/null && pwd -P)"
|
|
2831
|
-
printf '%s\n' " if [ -n \"\$_valise_active\" ]; then"
|
|
2832
|
-
printf '%s\n' " VALISE_ROOT=\"\$_valise_active\""
|
|
2833
|
-
printf '%s\n' " . \"\$_valise_active/entrypoints/profile.sh\""
|
|
2834
|
-
printf '%s\n' " fi"
|
|
2835
|
-
printf '%s\n' " unset _valise_active"
|
|
2836
|
-
printf '%s\n' "fi"
|
|
2837
|
-
printf '%s\n' "unset _valise_active_link"
|
|
2838
|
-
} >> "$_posix_tmp" || die "cannot write profile snippet"
|
|
2839
|
-
_chmod_path=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
2840
|
-
if [ -f "$_posix_profile" ]; then
|
|
2841
|
-
_posix_existing_mode=$(file_mode "$_posix_profile")
|
|
2842
|
-
[ -n "$_posix_existing_mode" ] || die "cannot read existing profile mode: $_posix_profile"
|
|
2843
|
-
"$_chmod_path" "$_posix_existing_mode" "$_posix_tmp" || die "cannot preserve profile mode"
|
|
2844
|
-
else
|
|
2845
|
-
"$_chmod_path" 0644 "$_posix_tmp" || die "cannot set profile mode"
|
|
2846
|
-
fi
|
|
2847
|
-
_mv_path=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
2848
|
-
"$_mv_path" -f "$_posix_tmp" "$_posix_profile" || die "cannot publish profile snippet"
|
|
2849
|
-
_posix_tmp=""
|
|
2850
|
-
info "shell integration deployed: $_posix_profile"
|
|
2851
|
-
fi
|
|
2852
|
-
fi
|
|
2853
|
-
fi
|
|
3460
|
+
if [ "$_install_integration" -eq 1 ]; then
|
|
3461
|
+
write_posix_integration
|
|
2854
3462
|
fi
|
|
2855
3463
|
|
|
3464
|
+
release_lock
|
|
2856
3465
|
info "installation complete"
|
|
2857
3466
|
info "run 'valise init' to print the shell integration snippet for manual setup"
|
|
2858
3467
|
}
|
|
@@ -2903,9 +3512,19 @@ cmd_make_manifest() {
|
|
|
2903
3512
|
_mm_out="$_mm_cwd/$_mm_out"
|
|
2904
3513
|
;;
|
|
2905
3514
|
esac
|
|
3515
|
+
_mm_out=$(path_normalize "$_mm_out") || die "cannot normalize manifest output path"
|
|
2906
3516
|
_mm_out_dir=$(path_parent "$_mm_out") || die "cannot resolve manifest parent"
|
|
2907
3517
|
_mm_mkdir_path=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
2908
3518
|
"$_mm_mkdir_path" -p "$_mm_out_dir" || die "cannot create manifest directory: $_mm_out_dir"
|
|
3519
|
+
# Resolve the parent physically: find reports physical paths, and /tmp is a
|
|
3520
|
+
# symlink to /private/tmp on macOS. A lexical comparison would include the
|
|
3521
|
+
# manifest in itself on the second run.
|
|
3522
|
+
_mm_out_dir=$(cd "$_mm_out_dir" && pwd -P) || die "cannot resolve manifest parent: $_mm_out_dir"
|
|
3523
|
+
_mm_out="$_mm_out_dir/${_mm_out##*/}"
|
|
3524
|
+
if [ -e "$_mm_out" ] || [ -L "$_mm_out" ]; then
|
|
3525
|
+
[ -f "$_mm_out" ] && [ ! -L "$_mm_out" ] ||
|
|
3526
|
+
die "manifest output is not a regular file: $_mm_out"
|
|
3527
|
+
fi
|
|
2909
3528
|
_mm_find_path=$(trusted_utility_path find) || die "no trusted find utility available"
|
|
2910
3529
|
_mm_sort_path=$(trusted_utility_path sort) || die "no trusted sort utility available"
|
|
2911
3530
|
# Symlinks never enter a sealed release: find without -L reports them as
|
|
@@ -2959,27 +3578,48 @@ _MM_SORTED_EOF
|
|
|
2959
3578
|
info "manifest written: $_mm_out ($_mm_count files)"
|
|
2960
3579
|
}
|
|
2961
3580
|
|
|
3581
|
+
# release_bundle_roots <src-root> <shell> [bundle]
|
|
3582
|
+
# Print the seed modules for <shell> from shell/release/bundle-roots, a
|
|
3583
|
+
# tab-separated file of: bundle, shell, space-separated roots ("default" is
|
|
3584
|
+
# the bundle used when none is requested). A bundle with no rows is an error;
|
|
3585
|
+
# a shell with no row has no roots.
|
|
3586
|
+
release_bundle_roots() {
|
|
3587
|
+
_rbr_file="$1/shell/release/bundle-roots"
|
|
3588
|
+
[ -f "$_rbr_file" ] || { printf 'valise: error: missing %s\n' "$_rbr_file" >&2; return 1; }
|
|
3589
|
+
awk -F '\t' -v b="${3:-default}" -v s="$2" '
|
|
3590
|
+
/^[[:space:]]*#/ || NF == 0 { next }
|
|
3591
|
+
$1 == b { known = 1; if ($2 == s && $3 != "") print $3 }
|
|
3592
|
+
END { if (!known) { print "valise: error: unknown bundle: " b > "/dev/stderr"; exit 1 } }
|
|
3593
|
+
' "$_rbr_file"
|
|
3594
|
+
}
|
|
3595
|
+
|
|
2962
3596
|
cmd_compile() {
|
|
2963
3597
|
_out_dir=""
|
|
2964
|
-
_variant="
|
|
3598
|
+
_variant="default"
|
|
2965
3599
|
_bundle=""
|
|
2966
3600
|
_bundle_requested=0
|
|
2967
3601
|
for _arg do
|
|
2968
3602
|
case "$_arg" in
|
|
2969
3603
|
--variant=*) _variant="${_arg#--variant=}" ;;
|
|
2970
3604
|
--bundle=*) _bundle="${_arg#--bundle=}"; _bundle_requested=1 ;;
|
|
2971
|
-
--help|-h)
|
|
2972
|
-
|
|
3605
|
+
--help|-h) usage_compile; return 0 ;;
|
|
3606
|
+
--*) die "unknown compile option: $_arg" ;;
|
|
3607
|
+
*) [ -z "$_out_dir" ] || die "usage: valise compile [--bundle=<name>] [--variant=<name>] <output-dir>"; _out_dir=$_arg ;;
|
|
2973
3608
|
esac
|
|
2974
3609
|
done
|
|
2975
3610
|
[ -n "$_out_dir" ] || die "usage: valise compile [--bundle=<name>] [--variant=<name>] <output-dir>"
|
|
2976
|
-
|
|
3611
|
+
_out_dir=$(path_normalize "$_out_dir")
|
|
3612
|
+
[ "$_out_dir" != "/" ] || die "output directory must not be /"
|
|
3613
|
+
_script_dir=$(valise_script_dir)
|
|
2977
3614
|
_awk_compiler="$_script_dir/../lib/compiler.awk"
|
|
2978
3615
|
[ -f "$_awk_compiler" ] || die "compiler.awk not found at $_awk_compiler"
|
|
2979
3616
|
|
|
2980
3617
|
# Find source tree root (look for shell/release/ relative to script or cwd)
|
|
2981
3618
|
_src_root=""
|
|
2982
|
-
|
|
3619
|
+
# A project invoking the CLI from its root must win over a source tree that
|
|
3620
|
+
# happens to contain the package. Build resolves its prelude from the
|
|
3621
|
+
# working tree, so compile and build must agree on this root.
|
|
3622
|
+
for _candidate in "." "$_script_dir/.." "$_script_dir/../.." "$_script_dir/../../.."; do
|
|
2983
3623
|
if [ -f "$_candidate/shell/release/module-registry.tsv" ]; then
|
|
2984
3624
|
_src_root=$(cd "$_candidate" && pwd -P)
|
|
2985
3625
|
break
|
|
@@ -2988,18 +3628,16 @@ cmd_compile() {
|
|
|
2988
3628
|
[ -n "$_src_root" ] || die "cannot find source tree (need shell/release/module-registry.tsv)"
|
|
2989
3629
|
|
|
2990
3630
|
_registry="$_src_root/shell/release/module-registry.tsv"
|
|
2991
|
-
|
|
2992
|
-
_release_compiler="$_src_root/hack/compile-shell-profile-release.py"
|
|
2993
|
-
[ -f "$_release_compiler" ] || die "release compiler not found: $_release_compiler"
|
|
2994
|
-
_prelude="$_src_root/submodule/valise-sh/lib/capabilities.sh"
|
|
3631
|
+
_prelude="$_script_dir/../lib/capabilities.sh"
|
|
2995
3632
|
_variants_file="$_src_root/shell/release/install-variants"
|
|
3633
|
+
[ -f "$_variants_file" ] || die "install-variants not found: $_variants_file"
|
|
2996
3634
|
_variant_matches=$(awk -F'\t' -v v="$_variant" '$1 == v { count++ } END { print count + 0 }' "$_variants_file")
|
|
2997
3635
|
[ "$_variant_matches" -eq 1 ] || die "unknown or ambiguous variant: $_variant"
|
|
2998
3636
|
# Extract prelude function names
|
|
2999
3637
|
# Extract sp_cap_ functions matching Python SHELL_FUNCTION_DEFINITION regex
|
|
3000
3638
|
# Pattern: ^name() { (file-appearance order, no sorting)
|
|
3001
3639
|
[ -r "$_prelude" ] || die "capabilities prelude not found: $_prelude"
|
|
3002
|
-
_prelude_fns=$(sed -n 's/^\([a-z_][a-zA-Z0-9_]*\)()[[:space:]]*{.*/\1/p' "$_prelude" | grep '^sp_cap_' | tr '\n' ' ')
|
|
3640
|
+
_prelude_fns=$(sed -n 's/^\([a-z_][a-zA-Z0-9_]*\)()[[:space:]]*[({].*/\1/p' "$_prelude" | grep '^sp_cap_' | tr '\n' ' ')
|
|
3003
3641
|
[ -n "$_prelude_fns" ] || die "capabilities prelude contains no sp_cap_ functions"
|
|
3004
3642
|
|
|
3005
3643
|
# Extract exclusion list and validate each exclusion against the identity
|
|
@@ -3010,9 +3648,6 @@ cmd_compile() {
|
|
|
3010
3648
|
if [ -f "$_variants_file" ]; then
|
|
3011
3649
|
_exclude_list=$(awk -F' ' -v v="$_variant" '$1 == v { print $3 }' "$_variants_file")
|
|
3012
3650
|
fi
|
|
3013
|
-
if [ -z "$_exclude_list" ] && [ "$_variant" != "workstation-full" ]; then
|
|
3014
|
-
info "warning: variant $_variant not found in install-variants, using default"
|
|
3015
|
-
fi
|
|
3016
3651
|
for _excluded_module in $_exclude_list; do
|
|
3017
3652
|
_excluded_count=$(awk -F ' ' -v wanted="$_excluded_module" '
|
|
3018
3653
|
NR > 1 {
|
|
@@ -3029,7 +3664,7 @@ cmd_compile() {
|
|
|
3029
3664
|
if [ "$_bundle_requested" -eq 1 ]; then
|
|
3030
3665
|
[ -n "$_bundle" ] || die "bundle name must not be empty"
|
|
3031
3666
|
# Validate bundle identity before creating the output transaction.
|
|
3032
|
-
|
|
3667
|
+
release_bundle_roots "$_src_root" bash "$_bundle" >/dev/null ||
|
|
3033
3668
|
die "failed to resolve bundle roots for bash"
|
|
3034
3669
|
fi
|
|
3035
3670
|
|
|
@@ -3037,11 +3672,8 @@ cmd_compile() {
|
|
|
3037
3672
|
# rejected before any plan is generated, so a failed compile can never leave
|
|
3038
3673
|
# a partially replaced output tree. An empty existing directory is supported
|
|
3039
3674
|
# for callers that pre-create their destination; it is replaced only after
|
|
3040
|
-
# every plan has compiled successfully.
|
|
3041
|
-
#
|
|
3042
|
-
while [ "$_out_dir" != "/" ] && [ "${_out_dir%/}" != "$_out_dir" ]; do
|
|
3043
|
-
_out_dir=${_out_dir%/}
|
|
3044
|
-
done
|
|
3675
|
+
# every plan has compiled successfully. Dot segments and trailing slashes
|
|
3676
|
+
# were normalized above so one destination always has one staging path.
|
|
3045
3677
|
_out_parent=$(path_parent "$_out_dir")
|
|
3046
3678
|
_mkdir_path=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
3047
3679
|
if [ ! -d "$_out_parent" ]; then
|
|
@@ -3063,27 +3695,25 @@ cmd_compile() {
|
|
|
3063
3695
|
_stage=$(trusted_mktemp_at "$_out_parent" dir "$_out_parent/.valise-compile.XXXXXX") ||
|
|
3064
3696
|
die "cannot create compile staging directory"
|
|
3065
3697
|
|
|
3066
|
-
# Fish plans require native Fish syntax;
|
|
3067
|
-
#
|
|
3068
|
-
info "fish
|
|
3069
|
-
for _shell in bash zsh ksh
|
|
3698
|
+
# Fish plans require native Fish syntax; the plan compiler emits POSIX sh
|
|
3699
|
+
# only. A Fish integration uses the native Fish loader instead.
|
|
3700
|
+
info "fish plans are not generated; a Fish session uses the native Fish loader only when the release provides one"
|
|
3701
|
+
for _shell in bash zsh ksh; do
|
|
3070
3702
|
# Extract roots for this shell from JSON using POSIX tools
|
|
3071
3703
|
if [ "$_bundle_requested" -eq 1 ]; then
|
|
3072
|
-
_roots=$(
|
|
3704
|
+
_roots=$(release_bundle_roots "$_src_root" "$_shell" "$_bundle") || die "failed to resolve bundle roots for $_shell"
|
|
3073
3705
|
else
|
|
3074
|
-
_roots=$(
|
|
3706
|
+
_roots=$(release_bundle_roots "$_src_root" "$_shell") || die "failed to resolve bundle roots for $_shell"
|
|
3075
3707
|
fi
|
|
3076
3708
|
|
|
3077
3709
|
[ -n "$_roots" ] || continue
|
|
3078
3710
|
|
|
3079
|
-
# wrapper_lifecycle uses bash shell filtering (matches Python compiler)
|
|
3080
|
-
_effective_shell="$_shell"
|
|
3081
|
-
[ "$_shell" != "wrapper_lifecycle" ] || _effective_shell="bash"
|
|
3082
|
-
|
|
3083
3711
|
_plan_file="installed-modules.$_shell"
|
|
3084
|
-
[ "$_shell" != "wrapper_lifecycle" ] || _plan_file="wrapper-lifecycle.bash"
|
|
3085
3712
|
|
|
3086
|
-
# Filter registry for modules matching this shell and build
|
|
3713
|
+
# Filter registry for modules matching this shell and build the compiler's
|
|
3714
|
+
# input stream. The preprocessor runs to a file first so a validation
|
|
3715
|
+
# failure is not hidden by the following compiler stage.
|
|
3716
|
+
_preprocessed="$_stage/.preprocessed.$_shell"
|
|
3087
3717
|
{
|
|
3088
3718
|
# Filter seeds through exclusion list
|
|
3089
3719
|
_filtered_roots=""
|
|
@@ -3099,6 +3729,8 @@ cmd_compile() {
|
|
|
3099
3729
|
esac
|
|
3100
3730
|
[ "$_excluded" -eq 1 ] && break
|
|
3101
3731
|
done
|
|
3732
|
+
# Bare seed names are project modules, as for exclusions above.
|
|
3733
|
+
case "$_seed" in *@*) ;; *) _seed="$_seed@shell/module.d" ;; esac
|
|
3102
3734
|
[ "$_excluded" -eq 1 ] || _filtered_roots="$_filtered_roots $_seed"
|
|
3103
3735
|
done
|
|
3104
3736
|
_filtered_roots="${_filtered_roots# }"
|
|
@@ -3109,7 +3741,7 @@ cmd_compile() {
|
|
|
3109
3741
|
# Preprocess the registry with source-qualified identities. This keeps
|
|
3110
3742
|
# duplicate module names distinct and gives contribution bodies their
|
|
3111
3743
|
# installed layer prefix.
|
|
3112
|
-
awk -F' ' -v shell="$
|
|
3744
|
+
awk -F' ' -v shell="$_shell" -v excludes="$_exclude_list" '
|
|
3113
3745
|
function shell_matches(shells, wanted, count, i, values) {
|
|
3114
3746
|
if (shells == "any") return 1
|
|
3115
3747
|
count = split(shells, values, " ")
|
|
@@ -3121,25 +3753,86 @@ cmd_compile() {
|
|
|
3121
3753
|
NR == 1 { next }
|
|
3122
3754
|
{
|
|
3123
3755
|
name = $1; msrc = $11
|
|
3124
|
-
if (name == "" ||
|
|
3756
|
+
if (name == "" || !shell_matches($6, shell)) next
|
|
3757
|
+
if (name !~ /^[a-z_][a-zA-Z0-9_]*$/) {
|
|
3758
|
+
print "valise: error: invalid module name in registry: " name > "/dev/stderr"
|
|
3759
|
+
exit 1
|
|
3760
|
+
}
|
|
3761
|
+
if ($7 == "") {
|
|
3762
|
+
print "valise: error: missing platforms for registry module: " name > "/dev/stderr"
|
|
3763
|
+
exit 1
|
|
3764
|
+
}
|
|
3765
|
+
platform_count = split($7, platform_values, " ")
|
|
3766
|
+
for (platform_i = 1; platform_i <= platform_count; platform_i++) {
|
|
3767
|
+
if (platform_values[platform_i] !~ /^(any|darwin|linux|bsd|freebsd|openbsd|netbsd)$/) {
|
|
3768
|
+
print "valise: error: invalid platform for registry module " name ": " platform_values[platform_i] > "/dev/stderr"
|
|
3769
|
+
exit 1
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
if ($3 !~ /^(read-only|path-env|generated-file|update-network|install-time|local-secret|local-state|work-only)$/) {
|
|
3773
|
+
print "valise: error: invalid side_effect for registry module " name ": " $3 > "/dev/stderr"
|
|
3774
|
+
exit 1
|
|
3775
|
+
}
|
|
3776
|
+
if (msrc != "shell/module.d") {
|
|
3777
|
+
if (msrc !~ /^shell\/layers\/[^\/]+\/module\.d$/) {
|
|
3778
|
+
print "valise: error: invalid registry source for module " name ": " msrc > "/dev/stderr"
|
|
3779
|
+
exit 1
|
|
3780
|
+
}
|
|
3781
|
+
layer = msrc
|
|
3782
|
+
sub(/^shell\/layers\//, "", layer)
|
|
3783
|
+
sub(/\/module\.d$/, "", layer)
|
|
3784
|
+
if (layer !~ /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ || layer ~ /\.\./) {
|
|
3785
|
+
print "valise: error: invalid layer source in registry for module " name ": " msrc > "/dev/stderr"
|
|
3786
|
+
exit 1
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
export_count = split($8, export_values, " ")
|
|
3790
|
+
for (export_i = 1; export_i <= export_count; export_i++) {
|
|
3791
|
+
if (export_values[export_i] !~ /^[a-z_][a-zA-Z0-9_]*$/ ||
|
|
3792
|
+
export_values[export_i] ~ /^(case|do|done|elif|else|esac|fi|for|if|in|then|until|while|time)$/) {
|
|
3793
|
+
print "valise: error: invalid export for registry module " name ": " export_values[export_i] > "/dev/stderr"
|
|
3794
|
+
exit 1
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3125
3797
|
identity = name "@" msrc
|
|
3126
3798
|
row_exists[identity] = 1
|
|
3127
3799
|
row_name[identity] = name
|
|
3128
3800
|
row_source[identity] = msrc
|
|
3129
3801
|
row_req[identity] = $4
|
|
3802
|
+
row_opt[identity] = $5
|
|
3130
3803
|
row_load[identity] = $9
|
|
3804
|
+
row_platforms[identity] = $7
|
|
3805
|
+
row_side_effect[identity] = $3
|
|
3806
|
+
row_exports[identity] = $8
|
|
3807
|
+
}
|
|
3808
|
+
function excluded_by_variant(identity, name, source, ei, ex) {
|
|
3809
|
+
# An exclusion names a module identity. A qualified exclusion
|
|
3810
|
+
# (name@source) matches that exact source; a bare exclusion names a
|
|
3811
|
+
# primary module. Both forms apply to every registry source, so a
|
|
3812
|
+
# layer module can be excluded by its qualified identity.
|
|
3813
|
+
for (ei = 1; ei <= n_ex; ei++) {
|
|
3814
|
+
ex = ex_arr[ei]
|
|
3815
|
+
if (ex == "") continue
|
|
3816
|
+
if (index(ex, "@") > 0) {
|
|
3817
|
+
if (ex == identity) return 1
|
|
3818
|
+
} else {
|
|
3819
|
+
if (ex == name && source == "shell/module.d") return 1
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
return 0
|
|
3131
3823
|
}
|
|
3132
3824
|
END {
|
|
3825
|
+
# Excluded modules are reported to the compiler rather than silently
|
|
3826
|
+
# dropped, so an exclusion that breaks a required dependency is
|
|
3827
|
+
# diagnosed by name instead of surfacing as "unknown module".
|
|
3828
|
+
n_ex = 0
|
|
3829
|
+
if (excludes != "") n_ex = split(excludes, ex_arr, " ")
|
|
3133
3830
|
for (identity in row_exists) {
|
|
3134
3831
|
name = row_name[identity]; source = row_source[identity]
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
for (ei = 1; ei <= n_ex; ei++) {
|
|
3139
|
-
if (ex_arr[ei] == name || ex_arr[ei] == identity) { excluded = 1; break }
|
|
3140
|
-
}
|
|
3832
|
+
if (excluded_by_variant(identity, name, source)) {
|
|
3833
|
+
printf "EXCLUDED:%s\t%s\n", identity, name
|
|
3834
|
+
continue
|
|
3141
3835
|
}
|
|
3142
|
-
if (excluded) continue
|
|
3143
3836
|
req = ""
|
|
3144
3837
|
n_req = split(row_req[identity], req_arr, " ")
|
|
3145
3838
|
for (ri = 1; ri <= n_req; ri++) {
|
|
@@ -3156,13 +3849,44 @@ cmd_compile() {
|
|
|
3156
3849
|
}
|
|
3157
3850
|
req = req (req == "" ? "" : " ") resolved
|
|
3158
3851
|
}
|
|
3852
|
+
# An optional dependency that is absent for this shell or removed by
|
|
3853
|
+
# the variant is simply not there; only a present one joins the plan.
|
|
3854
|
+
opt = ""
|
|
3855
|
+
n_opt = split(row_opt[identity], opt_arr, " ")
|
|
3856
|
+
for (ri = 1; ri <= n_opt; ri++) {
|
|
3857
|
+
dep = opt_arr[ri]
|
|
3858
|
+
if (dep == "") continue
|
|
3859
|
+
resolved = ""
|
|
3860
|
+
candidate = dep "@" source
|
|
3861
|
+
if ((candidate in row_exists) && !excluded_by_variant(candidate, dep, source)) {
|
|
3862
|
+
resolved = candidate
|
|
3863
|
+
} else if (!(candidate in row_exists)) {
|
|
3864
|
+
candidate_count = 0
|
|
3865
|
+
for (candidate_key in row_exists) {
|
|
3866
|
+
if (row_name[candidate_key] == dep) { resolved = candidate_key; candidate_count++ }
|
|
3867
|
+
}
|
|
3868
|
+
if (candidate_count != 1 || excluded_by_variant(resolved, dep, row_source[resolved])) resolved = ""
|
|
3869
|
+
}
|
|
3870
|
+
if (resolved != "") opt = opt (opt == "" ? "" : " ") resolved
|
|
3871
|
+
}
|
|
3159
3872
|
load_path = row_load[identity]
|
|
3160
|
-
|
|
3161
|
-
|
|
3873
|
+
# A meta module has no body, so it has no installed path to prefix.
|
|
3874
|
+
if (load_path != "" && source ~ /^shell\/layers\/[^\/]+\/module\.d$/) {
|
|
3875
|
+
layer = source
|
|
3876
|
+
sub(/^shell\/layers\//, "", layer)
|
|
3877
|
+
sub(/\/module\.d$/, "", layer)
|
|
3878
|
+
load_path = "layers/" layer "/" load_path
|
|
3879
|
+
}
|
|
3880
|
+
printf "MOD:%s\t%s\t%s\t%s\t%s\t%s\t%s\n", identity, req, load_path, row_platforms[identity], row_side_effect[identity], row_exports[identity], opt
|
|
3162
3881
|
}
|
|
3163
3882
|
}
|
|
3164
3883
|
' "$_registry"
|
|
3165
|
-
}
|
|
3884
|
+
} >"$_preprocessed" || die "cannot preprocess registry for $_shell"
|
|
3885
|
+
awk -v target_shell="$_shell" -v plan_name="$_shell" -f "$_awk_compiler" \
|
|
3886
|
+
"$_preprocessed" >"$_stage/$_plan_file" || die "cannot compile plan for $_shell"
|
|
3887
|
+
_rm_path=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
3888
|
+
"$_rm_path" -f "$_preprocessed" || die "cannot remove compiler preprocessing file"
|
|
3889
|
+
_preprocessed=""
|
|
3166
3890
|
|
|
3167
3891
|
info "compiled plan for $_shell"
|
|
3168
3892
|
done
|
|
@@ -3182,9 +3906,1011 @@ cmd_compile() {
|
|
|
3182
3906
|
}
|
|
3183
3907
|
|
|
3184
3908
|
|
|
3909
|
+
# registry_generate <project-root> <output-file> [check]
|
|
3910
|
+
# Write the deterministic module registry implied by the project's metadata.
|
|
3911
|
+
# With check=1, compare the generated bytes with output-file and fail on drift.
|
|
3912
|
+
registry_generate() {
|
|
3913
|
+
_rg_root=$1
|
|
3914
|
+
_rg_out=$2
|
|
3915
|
+
_rg_check=${3:-0}
|
|
3916
|
+
export LC_ALL=C
|
|
3917
|
+
_rg_script_dir=$(valise_script_dir) || return 1
|
|
3918
|
+
_rg_metadata="$_rg_script_dir/../lib/metadata.sh"
|
|
3919
|
+
[ -r "$_rg_metadata" ] || die "metadata library not found: $_rg_metadata"
|
|
3920
|
+
_rg_out=$(path_normalize "$_rg_out") || return 1
|
|
3921
|
+
_rg_parent=$(path_parent "$_rg_out")
|
|
3922
|
+
_rg_mkdir=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
3923
|
+
_rg_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
3924
|
+
_rg_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
3925
|
+
_rg_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
3926
|
+
_rg_cmp=$(trusted_utility_path cmp) || die "no trusted cmp utility available"
|
|
3927
|
+
if [ "$_rg_check" = 1 ]; then
|
|
3928
|
+
[ -d "$_rg_parent" ] || die "module registry is missing: $_rg_out"
|
|
3929
|
+
[ -f "$_rg_out" ] && [ ! -L "$_rg_out" ] || die "module registry is missing: $_rg_out"
|
|
3930
|
+
_rg_tmp=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-registry.XXXXXX") ||
|
|
3931
|
+
die "cannot create registry comparison file"
|
|
3932
|
+
else
|
|
3933
|
+
if [ -e "$_rg_out" ] || [ -L "$_rg_out" ]; then
|
|
3934
|
+
[ -f "$_rg_out" ] && [ ! -L "$_rg_out" ] || die "registry output path is not a regular file: $_rg_out"
|
|
3935
|
+
fi
|
|
3936
|
+
[ -d "$_rg_parent" ] || "$_rg_mkdir" -p "$_rg_parent" || die "cannot create registry output parent: $_rg_parent"
|
|
3937
|
+
_rg_tmp=$(trusted_mktemp_at "$_rg_parent" file "$_rg_parent/.valise-registry.XXXXXX") ||
|
|
3938
|
+
die "cannot create registry staging file"
|
|
3939
|
+
fi
|
|
3940
|
+
(
|
|
3941
|
+
# shellcheck disable=SC1090
|
|
3942
|
+
. "$_rg_metadata"
|
|
3943
|
+
printf 'name\tversion\tside_effect\trequires\toptional\tshells\tplatforms\texports\tload\tupdates\tsource\n'
|
|
3944
|
+
for _rg_meta in "$_rg_root"/shell/module.d/*.meta "$_rg_root"/shell/layers/*/module.d/*.meta; do
|
|
3945
|
+
[ -f "$_rg_meta" ] || continue
|
|
3946
|
+
source_path_is_physical "$_rg_root" "${_rg_meta#"$_rg_root"/}" || {
|
|
3947
|
+
printf 'valise: error: registry metadata path contains a symlink: %s\n' "${_rg_meta#"$_rg_root"/}" >&2
|
|
3948
|
+
exit 1
|
|
3949
|
+
}
|
|
3950
|
+
_rg_rel=${_rg_meta#"$_rg_root"/}
|
|
3951
|
+
_rg_source=${_rg_rel%/*}
|
|
3952
|
+
case "$_rg_source" in
|
|
3953
|
+
shell/module.d) ;;
|
|
3954
|
+
shell/layers/*/module.d)
|
|
3955
|
+
_rg_layer=${_rg_source#shell/layers/}
|
|
3956
|
+
_rg_layer=${_rg_layer%/module.d}
|
|
3957
|
+
case "$_rg_layer" in
|
|
3958
|
+
[A-Za-z0-9_]*) ;;
|
|
3959
|
+
*)
|
|
3960
|
+
printf 'valise: error: invalid layer source for registry: %s\n' "$_rg_rel" >&2
|
|
3961
|
+
exit 1
|
|
3962
|
+
;;
|
|
3963
|
+
esac
|
|
3964
|
+
case "$_rg_layer" in
|
|
3965
|
+
*/*|*..*|*[!A-Za-z0-9_.-]*)
|
|
3966
|
+
printf 'valise: error: invalid layer source for registry: %s\n' "$_rg_rel" >&2
|
|
3967
|
+
exit 1
|
|
3968
|
+
;;
|
|
3969
|
+
esac
|
|
3970
|
+
;;
|
|
3971
|
+
*)
|
|
3972
|
+
printf 'valise: error: unsupported metadata source for registry: %s\n' "$_rg_rel" >&2
|
|
3973
|
+
exit 1
|
|
3974
|
+
;;
|
|
3975
|
+
esac
|
|
3976
|
+
_rg_record=$(sp_metadata_read "$_rg_meta") || {
|
|
3977
|
+
printf 'valise: error: %s: %s\n' "$_rg_rel" "${_rg_record:-invalid metadata}" >&2
|
|
3978
|
+
exit 1
|
|
3979
|
+
}
|
|
3980
|
+
_rg_name=
|
|
3981
|
+
_rg_version=
|
|
3982
|
+
_rg_side_effect=
|
|
3983
|
+
_rg_requires=
|
|
3984
|
+
_rg_optional=
|
|
3985
|
+
_rg_shells=
|
|
3986
|
+
_rg_platforms=
|
|
3987
|
+
_rg_exports=
|
|
3988
|
+
_rg_load=
|
|
3989
|
+
_rg_updates=
|
|
3990
|
+
while IFS= read -r _rg_line; do
|
|
3991
|
+
case "$_rg_line" in
|
|
3992
|
+
name=*) _rg_name=${_rg_line#name=} ;;
|
|
3993
|
+
version=*) _rg_version=${_rg_line#version=} ;;
|
|
3994
|
+
side_effect=*) _rg_side_effect=${_rg_line#side_effect=} ;;
|
|
3995
|
+
requires=*) _rg_requires=${_rg_line#requires=} ;;
|
|
3996
|
+
optional=*) _rg_optional=${_rg_line#optional=} ;;
|
|
3997
|
+
shells=*) _rg_shells=${_rg_line#shells=} ;;
|
|
3998
|
+
platforms=*) _rg_platforms=${_rg_line#platforms=} ;;
|
|
3999
|
+
exports=*) _rg_exports=${_rg_line#exports=} ;;
|
|
4000
|
+
load=*) _rg_load=${_rg_line#load=} ;;
|
|
4001
|
+
updates=*) _rg_updates=${_rg_line#updates=} ;;
|
|
4002
|
+
esac
|
|
4003
|
+
done <<EOF
|
|
4004
|
+
$_rg_record
|
|
4005
|
+
EOF
|
|
4006
|
+
_rg_file_name=${_rg_meta##*/}
|
|
4007
|
+
_rg_file_name=${_rg_file_name%.meta}
|
|
4008
|
+
if [ "$_rg_name" != "$_rg_file_name" ]; then
|
|
4009
|
+
printf 'valise: error: %s: metadata name %s does not match file name %s\n' "$_rg_rel" "$_rg_name" "$_rg_file_name" >&2
|
|
4010
|
+
exit 1
|
|
4011
|
+
fi
|
|
4012
|
+
if [ "$_rg_source" != "shell/module.d" ]; then
|
|
4013
|
+
case "$_rg_name" in
|
|
4014
|
+
contrib_*) ;;
|
|
4015
|
+
*)
|
|
4016
|
+
printf 'valise: error: %s: layer module name %s must use the contrib_ prefix\n' "$_rg_rel" "$_rg_name" >&2
|
|
4017
|
+
exit 1
|
|
4018
|
+
;;
|
|
4019
|
+
esac
|
|
4020
|
+
fi
|
|
4021
|
+
if [ -n "$_rg_load" ]; then
|
|
4022
|
+
_rg_module_root=${_rg_source%/module.d}
|
|
4023
|
+
if ! source_path_is_physical "$_rg_root" "$_rg_module_root/$_rg_load"; then
|
|
4024
|
+
printf 'valise: error: %s: load path contains a symlink: %s\n' "$_rg_rel" "$_rg_load" >&2
|
|
4025
|
+
exit 1
|
|
4026
|
+
fi
|
|
4027
|
+
if [ ! -f "$_rg_root/$_rg_module_root/$_rg_load" ]; then
|
|
4028
|
+
printf 'valise: error: %s: load path does not exist: %s\n' "$_rg_rel" "$_rg_load" >&2
|
|
4029
|
+
exit 1
|
|
4030
|
+
fi
|
|
4031
|
+
fi
|
|
4032
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
4033
|
+
"$_rg_name" "$_rg_version" "$_rg_side_effect" "$_rg_requires" \
|
|
4034
|
+
"$_rg_optional" "$_rg_shells" "$_rg_platforms" "$_rg_exports" \
|
|
4035
|
+
"$_rg_load" "$_rg_updates" "$_rg_source"
|
|
4036
|
+
done
|
|
4037
|
+
) >"$_rg_tmp" || {
|
|
4038
|
+
"$_rg_rm" -f "$_rg_tmp" 2>/dev/null || :
|
|
4039
|
+
die "cannot generate module registry"
|
|
4040
|
+
}
|
|
4041
|
+
"$_rg_chmod" 0644 "$_rg_tmp" || {
|
|
4042
|
+
"$_rg_rm" -f "$_rg_tmp" 2>/dev/null || :
|
|
4043
|
+
die "cannot set registry file mode"
|
|
4044
|
+
}
|
|
4045
|
+
if [ "$_rg_check" = 1 ]; then
|
|
4046
|
+
if [ ! -f "$_rg_out" ]; then
|
|
4047
|
+
"$_rg_rm" -f "$_rg_tmp" 2>/dev/null || :
|
|
4048
|
+
die "module registry is missing: $_rg_out"
|
|
4049
|
+
fi
|
|
4050
|
+
if ! "$_rg_cmp" -s "$_rg_tmp" "$_rg_out"; then
|
|
4051
|
+
"$_rg_rm" -f "$_rg_tmp" 2>/dev/null || :
|
|
4052
|
+
die "module registry is out of date: $_rg_out"
|
|
4053
|
+
fi
|
|
4054
|
+
"$_rg_rm" -f "$_rg_tmp" 2>/dev/null || :
|
|
4055
|
+
_rg_tmp=""
|
|
4056
|
+
return 0
|
|
4057
|
+
fi
|
|
4058
|
+
"$_rg_mv" "$_rg_tmp" "$_rg_out" || {
|
|
4059
|
+
"$_rg_rm" -f "$_rg_tmp" 2>/dev/null || :
|
|
4060
|
+
die "cannot publish module registry"
|
|
4061
|
+
}
|
|
4062
|
+
_rg_tmp=""
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
cmd_registry() {
|
|
4066
|
+
_registry_out=""
|
|
4067
|
+
_registry_check=0
|
|
4068
|
+
for _registry_arg do
|
|
4069
|
+
case "$_registry_arg" in
|
|
4070
|
+
--check) _registry_check=1 ;;
|
|
4071
|
+
--help|-h) usage_registry; return 0 ;;
|
|
4072
|
+
--*) die "unknown registry option: $_registry_arg" ;;
|
|
4073
|
+
*) [ -z "$_registry_out" ] || die "usage: valise registry [--check] [registry-path]"; _registry_out=$_registry_arg ;;
|
|
4074
|
+
esac
|
|
4075
|
+
done
|
|
4076
|
+
_registry_root=$(pwd -P) || die "cannot resolve working directory"
|
|
4077
|
+
[ -d "$_registry_root/shell" ] || die "run from the consuming project root (need shell/)"
|
|
4078
|
+
[ -n "$_registry_out" ] || _registry_out="$_registry_root/shell/release/module-registry.tsv"
|
|
4079
|
+
registry_generate "$_registry_root" "$_registry_out" "$_registry_check"
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
|
|
4083
|
+
project_config_load() {
|
|
4084
|
+
_pc_root=$(pwd -P) || die "cannot resolve working directory"
|
|
4085
|
+
[ -d "$_pc_root/shell/module.d" ] || die "run from the consuming project root (need shell/module.d)"
|
|
4086
|
+
_project_root=$_pc_root
|
|
4087
|
+
_project_variant=default
|
|
4088
|
+
_project_bundle=default
|
|
4089
|
+
[ -n "${HOME:-}" ] || die "HOME is required for project commands"
|
|
4090
|
+
_project_conf="$_project_root/shell/release/project.conf"
|
|
4091
|
+
if [ -f "$_project_conf" ]; then
|
|
4092
|
+
[ ! -L "$_project_conf" ] || die "project.conf must not be a symlink: $_project_conf"
|
|
4093
|
+
while IFS= read -r _pc_line || [ -n "$_pc_line" ]; do
|
|
4094
|
+
case "$_pc_line" in
|
|
4095
|
+
'' | '#'*) continue ;;
|
|
4096
|
+
variant=*) _project_variant=${_pc_line#variant=} ;;
|
|
4097
|
+
bundle=*) _project_bundle=${_pc_line#bundle=} ;;
|
|
4098
|
+
*) die "invalid project.conf line: $_pc_line" ;;
|
|
4099
|
+
esac
|
|
4100
|
+
done <"$_project_conf"
|
|
4101
|
+
fi
|
|
4102
|
+
[ -n "$_project_variant" ] || die "project variant must not be empty"
|
|
4103
|
+
[ -n "$_project_bundle" ] || die "project bundle must not be empty"
|
|
4104
|
+
unset _pc_root _pc_line _project_conf
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
project_meta_add_require() {
|
|
4108
|
+
_pm_meta=$1
|
|
4109
|
+
_pm_module=$2
|
|
4110
|
+
[ -f "$_pm_meta" ] && [ ! -L "$_pm_meta" ] || die "seed metadata file is missing or unsafe: $_pm_meta"
|
|
4111
|
+
_pm_parent=$(path_parent "$_pm_meta")
|
|
4112
|
+
_pm_tmp=$(trusted_mktemp_at "$_pm_parent" file "$_pm_parent/.valise-meta.XXXXXX") ||
|
|
4113
|
+
die "cannot create seed metadata staging file"
|
|
4114
|
+
_pm_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4115
|
+
"$_pm_awk" -v module="$_pm_module" '
|
|
4116
|
+
BEGIN { have = 0 }
|
|
4117
|
+
/^requires=/ {
|
|
4118
|
+
value = substr($0, 10)
|
|
4119
|
+
count = split(value, parts, " ")
|
|
4120
|
+
output = ""
|
|
4121
|
+
seen = 0
|
|
4122
|
+
for (i = 1; i <= count; i++) {
|
|
4123
|
+
if (parts[i] == "") continue
|
|
4124
|
+
if (parts[i] == module) seen = 1
|
|
4125
|
+
output = output (output == "" ? "" : " ") parts[i]
|
|
4126
|
+
}
|
|
4127
|
+
if (!seen) output = output (output == "" ? "" : " ") module
|
|
4128
|
+
print "requires=" output
|
|
4129
|
+
have = 1
|
|
4130
|
+
next
|
|
4131
|
+
}
|
|
4132
|
+
{ print }
|
|
4133
|
+
END { if (!have) print "requires=" module }
|
|
4134
|
+
' "$_pm_meta" >"$_pm_tmp" || {
|
|
4135
|
+
_pm_rm=$(trusted_utility_path rm) || _pm_rm=""
|
|
4136
|
+
[ -z "$_pm_rm" ] || "$_pm_rm" -f "$_pm_tmp" 2>/dev/null || :
|
|
4137
|
+
die "cannot update seed metadata: $_pm_meta"
|
|
4138
|
+
}
|
|
4139
|
+
_pm_mode=$(file_mode "$_pm_meta")
|
|
4140
|
+
[ -n "$_pm_mode" ] || die "cannot read seed metadata mode: $_pm_meta"
|
|
4141
|
+
_pm_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
4142
|
+
"$_pm_chmod" "$_pm_mode" "$_pm_tmp" || die "cannot preserve seed metadata mode"
|
|
4143
|
+
_pm_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
4144
|
+
"$_pm_mv" "$_pm_tmp" "$_pm_meta" || die "cannot publish seed metadata: $_pm_meta"
|
|
4145
|
+
_pm_tmp=""
|
|
4146
|
+
unset _pm_meta _pm_module _pm_parent _pm_awk _pm_mode _pm_chmod _pm_mv _pm_rm
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
project_meta_remove_module() {
|
|
4150
|
+
_pm_meta=$1
|
|
4151
|
+
_pm_module=$2
|
|
4152
|
+
[ -f "$_pm_meta" ] && [ ! -L "$_pm_meta" ] || die "metadata file is missing or unsafe: $_pm_meta"
|
|
4153
|
+
_pm_parent=$(path_parent "$_pm_meta")
|
|
4154
|
+
_pm_tmp=$(trusted_mktemp_at "$_pm_parent" file "$_pm_parent/.valise-meta.XXXXXX") ||
|
|
4155
|
+
die "cannot create metadata staging file"
|
|
4156
|
+
_pm_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4157
|
+
"$_pm_awk" -v module="$_pm_module" '
|
|
4158
|
+
/^requires=/ {
|
|
4159
|
+
value = substr($0, 10)
|
|
4160
|
+
count = split(value, parts, " ")
|
|
4161
|
+
output = ""
|
|
4162
|
+
for (i = 1; i <= count; i++) {
|
|
4163
|
+
if (parts[i] == "" || parts[i] == module) continue
|
|
4164
|
+
output = output (output == "" ? "" : " ") parts[i]
|
|
4165
|
+
}
|
|
4166
|
+
print "requires=" output
|
|
4167
|
+
next
|
|
4168
|
+
}
|
|
4169
|
+
/^optional=/ {
|
|
4170
|
+
value = substr($0, 10)
|
|
4171
|
+
count = split(value, parts, " ")
|
|
4172
|
+
output = ""
|
|
4173
|
+
for (i = 1; i <= count; i++) {
|
|
4174
|
+
if (parts[i] == "" || parts[i] == module) continue
|
|
4175
|
+
output = output (output == "" ? "" : " ") parts[i]
|
|
4176
|
+
}
|
|
4177
|
+
print "optional=" output
|
|
4178
|
+
next
|
|
4179
|
+
}
|
|
4180
|
+
{ print }
|
|
4181
|
+
' "$_pm_meta" >"$_pm_tmp" || {
|
|
4182
|
+
_pm_rm=$(trusted_utility_path rm) || _pm_rm=""
|
|
4183
|
+
[ -z "$_pm_rm" ] || "$_pm_rm" -f "$_pm_tmp" 2>/dev/null || :
|
|
4184
|
+
die "cannot update metadata: $_pm_meta"
|
|
4185
|
+
}
|
|
4186
|
+
_pm_mode=$(file_mode "$_pm_meta")
|
|
4187
|
+
[ -n "$_pm_mode" ] || die "cannot read metadata mode: $_pm_meta"
|
|
4188
|
+
_pm_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
4189
|
+
"$_pm_chmod" "$_pm_mode" "$_pm_tmp" || die "cannot preserve metadata mode"
|
|
4190
|
+
_pm_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
4191
|
+
"$_pm_mv" "$_pm_tmp" "$_pm_meta" || die "cannot publish metadata: $_pm_meta"
|
|
4192
|
+
_pm_tmp=""
|
|
4193
|
+
unset _pm_meta _pm_module _pm_parent _pm_awk _pm_mode _pm_chmod _pm_mv _pm_rm
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
# project_meta_loses_all_targets <meta> <module>
|
|
4197
|
+
# Succeed when removing module from requires= would leave no load target at all.
|
|
4198
|
+
project_meta_loses_all_targets() {
|
|
4199
|
+
_pl_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4200
|
+
"$_pl_awk" -v module="$2" '
|
|
4201
|
+
/^load=./ { target = 1 }
|
|
4202
|
+
/^requires=/ {
|
|
4203
|
+
count = split(substr($0, 10), parts, " ")
|
|
4204
|
+
for (i = 1; i <= count; i++) {
|
|
4205
|
+
if (parts[i] == module) named = 1
|
|
4206
|
+
else if (parts[i] != "") other = 1
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
END { exit (named && !other && !target) ? 0 : 1 }
|
|
4210
|
+
' "$1"
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
# project_release_filter <file> <column> <module> <source>
|
|
4214
|
+
# Print a release file without the module's names in the given column. Bare names
|
|
4215
|
+
# in these files mean primary modules; a layer module is named module@source.
|
|
4216
|
+
# Exit 3 when a row that named the module is left with no names.
|
|
4217
|
+
project_release_filter() {
|
|
4218
|
+
_rf_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4219
|
+
"$_rf_awk" -F '\t' -v OFS='\t' -v column="$2" -v module="$3" -v source="$4" '
|
|
4220
|
+
/^[[:space:]]*#/ || NF < column { print; next }
|
|
4221
|
+
{
|
|
4222
|
+
count = split($column, parts, " ")
|
|
4223
|
+
output = ""
|
|
4224
|
+
hit = 0
|
|
4225
|
+
for (i = 1; i <= count; i++) {
|
|
4226
|
+
if (parts[i] == "") continue
|
|
4227
|
+
if (parts[i] == module "@" source || (source == "shell/module.d" && parts[i] == module)) {
|
|
4228
|
+
hit = 1
|
|
4229
|
+
continue
|
|
4230
|
+
}
|
|
4231
|
+
output = output (output == "" ? "" : " ") parts[i]
|
|
4232
|
+
}
|
|
4233
|
+
if (hit) {
|
|
4234
|
+
$column = output
|
|
4235
|
+
if (output == "") emptied = 1
|
|
4236
|
+
}
|
|
4237
|
+
print
|
|
4238
|
+
}
|
|
4239
|
+
END { if (emptied) exit 3 }
|
|
4240
|
+
' "$1"
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
# project_release_strip <file> <column> <module> <source>
|
|
4244
|
+
project_release_strip() {
|
|
4245
|
+
[ -f "$1" ] || return 0
|
|
4246
|
+
_rs_parent=$(path_parent "$1")
|
|
4247
|
+
_rr_tmp=$(trusted_mktemp_at "$_rs_parent" file "$_rs_parent/.valise-release.XXXXXX") ||
|
|
4248
|
+
die "cannot create release file staging file"
|
|
4249
|
+
_rs_status=0
|
|
4250
|
+
project_release_filter "$1" "$2" "$3" "$4" >"$_rr_tmp" || _rs_status=$?
|
|
4251
|
+
case "$_rs_status" in
|
|
4252
|
+
0 | 3) ;;
|
|
4253
|
+
*) die "cannot update release file: $1" ;;
|
|
4254
|
+
esac
|
|
4255
|
+
_rs_mode=$(file_mode "$1")
|
|
4256
|
+
[ -n "$_rs_mode" ] || die "cannot read release file mode: $1"
|
|
4257
|
+
_rs_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
4258
|
+
"$_rs_chmod" "$_rs_mode" "$_rr_tmp" || die "cannot preserve release file mode"
|
|
4259
|
+
_rs_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
4260
|
+
"$_rs_mv" "$_rr_tmp" "$1" || die "cannot publish release file: $1"
|
|
4261
|
+
_rr_tmp=""
|
|
4262
|
+
unset _rs_parent _rs_status _rs_mode _rs_chmod _rs_mv
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
# project_write_integration <file> <block>
|
|
4266
|
+
# Replace the valise managed block in place, or append one. A symlink belongs to
|
|
4267
|
+
# whatever manages it, so it is reported and left alone. Unpaired markers mean the
|
|
4268
|
+
# file was edited by hand: rewriting could delete the user's lines, so refuse.
|
|
4269
|
+
project_write_integration() {
|
|
4270
|
+
_pi_target=$1
|
|
4271
|
+
_pi_snippet=$2
|
|
4272
|
+
if [ -L "$_pi_target" ]; then
|
|
4273
|
+
info "warning: $_pi_target is a symlink; add the output of 'valise init' to it yourself"
|
|
4274
|
+
unset _pi_target _pi_snippet
|
|
4275
|
+
return 0
|
|
4276
|
+
fi
|
|
4277
|
+
_pi_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4278
|
+
if [ -f "$_pi_target" ]; then
|
|
4279
|
+
"$_pi_awk" '
|
|
4280
|
+
BEGIN { open = 0; bad = 0 }
|
|
4281
|
+
/^# BEGIN valise managed block$/ { if (open) bad = 1; open = 1; next }
|
|
4282
|
+
/^# END valise managed block$/ { if (!open) bad = 1; open = 0; next }
|
|
4283
|
+
END { exit (bad || open) ? 1 : 0 }
|
|
4284
|
+
' "$_pi_target" ||
|
|
4285
|
+
die "unpaired valise managed block markers in $_pi_target (restore or remove the BEGIN and END lines, then retry)"
|
|
4286
|
+
elif [ -e "$_pi_target" ]; then
|
|
4287
|
+
die "startup path is not a regular file: $_pi_target"
|
|
4288
|
+
fi
|
|
4289
|
+
_pi_parent=$(path_parent "$_pi_target")
|
|
4290
|
+
_pi_tmp=$(trusted_mktemp_at "$_pi_parent" file "$_pi_parent/.valise-integration.XXXXXX") ||
|
|
4291
|
+
die "cannot create integration staging file"
|
|
4292
|
+
if [ -f "$_pi_target" ]; then
|
|
4293
|
+
# The block travels through ENVIRON: awk -v would interpret its backslashes.
|
|
4294
|
+
_VALISE_BLOCK=$_pi_snippet "$_pi_awk" '
|
|
4295
|
+
/^# BEGIN valise managed block$/ {
|
|
4296
|
+
if (!written) print ENVIRON["_VALISE_BLOCK"]
|
|
4297
|
+
written = 1
|
|
4298
|
+
skip = 1
|
|
4299
|
+
next
|
|
4300
|
+
}
|
|
4301
|
+
/^# END valise managed block$/ { skip = 0; next }
|
|
4302
|
+
!skip { print; last = $0; lines++ }
|
|
4303
|
+
END {
|
|
4304
|
+
if (!written) {
|
|
4305
|
+
if (lines > 0 && last != "") print ""
|
|
4306
|
+
print ENVIRON["_VALISE_BLOCK"]
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
' "$_pi_target" >"$_pi_tmp" || die "cannot read startup file: $_pi_target"
|
|
4310
|
+
_pi_cmp=$(trusted_utility_path cmp) || die "no trusted cmp utility available"
|
|
4311
|
+
if "$_pi_cmp" -s "$_pi_tmp" "$_pi_target"; then
|
|
4312
|
+
_pi_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
4313
|
+
"$_pi_rm" -f "$_pi_tmp" || die "cannot remove unchanged integration staging file"
|
|
4314
|
+
_pi_tmp=""
|
|
4315
|
+
info "shell integration current: $_pi_target"
|
|
4316
|
+
unset _pi_target _pi_snippet _pi_parent _pi_awk _pi_cmp _pi_rm
|
|
4317
|
+
return 0
|
|
4318
|
+
fi
|
|
4319
|
+
_pi_mode=$(file_mode "$_pi_target")
|
|
4320
|
+
[ -n "$_pi_mode" ] || die "cannot read startup file mode: $_pi_target"
|
|
4321
|
+
else
|
|
4322
|
+
printf '%s\n' "$_pi_snippet" >"$_pi_tmp" || die "cannot write integration staging file"
|
|
4323
|
+
_pi_mode=0644
|
|
4324
|
+
fi
|
|
4325
|
+
_pi_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
4326
|
+
"$_pi_chmod" "$_pi_mode" "$_pi_tmp" || die "cannot set startup file mode"
|
|
4327
|
+
_pi_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
4328
|
+
"$_pi_mv" "$_pi_tmp" "$_pi_target" || die "cannot publish startup file: $_pi_target"
|
|
4329
|
+
_pi_tmp=""
|
|
4330
|
+
info "shell integration written: $_pi_target"
|
|
4331
|
+
unset _pi_target _pi_snippet _pi_parent _pi_awk _pi_cmp _pi_rm _pi_mode _pi_chmod _pi_mv
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
cmd_new() {
|
|
4335
|
+
_new_target=
|
|
4336
|
+
for _new_arg do
|
|
4337
|
+
case "$_new_arg" in
|
|
4338
|
+
--help|-h) usage_new; return 0 ;;
|
|
4339
|
+
--*) die "unknown new option: $_new_arg" ;;
|
|
4340
|
+
*) [ -z "$_new_target" ] || die "usage: valise new [dir]"; _new_target=$_new_arg ;;
|
|
4341
|
+
esac
|
|
4342
|
+
done
|
|
4343
|
+
[ -n "$_new_target" ] || _new_target=.
|
|
4344
|
+
_new_target=$(path_normalize "$_new_target") || die "cannot resolve new project path"
|
|
4345
|
+
[ "$_new_target" != "/" ] || die "new project path must not be /"
|
|
4346
|
+
if [ -e "$_new_target" ] || [ -L "$_new_target" ]; then
|
|
4347
|
+
[ -d "$_new_target" ] && [ ! -L "$_new_target" ] || die "new project path is not a directory: $_new_target"
|
|
4348
|
+
for _new_entry in "$_new_target"/* "$_new_target"/.[!.]* "$_new_target"/..?*; do
|
|
4349
|
+
if [ -e "$_new_entry" ] || [ -L "$_new_entry" ]; then
|
|
4350
|
+
die "new project directory must be empty: $_new_target"
|
|
4351
|
+
fi
|
|
4352
|
+
done
|
|
4353
|
+
fi
|
|
4354
|
+
_new_mkdir=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
4355
|
+
"$_new_mkdir" -p "$_new_target/shell/module.d" "$_new_target/shell/modules" "$_new_target/shell/release" ||
|
|
4356
|
+
die "cannot create project directories: $_new_target"
|
|
4357
|
+
printf '%s\n' \
|
|
4358
|
+
'name=profile_main' \
|
|
4359
|
+
'version=' \
|
|
4360
|
+
'side_effect=read-only' \
|
|
4361
|
+
'requires=' \
|
|
4362
|
+
'optional=' \
|
|
4363
|
+
'shells=bash zsh ksh' \
|
|
4364
|
+
'platforms=any' \
|
|
4365
|
+
'exports=profile_main_loaded' \
|
|
4366
|
+
'updates=' \
|
|
4367
|
+
'load=modules/profile_main.sh' \
|
|
4368
|
+
>"$_new_target/shell/module.d/profile_main.meta"
|
|
4369
|
+
printf '%s\n' \
|
|
4370
|
+
'# Shared project initialization. Add shell functions or exports here.' \
|
|
4371
|
+
'profile_main_loaded() { :; }' \
|
|
4372
|
+
>"$_new_target/shell/modules/profile_main.sh"
|
|
4373
|
+
printf 'variant_name\tdescription\texcluded_modules\ndefault\tAll project modules\t\n' \
|
|
4374
|
+
>"$_new_target/shell/release/install-variants"
|
|
4375
|
+
printf '# bundle\tshell\troots\ndefault\tbash\tprofile_main\ndefault\tzsh\tprofile_main\ndefault\tksh\tprofile_main\n' \
|
|
4376
|
+
>"$_new_target/shell/release/bundle-roots"
|
|
4377
|
+
printf 'variant=default\nbundle=default\n' \
|
|
4378
|
+
>"$_new_target/shell/release/project.conf"
|
|
4379
|
+
printf '%s\n' \
|
|
4380
|
+
"created valise project: $_new_target" \
|
|
4381
|
+
"next steps:" \
|
|
4382
|
+
" cd ${_new_target#"$(pwd -P)"/}" \
|
|
4383
|
+
" valise module new hello --exports=hello" \
|
|
4384
|
+
" valise apply"
|
|
4385
|
+
unset _new_target _new_arg _new_entry _new_mkdir
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
cmd_module() {
|
|
4389
|
+
_module_subcmd=${1:-}
|
|
4390
|
+
[ -n "$_module_subcmd" ] || die "usage: valise module <new|list|remove> [args]"
|
|
4391
|
+
case "$_module_subcmd" in
|
|
4392
|
+
--help|-h) usage_module; return 0 ;;
|
|
4393
|
+
new)
|
|
4394
|
+
shift
|
|
4395
|
+
_module_name=
|
|
4396
|
+
_module_exports=
|
|
4397
|
+
for _module_arg do
|
|
4398
|
+
case "$_module_arg" in
|
|
4399
|
+
--exports=*) _module_exports=${_module_arg#--exports=} ;;
|
|
4400
|
+
--help|-h) usage_module; return 0 ;;
|
|
4401
|
+
--*) die "unknown module new option: $_module_arg" ;;
|
|
4402
|
+
*) [ -z "$_module_name" ] || die "usage: valise module new <name> [--exports=<list>]"; _module_name=$_module_arg ;;
|
|
4403
|
+
esac
|
|
4404
|
+
done
|
|
4405
|
+
[ -n "$_module_name" ] || die "module new requires a module name"
|
|
4406
|
+
case "$_module_name" in
|
|
4407
|
+
[abcdefghijklmnopqrstuvwxyz_]*) ;;
|
|
4408
|
+
*) die "invalid module name: $_module_name" ;;
|
|
4409
|
+
esac
|
|
4410
|
+
case "$_module_name" in
|
|
4411
|
+
*[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_]*) die "invalid module name: $_module_name" ;;
|
|
4412
|
+
esac
|
|
4413
|
+
project_config_load
|
|
4414
|
+
_module_seed="$_project_root/shell/module.d/profile_main.meta"
|
|
4415
|
+
[ -f "$_module_seed" ] && [ ! -L "$_module_seed" ] ||
|
|
4416
|
+
die "seed metadata file is missing or unsafe: $_module_seed"
|
|
4417
|
+
[ -d "$_project_root/shell/modules" ] ||
|
|
4418
|
+
die "module body directory is missing: $_project_root/shell/modules"
|
|
4419
|
+
[ -w "$_project_root/shell/module.d" ] && [ -w "$_project_root/shell/modules" ] ||
|
|
4420
|
+
die "module directories are not writable: $_project_root/shell"
|
|
4421
|
+
_module_meta="$_project_root/shell/module.d/$_module_name.meta"
|
|
4422
|
+
_module_body="$_project_root/shell/modules/$_module_name.sh"
|
|
4423
|
+
[ ! -e "$_module_meta" ] && [ ! -L "$_module_meta" ] || die "module metadata already exists: $_module_name"
|
|
4424
|
+
[ ! -e "$_module_body" ] && [ ! -L "$_module_body" ] || die "module body already exists: $_module_name"
|
|
4425
|
+
_module_export_words=
|
|
4426
|
+
if [ -n "$_module_exports" ]; then
|
|
4427
|
+
_module_old_ifs=${IFS-}
|
|
4428
|
+
IFS=', '
|
|
4429
|
+
for _module_export in $_module_exports; do
|
|
4430
|
+
[ -n "$_module_export" ] || continue
|
|
4431
|
+
case "$_module_export" in
|
|
4432
|
+
[abcdefghijklmnopqrstuvwxyz_]*) ;;
|
|
4433
|
+
*) die "invalid export name: $_module_export" ;;
|
|
4434
|
+
esac
|
|
4435
|
+
case "$_module_export" in
|
|
4436
|
+
*[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_]*) die "invalid export name: $_module_export" ;;
|
|
4437
|
+
esac
|
|
4438
|
+
case "$_module_export" in
|
|
4439
|
+
case|do|done|elif|else|esac|fi|for|if|in|then|until|while|time)
|
|
4440
|
+
die "invalid export name (reserved word): $_module_export"
|
|
4441
|
+
;;
|
|
4442
|
+
esac
|
|
4443
|
+
_module_export_words="$_module_export_words $_module_export"
|
|
4444
|
+
done
|
|
4445
|
+
if [ "${_module_old_ifs+x}" = x ]; then
|
|
4446
|
+
IFS=$_module_old_ifs
|
|
4447
|
+
else
|
|
4448
|
+
unset IFS
|
|
4449
|
+
fi
|
|
4450
|
+
fi
|
|
4451
|
+
_module_export_words=${_module_export_words# }
|
|
4452
|
+
_module_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4453
|
+
for _module_export in $_module_export_words; do
|
|
4454
|
+
for _module_candidate in "$_project_root"/shell/module.d/*.meta "$_project_root"/shell/layers/*/module.d/*.meta; do
|
|
4455
|
+
[ -f "$_module_candidate" ] || continue
|
|
4456
|
+
if "$_module_awk" -F= -v wanted="$_module_export" '
|
|
4457
|
+
$1 == "exports" {
|
|
4458
|
+
count = split(substr($0, length($1) + 2), words, " ")
|
|
4459
|
+
for (i = 1; i <= count; i++) {
|
|
4460
|
+
if (words[i] == wanted) found = 1
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
END { exit found ? 0 : 1 }
|
|
4464
|
+
' "$_module_candidate" >/dev/null 2>&1; then
|
|
4465
|
+
die "export already exists in another module: $_module_export"
|
|
4466
|
+
fi
|
|
4467
|
+
done
|
|
4468
|
+
done
|
|
4469
|
+
printf '%s\n' \
|
|
4470
|
+
"name=$_module_name" \
|
|
4471
|
+
'version=' \
|
|
4472
|
+
'side_effect=read-only' \
|
|
4473
|
+
'requires=' \
|
|
4474
|
+
'optional=' \
|
|
4475
|
+
'shells=sh' \
|
|
4476
|
+
'platforms=any' \
|
|
4477
|
+
"exports=$_module_export_words" \
|
|
4478
|
+
'updates=' \
|
|
4479
|
+
"load=modules/$_module_name.sh" \
|
|
4480
|
+
>"$_module_meta"
|
|
4481
|
+
{
|
|
4482
|
+
printf '%s\n' "# Edit this module body; valise apply reloads it."
|
|
4483
|
+
if [ -n "$_module_export_words" ]; then
|
|
4484
|
+
_module_old_ifs=${IFS-}
|
|
4485
|
+
IFS=' '
|
|
4486
|
+
for _module_export in $_module_export_words; do
|
|
4487
|
+
printf '%s() {\n printf "%%s\\n" "hello from %s"\n}\n\n' "$_module_export" "$_module_name"
|
|
4488
|
+
done
|
|
4489
|
+
if [ "${_module_old_ifs+x}" = x ]; then
|
|
4490
|
+
IFS=$_module_old_ifs
|
|
4491
|
+
else
|
|
4492
|
+
unset IFS
|
|
4493
|
+
fi
|
|
4494
|
+
else
|
|
4495
|
+
printf '%s\n' "# This module has no declared exports."
|
|
4496
|
+
fi
|
|
4497
|
+
} >"$_module_body"
|
|
4498
|
+
project_meta_add_require "$_project_root/shell/module.d/profile_main.meta" "$_module_name"
|
|
4499
|
+
info "created module $_module_name and wired it into profile_main"
|
|
4500
|
+
;;
|
|
4501
|
+
list)
|
|
4502
|
+
shift
|
|
4503
|
+
[ "$#" -eq 0 ] || die "usage: valise module list"
|
|
4504
|
+
project_config_load
|
|
4505
|
+
_module_list_tmp=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-module-list.XXXXXX") ||
|
|
4506
|
+
die "cannot create module list staging file"
|
|
4507
|
+
registry_generate "$_project_root" "$_module_list_tmp"
|
|
4508
|
+
_module_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4509
|
+
"$_module_awk" -F '\t' 'NR > 1 { print $1 }' "$_module_list_tmp"
|
|
4510
|
+
_module_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
4511
|
+
"$_module_rm" -f "$_module_list_tmp" || die "cannot remove module list staging file"
|
|
4512
|
+
_module_list_tmp=""
|
|
4513
|
+
;;
|
|
4514
|
+
remove)
|
|
4515
|
+
shift
|
|
4516
|
+
_module_name=${1:-}
|
|
4517
|
+
[ -n "$_module_name" ] || die "module remove requires a module name"
|
|
4518
|
+
project_config_load
|
|
4519
|
+
_module_meta=
|
|
4520
|
+
_module_source=
|
|
4521
|
+
for _module_candidate in "$_project_root"/shell/module.d/*.meta "$_project_root"/shell/layers/*/module.d/*.meta; do
|
|
4522
|
+
[ -f "$_module_candidate" ] || continue
|
|
4523
|
+
_module_candidate_name=${_module_candidate##*/}
|
|
4524
|
+
_module_candidate_name=${_module_candidate_name%.meta}
|
|
4525
|
+
if [ "$_module_candidate_name" = "$_module_name" ]; then
|
|
4526
|
+
_module_meta=$_module_candidate
|
|
4527
|
+
_module_source=${_module_candidate%/*}
|
|
4528
|
+
break
|
|
4529
|
+
fi
|
|
4530
|
+
done
|
|
4531
|
+
[ -n "$_module_meta" ] || die "module not found: $_module_name"
|
|
4532
|
+
# Everything that would make the removal fail is checked here, before any
|
|
4533
|
+
# file changes: a half-applied removal leaves a project that cannot build.
|
|
4534
|
+
_module_registry_source=${_module_source#"$_project_root"/}
|
|
4535
|
+
_module_bundle_roots=$_project_root/shell/release/bundle-roots
|
|
4536
|
+
_module_variants=$_project_root/shell/release/install-variants
|
|
4537
|
+
for _module_candidate in "$_project_root"/shell/module.d/*.meta "$_project_root"/shell/layers/*/module.d/*.meta; do
|
|
4538
|
+
[ -f "$_module_candidate" ] || continue
|
|
4539
|
+
[ "$_module_candidate" != "$_module_meta" ] || continue
|
|
4540
|
+
if project_meta_loses_all_targets "$_module_candidate" "$_module_name"; then
|
|
4541
|
+
_module_candidate_name=${_module_candidate##*/}
|
|
4542
|
+
die "cannot remove $_module_name: ${_module_candidate_name%.meta} requires only this module and would have nothing to load; add another dependency or remove ${_module_candidate_name%.meta} first"
|
|
4543
|
+
fi
|
|
4544
|
+
done
|
|
4545
|
+
for _module_candidate in "$_module_bundle_roots" "$_module_variants"; do
|
|
4546
|
+
if [ -e "$_module_candidate" ] || [ -L "$_module_candidate" ]; then
|
|
4547
|
+
[ -f "$_module_candidate" ] && [ ! -L "$_module_candidate" ] || die "release file is missing or unsafe: $_module_candidate"
|
|
4548
|
+
fi
|
|
4549
|
+
done
|
|
4550
|
+
if [ -f "$_module_bundle_roots" ]; then
|
|
4551
|
+
_module_status=0
|
|
4552
|
+
project_release_filter "$_module_bundle_roots" 3 "$_module_name" "$_module_registry_source" >/dev/null || _module_status=$?
|
|
4553
|
+
case "$_module_status" in
|
|
4554
|
+
0) ;;
|
|
4555
|
+
3) die "cannot remove $_module_name: it is the only root of a bundle in shell/release/bundle-roots; add another root first" ;;
|
|
4556
|
+
*) die "cannot read release file: $_module_bundle_roots" ;;
|
|
4557
|
+
esac
|
|
4558
|
+
fi
|
|
4559
|
+
_module_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4560
|
+
_module_load=$("$_module_awk" -F= '$1 == "load" { print substr($0, length($1) + 2); exit }' "$_module_meta")
|
|
4561
|
+
_module_body_path=
|
|
4562
|
+
_module_body_shared=0
|
|
4563
|
+
if [ -n "$_module_load" ]; then
|
|
4564
|
+
case "$_module_load" in
|
|
4565
|
+
''|/*|.|..|./*|../*|*/.|*/..|*/./*|*/../*|*//*)
|
|
4566
|
+
die "invalid module load path: $_module_load"
|
|
4567
|
+
;;
|
|
4568
|
+
esac
|
|
4569
|
+
_module_root=${_module_source%/module.d}
|
|
4570
|
+
source_path_is_physical "$_module_root" "$_module_load" ||
|
|
4571
|
+
die "module load path contains a symlink: $_module_load"
|
|
4572
|
+
_module_body_path="$_module_root/$_module_load"
|
|
4573
|
+
[ ! -d "$_module_body_path" ] || die "module load path is a directory: $_module_load"
|
|
4574
|
+
[ -f "$_module_body_path" ] || die "module body is missing: $_module_load"
|
|
4575
|
+
# Another module in the same source may declare the same body. Keep it
|
|
4576
|
+
# when that is true: removing the metadata is enough, and deleting the
|
|
4577
|
+
# body would break the module that still loads it.
|
|
4578
|
+
for _module_candidate in "$_module_source"/*.meta; do
|
|
4579
|
+
[ -f "$_module_candidate" ] || continue
|
|
4580
|
+
[ "$_module_candidate" != "$_module_meta" ] || continue
|
|
4581
|
+
_module_candidate_load=$("$_module_awk" -F= '$1 == "load" { print substr($0, length($1) + 2); exit }' "$_module_candidate")
|
|
4582
|
+
if [ "$_module_candidate_load" = "$_module_load" ]; then
|
|
4583
|
+
_module_body_shared=1
|
|
4584
|
+
break
|
|
4585
|
+
fi
|
|
4586
|
+
done
|
|
4587
|
+
[ -w "$(path_parent "$_module_body_path")" ] ||
|
|
4588
|
+
die "module body directory is not writable: $(path_parent "$_module_body_path")"
|
|
4589
|
+
fi
|
|
4590
|
+
# Every path the removal rewrites must be writable before the first change.
|
|
4591
|
+
[ -w "$_module_source" ] || die "module metadata directory is not writable: $_module_source"
|
|
4592
|
+
[ -w "$_project_root/shell/release" ] || die "release directory is not writable: $_project_root/shell/release"
|
|
4593
|
+
for _module_candidate in "$_project_root"/shell/module.d/*.meta "$_project_root"/shell/layers/*/module.d/*.meta; do
|
|
4594
|
+
[ -f "$_module_candidate" ] || continue
|
|
4595
|
+
_module_candidate_parent=$(path_parent "$_module_candidate")
|
|
4596
|
+
[ -w "$_module_candidate_parent" ] ||
|
|
4597
|
+
die "metadata directory is not writable: $_module_candidate_parent"
|
|
4598
|
+
done
|
|
4599
|
+
for _module_candidate in "$_module_bundle_roots" "$_module_variants"; do
|
|
4600
|
+
if [ -f "$_module_candidate" ]; then
|
|
4601
|
+
[ -w "$_module_candidate" ] || die "release file is not writable: $_module_candidate"
|
|
4602
|
+
fi
|
|
4603
|
+
done
|
|
4604
|
+
_module_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
4605
|
+
if [ -n "$_module_body_path" ] && [ "$_module_body_shared" -eq 0 ]; then
|
|
4606
|
+
"$_module_rm" -f "$_module_body_path" || die "cannot remove module body: $_module_load"
|
|
4607
|
+
fi
|
|
4608
|
+
"$_module_rm" -f "$_module_meta" || die "cannot remove module metadata: $_module_name"
|
|
4609
|
+
for _module_candidate in "$_project_root"/shell/module.d/*.meta "$_project_root"/shell/layers/*/module.d/*.meta; do
|
|
4610
|
+
[ -f "$_module_candidate" ] || continue
|
|
4611
|
+
project_meta_remove_module "$_module_candidate" "$_module_name"
|
|
4612
|
+
done
|
|
4613
|
+
project_release_strip "$_module_bundle_roots" 3 "$_module_name" "$_module_registry_source"
|
|
4614
|
+
project_release_strip "$_module_variants" 3 "$_module_name" "$_module_registry_source"
|
|
4615
|
+
registry_generate "$_project_root" "$_project_root/shell/release/module-registry.tsv"
|
|
4616
|
+
info "removed module $_module_name"
|
|
4617
|
+
;;
|
|
4618
|
+
*)
|
|
4619
|
+
die "unknown module subcommand: $_module_subcmd (use new, list, or remove)"
|
|
4620
|
+
;;
|
|
4621
|
+
esac
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
cmd_build() {
|
|
4625
|
+
_build_out=
|
|
4626
|
+
_build_variant=
|
|
4627
|
+
_build_bundle=
|
|
4628
|
+
for _build_arg do
|
|
4629
|
+
case "$_build_arg" in
|
|
4630
|
+
--variant=*) _build_variant=${_build_arg#--variant=} ;;
|
|
4631
|
+
--bundle=*) _build_bundle=${_build_arg#--bundle=} ;;
|
|
4632
|
+
--help|-h) usage_build; return 0 ;;
|
|
4633
|
+
--*) die "unknown build option: $_build_arg" ;;
|
|
4634
|
+
*) [ -z "$_build_out" ] || die "usage: valise build [--variant=<name>] [--bundle=<name>] [out]"; _build_out=$_build_arg ;;
|
|
4635
|
+
esac
|
|
4636
|
+
done
|
|
4637
|
+
project_config_load
|
|
4638
|
+
[ -n "$_build_variant" ] || _build_variant=$_project_variant
|
|
4639
|
+
[ -n "$_build_bundle" ] || _build_bundle=$_project_bundle
|
|
4640
|
+
[ -n "$_build_out" ] || _build_out=$_project_root/build/release
|
|
4641
|
+
_build_out=$(path_normalize "$_build_out") || die "cannot resolve build output path"
|
|
4642
|
+
# The previous default output stays in place until the new tree has compiled
|
|
4643
|
+
# and been manifested, so a failed build never destroys a good one.
|
|
4644
|
+
if [ "$_build_out" = "$_project_root/build/release" ]; then
|
|
4645
|
+
_build_release_tree --replace-default --variant="$_build_variant" --bundle="$_build_bundle" "$_build_out"
|
|
4646
|
+
else
|
|
4647
|
+
_build_release_tree --variant="$_build_variant" --bundle="$_build_bundle" "$_build_out"
|
|
4648
|
+
fi
|
|
4649
|
+
unset _build_out _build_arg _build_variant _build_bundle
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4652
|
+
cmd_apply() {
|
|
4653
|
+
_apply_variant=
|
|
4654
|
+
_apply_bundle=
|
|
4655
|
+
_apply_install_arg=
|
|
4656
|
+
for _apply_arg do
|
|
4657
|
+
case "$_apply_arg" in
|
|
4658
|
+
--variant=*) _apply_variant=${_apply_arg#--variant=} ;;
|
|
4659
|
+
--bundle=*) _apply_bundle=${_apply_arg#--bundle=} ;;
|
|
4660
|
+
--no-integration) _apply_install_arg=--no-integration ;;
|
|
4661
|
+
--help|-h) usage_apply; return 0 ;;
|
|
4662
|
+
*) die "unknown apply option: $_apply_arg" ;;
|
|
4663
|
+
esac
|
|
4664
|
+
done
|
|
4665
|
+
project_config_load
|
|
4666
|
+
[ -n "$_apply_variant" ] || _apply_variant=$_project_variant
|
|
4667
|
+
[ -n "$_apply_bundle" ] || _apply_bundle=$_project_bundle
|
|
4668
|
+
cmd_build --variant="$_apply_variant" --bundle="$_apply_bundle"
|
|
4669
|
+
_apply_build_out=$_project_root/build/release
|
|
4670
|
+
_apply_old_pwd=$(pwd -P) || die "cannot resolve current directory"
|
|
4671
|
+
cd "$_apply_build_out" || die "cannot enter build output: $_apply_build_out"
|
|
4672
|
+
cmd_install $_apply_install_arg || die "valise install failed"
|
|
4673
|
+
cd "$_apply_old_pwd" || die "cannot restore project directory: $_apply_old_pwd"
|
|
4674
|
+
cmd_verify_active
|
|
4675
|
+
info "applied project: $_project_root"
|
|
4676
|
+
unset _apply_arg _apply_variant _apply_bundle _apply_install_arg _apply_build_out _apply_old_pwd
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
cmd_status() {
|
|
4680
|
+
for _status_arg do
|
|
4681
|
+
case "$_status_arg" in
|
|
4682
|
+
--help|-h) usage_status; return 0 ;;
|
|
4683
|
+
*) die "unknown status option: $_status_arg" ;;
|
|
4684
|
+
esac
|
|
4685
|
+
done
|
|
4686
|
+
project_config_load
|
|
4687
|
+
printf 'project: %s\n' "$_project_root"
|
|
4688
|
+
printf 'variant: %s\n' "$_project_variant"
|
|
4689
|
+
printf 'bundle: %s\n' "$_project_bundle"
|
|
4690
|
+
_status_home=$(path_normalize "$HOME") || die "cannot resolve HOME"
|
|
4691
|
+
_status_targets=$(posix_startup_targets "$_status_home")
|
|
4692
|
+
while IFS= read -r _status_target; do
|
|
4693
|
+
[ -n "$_status_target" ] || continue
|
|
4694
|
+
if [ -L "$_status_target" ]; then
|
|
4695
|
+
printf 'integration: %s: symlink, not managed by valise\n' "$_status_target"
|
|
4696
|
+
elif [ ! -f "$_status_target" ]; then
|
|
4697
|
+
printf 'integration: %s: missing\n' "$_status_target"
|
|
4698
|
+
elif integration_has_managed_block "$_status_target"; then
|
|
4699
|
+
printf 'integration: %s: managed block present\n' "$_status_target"
|
|
4700
|
+
else
|
|
4701
|
+
printf 'integration: %s: managed block missing\n' "$_status_target"
|
|
4702
|
+
fi
|
|
4703
|
+
done <<EOF
|
|
4704
|
+
$_status_targets
|
|
4705
|
+
EOF
|
|
4706
|
+
unset _status_home _status_targets _status_target
|
|
4707
|
+
if (registry_generate "$_project_root" "$_project_root/shell/release/module-registry.tsv" 1) >/dev/null 2>&1; then
|
|
4708
|
+
printf 'registry: up to date\n'
|
|
4709
|
+
else
|
|
4710
|
+
printf 'registry: out of date\n'
|
|
4711
|
+
fi
|
|
4712
|
+
if [ -L "$VALISE_ACTIVE_LINK" ]; then
|
|
4713
|
+
_status_active_target=$(active_release_dir 2>/dev/null || :)
|
|
4714
|
+
if [ -n "$_status_active_target" ]; then
|
|
4715
|
+
_status_active_id=${_status_active_target##*/}
|
|
4716
|
+
printf 'active release: %s\n' "$_status_active_id"
|
|
4717
|
+
else
|
|
4718
|
+
printf 'active release: broken symlink\n'
|
|
4719
|
+
fi
|
|
4720
|
+
else
|
|
4721
|
+
printf 'active release: none\n'
|
|
4722
|
+
fi
|
|
4723
|
+
_status_build_manifest="$_project_root/build/release/shell/install/profile-release.manifest"
|
|
4724
|
+
if [ -f "$_status_build_manifest" ]; then
|
|
4725
|
+
_status_build_hash=$(file_sha256 "$_status_build_manifest") || die "cannot hash build manifest"
|
|
4726
|
+
printf 'build output: %s\n' "$_project_root/build/release"
|
|
4727
|
+
if [ -n "${_status_active_target:-}" ] && [ -d "$_status_active_target" ]; then
|
|
4728
|
+
if [ -f "$_status_active_target/.valise-manifest" ]; then
|
|
4729
|
+
_status_active_manifest="$_status_active_target/.valise-manifest"
|
|
4730
|
+
else
|
|
4731
|
+
_status_active_manifest="$_status_active_target/shell/install/profile-release.manifest"
|
|
4732
|
+
fi
|
|
4733
|
+
if [ -f "$_status_active_manifest" ]; then
|
|
4734
|
+
_status_active_hash=$(file_sha256 "$_status_active_manifest") || die "cannot hash active manifest"
|
|
4735
|
+
if [ "$_status_build_hash" = "$_status_active_hash" ]; then
|
|
4736
|
+
printf 'drift: active release matches build output\n'
|
|
4737
|
+
else
|
|
4738
|
+
printf 'drift: active release differs from build output\n'
|
|
4739
|
+
fi
|
|
4740
|
+
else
|
|
4741
|
+
printf 'drift: active manifest missing\n'
|
|
4742
|
+
fi
|
|
4743
|
+
else
|
|
4744
|
+
printf 'drift: no active release to compare\n'
|
|
4745
|
+
fi
|
|
4746
|
+
else
|
|
4747
|
+
printf 'build output: not built\n'
|
|
4748
|
+
fi
|
|
4749
|
+
unset _status_awk _status_active_target _status_active_id _status_build_manifest _status_build_hash _status_active_manifest _status_active_hash
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
|
|
4753
|
+
# _build_release_tree [--variant=<name>] [--bundle=<name>] <out-dir>
|
|
4754
|
+
# Internal implementation of valise build. Build a release tree for compiled
|
|
4755
|
+
# plans from the consuming project in the current directory. The tree holds exactly what the plans source, at the paths
|
|
4756
|
+
# they source it from:
|
|
4757
|
+
# generated/installed-modules.<shell> valise compile output
|
|
4758
|
+
# vendor/shell-profile/lib/capabilities.sh the engine capability prelude
|
|
4759
|
+
# <load> and layers/<provider>/<load> every registry module body, with
|
|
4760
|
+
# its .meta beside it in module.d/
|
|
4761
|
+
# shell/release/ the registry, roots and variants
|
|
4762
|
+
_build_release_tree() {
|
|
4763
|
+
_as_out=""
|
|
4764
|
+
_as_variant=""
|
|
4765
|
+
_as_bundle=""
|
|
4766
|
+
_as_variant_set=0
|
|
4767
|
+
_as_bundle_set=0
|
|
4768
|
+
_as_replace_default=0
|
|
4769
|
+
for _as_arg do
|
|
4770
|
+
case "$_as_arg" in
|
|
4771
|
+
--variant=*) [ "$_as_variant_set" -eq 0 ] || die "duplicate --variant option"; _as_variant=${_as_arg#--variant=}; _as_variant_set=1 ;;
|
|
4772
|
+
--bundle=*) [ "$_as_bundle_set" -eq 0 ] || die "duplicate --bundle option"; _as_bundle=${_as_arg#--bundle=}; _as_bundle_set=1 ;;
|
|
4773
|
+
--replace-default) _as_replace_default=1 ;;
|
|
4774
|
+
--*) die "unknown build option: $_as_arg" ;;
|
|
4775
|
+
*) [ -z "$_as_out" ] || die "usage: valise build [--variant=<name>] [--bundle=<name>] [out]"; _as_out=$_as_arg ;;
|
|
4776
|
+
esac
|
|
4777
|
+
done
|
|
4778
|
+
[ -n "$_as_out" ] || die "usage: valise build [--variant=<name>] [--bundle=<name>] [out]"
|
|
4779
|
+
[ "$_as_variant_set" -eq 0 ] || [ -n "$_as_variant" ] || die "variant name must not be empty"
|
|
4780
|
+
[ "$_as_bundle_set" -eq 0 ] || [ -n "$_as_bundle" ] || die "bundle name must not be empty"
|
|
4781
|
+
_as_src=$(pwd -P) || die "cannot resolve working directory"
|
|
4782
|
+
[ -d "$_as_src/shell/module.d" ] || die "run from the consuming project root (need shell/module.d)"
|
|
4783
|
+
_as_out=$(path_normalize "$_as_out")
|
|
4784
|
+
[ "$_as_out" != "/" ] || die "output directory must not be /"
|
|
4785
|
+
case "$_as_out" in
|
|
4786
|
+
"$_as_src"/*)
|
|
4787
|
+
source_path_is_physical "$_as_src" "${_as_out#"$_as_src"/}" ||
|
|
4788
|
+
die "output path contains a symlink: $_as_out"
|
|
4789
|
+
;;
|
|
4790
|
+
esac
|
|
4791
|
+
_as_output_existed=0
|
|
4792
|
+
if [ -e "$_as_out" ] || [ -L "$_as_out" ]; then
|
|
4793
|
+
[ -d "$_as_out" ] && [ ! -L "$_as_out" ] || die "output path is not a directory: $_as_out"
|
|
4794
|
+
_as_output_empty=1
|
|
4795
|
+
for _as_entry in "$_as_out"/* "$_as_out"/.[!.]* "$_as_out"/..?*; do
|
|
4796
|
+
if [ -e "$_as_entry" ] || [ -L "$_as_entry" ]; then
|
|
4797
|
+
_as_output_empty=0
|
|
4798
|
+
break
|
|
4799
|
+
fi
|
|
4800
|
+
done
|
|
4801
|
+
[ "$_as_output_empty" -eq 1 ] || [ "$_as_replace_default" -eq 1 ] ||
|
|
4802
|
+
die "output directory must be empty: $_as_out"
|
|
4803
|
+
_as_output_existed=1
|
|
4804
|
+
fi
|
|
4805
|
+
_as_registry="$_as_src/shell/release/module-registry.tsv"
|
|
4806
|
+
registry_generate "$_as_src" "$_as_registry"
|
|
4807
|
+
_as_script_dir=$(valise_script_dir) || die "cannot locate the valise engine"
|
|
4808
|
+
_as_prelude="$_as_script_dir/../lib/capabilities.sh"
|
|
4809
|
+
[ -r "$_as_prelude" ] || die "capabilities prelude not found: $_as_prelude"
|
|
4810
|
+
_as_parent=$(path_parent "$_as_out")
|
|
4811
|
+
_as_mkdir=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
4812
|
+
_as_cp=$(trusted_utility_path cp) || die "no trusted cp utility available"
|
|
4813
|
+
_as_find=$(trusted_utility_path find) || die "no trusted find utility available"
|
|
4814
|
+
_as_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
4815
|
+
_as_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
4816
|
+
[ -d "$_as_parent" ] || "$_as_mkdir" -p "$_as_parent" || die "cannot create output parent: $_as_parent"
|
|
4817
|
+
# Build beside the destination and publish only after the complete tree and
|
|
4818
|
+
# manifest exist, so a failed compile or copy never leaves a partial release.
|
|
4819
|
+
_stage=$(trusted_mktemp_at "$_as_parent" dir "$_as_parent/.valise-build.XXXXXX") ||
|
|
4820
|
+
die "cannot create build staging directory"
|
|
4821
|
+
|
|
4822
|
+
if [ -n "$_as_variant" ] && [ -n "$_as_bundle" ]; then
|
|
4823
|
+
"$0" compile --variant="$_as_variant" --bundle="$_as_bundle" "$_stage/generated" || die "valise compile failed"
|
|
4824
|
+
elif [ -n "$_as_variant" ]; then
|
|
4825
|
+
"$0" compile --variant="$_as_variant" "$_stage/generated" || die "valise compile failed"
|
|
4826
|
+
elif [ -n "$_as_bundle" ]; then
|
|
4827
|
+
"$0" compile --bundle="$_as_bundle" "$_stage/generated" || die "valise compile failed"
|
|
4828
|
+
else
|
|
4829
|
+
"$0" compile "$_stage/generated" || die "valise compile failed"
|
|
4830
|
+
fi
|
|
4831
|
+
"$_as_mkdir" -p "$_stage/vendor/shell-profile/lib" "$_stage/shell" || die "cannot create release tree"
|
|
4832
|
+
"$_as_cp" "$_as_prelude" "$_stage/vendor/shell-profile/lib/capabilities.sh" || die "cannot copy prelude"
|
|
4833
|
+
"$_as_cp" -R "$_as_src/shell/release" "$_stage/shell/release" || die "cannot copy shell/release"
|
|
4834
|
+
|
|
4835
|
+
# Registry columns: 1 name, 9 load, 11 source (the directory holding the
|
|
4836
|
+
# .meta). A primary module resolves against the parent of its source
|
|
4837
|
+
# directory; a layer module under shell/layers/<provider>/module.d lands at
|
|
4838
|
+
# layers/<provider>/ in the release, matching the compiled plan paths.
|
|
4839
|
+
_as_tab=$(printf '\t')
|
|
4840
|
+
_as_rows="$_stage/.registry-rows"
|
|
4841
|
+
"$_as_awk" -F "$_as_tab" 'NR > 1 && $1 != "" { print $1 "|" $9 "|" $11 }' \
|
|
4842
|
+
"$_as_registry" >"$_as_rows" || die "cannot read module registry"
|
|
4843
|
+
while IFS='|' read -r _as_name _as_load _as_source; do
|
|
4844
|
+
[ -n "$_as_name" ] || continue
|
|
4845
|
+
# Module names index both source and destination paths. Reject anything
|
|
4846
|
+
# that is not a metadata identifier before either path is constructed.
|
|
4847
|
+
case "$_as_name" in
|
|
4848
|
+
[abcdefghijklmnopqrstuvwxyz_]*) ;;
|
|
4849
|
+
*) die "invalid module name in registry: $_as_name" ;;
|
|
4850
|
+
esac
|
|
4851
|
+
case "$_as_name" in
|
|
4852
|
+
*[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_]*) die "invalid module name in registry: $_as_name" ;;
|
|
4853
|
+
esac
|
|
4854
|
+
case "$_as_source" in
|
|
4855
|
+
shell/module.d) _as_prefix= ;;
|
|
4856
|
+
shell/layers/*/module.d)
|
|
4857
|
+
_as_layer=${_as_source#shell/layers/}
|
|
4858
|
+
_as_layer=${_as_layer%/module.d}
|
|
4859
|
+
case "$_as_layer" in
|
|
4860
|
+
[A-Za-z0-9_]*) ;;
|
|
4861
|
+
*) die "invalid layer source in registry: $_as_source" ;;
|
|
4862
|
+
esac
|
|
4863
|
+
case "$_as_layer" in
|
|
4864
|
+
*/*|*..*|*[!A-Za-z0-9_.-]*) die "invalid layer source in registry: $_as_source" ;;
|
|
4865
|
+
esac
|
|
4866
|
+
_as_prefix="layers/$_as_layer/"
|
|
4867
|
+
;;
|
|
4868
|
+
*) die "unsupported registry source: $_as_source (expected shell/module.d or shell/layers/<provider>/module.d)" ;;
|
|
4869
|
+
esac
|
|
4870
|
+
case "$_as_load" in
|
|
4871
|
+
/*|.|..|./*|../*|*/.|*/..|*/./*|*/../*|*//*) die "invalid load path in registry for $_as_name: $_as_load" ;;
|
|
4872
|
+
esac
|
|
4873
|
+
_as_root=${_as_source%/module.d}
|
|
4874
|
+
_as_pairs="$_as_source/$_as_name.meta:${_as_prefix}module.d/$_as_name.meta"
|
|
4875
|
+
if [ -n "$_as_load" ]; then
|
|
4876
|
+
_as_pairs="$_as_pairs $_as_root/$_as_load:$_as_prefix$_as_load"
|
|
4877
|
+
fi
|
|
4878
|
+
for _as_pair in $_as_pairs; do
|
|
4879
|
+
_as_from="$_as_src/${_as_pair%%:*}"
|
|
4880
|
+
_as_to="$_stage/${_as_pair#*:}"
|
|
4881
|
+
source_path_is_physical "$_as_src" "${_as_pair%%:*}" ||
|
|
4882
|
+
die "registry file path contains a symlink: ${_as_pair%%:*}"
|
|
4883
|
+
[ -f "$_as_from" ] && [ ! -L "$_as_from" ] ||
|
|
4884
|
+
die "registry file missing or a symlink: ${_as_pair%%:*}"
|
|
4885
|
+
[ ! -e "$_as_to" ] && [ ! -L "$_as_to" ] || die "release path collision: ${_as_pair#*:}"
|
|
4886
|
+
"$_as_mkdir" -p "$(path_parent "$_as_to")" &&
|
|
4887
|
+
"$_as_cp" "$_as_from" "$_as_to" || die "cannot copy ${_as_pair%%:*}"
|
|
4888
|
+
done
|
|
4889
|
+
done <"$_as_rows"
|
|
4890
|
+
_as_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
4891
|
+
"$_as_rm" -f "$_as_rows" || die "cannot remove build scratch file"
|
|
4892
|
+
"$_as_find" "$_stage" -type f -exec "$_as_chmod" 0644 {} + || die "cannot normalise file modes"
|
|
4893
|
+
"$0" make-manifest "$_stage" || die "valise make-manifest failed"
|
|
4894
|
+
|
|
4895
|
+
_as_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
4896
|
+
if [ "$_as_output_existed" -eq 1 ]; then
|
|
4897
|
+
if [ "$_as_replace_default" -eq 1 ]; then
|
|
4898
|
+
remove_tree "$_as_out" || die "cannot clear previous build output: $_as_out"
|
|
4899
|
+
else
|
|
4900
|
+
_as_rmdir=$(trusted_utility_path rmdir) || die "no trusted rmdir utility available"
|
|
4901
|
+
"$_as_rmdir" "$_as_out" || die "cannot clear empty output directory"
|
|
4902
|
+
fi
|
|
4903
|
+
fi
|
|
4904
|
+
"$_as_mv" "$_stage" "$_as_out" || die "cannot publish release tree"
|
|
4905
|
+
_stage=""
|
|
4906
|
+
}
|
|
4907
|
+
|
|
3185
4908
|
cmd_explain_variant() {
|
|
3186
|
-
_variant="${1:-
|
|
3187
|
-
|
|
4909
|
+
_variant="${1:-default}"
|
|
4910
|
+
case "$_variant" in
|
|
4911
|
+
--help|-h) usage_explain_variant; return 0 ;;
|
|
4912
|
+
esac
|
|
4913
|
+
_script_dir=$(valise_script_dir)
|
|
3188
4914
|
_src_root=""
|
|
3189
4915
|
for _candidate in "$_script_dir/../../.." "$_script_dir/../.." "$_script_dir/.." "."; do
|
|
3190
4916
|
if [ -f "$_candidate/shell/release/install-variants" ]; then
|
|
@@ -3196,9 +4922,8 @@ cmd_explain_variant() {
|
|
|
3196
4922
|
|
|
3197
4923
|
_variants_file="$_src_root/shell/release/install-variants"
|
|
3198
4924
|
[ -f "$_variants_file" ] || die "install-variants file not found"
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
[ -f "$_release_compiler" ] || die "release compiler not found: $_release_compiler"
|
|
4925
|
+
_variant_matches=$(awk -F'\t' -v v="$_variant" '$1 == v { count++ } END { print count + 0 }' "$_variants_file")
|
|
4926
|
+
[ "$_variant_matches" -eq 1 ] || die "unknown or ambiguous variant: $_variant"
|
|
3202
4927
|
|
|
3203
4928
|
_exclude_list=$(awk -F' ' -v v="$_variant" '$1 == v { print $3 }' "$_variants_file")
|
|
3204
4929
|
_desc=$(awk -F' ' -v v="$_variant" '$1 == v { print $2 }' "$_variants_file")
|
|
@@ -3213,8 +4938,8 @@ cmd_explain_variant() {
|
|
|
3213
4938
|
Per-shell impact:
|
|
3214
4939
|
'
|
|
3215
4940
|
|
|
3216
|
-
for _shell in bash zsh ksh
|
|
3217
|
-
_roots=$(
|
|
4941
|
+
for _shell in bash zsh ksh fish; do
|
|
4942
|
+
_roots=$(release_bundle_roots "$_src_root" "$_shell") || continue
|
|
3218
4943
|
|
|
3219
4944
|
_count=0
|
|
3220
4945
|
_excluded=0
|
|
@@ -3242,42 +4967,432 @@ Per-shell impact:
|
|
|
3242
4967
|
}
|
|
3243
4968
|
|
|
3244
4969
|
cmd_explain_bundles() {
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
for
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
4970
|
+
_eb_bundle=""
|
|
4971
|
+
_eb_bundle_requested=0
|
|
4972
|
+
for _eb_arg do
|
|
4973
|
+
case "$_eb_arg" in
|
|
4974
|
+
--bundle=*) _eb_bundle_requested=1; _eb_bundle=${_eb_arg#--bundle=} ;;
|
|
4975
|
+
--help|-h) usage_explain_bundles; return 0 ;;
|
|
4976
|
+
*) die "unknown explain-bundles option: $_eb_arg" ;;
|
|
4977
|
+
esac
|
|
3252
4978
|
done
|
|
3253
|
-
[ -n "$
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
4979
|
+
[ "$_eb_bundle_requested" -eq 0 ] || [ -n "$_eb_bundle" ] || die "bundle name must not be empty"
|
|
4980
|
+
|
|
4981
|
+
_eb_root=$(resolve_project_root) || die "cannot find source tree (need shell/release/bundle-roots)"
|
|
4982
|
+
_eb_roots_file="$_eb_root/shell/release/bundle-roots"
|
|
4983
|
+
|
|
4984
|
+
[ -f "$_eb_roots_file" ] || die "cannot find shell/release/bundle-roots in the project"
|
|
4985
|
+
|
|
4986
|
+
# The registry is the module source of truth. Generate it into a scratch
|
|
4987
|
+
# file when the project has not written one yet, so explain-bundles never
|
|
4988
|
+
# mutates the project.
|
|
4989
|
+
_eb_registry="$_eb_root/shell/release/module-registry.tsv"
|
|
4990
|
+
_eb_registry_tmp=""
|
|
4991
|
+
if [ ! -f "$_eb_registry" ]; then
|
|
4992
|
+
_eb_registry_tmp=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-explain-registry.XXXXXX") ||
|
|
4993
|
+
die "cannot create registry scratch file"
|
|
4994
|
+
registry_generate "$_eb_root" "$_eb_registry_tmp"
|
|
4995
|
+
_eb_registry="$_eb_registry_tmp"
|
|
4996
|
+
fi
|
|
4997
|
+
|
|
4998
|
+
# The default variant's exclusion list applies to the closure exactly as
|
|
4999
|
+
# it does at compile time.
|
|
5000
|
+
_eb_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
5001
|
+
_eb_excludes=""
|
|
5002
|
+
_eb_variants="$_eb_root/shell/release/install-variants"
|
|
5003
|
+
if [ -f "$_eb_variants" ]; then
|
|
5004
|
+
_eb_excludes=$("$_eb_awk" -F '\t' '$1 == "default" { print $3; exit }' "$_eb_variants")
|
|
5005
|
+
fi
|
|
5006
|
+
|
|
5007
|
+
"$_eb_awk" -F '\t' \
|
|
5008
|
+
-v roots_file="$_eb_roots_file" \
|
|
5009
|
+
-v bundle="$_eb_bundle" \
|
|
5010
|
+
-v bundle_requested="$_eb_bundle_requested" \
|
|
5011
|
+
-v excludes="$_eb_excludes" '
|
|
5012
|
+
function shell_matches(shells, wanted, count, i, values) {
|
|
5013
|
+
if (shells == "") return 0
|
|
5014
|
+
count = split(shells, values, " ")
|
|
5015
|
+
for (i = 1; i <= count; i++) {
|
|
5016
|
+
if (values[i] == wanted) return 1
|
|
5017
|
+
# shells=sh means every POSIX shell, not Fish.
|
|
5018
|
+
if (wanted != "fish" && values[i] == "sh") return 1
|
|
5019
|
+
}
|
|
5020
|
+
return 0
|
|
5021
|
+
}
|
|
5022
|
+
function is_excluded(id, i, ex) {
|
|
5023
|
+
for (i = 1; i <= excl_count; i++) {
|
|
5024
|
+
ex = excl_list[i]
|
|
5025
|
+
if (index(ex, "@") > 0) {
|
|
5026
|
+
if (ex == id) return 1
|
|
5027
|
+
} else if (ex == mod_id[id] && mod_src[id] == "shell/module.d") {
|
|
5028
|
+
return 1
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
return 0
|
|
5032
|
+
}
|
|
5033
|
+
function resolve_dep(dep, src, candidate, count, found, key) {
|
|
5034
|
+
candidate = dep "@" src
|
|
5035
|
+
if (candidate in mod_id && shell_matches(mod_shells[candidate], eff_shell) && !is_excluded(candidate)) return candidate
|
|
5036
|
+
count = 0; found = ""
|
|
5037
|
+
for (key in mod_id) {
|
|
5038
|
+
if (mod_id[key] == dep && shell_matches(mod_shells[key], eff_shell) && !is_excluded(key)) {
|
|
5039
|
+
count++; found = key
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
if (count == 1) return found
|
|
5043
|
+
return ""
|
|
5044
|
+
}
|
|
5045
|
+
function add_module(id, deps, count, i, dep, resolved) {
|
|
5046
|
+
if (id in closure_seen) return
|
|
5047
|
+
if (!(id in mod_id)) {
|
|
5048
|
+
if (!(id in missing)) { missing[id] = 1; missing_count++ }
|
|
5049
|
+
return
|
|
5050
|
+
}
|
|
5051
|
+
if (!shell_matches(mod_shells[id], eff_shell)) {
|
|
5052
|
+
if (!(id in skipped)) { skipped[id] = 1; skipped_count++ }
|
|
5053
|
+
return
|
|
5054
|
+
}
|
|
5055
|
+
if (is_excluded(id)) return
|
|
5056
|
+
closure_seen[id] = 1
|
|
5057
|
+
closure_list[++closure_count] = id
|
|
5058
|
+
count = split(mod_req[id], deps, " ")
|
|
5059
|
+
for (i = 1; i <= count; i++) {
|
|
5060
|
+
dep = deps[i]
|
|
5061
|
+
if (dep == "") continue
|
|
5062
|
+
resolved = resolve_dep(dep, mod_src[id])
|
|
5063
|
+
if (resolved == "") {
|
|
5064
|
+
if (!(dep in unresolved)) { unresolved[dep] = 1; unresolved_count++ }
|
|
5065
|
+
continue
|
|
5066
|
+
}
|
|
5067
|
+
add_module(resolved)
|
|
5068
|
+
}
|
|
5069
|
+
# An optional dependency joins the closure only when it is present for
|
|
5070
|
+
# this shell and not excluded; an absent optional is silent.
|
|
5071
|
+
count = split(mod_opt[id], deps, " ")
|
|
5072
|
+
for (i = 1; i <= count; i++) {
|
|
5073
|
+
dep = deps[i]
|
|
5074
|
+
if (dep == "") continue
|
|
5075
|
+
resolved = resolve_dep(dep, mod_src[id])
|
|
5076
|
+
if (resolved == "") continue
|
|
5077
|
+
add_module(resolved)
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
function sort_closure( i, j, key) {
|
|
5081
|
+
for (i = 2; i <= closure_count; i++) {
|
|
5082
|
+
key = closure_list[i]
|
|
5083
|
+
j = i - 1
|
|
5084
|
+
while (j >= 1 && closure_list[j] > key) {
|
|
5085
|
+
closure_list[j + 1] = closure_list[j]
|
|
5086
|
+
j--
|
|
5087
|
+
}
|
|
5088
|
+
closure_list[j + 1] = key
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
function seed_dropped(seed, seed_name, seed_src, at, e) {
|
|
5092
|
+
seed_name = seed; seed_src = "shell/module.d"
|
|
5093
|
+
at = index(seed, "@")
|
|
5094
|
+
if (at > 0) { seed_name = substr(seed, 1, at - 1); seed_src = substr(seed, at + 1) }
|
|
5095
|
+
for (e = 1; e <= excl_count; e++) {
|
|
5096
|
+
if (index(excl_list[e], "@") > 0) {
|
|
5097
|
+
if (excl_list[e] == seed) return 1
|
|
5098
|
+
} else {
|
|
5099
|
+
if (excl_list[e] == seed_name && seed_src == "shell/module.d") return 1
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
return 0
|
|
5103
|
+
}
|
|
5104
|
+
FILENAME == roots_file {
|
|
5105
|
+
if ($0 ~ /^[[:space:]]*#/ || NF < 2) next
|
|
5106
|
+
# The first row names the columns; it is not a bundle definition.
|
|
5107
|
+
if (!roots_header_seen) {
|
|
5108
|
+
roots_header_seen = 1
|
|
5109
|
+
if ($1 == "bundle" && $2 == "shell") next
|
|
5110
|
+
}
|
|
5111
|
+
b = $1; sh = $2; rts = $3
|
|
5112
|
+
if (b == "" || sh == "") next
|
|
5113
|
+
if (!(b in bundle_seen)) { bundle_seen[b] = 1; bundle_order[++bundle_count] = b }
|
|
5114
|
+
key = b SUBSEP sh
|
|
5115
|
+
if (key in bundle_roots) bundle_roots[key] = bundle_roots[key] " " rts
|
|
5116
|
+
else bundle_roots[key] = rts
|
|
5117
|
+
if (!(sh in shell_seen)) { shell_seen[sh] = 1; shell_order[++shell_count] = sh }
|
|
5118
|
+
next
|
|
5119
|
+
}
|
|
5120
|
+
FNR == 1 { next }
|
|
5121
|
+
{
|
|
5122
|
+
if (NF < 11) next
|
|
5123
|
+
name = $1; src = $11
|
|
5124
|
+
if (name == "" || src == "") next
|
|
5125
|
+
id = name "@" src
|
|
5126
|
+
mod_id[id] = name
|
|
5127
|
+
mod_src[id] = src
|
|
5128
|
+
mod_req[id] = $4
|
|
5129
|
+
mod_opt[id] = $5
|
|
5130
|
+
mod_shells[id] = $6
|
|
5131
|
+
mod_ver[id] = $2
|
|
5132
|
+
mod_exports[id] = $8
|
|
5133
|
+
mod_load[id] = $9
|
|
5134
|
+
}
|
|
5135
|
+
END {
|
|
5136
|
+
if (bundle_requested == 1 && !(bundle in bundle_seen)) {
|
|
5137
|
+
printf "valise: error: unknown bundle: %s\n", bundle > "/dev/stderr"
|
|
5138
|
+
exit 1
|
|
5139
|
+
}
|
|
5140
|
+
excl_count = 0
|
|
5141
|
+
if (excludes != "") {
|
|
5142
|
+
n = split(excludes, ex_arr, " ")
|
|
5143
|
+
for (i = 1; i <= n; i++) if (ex_arr[i] != "") excl_list[++excl_count] = ex_arr[i]
|
|
5144
|
+
}
|
|
5145
|
+
printf "Installed bundle resolution:\n\n"
|
|
5146
|
+
for (bi = 1; bi <= bundle_count; bi++) {
|
|
5147
|
+
b = bundle_order[bi]
|
|
5148
|
+
if (bundle_requested == 1 && b != bundle) continue
|
|
5149
|
+
printf "Bundle: %s\n", b
|
|
5150
|
+
for (si = 1; si <= shell_count; si++) {
|
|
5151
|
+
sh = shell_order[si]
|
|
5152
|
+
key = b SUBSEP sh
|
|
5153
|
+
if (!(key in bundle_roots)) continue
|
|
5154
|
+
eff_shell = sh
|
|
5155
|
+
closure_count = 0; missing_count = 0; skipped_count = 0; unresolved_count = 0
|
|
5156
|
+
delete closure_seen
|
|
5157
|
+
delete missing
|
|
5158
|
+
delete unresolved
|
|
5159
|
+
delete skipped
|
|
5160
|
+
root_count = 0
|
|
5161
|
+
n = split(bundle_roots[key], seeds, " ")
|
|
5162
|
+
for (i = 1; i <= n; i++) {
|
|
5163
|
+
seed = seeds[i]
|
|
5164
|
+
if (seed == "") continue
|
|
5165
|
+
if (seed_dropped(seed)) continue
|
|
5166
|
+
root_count++
|
|
5167
|
+
seed_name = seed; seed_src = "shell/module.d"
|
|
5168
|
+
at = index(seed, "@")
|
|
5169
|
+
if (at > 0) { seed_name = substr(seed, 1, at - 1); seed_src = substr(seed, at + 1) }
|
|
5170
|
+
add_module(seed_name "@" seed_src)
|
|
5171
|
+
}
|
|
5172
|
+
sort_closure()
|
|
5173
|
+
printf " %s: %d root(s), %d module(s)\n", sh, root_count, closure_count
|
|
5174
|
+
for (i = 1; i <= closure_count; i++) {
|
|
5175
|
+
id = closure_list[i]
|
|
5176
|
+
printf " module %s (%s)\n", mod_id[id], mod_src[id]
|
|
5177
|
+
}
|
|
5178
|
+
for (id in missing) printf " MISSING module %s\n", id
|
|
5179
|
+
for (dep in unresolved) printf " UNRESOLVED dependency %s\n", dep
|
|
5180
|
+
}
|
|
5181
|
+
printf "\n"
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
' "$_eb_roots_file" "$_eb_registry"
|
|
5185
|
+
_eb_rc=$?
|
|
5186
|
+
if [ -n "$_eb_registry_tmp" ]; then
|
|
5187
|
+
_eb_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
5188
|
+
"$_eb_rm" -f "$_eb_registry_tmp" || die "cannot remove registry scratch file"
|
|
5189
|
+
fi
|
|
5190
|
+
[ "$_eb_rc" -eq 0 ] || die "explain-bundles failed"
|
|
3260
5191
|
}
|
|
3261
5192
|
|
|
3262
5193
|
cmd_layer_lint() {
|
|
3263
|
-
|
|
3264
|
-
|
|
5194
|
+
_ll_name="${1:-}"
|
|
5195
|
+
# A failing shift is a special-builtin error: POSIX requires the shell to
|
|
5196
|
+
# abort, and the || guard does not prevent that. Shift only when there is
|
|
5197
|
+
# an argument to shift, so a bare "valise layer-lint" reaches the usage
|
|
5198
|
+
# check on every shell instead of dying with a shell-specific status.
|
|
5199
|
+
if [ "$#" -gt 0 ]; then shift; fi
|
|
5200
|
+
case "$_ll_name" in
|
|
5201
|
+
--help|-h) usage_layer_lint; return 0 ;;
|
|
5202
|
+
esac
|
|
5203
|
+
for _ll_arg do
|
|
5204
|
+
case "$_ll_arg" in
|
|
5205
|
+
--help|-h) usage_layer_lint; return 0 ;;
|
|
5206
|
+
*) die "unknown layer-lint option: $_ll_arg" ;;
|
|
5207
|
+
esac
|
|
5208
|
+
done
|
|
5209
|
+
[ -n "$_ll_name" ] || die "usage: valise layer-lint <layer-name>"
|
|
3265
5210
|
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
5211
|
+
# The layer lives in the consuming project, but the layer may also be
|
|
5212
|
+
# given as a path. The engine checkout supplies the module names a layer
|
|
5213
|
+
# may depend on.
|
|
5214
|
+
_ll_root=$(resolve_project_root 2>/dev/null || :)
|
|
5215
|
+
_ll_engine=$(resolve_engine_root 2>/dev/null || :)
|
|
5216
|
+
[ -n "$_ll_root" ] || [ -n "$_ll_engine" ] ||
|
|
5217
|
+
die "cannot find source tree (need shell/layers/ or module.d/)"
|
|
5218
|
+
_ll_layers="$_ll_root/shell/layers"
|
|
5219
|
+
[ -n "$_ll_layers" ] || _ll_layers=""
|
|
5220
|
+
|
|
5221
|
+
# A layer is addressed by its provider directory name, or by a path to a
|
|
5222
|
+
# layer tree that contains module.d/.
|
|
5223
|
+
_ll_layer=""
|
|
5224
|
+
if [ -n "$_ll_layers" ] && [ -d "$_ll_layers/$_ll_name" ]; then
|
|
5225
|
+
_ll_layer="$_ll_layers/$_ll_name"
|
|
5226
|
+
elif [ -d "$_ll_name/module.d" ]; then
|
|
5227
|
+
_ll_layer=$(cd "$_ll_name" && pwd -P) || die "cannot resolve layer path: $_ll_name"
|
|
5228
|
+
elif [ -n "$_ll_engine" ] && [ -d "$_ll_engine/shell/layers/$_ll_name" ]; then
|
|
5229
|
+
_ll_layer="$_ll_engine/shell/layers/$_ll_name"
|
|
5230
|
+
else
|
|
5231
|
+
die "layer not found: $_ll_name (expected $_ll_layers/$_ll_name or a path containing module.d/)"
|
|
5232
|
+
fi
|
|
5233
|
+
|
|
5234
|
+
[ -d "$_ll_layer/module.d" ] || die "layer has no module.d directory: $_ll_layer"
|
|
5235
|
+
|
|
5236
|
+
_ll_script_dir=$(valise_script_dir)
|
|
5237
|
+
_ll_metadata="$_ll_script_dir/../lib/metadata.sh"
|
|
5238
|
+
[ -r "$_ll_metadata" ] || die "metadata library not found: $_ll_metadata"
|
|
5239
|
+
|
|
5240
|
+
_ll_status=0
|
|
5241
|
+
_ll_fail() {
|
|
5242
|
+
printf ' FAIL: %s\n' "$*" >&2
|
|
5243
|
+
_ll_status=1
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
# The engine's own modules and the project's primary-root modules are the
|
|
5247
|
+
# ones a layer may depend on. A layer module may require another module
|
|
5248
|
+
# in its own layer, or one of these; anything else is a dependency the
|
|
5249
|
+
# layer does not own.
|
|
5250
|
+
# The name lists are space-delimited with a LEADING and a TRAILING space,
|
|
5251
|
+
# so that " $name " matches every entry including the first and the last.
|
|
5252
|
+
_ll_engine_names=" "
|
|
5253
|
+
for _ll_engine_root in "$_ll_engine" "$_ll_root"; do
|
|
5254
|
+
[ -n "$_ll_engine_root" ] || continue
|
|
5255
|
+
for _ll_engine_name in $(engine_module_names "$_ll_engine_root"); do
|
|
5256
|
+
case "$_ll_engine_names" in
|
|
5257
|
+
*" $_ll_engine_name "*) continue ;;
|
|
5258
|
+
esac
|
|
5259
|
+
_ll_engine_names="$_ll_engine_names$_ll_engine_name "
|
|
5260
|
+
done
|
|
5261
|
+
done
|
|
5262
|
+
|
|
5263
|
+
_ll_meta_dir="$_ll_layer/module.d"
|
|
5264
|
+
_ll_count=0
|
|
5265
|
+
_ll_layer_names=" "
|
|
5266
|
+
for _ll_meta in "$_ll_meta_dir"/*.meta; do
|
|
5267
|
+
[ -f "$_ll_meta" ] || continue
|
|
5268
|
+
_ll_count=$((_ll_count + 1))
|
|
5269
|
+
_ll_file=${_ll_meta##*/}
|
|
5270
|
+
_ll_mod_name=${_ll_file%.meta}
|
|
5271
|
+
|
|
5272
|
+
# Structure: the metadata file must not be a symlink and must carry a
|
|
5273
|
+
# safe mode.
|
|
5274
|
+
if [ -L "$_ll_meta" ]; then
|
|
5275
|
+
_ll_fail "$_ll_file: metadata file must not be a symlink"
|
|
5276
|
+
continue
|
|
5277
|
+
fi
|
|
5278
|
+
_ll_mode=$(file_mode "$_ll_meta") || _ll_mode=""
|
|
5279
|
+
case "$_ll_mode" in
|
|
5280
|
+
0644 | 0444) ;;
|
|
5281
|
+
*) _ll_fail "$_ll_file: metadata mode must be 0644 or 0444, got ${_ll_mode:-unknown}" ;;
|
|
5282
|
+
esac
|
|
5283
|
+
|
|
5284
|
+
# Namespace: a layer-only module must carry the contrib_ prefix.
|
|
5285
|
+
case "$_ll_mod_name" in
|
|
5286
|
+
contrib_*) ;;
|
|
5287
|
+
*) _ll_fail "$_ll_file: layer module name must use the contrib_ prefix" ;;
|
|
5288
|
+
esac
|
|
5289
|
+
|
|
5290
|
+
# Metadata: the record must validate as data.
|
|
5291
|
+
_ll_record=$(. "$_ll_metadata"; sp_metadata_read "$_ll_meta") || {
|
|
5292
|
+
_ll_fail "$_ll_file: invalid metadata"
|
|
5293
|
+
continue
|
|
5294
|
+
}
|
|
5295
|
+
_ll_m_name=
|
|
5296
|
+
_ll_m_version=
|
|
5297
|
+
_ll_m_requires=
|
|
5298
|
+
_ll_m_optional=
|
|
5299
|
+
_ll_m_load=
|
|
5300
|
+
while IFS= read -r _ll_line; do
|
|
5301
|
+
case "$_ll_line" in
|
|
5302
|
+
name=*) _ll_m_name=${_ll_line#name=} ;;
|
|
5303
|
+
version=*) _ll_m_version=${_ll_line#version=} ;;
|
|
5304
|
+
requires=*) _ll_m_requires=${_ll_line#requires=} ;;
|
|
5305
|
+
optional=*) _ll_m_optional=${_ll_line#optional=} ;;
|
|
5306
|
+
load=*) _ll_m_load=${_ll_line#load=} ;;
|
|
5307
|
+
esac
|
|
5308
|
+
done <<EOF
|
|
5309
|
+
$_ll_record
|
|
5310
|
+
EOF
|
|
5311
|
+
if [ "$_ll_m_name" != "$_ll_mod_name" ]; then
|
|
5312
|
+
_ll_fail "$_ll_file: metadata name $_ll_m_name does not match the file name $_ll_mod_name"
|
|
5313
|
+
fi
|
|
5314
|
+
if [ -z "$_ll_m_version" ]; then
|
|
5315
|
+
_ll_fail "$_ll_file: layer modules must declare a version"
|
|
5316
|
+
fi
|
|
5317
|
+
|
|
5318
|
+
# Load target: the body must exist, must not be a symlink, and must
|
|
5319
|
+
# carry a safe mode.
|
|
5320
|
+
if [ -n "$_ll_m_load" ]; then
|
|
5321
|
+
_ll_body="$_ll_layer/$_ll_m_load"
|
|
5322
|
+
if [ -L "$_ll_body" ]; then
|
|
5323
|
+
_ll_fail "$_ll_file: load target must not be a symlink: $_ll_m_load"
|
|
5324
|
+
elif [ ! -f "$_ll_body" ]; then
|
|
5325
|
+
_ll_fail "$_ll_file: load target does not exist: $_ll_m_load"
|
|
5326
|
+
else
|
|
5327
|
+
_ll_body_mode=$(file_mode "$_ll_body") || _ll_body_mode=""
|
|
5328
|
+
case "$_ll_body_mode" in
|
|
5329
|
+
0644 | 0444 | 0755 | 0555) ;;
|
|
5330
|
+
*) _ll_fail "$_ll_file: load target mode must be 0644, 0444, 0755 or 0555, got ${_ll_body_mode:-unknown}" ;;
|
|
5331
|
+
esac
|
|
5332
|
+
fi
|
|
5333
|
+
else
|
|
5334
|
+
_ll_fail "$_ll_file: layer module must declare a load target"
|
|
3272
5335
|
fi
|
|
5336
|
+
|
|
5337
|
+
_ll_layer_names="$_ll_layer_names$_ll_mod_name "
|
|
3273
5338
|
done
|
|
3274
|
-
[ -n "$_src_root" ] || die "cannot find source tree with shell/layers/"
|
|
3275
5339
|
|
|
3276
|
-
|
|
3277
|
-
|
|
5340
|
+
if [ "$_ll_count" -eq 0 ]; then
|
|
5341
|
+
_ll_fail "$_ll_layer: layer contains no modules"
|
|
5342
|
+
fi
|
|
5343
|
+
|
|
5344
|
+
# Dependencies: every requirement must resolve inside the layer or to an
|
|
5345
|
+
# engine module. An optional dependency must resolve the same way.
|
|
5346
|
+
for _ll_meta in "$_ll_meta_dir"/*.meta; do
|
|
5347
|
+
[ -f "$_ll_meta" ] || continue
|
|
5348
|
+
_ll_file=${_ll_meta##*/}
|
|
5349
|
+
_ll_record=$(. "$_ll_metadata"; sp_metadata_read "$_ll_meta") || continue
|
|
5350
|
+
_ll_m_requires=
|
|
5351
|
+
_ll_m_optional=
|
|
5352
|
+
while IFS= read -r _ll_line; do
|
|
5353
|
+
case "$_ll_line" in
|
|
5354
|
+
requires=*) _ll_m_requires=${_ll_line#requires=} ;;
|
|
5355
|
+
optional=*) _ll_m_optional=${_ll_line#optional=} ;;
|
|
5356
|
+
esac
|
|
5357
|
+
done <<EOF
|
|
5358
|
+
$_ll_record
|
|
5359
|
+
EOF
|
|
5360
|
+
for _ll_kind in requires optional; do
|
|
5361
|
+
if [ "$_ll_kind" = requires ]; then
|
|
5362
|
+
_ll_deps=$_ll_m_requires
|
|
5363
|
+
else
|
|
5364
|
+
_ll_deps=$_ll_m_optional
|
|
5365
|
+
fi
|
|
5366
|
+
[ -n "$_ll_deps" ] || continue
|
|
5367
|
+
_ll_ifs_was_set=0
|
|
5368
|
+
if [ "${IFS+x}" = x ]; then
|
|
5369
|
+
_ll_ifs_was_set=1
|
|
5370
|
+
_ll_ifs=${IFS-}
|
|
5371
|
+
fi
|
|
5372
|
+
IFS=' '
|
|
5373
|
+
for _ll_dep in $_ll_deps; do
|
|
5374
|
+
[ -n "$_ll_dep" ] || continue
|
|
5375
|
+
case "$_ll_layer_names" in
|
|
5376
|
+
*" $_ll_dep "*) continue ;;
|
|
5377
|
+
esac
|
|
5378
|
+
case "$_ll_engine_names" in
|
|
5379
|
+
*" $_ll_dep "*) continue ;;
|
|
5380
|
+
esac
|
|
5381
|
+
_ll_fail "$_ll_file: $_ll_kind dependency $_ll_dep is neither in this layer nor an engine module"
|
|
5382
|
+
done
|
|
5383
|
+
if [ "$_ll_ifs_was_set" -eq 1 ]; then
|
|
5384
|
+
IFS=$_ll_ifs
|
|
5385
|
+
else
|
|
5386
|
+
unset IFS
|
|
5387
|
+
fi
|
|
5388
|
+
done
|
|
5389
|
+
done
|
|
3278
5390
|
|
|
3279
|
-
|
|
3280
|
-
|
|
5391
|
+
if [ "$_ll_status" -eq 0 ]; then
|
|
5392
|
+
printf 'layer %s: %d module(s) validated\n' "$_ll_name" "$_ll_count"
|
|
5393
|
+
return 0
|
|
5394
|
+
fi
|
|
5395
|
+
die "layer $_ll_name failed validation"
|
|
3281
5396
|
}
|
|
3282
5397
|
|
|
3283
5398
|
# --- Main dispatch ---
|
|
@@ -3293,13 +5408,18 @@ case "$_cmd" in
|
|
|
3293
5408
|
init) cmd_init "$@" ;;
|
|
3294
5409
|
install) cmd_install "$@" ;;
|
|
3295
5410
|
_verify-release-root) cmd_verify_release_root "$@" ;;
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
variant) cmd_variant "$@" ;;
|
|
5411
|
+
doctor) cmd_doctor "$@" ;;
|
|
5412
|
+
release) cmd_release "$@" ;;
|
|
3299
5413
|
make-manifest) cmd_make_manifest "$@" ;;
|
|
5414
|
+
registry) cmd_registry "$@" ;;
|
|
5415
|
+
new) cmd_new "$@" ;;
|
|
5416
|
+
module) cmd_module "$@" ;;
|
|
5417
|
+
build) cmd_build "$@" ;;
|
|
5418
|
+
apply) cmd_apply "$@" ;;
|
|
5419
|
+
status) cmd_status "$@" ;;
|
|
3300
5420
|
compile) cmd_compile "$@" ;;
|
|
3301
5421
|
explain-variant) cmd_explain_variant "$@" ;;
|
|
3302
|
-
explain-bundles) cmd_explain_bundles ;;
|
|
5422
|
+
explain-bundles) cmd_explain_bundles "$@" ;;
|
|
3303
5423
|
layer-lint) cmd_layer_lint "$@" ;;
|
|
3304
5424
|
version|--version|-v) cmd_version ;;
|
|
3305
5425
|
help|--help|-h|'') usage ;;
|