valise-sh 0.2.0 → 0.3.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 +152 -2
- package/README.md +99 -146
- package/bin/valise +57 -5376
- package/docs/architecture.md +77 -121
- package/docs/contrib-modules.md +172 -218
- package/docs/installation.md +137 -83
- package/docs/module-abi.md +125 -126
- package/docs/projects.md +249 -0
- package/docs/release-policy.md +68 -93
- package/docs/shared-modules.md +40 -39
- package/examples/README.md +12 -11
- package/examples/e2e/README.md +59 -66
- package/examples/e2e/bundles.tsv +6 -0
- package/examples/e2e/module.d/e2e_aliases.meta +1 -2
- package/examples/e2e/module.d/e2e_completions_bash.meta +0 -1
- package/examples/e2e/module.d/e2e_completions_zsh.meta +0 -1
- package/examples/e2e/module.d/e2e_env_setup.meta +2 -3
- package/examples/e2e/module.d/e2e_fish_features.meta +0 -1
- package/examples/e2e/module.d/e2e_generated_config.meta +1 -2
- package/examples/e2e/module.d/e2e_ksh_features.meta +0 -1
- package/examples/e2e/module.d/e2e_layer_probe.meta +1 -2
- package/examples/e2e/module.d/e2e_optional_network.meta +1 -2
- package/examples/e2e/module.d/e2e_platform_tools.meta +8 -0
- package/examples/e2e/module.d/e2e_platform_tools_bsd.meta +2 -3
- package/examples/e2e/module.d/e2e_platform_tools_darwin.meta +2 -3
- package/examples/e2e/module.d/e2e_platform_tools_linux.meta +2 -3
- package/examples/e2e/valise.conf +4 -0
- package/examples/e2e/variants.tsv +3 -0
- package/examples/local/module.d/hello_valise.meta +1 -2
- package/examples/profile-project/README.md +78 -108
- package/examples/profile-project/bundles.tsv +8 -0
- package/examples/profile-project/{shell/layers → layers}/demo/module.d/contrib_demo_greeting.meta +1 -2
- package/examples/profile-project/{shell/module.d → module.d}/greeting.meta +1 -2
- package/examples/profile-project/{shell/module.d → module.d}/profile_main.meta +0 -1
- package/examples/profile-project/{shell/modules → modules}/greeting.sh +1 -1
- package/examples/profile-project/valise.conf +4 -0
- package/examples/profile-project/variants.tsv +3 -0
- package/examples/shared/README.md +13 -34
- package/examples/shared/module.d/contrib_shared_paths.meta +2 -3
- package/examples/shared/module.d/contrib_shared_umask.meta +1 -2
- package/examples/shared/modules/shared/contrib_shared_paths.sh +2 -2
- package/lib/capabilities.sh +40 -13
- package/lib/cli/commands.sh +278 -0
- package/lib/cli/common.sh +326 -0
- package/lib/cli/devtools.sh +539 -0
- package/lib/cli/fish-hook.sh +986 -0
- package/lib/cli/integration.sh +337 -0
- package/lib/cli/lifecycle.sh +578 -0
- package/lib/cli/project.sh +1807 -0
- package/lib/cli/store.sh +1023 -0
- package/lib/cli/usage.sh +308 -0
- package/lib/compiler.awk +11 -4
- package/lib/fish/loader.fish +69 -9
- package/lib/fish/metadata.fish +49 -5
- package/lib/fish/profile.fish +170 -0
- package/lib/loader.sh +6 -8
- package/lib/metadata.sh +111 -5
- package/manifests/parser-support.tsv +2 -2
- package/module.d/valise_editor.meta +9 -0
- package/module.d/valise_history.meta +9 -0
- package/module.d/valise_pager.meta +9 -0
- package/module.d/valise_path.meta +9 -0
- package/module.d/{platform_bsd.meta → valise_platform_bsd.meta} +3 -4
- package/module.d/{platform_linux.meta → valise_platform_linux.meta} +3 -4
- package/module.d/{platform_macos.meta → valise_platform_macos.meta} +3 -4
- package/module.d/{platform_posix.meta → valise_platform_posix.meta} +2 -3
- package/module.d/valise_xdg.meta +9 -0
- package/modules/system/{contrib_editor.sh → valise_editor.sh} +2 -2
- package/modules/system/{contrib_history.sh → valise_history.sh} +2 -2
- package/modules/system/{contrib_pager.sh → valise_pager.sh} +2 -2
- package/modules/system/{contrib_path.sh → valise_path.sh} +3 -3
- package/modules/system/{contrib_xdg.sh → valise_xdg.sh} +2 -2
- package/package.json +91 -10
- package/docs/variants.md +0 -327
- package/entrypoints/.gitkeep +0 -0
- package/entrypoints/fish/profile.fish +0 -99
- package/examples/e2e/build-manifest.sh +0 -138
- package/examples/local/.gitkeep +0 -0
- package/examples/profile-project/shell/release/bundle-roots +0 -9
- package/examples/profile-project/shell/release/install-variants +0 -3
- package/examples/profile-project/shell/release/module-registry.tsv +0 -4
- package/examples/profile-project/shell/release/project.conf +0 -3
- package/lib/.gitkeep +0 -0
- package/manifests/.gitkeep +0 -0
- package/module.d/.gitkeep +0 -0
- package/module.d/contrib_editor.meta +0 -10
- package/module.d/contrib_history.meta +0 -10
- package/module.d/contrib_pager.meta +0 -10
- package/module.d/contrib_path.meta +0 -10
- package/module.d/contrib_xdg.meta +0 -10
- package/modules/.gitkeep +0 -0
- /package/examples/profile-project/{shell/layers → layers}/demo/modules/shared/contrib_demo_greeting.sh +0 -0
package/lib/cli/store.sh
ADDED
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
# Copyright (c) 2026 Joseph Cumines
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# valise CLI: release store: locking, manifests, verification, install, release management.
|
|
4
|
+
# Sourced by bin/valise; not executable on its own.
|
|
5
|
+
|
|
6
|
+
# Acquire atomic lock via mkdir (POSIX atomic operation). Every utility that
|
|
7
|
+
# participates in lock integrity is resolved by absolute path so PATH cannot
|
|
8
|
+
# forge ownership or suppress stale-lock cleanup.
|
|
9
|
+
_lock_owned=0
|
|
10
|
+
_publish_tmp=""
|
|
11
|
+
_manifest_rows=""
|
|
12
|
+
_manifest_parse=""
|
|
13
|
+
_stage=""
|
|
14
|
+
_hook_tmp=""
|
|
15
|
+
_mm_tmp=""
|
|
16
|
+
_rr_tmp=""
|
|
17
|
+
_mv_path=""
|
|
18
|
+
_cleanup_failed=0
|
|
19
|
+
acquire_lock() {
|
|
20
|
+
_lock_cat_path=$(trusted_utility_path cat) || die "no trusted cat utility available"
|
|
21
|
+
_lock_mkdir_path=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
22
|
+
_lock_rm_path=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
23
|
+
_lock_kill_path=$(trusted_utility_path kill) || die "no trusted kill utility available"
|
|
24
|
+
|
|
25
|
+
# Reentrant ownership is valid only for a real lock directory and a real,
|
|
26
|
+
# non-symlink pid file containing this process's exact PID.
|
|
27
|
+
if [ "$_lock_owned" -eq 1 ] && [ -d "$VALISE_LOCK_DIR" ] && [ ! -L "$VALISE_LOCK_DIR" ] &&
|
|
28
|
+
[ -f "$VALISE_LOCK_DIR/pid" ] && [ ! -L "$VALISE_LOCK_DIR/pid" ]; then
|
|
29
|
+
_existing_pid=$("$_lock_cat_path" "$VALISE_LOCK_DIR/pid" 2>/dev/null || :)
|
|
30
|
+
case "$_existing_pid" in
|
|
31
|
+
''|*[!0-9]*) : ;;
|
|
32
|
+
"$$") _lock_owned=1; return 0 ;;
|
|
33
|
+
esac
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
_lock_parent=${VALISE_LOCK_DIR%/*}
|
|
37
|
+
[ -n "$_lock_parent" ] || _lock_parent=/
|
|
38
|
+
if [ ! -d "$_lock_parent" ]; then
|
|
39
|
+
"$_lock_mkdir_path" -p "$_lock_parent" 2>/dev/null || die "cannot create lock parent: $_lock_parent"
|
|
40
|
+
fi
|
|
41
|
+
if ! "$_lock_mkdir_path" -m 700 "$VALISE_LOCK_DIR" 2>/dev/null; then
|
|
42
|
+
# Check for stale lock using only the trusted utility paths. An invalid or
|
|
43
|
+
# unreadable lock is fail-closed rather than treated as stale.
|
|
44
|
+
if [ -d "$VALISE_LOCK_DIR" ] && [ ! -L "$VALISE_LOCK_DIR" ] &&
|
|
45
|
+
[ -f "$VALISE_LOCK_DIR/pid" ] && [ ! -L "$VALISE_LOCK_DIR/pid" ]; then
|
|
46
|
+
_lock_pid=$("$_lock_cat_path" "$VALISE_LOCK_DIR/pid" 2>/dev/null || :)
|
|
47
|
+
case "$_lock_pid" in
|
|
48
|
+
''|*[!0-9]*) die "another valise process is running (unreadable lock)" ;;
|
|
49
|
+
esac
|
|
50
|
+
if ! "$_lock_kill_path" -0 "$_lock_pid" 2>/dev/null; then
|
|
51
|
+
info "removing stale lock (pid $_lock_pid)"
|
|
52
|
+
"$_lock_rm_path" -rf "$VALISE_LOCK_DIR" 2>/dev/null || die "failed to remove stale lock"
|
|
53
|
+
if [ -e "$VALISE_LOCK_DIR" ] || [ -L "$VALISE_LOCK_DIR" ]; then
|
|
54
|
+
die "failed to remove stale lock"
|
|
55
|
+
fi
|
|
56
|
+
"$_lock_mkdir_path" -m 700 "$VALISE_LOCK_DIR" 2>/dev/null || die "failed to acquire lock after stale cleanup"
|
|
57
|
+
else
|
|
58
|
+
die "another valise process is running (pid $_lock_pid)"
|
|
59
|
+
fi
|
|
60
|
+
else
|
|
61
|
+
die "failed to acquire lock: $VALISE_LOCK_DIR"
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
[ -d "$VALISE_LOCK_DIR" ] && [ ! -L "$VALISE_LOCK_DIR" ] || die "lock directory is not safe"
|
|
65
|
+
printf '%s\n' "$$" > "$VALISE_LOCK_DIR/pid" || die "failed to write lock ownership"
|
|
66
|
+
[ -f "$VALISE_LOCK_DIR/pid" ] && [ ! -L "$VALISE_LOCK_DIR/pid" ] || die "lock ownership file is not safe"
|
|
67
|
+
_lock_pid=$("$_lock_cat_path" "$VALISE_LOCK_DIR/pid" 2>/dev/null || :)
|
|
68
|
+
[ "$_lock_pid" = "$$" ] || die "failed to verify lock ownership"
|
|
69
|
+
_lock_owned=1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
remove_tree() {
|
|
73
|
+
_remove_tree_path=$1
|
|
74
|
+
_remove_chmod_path=$(trusted_utility_path chmod) || return 1
|
|
75
|
+
_remove_rm_path=$(trusted_utility_path rm) || return 1
|
|
76
|
+
if [ -d "$_remove_tree_path" ] && [ ! -L "$_remove_tree_path" ]; then
|
|
77
|
+
"$_remove_chmod_path" -R u+w "$_remove_tree_path" 2>/dev/null || return 1
|
|
78
|
+
fi
|
|
79
|
+
"$_remove_rm_path" -rf "$_remove_tree_path" 2>/dev/null || return 1
|
|
80
|
+
[ ! -e "$_remove_tree_path" ] && [ ! -L "$_remove_tree_path" ]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
seal_tree() {
|
|
84
|
+
_seal_tree_root=$1
|
|
85
|
+
_find_path=$(trusted_utility_path find) || return 1
|
|
86
|
+
_seal_chmod_path=$(trusted_utility_path chmod) || return 1
|
|
87
|
+
"$_find_path" "$_seal_tree_root" -type d -exec "$_seal_chmod_path" 0555 {} + 2>/dev/null || return 1
|
|
88
|
+
_seal_tree_dirs=$("$_find_path" "$_seal_tree_root" -type d -print) || return 1
|
|
89
|
+
while IFS= read -r _seal_tree_dir; do
|
|
90
|
+
[ -n "$_seal_tree_dir" ] || continue
|
|
91
|
+
[ "$(file_mode "$_seal_tree_dir")" = "0555" ] || return 1
|
|
92
|
+
done <<EOF
|
|
93
|
+
$_seal_tree_dirs
|
|
94
|
+
EOF
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
release_lock() {
|
|
99
|
+
_release_rm_path=$(trusted_utility_path rm 2>/dev/null || :)
|
|
100
|
+
_release_rmdir_path=$(trusted_utility_path rmdir 2>/dev/null || :)
|
|
101
|
+
if [ -n "$_publish_tmp" ]; then
|
|
102
|
+
remove_tree "$_publish_tmp" || _cleanup_failed=1
|
|
103
|
+
_publish_tmp=""
|
|
104
|
+
fi
|
|
105
|
+
if [ -n "$_manifest_rows" ]; then
|
|
106
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_manifest_rows" 2>/dev/null || :
|
|
107
|
+
if [ -e "$_manifest_rows" ] || [ -L "$_manifest_rows" ]; then _cleanup_failed=1; fi
|
|
108
|
+
_manifest_rows=""
|
|
109
|
+
fi
|
|
110
|
+
if [ -n "$_manifest_parse" ]; then
|
|
111
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_manifest_parse" 2>/dev/null || :
|
|
112
|
+
if [ -e "$_manifest_parse" ] || [ -L "$_manifest_parse" ]; then _cleanup_failed=1; fi
|
|
113
|
+
_manifest_parse=""
|
|
114
|
+
fi
|
|
115
|
+
if [ -n "${_pm_tmp:-}" ]; then
|
|
116
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_pm_tmp" 2>/dev/null || :
|
|
117
|
+
if [ -e "$_pm_tmp" ] || [ -L "$_pm_tmp" ]; then _cleanup_failed=1; fi
|
|
118
|
+
_pm_tmp=""
|
|
119
|
+
fi
|
|
120
|
+
if [ -n "${_rr_tmp:-}" ]; then
|
|
121
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_rr_tmp" 2>/dev/null || :
|
|
122
|
+
if [ -e "$_rr_tmp" ] || [ -L "$_rr_tmp" ]; then _cleanup_failed=1; fi
|
|
123
|
+
_rr_tmp=""
|
|
124
|
+
fi
|
|
125
|
+
if [ -n "${_pi_tmp:-}" ]; then
|
|
126
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_pi_tmp" 2>/dev/null || :
|
|
127
|
+
if [ -e "$_pi_tmp" ] || [ -L "$_pi_tmp" ]; then _cleanup_failed=1; fi
|
|
128
|
+
_pi_tmp=""
|
|
129
|
+
fi
|
|
130
|
+
if [ -n "${_eb_registry_tmp:-}" ]; then
|
|
131
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_eb_registry_tmp" 2>/dev/null || :
|
|
132
|
+
if [ -e "$_eb_registry_tmp" ] || [ -L "$_eb_registry_tmp" ]; then _cleanup_failed=1; fi
|
|
133
|
+
_eb_registry_tmp=""
|
|
134
|
+
fi
|
|
135
|
+
if [ -n "${_doctor_cand_file:-}" ]; then
|
|
136
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_doctor_cand_file" 2>/dev/null || :
|
|
137
|
+
if [ -e "$_doctor_cand_file" ] || [ -L "$_doctor_cand_file" ]; then _cleanup_failed=1; fi
|
|
138
|
+
_doctor_cand_file=""
|
|
139
|
+
fi
|
|
140
|
+
if [ -n "${_module_list_tmp:-}" ]; then
|
|
141
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_module_list_tmp" 2>/dev/null || :
|
|
142
|
+
if [ -e "$_module_list_tmp" ] || [ -L "$_module_list_tmp" ]; then _cleanup_failed=1; fi
|
|
143
|
+
_module_list_tmp=""
|
|
144
|
+
fi
|
|
145
|
+
if [ -n "${_rg_tmp:-}" ]; then
|
|
146
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_rg_tmp" 2>/dev/null || :
|
|
147
|
+
if [ -e "$_rg_tmp" ] || [ -L "$_rg_tmp" ]; then _cleanup_failed=1; fi
|
|
148
|
+
_rg_tmp=""
|
|
149
|
+
fi
|
|
150
|
+
if [ -n "${_publish_previous:-}" ]; then
|
|
151
|
+
# A publication that failed after moving the previous output aside left
|
|
152
|
+
# it here; the error already named the output, so keep the tree for the
|
|
153
|
+
# user rather than deleting the only copy.
|
|
154
|
+
printf 'valise: previous output kept at %s\n' "$_publish_previous/tree" >&2
|
|
155
|
+
_publish_previous=""
|
|
156
|
+
fi
|
|
157
|
+
if [ -n "${_dev_tmp:-}" ]; then
|
|
158
|
+
remove_tree "$_dev_tmp" || _cleanup_failed=1
|
|
159
|
+
_dev_tmp=""
|
|
160
|
+
fi
|
|
161
|
+
if [ -n "$_stage" ]; then
|
|
162
|
+
remove_tree "$_stage" || _cleanup_failed=1
|
|
163
|
+
_stage=""
|
|
164
|
+
fi
|
|
165
|
+
if [ -n "$_hook_tmp" ]; then
|
|
166
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_hook_tmp" 2>/dev/null || :
|
|
167
|
+
if [ -e "$_hook_tmp" ] || [ -L "$_hook_tmp" ]; then _cleanup_failed=1; fi
|
|
168
|
+
_hook_tmp=""
|
|
169
|
+
fi
|
|
170
|
+
for _release_tmp in "${_rc_tmp:-}" "${_rc_stub:-}" "${_rc_pub:-}" "${_prev_tmp:-}" "${_dry_tmp:-}" "${_un_tmp:-}"; do
|
|
171
|
+
[ -n "$_release_tmp" ] || continue
|
|
172
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_release_tmp" 2>/dev/null || :
|
|
173
|
+
if [ -e "$_release_tmp" ] || [ -L "$_release_tmp" ]; then _cleanup_failed=1; fi
|
|
174
|
+
done
|
|
175
|
+
_rc_tmp=""
|
|
176
|
+
_rc_stub=""
|
|
177
|
+
_rc_pub=""
|
|
178
|
+
_prev_tmp=""
|
|
179
|
+
_dry_tmp=""
|
|
180
|
+
_un_tmp=""
|
|
181
|
+
if [ -n "$_mm_tmp" ]; then
|
|
182
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$_mm_tmp" 2>/dev/null || :
|
|
183
|
+
if [ -e "$_mm_tmp" ] || [ -L "$_mm_tmp" ]; then _cleanup_failed=1; fi
|
|
184
|
+
_mm_tmp=""
|
|
185
|
+
fi
|
|
186
|
+
[ "$_lock_owned" -eq 1 ] || { [ "$_cleanup_failed" -eq 0 ]; return; }
|
|
187
|
+
[ -n "$_release_rm_path" ] && "$_release_rm_path" -f "$VALISE_LOCK_DIR/pid" 2>/dev/null || :
|
|
188
|
+
if [ -e "$VALISE_LOCK_DIR/pid" ] || [ -L "$VALISE_LOCK_DIR/pid" ]; then _cleanup_failed=1; fi
|
|
189
|
+
[ -n "$_release_rmdir_path" ] && "$_release_rmdir_path" "$VALISE_LOCK_DIR" 2>/dev/null || :
|
|
190
|
+
if [ -e "$VALISE_LOCK_DIR" ] || [ -L "$VALISE_LOCK_DIR" ]; then _cleanup_failed=1; fi
|
|
191
|
+
_lock_owned=0
|
|
192
|
+
[ "$_cleanup_failed" -eq 0 ]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_handle_int() {
|
|
196
|
+
trap - INT
|
|
197
|
+
[ "${_install_txn:-0}" -ne 1 ] || install_txn_abort
|
|
198
|
+
release_lock
|
|
199
|
+
exit 130
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_handle_term() {
|
|
203
|
+
trap - TERM
|
|
204
|
+
[ "${_install_txn:-0}" -ne 1 ] || install_txn_abort
|
|
205
|
+
release_lock
|
|
206
|
+
exit 143
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
trap '_exit_status=$?; [ "${_install_txn:-0}" -ne 1 ] || install_txn_abort; release_lock; if [ "$_exit_status" -eq 0 ] && [ "$_cleanup_failed" -ne 0 ]; then _exit_status=1; fi; exit "$_exit_status"' EXIT
|
|
210
|
+
trap '_handle_int' INT
|
|
211
|
+
trap '_handle_term' TERM
|
|
212
|
+
|
|
213
|
+
switch_active_link() {
|
|
214
|
+
_ln_path=$(trusted_utility_path ln) || die "no trusted ln utility available"
|
|
215
|
+
_readlink_path=$(trusted_utility_path readlink) || die "no trusted readlink utility available"
|
|
216
|
+
if [ -e "$VALISE_ACTIVE_LINK" ] && [ ! -L "$VALISE_ACTIVE_LINK" ]; then
|
|
217
|
+
die "active path exists and is not a symlink: $VALISE_ACTIVE_LINK"
|
|
218
|
+
fi
|
|
219
|
+
# ln -sfn replaces the destination symlink. The -n flag prevents following a
|
|
220
|
+
# destination that is a symlink to a directory. If the swap fails after the
|
|
221
|
+
# old link was removed, restore the prior target so the pointer is never
|
|
222
|
+
# left missing.
|
|
223
|
+
_switch_old_target=
|
|
224
|
+
if [ -L "$VALISE_ACTIVE_LINK" ]; then
|
|
225
|
+
_switch_old_target=$(readlink_target "$VALISE_ACTIVE_LINK" 2>/dev/null || :)
|
|
226
|
+
fi
|
|
227
|
+
if ! "$_ln_path" -sfn "$1" "$VALISE_ACTIVE_LINK"; then
|
|
228
|
+
if [ -n "$_switch_old_target" ]; then
|
|
229
|
+
"$_ln_path" -sfn "$_switch_old_target" "$VALISE_ACTIVE_LINK" 2>/dev/null || :
|
|
230
|
+
fi
|
|
231
|
+
die "failed to update active symlink"
|
|
232
|
+
fi
|
|
233
|
+
[ -L "$VALISE_ACTIVE_LINK" ] || die "active symlink was not created"
|
|
234
|
+
[ "$($_readlink_path "$VALISE_ACTIVE_LINK" 2>/dev/null)" = "$1" ] || die "active symlink target mismatch"
|
|
235
|
+
note_switch "$_switch_old_target" "$1"
|
|
236
|
+
unset _switch_old_target
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
source_path_is_physical() {
|
|
240
|
+
_source_root=$1
|
|
241
|
+
_source_relative=$2
|
|
242
|
+
_source_path=$_source_root
|
|
243
|
+
_source_remaining=$_source_relative
|
|
244
|
+
while [ -n "$_source_remaining" ]; do
|
|
245
|
+
case "$_source_remaining" in
|
|
246
|
+
*/*)
|
|
247
|
+
_source_component=${_source_remaining%%/*}
|
|
248
|
+
_source_remaining=${_source_remaining#*/}
|
|
249
|
+
;;
|
|
250
|
+
*)
|
|
251
|
+
_source_component=$_source_remaining
|
|
252
|
+
_source_remaining=
|
|
253
|
+
;;
|
|
254
|
+
esac
|
|
255
|
+
[ -n "$_source_component" ] || return 1
|
|
256
|
+
_source_path=$_source_path/$_source_component
|
|
257
|
+
[ ! -L "$_source_path" ] || return 1
|
|
258
|
+
done
|
|
259
|
+
return 0
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
validate_manifest() {
|
|
263
|
+
_awk_path=$(trusted_utility_path awk) || return 1
|
|
264
|
+
"$_awk_path" -F '\t' '
|
|
265
|
+
NR == 1 { if ($0 != "valise-manifest-v2") exit 10; next }
|
|
266
|
+
NR == 2 { if ($1 != "destination" || $2 != "source" || $3 != "source_mode" || $4 != "install_mode" || $5 != "bytes" || $6 != "sha256" || NF != 6) exit 11; next }
|
|
267
|
+
NF == 0 { next }
|
|
268
|
+
NF != 6 || $1 == "" || $2 == "" || $1 !~ /^[A-Za-z0-9._\/-]+$/ || $2 !~ /^[A-Za-z0-9._\/-]+$/ || $1 ~ /^\// || $2 ~ /^\// || $1 ~ /\/$/ || $2 ~ /\/$/ || $1 ~ /^#/ || $1 == "destination" || $1 == "valise-manifest-v2" || $3 !~ /^0(644|755)$/ || $4 != (($3 == "0644") ? "0444" : "0555") || $5 !~ /^[0-9]+$/ || length($6) != 64 || $6 !~ /^[0-9a-fA-F]+$/ || $1 ~ /(^|\/)\.($|\/)/ || index($1, "//") || $1 ~ /(^|\/)\.\.($|\/)/ || index($2, "//") || $2 ~ /(^|\/)\.($|\/)/ || $2 ~ /(^|\/)\.\.($|\/)/ || seen[$1]++ || seen_source[$2]++ { exit 12 }
|
|
269
|
+
{ data_rows = 1 }
|
|
270
|
+
END { if (NR < 3 || data_rows != 1) exit 13 }
|
|
271
|
+
' "$1"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
temp_path_is_safe() {
|
|
275
|
+
_temp_candidate=$1
|
|
276
|
+
_temp_kind=$2
|
|
277
|
+
_temp_root=${3:-${TMPDIR:-/tmp}}
|
|
278
|
+
case "$_temp_root" in /*) ;; *) return 1 ;; esac
|
|
279
|
+
case "$_temp_candidate" in /*) ;; *) return 1 ;; esac
|
|
280
|
+
[ -e "$_temp_candidate" ] || return 1
|
|
281
|
+
[ ! -L "$_temp_candidate" ] || return 1
|
|
282
|
+
case "$_temp_kind" in
|
|
283
|
+
file) [ -f "$_temp_candidate" ] || return 1 ;;
|
|
284
|
+
dir) [ -d "$_temp_candidate" ] || return 1 ;;
|
|
285
|
+
*) return 1 ;;
|
|
286
|
+
esac
|
|
287
|
+
_temp_root_canonical=$(cd "$_temp_root" 2>/dev/null && pwd -P) || return 1
|
|
288
|
+
_temp_parent=${_temp_candidate%/*}
|
|
289
|
+
[ "$_temp_parent" != "$_temp_candidate" ] || _temp_parent=/
|
|
290
|
+
_temp_parent_canonical=$(cd "$_temp_parent" 2>/dev/null && pwd -P) || return 1
|
|
291
|
+
case "$_temp_parent_canonical/" in
|
|
292
|
+
"$_temp_root_canonical/"*) ;;
|
|
293
|
+
*) return 1 ;;
|
|
294
|
+
esac
|
|
295
|
+
return 0
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
trusted_mktemp_at() {
|
|
299
|
+
_temp_root=$1
|
|
300
|
+
_temp_kind=$2
|
|
301
|
+
_temp_template=$3
|
|
302
|
+
_mktemp_path=$(trusted_utility_path mktemp) || return 1
|
|
303
|
+
case "$_temp_kind" in
|
|
304
|
+
file) _temp_candidate=$("$_mktemp_path" "$_temp_template") || return 1 ;;
|
|
305
|
+
dir) _temp_candidate=$("$_mktemp_path" -d "$_temp_template") || return 1 ;;
|
|
306
|
+
*) return 1 ;;
|
|
307
|
+
esac
|
|
308
|
+
temp_path_is_safe "$_temp_candidate" "$_temp_kind" "$_temp_root" || return 1
|
|
309
|
+
printf '%s\n' "$_temp_candidate"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
trusted_mktemp() {
|
|
313
|
+
trusted_mktemp_at "${TMPDIR:-/tmp}" "$@"
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
prepare_manifest_rows() {
|
|
317
|
+
_cleanup_rm_path=$(trusted_utility_path rm) || return 1
|
|
318
|
+
if [ -n "$_manifest_rows" ]; then
|
|
319
|
+
"$_cleanup_rm_path" -f "$_manifest_rows" 2>/dev/null || :
|
|
320
|
+
_manifest_rows=""
|
|
321
|
+
fi
|
|
322
|
+
if [ -n "$_manifest_parse" ]; then
|
|
323
|
+
"$_cleanup_rm_path" -f "$_manifest_parse" 2>/dev/null || :
|
|
324
|
+
_manifest_parse=""
|
|
325
|
+
fi
|
|
326
|
+
# _manifest_rows is the exact private snapshot used for identity and
|
|
327
|
+
# publication. Parsing is performed from a second file that only adds a
|
|
328
|
+
# final newline, so an unterminated source row is still consumed by POSIX
|
|
329
|
+
# read without changing the bytes whose digest defines the release.
|
|
330
|
+
_manifest_rows=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-manifest.XXXXXX") || return 1
|
|
331
|
+
_cat_path=$(trusted_utility_path cat) || {
|
|
332
|
+
"$_cleanup_rm_path" -f "$_manifest_rows" 2>/dev/null || :
|
|
333
|
+
_manifest_rows=""
|
|
334
|
+
return 1
|
|
335
|
+
}
|
|
336
|
+
"$_cat_path" "$1" >"$_manifest_rows" || {
|
|
337
|
+
"$_cleanup_rm_path" -f "$_manifest_rows" 2>/dev/null || :
|
|
338
|
+
_manifest_rows=""
|
|
339
|
+
return 1
|
|
340
|
+
}
|
|
341
|
+
_manifest_parse=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-manifest-parse.XXXXXX") || {
|
|
342
|
+
"$_cleanup_rm_path" -f "$_manifest_rows" 2>/dev/null || :
|
|
343
|
+
_manifest_rows=""
|
|
344
|
+
return 1
|
|
345
|
+
}
|
|
346
|
+
_awk_path=$(trusted_utility_path awk) || {
|
|
347
|
+
"$_cleanup_rm_path" -f "$_manifest_rows" "$_manifest_parse" 2>/dev/null || :
|
|
348
|
+
_manifest_rows=""
|
|
349
|
+
_manifest_parse=""
|
|
350
|
+
return 1
|
|
351
|
+
}
|
|
352
|
+
"$_awk_path" '{ print }' "$_manifest_rows" >"$_manifest_parse" || {
|
|
353
|
+
"$_cleanup_rm_path" -f "$_manifest_rows" "$_manifest_parse" 2>/dev/null || :
|
|
354
|
+
_manifest_rows=""
|
|
355
|
+
_manifest_parse=""
|
|
356
|
+
return 1
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
locate_manifest() {
|
|
361
|
+
if [ -f "./manifest.tsv" ] && [ ! -L "./manifest.tsv" ]; then
|
|
362
|
+
printf '%s\n' "./manifest.tsv"
|
|
363
|
+
return 0
|
|
364
|
+
fi
|
|
365
|
+
return 1
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
_manifest_has_file() {
|
|
369
|
+
_awk_path=$(trusted_utility_path awk) || return 1
|
|
370
|
+
"$_awk_path" -F '\t' -v destination="$2" '
|
|
371
|
+
NR > 2 && NF && $1 == destination { found = 1 }
|
|
372
|
+
END { exit found ? 0 : 1 }
|
|
373
|
+
' "$1"
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_manifest_has_descendant() {
|
|
377
|
+
_awk_path=$(trusted_utility_path awk) || return 1
|
|
378
|
+
"$_awk_path" -F '\t' -v prefix="$2/" '
|
|
379
|
+
NR > 2 && NF && index($1, prefix) == 1 { found = 1 }
|
|
380
|
+
END { exit found ? 0 : 1 }
|
|
381
|
+
' "$1"
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
verify_release_dir() {
|
|
385
|
+
_verify_dir=$1
|
|
386
|
+
_verify_manifest=$2
|
|
387
|
+
_verify_canonical=$(cd "$_verify_dir" 2>/dev/null && pwd -P) || _verify_fail "cannot resolve release root" || return 1
|
|
388
|
+
[ "$(file_mode "$_verify_dir")" = "0555" ] || _verify_fail "release root mode mismatch" || return 1
|
|
389
|
+
_verify_snapshot="$_verify_dir/.valise-manifest"
|
|
390
|
+
if [ -e "$_verify_snapshot" ] || [ -L "$_verify_snapshot" ]; then
|
|
391
|
+
[ -f "$_verify_snapshot" ] || _verify_fail "release manifest snapshot is not a regular file" || return 1
|
|
392
|
+
[ ! -L "$_verify_snapshot" ] || _verify_fail "release manifest snapshot must not be a symlink" || return 1
|
|
393
|
+
[ "$(file_mode "$_verify_snapshot")" = "0444" ] || _verify_fail "release manifest snapshot mode mismatch" || return 1
|
|
394
|
+
[ "$(file_bytes "$_verify_snapshot")" = "$(file_bytes "$_verify_manifest")" ] || _verify_fail "release manifest snapshot byte count mismatch" || return 1
|
|
395
|
+
_verify_snapshot_digest=$(file_sha256 "$_verify_snapshot") || _verify_fail "no SHA-256 utility available" || return 1
|
|
396
|
+
_verify_manifest_digest=$(file_sha256 "$_verify_manifest") || _verify_fail "no SHA-256 utility available" || return 1
|
|
397
|
+
[ "$_verify_snapshot_digest" = "$_verify_manifest_digest" ] || _verify_fail "release manifest snapshot digest mismatch" || return 1
|
|
398
|
+
_verify_expected_id="${3:-}"
|
|
399
|
+
if [ -z "$_verify_expected_id" ]; then
|
|
400
|
+
_verify_expected_id=$(basename "$_verify_dir")
|
|
401
|
+
fi
|
|
402
|
+
[ "$_verify_snapshot_digest" = "$_verify_expected_id" ] || _verify_fail "release manifest snapshot identity mismatch" || return 1
|
|
403
|
+
fi
|
|
404
|
+
while IFS="$(printf '\t')" read -r _dest _src _mode _imode _size _sha; do
|
|
405
|
+
[ -n "$_dest" ] || continue
|
|
406
|
+
case "$_dest" in valise-manifest-v2|destination) continue ;; esac
|
|
407
|
+
_verify_file="$_verify_dir/$_dest"
|
|
408
|
+
[ -f "$_verify_file" ] || _verify_fail "release file missing: $_dest" || return 1
|
|
409
|
+
[ ! -L "$_verify_file" ] || _verify_fail "release file must not be a symlink: $_dest" || return 1
|
|
410
|
+
_verify_parent=$(cd "$(path_parent "$_verify_file")" 2>/dev/null && pwd -P) || _verify_fail "release parent missing: $_dest" || return 1
|
|
411
|
+
case "$_verify_parent/" in "$_verify_canonical/"*) : ;; *) _verify_fail "release path escapes target: $_dest" || return 1 ;; esac
|
|
412
|
+
[ "$(file_mode "$_verify_file")" = "$_imode" ] || _verify_fail "release mode mismatch for $_dest" || return 1
|
|
413
|
+
[ "$(file_bytes "$_verify_file")" = "$_size" ] || _verify_fail "release byte count mismatch for $_dest" || return 1
|
|
414
|
+
case "$_sha" in *[!0123456789abcdefABCDEF]*) _verify_fail "invalid release digest for $_dest" || return 1 ;; esac
|
|
415
|
+
[ "${#_sha}" -eq 64 ] || _verify_fail "invalid release digest for $_dest" || return 1
|
|
416
|
+
_verify_actual=$(file_sha256 "$_verify_file") || _verify_fail "no trusted SHA-256 utility available" || return 1
|
|
417
|
+
_verify_expected=$(printf '%s' "$_sha" | lower_hex) || _verify_fail "no trusted tr utility available" || return 1
|
|
418
|
+
[ "$_verify_actual" = "$_verify_expected" ] || _verify_fail "release digest mismatch for $_dest" || return 1
|
|
419
|
+
done < "${_manifest_parse:-$_manifest_rows}"
|
|
420
|
+
_find_path=$(trusted_utility_path find) || _verify_fail "no trusted find utility available" || return 1
|
|
421
|
+
_verify_bad=$("$_find_path" "$_verify_dir" -print | while IFS= read -r _verify_path; do
|
|
422
|
+
[ "$_verify_path" = "$_verify_dir" ] && continue
|
|
423
|
+
if [ -L "$_verify_path" ]; then
|
|
424
|
+
printf '%s\n' "$_verify_path"
|
|
425
|
+
break
|
|
426
|
+
fi
|
|
427
|
+
_verify_relative=${_verify_path#"$_verify_dir"/}
|
|
428
|
+
[ "$_verify_relative" = ".valise-manifest" ] && continue
|
|
429
|
+
if [ -f "$_verify_path" ]; then
|
|
430
|
+
_manifest_has_file "$_verify_manifest" "$_verify_relative" || { printf '%s\n' "$_verify_path"; break; }
|
|
431
|
+
elif [ -d "$_verify_path" ]; then
|
|
432
|
+
if [ "$(file_mode "$_verify_path")" != "0555" ] || ! _manifest_has_descendant "$_verify_manifest" "$_verify_relative"; then
|
|
433
|
+
printf '%s\n' "$_verify_path"
|
|
434
|
+
break
|
|
435
|
+
fi
|
|
436
|
+
else
|
|
437
|
+
printf '%s\n' "$_verify_path"
|
|
438
|
+
break
|
|
439
|
+
fi
|
|
440
|
+
done)
|
|
441
|
+
[ -z "$_verify_bad" ] || _verify_fail "release contains unmanifested or mutable path: $_verify_bad" || return 1
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
cmd_verify_release_root() {
|
|
446
|
+
[ "$#" -eq 1 ] || die "usage: valise _verify-release-root <release-root>"
|
|
447
|
+
_verify_release_arg=$1
|
|
448
|
+
case "$_verify_release_arg" in
|
|
449
|
+
/*) : ;;
|
|
450
|
+
*) die "release root must be absolute" ;;
|
|
451
|
+
esac
|
|
452
|
+
[ -d "$_verify_release_arg" ] || die "release root is not a directory"
|
|
453
|
+
[ ! -L "$_verify_release_arg" ] || die "release root must not be a symlink"
|
|
454
|
+
_verify_release_canonical=$(cd "$_verify_release_arg" 2>/dev/null && pwd -P) || die "cannot resolve release root"
|
|
455
|
+
_verify_releases_canonical=$(cd "$VALISE_RELEASES_DIR" 2>/dev/null && pwd -P) || die "release storage is unavailable"
|
|
456
|
+
_verify_release_parent=$(cd "$(path_parent "$_verify_release_canonical")" 2>/dev/null && pwd -P) || die "release parent unavailable"
|
|
457
|
+
[ "$_verify_release_parent" = "$_verify_releases_canonical" ] || die "release root escapes release storage"
|
|
458
|
+
_verify_release_id=${_verify_release_canonical##*/}
|
|
459
|
+
case "$_verify_release_id" in
|
|
460
|
+
*[!0123456789abcdefABCDEF]*|'') die "release root identity is not a SHA-256 digest" ;;
|
|
461
|
+
esac
|
|
462
|
+
[ "${#_verify_release_id}" -eq 64 ] || die "release root identity is not a SHA-256 digest"
|
|
463
|
+
_verify_release_snapshot="$_verify_release_canonical/.valise-manifest"
|
|
464
|
+
[ -f "$_verify_release_snapshot" ] || die "release manifest snapshot is missing"
|
|
465
|
+
[ ! -L "$_verify_release_snapshot" ] || die "release manifest snapshot must not be a symlink"
|
|
466
|
+
prepare_manifest_rows "$_verify_release_snapshot" || die "cannot read release manifest snapshot"
|
|
467
|
+
_verify_release_rows=$_manifest_rows
|
|
468
|
+
validate_manifest "$_verify_release_rows" || die "invalid release manifest snapshot"
|
|
469
|
+
verify_release_dir "$_verify_release_canonical" "$_verify_release_rows" "$_verify_release_id"
|
|
470
|
+
printf '%s\n' "$_verify_release_canonical"
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
cmd_verify_active() {
|
|
474
|
+
_verify_print_root=0
|
|
475
|
+
for _verify_arg do
|
|
476
|
+
case "$_verify_arg" in
|
|
477
|
+
--print-root) _verify_print_root=1 ;;
|
|
478
|
+
--help|-h) usage_release; return 0 ;;
|
|
479
|
+
*) die "unknown release verify option: $_verify_arg" ;;
|
|
480
|
+
esac
|
|
481
|
+
done
|
|
482
|
+
[ -L "$VALISE_ACTIVE_LINK" ] || die "active pointer is not a symlink"
|
|
483
|
+
_verify_active_target=$(readlink_target "$VALISE_ACTIVE_LINK" 2>/dev/null) || die "cannot read active pointer"
|
|
484
|
+
case "$_verify_active_target" in
|
|
485
|
+
/*) : ;;
|
|
486
|
+
*)
|
|
487
|
+
_verify_active_link_parent=$(cd "$(path_parent "$VALISE_ACTIVE_LINK")" 2>/dev/null && pwd -P) || die "active pointer parent unavailable"
|
|
488
|
+
_verify_active_target="$_verify_active_link_parent/$_verify_active_target"
|
|
489
|
+
;;
|
|
490
|
+
esac
|
|
491
|
+
[ -d "$_verify_active_target" ] || die "active release target is not a directory"
|
|
492
|
+
_verify_active_root=$(cd "$_verify_active_target" 2>/dev/null && pwd -P) || die "cannot resolve active release root"
|
|
493
|
+
_verify_releases_root=$(cd "$VALISE_RELEASES_DIR" 2>/dev/null && pwd -P) || die "release root unavailable"
|
|
494
|
+
_verify_active_parent=$(cd "$(path_parent "$_verify_active_root")" 2>/dev/null && pwd -P) || die "active release parent unavailable"
|
|
495
|
+
[ "$_verify_active_parent" = "$_verify_releases_root" ] || die "active release escapes release root"
|
|
496
|
+
_verify_active_id=${_verify_active_root##*/}
|
|
497
|
+
case "$_verify_active_id" in
|
|
498
|
+
*[!0123456789abcdefABCDEF]*|'') die "active release identity is not a SHA-256 digest" ;;
|
|
499
|
+
esac
|
|
500
|
+
[ "${#_verify_active_id}" -eq 64 ] || die "active release identity is not a SHA-256 digest"
|
|
501
|
+
_verify_active_manifest="$_verify_active_root/.valise-manifest"
|
|
502
|
+
[ -f "$_verify_active_manifest" ] || die "active release manifest is missing"
|
|
503
|
+
[ ! -L "$_verify_active_manifest" ] || die "active release manifest must not be a symlink"
|
|
504
|
+
prepare_manifest_rows "$_verify_active_manifest" || die "cannot read active release manifest"
|
|
505
|
+
_verify_active_manifest_rows="$_manifest_rows"
|
|
506
|
+
validate_manifest "$_verify_active_manifest_rows" || die "invalid active release manifest"
|
|
507
|
+
verify_release_dir "$_verify_active_root" "$_verify_active_manifest_rows" "$_verify_active_id"
|
|
508
|
+
if [ "$_verify_print_root" -eq 1 ]; then
|
|
509
|
+
printf '%s\n' "$_verify_active_root"
|
|
510
|
+
fi
|
|
511
|
+
return 0
|
|
512
|
+
}
|
|
513
|
+
cmd_release() {
|
|
514
|
+
_subcmd="${1:-list}"
|
|
515
|
+
case "$_subcmd" in
|
|
516
|
+
--help|-h) usage_release; return 0 ;;
|
|
517
|
+
list)
|
|
518
|
+
case "${2:-}" in
|
|
519
|
+
--help|-h) usage_release; return 0 ;;
|
|
520
|
+
esac
|
|
521
|
+
if [ -d "$VALISE_RELEASES_DIR" ]; then
|
|
522
|
+
_list_active_id=$(active_release_id 2>/dev/null || :)
|
|
523
|
+
for _rel in "$VALISE_RELEASES_DIR"/*/; do
|
|
524
|
+
[ -d "$_rel" ] || continue
|
|
525
|
+
_name=$(basename "$_rel")
|
|
526
|
+
if [ -n "$_list_active_id" ] && [ "$_name" = "$_list_active_id" ]; then
|
|
527
|
+
printf '* %s (active)\n' "$_name"
|
|
528
|
+
else
|
|
529
|
+
printf ' %s\n' "$_name"
|
|
530
|
+
fi
|
|
531
|
+
done
|
|
532
|
+
unset _list_active_id
|
|
533
|
+
else
|
|
534
|
+
info "no releases installed"
|
|
535
|
+
fi
|
|
536
|
+
;;
|
|
537
|
+
use)
|
|
538
|
+
case "${2:-}" in
|
|
539
|
+
--help|-h) usage_release; return 0 ;;
|
|
540
|
+
esac
|
|
541
|
+
_target="${2:-}"
|
|
542
|
+
[ -n "$_target" ] || die "usage: valise release use <sha256>"
|
|
543
|
+
[ "$#" -le 2 ] || die "usage: valise release use <sha256>"
|
|
544
|
+
release_activate "$_target"
|
|
545
|
+
info "switched to release $_target"
|
|
546
|
+
;;
|
|
547
|
+
prune)
|
|
548
|
+
shift
|
|
549
|
+
_prune_keep=3
|
|
550
|
+
_prune_keep_set=0
|
|
551
|
+
for _prune_arg do
|
|
552
|
+
case "$_prune_arg" in
|
|
553
|
+
--keep=*)
|
|
554
|
+
_prune_keep_set=1
|
|
555
|
+
_prune_keep=${_prune_arg#--keep=}
|
|
556
|
+
case "$_prune_keep" in
|
|
557
|
+
'' | *[!0-9]*) die "--keep requires a non-negative integer: $_prune_keep" ;;
|
|
558
|
+
esac
|
|
559
|
+
# Strip leading zeros so the shell does not read the value as
|
|
560
|
+
# octal in the arithmetic comparison below, and so the digit
|
|
561
|
+
# count measures the value rather than its spelling.
|
|
562
|
+
while [ "$_prune_keep" != "0" ] && [ "${_prune_keep#0}" != "$_prune_keep" ]; do
|
|
563
|
+
_prune_keep=${_prune_keep#0}
|
|
564
|
+
done
|
|
565
|
+
# A value beyond the shell's integer range makes the comparison
|
|
566
|
+
# below fail, which would read as "keep nothing" and remove every
|
|
567
|
+
# release. Reject it instead. Nine digits is the bound: every
|
|
568
|
+
# nine-digit value fits in a signed 32-bit integer, so no
|
|
569
|
+
# lexicographic comparison against the maximum is needed.
|
|
570
|
+
if [ "${#_prune_keep}" -gt 9 ]; then
|
|
571
|
+
die "--keep is too large: $_prune_keep (maximum is 999999999)"
|
|
572
|
+
fi
|
|
573
|
+
;;
|
|
574
|
+
--help|-h) usage_release; return 0 ;;
|
|
575
|
+
*) die "unknown release prune option: $_prune_arg" ;;
|
|
576
|
+
esac
|
|
577
|
+
done
|
|
578
|
+
[ -d "$VALISE_RELEASES_DIR" ] || die "no releases installed"
|
|
579
|
+
# The active release is identified by its canonical directory name, not
|
|
580
|
+
# by a path spelling. A trailing slash, a relative target, or an alias
|
|
581
|
+
# symlink must never make prune delete the release the pointer names.
|
|
582
|
+
_prune_active_id=$(active_release_id 2>/dev/null || :)
|
|
583
|
+
_prune_active=$_prune_active_id
|
|
584
|
+
# Collect the candidate releases with their modification times. The
|
|
585
|
+
# active release is excluded here and kept unconditionally below.
|
|
586
|
+
_prune_sort=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-prune.XXXXXX") ||
|
|
587
|
+
die "cannot create prune staging file"
|
|
588
|
+
for _prune_rel in "$VALISE_RELEASES_DIR"/*/; do
|
|
589
|
+
[ -d "$_prune_rel" ] || continue
|
|
590
|
+
[ ! -L "$_prune_rel" ] || continue
|
|
591
|
+
_prune_dir=${_prune_rel%/}
|
|
592
|
+
_prune_name=${_prune_dir##*/}
|
|
593
|
+
[ "$_prune_name" = "$_prune_active_id" ] && continue
|
|
594
|
+
_prune_mtime=$(file_mtime "$_prune_dir") || _prune_mtime=0
|
|
595
|
+
printf '%s\t%s\n' "$_prune_mtime" "$_prune_name" >>"$_prune_sort"
|
|
596
|
+
done
|
|
597
|
+
_prune_sort_path=$(trusted_utility_path sort) || die "no trusted sort utility available"
|
|
598
|
+
_prune_sorted=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-prune-sorted.XXXXXX") ||
|
|
599
|
+
die "cannot create prune ordering file"
|
|
600
|
+
# Newest first; equal timestamps fall back to the release name so the
|
|
601
|
+
# ordering is deterministic.
|
|
602
|
+
"$_prune_sort_path" -k1,1nr -k2,2 "$_prune_sort" >"$_prune_sorted" ||
|
|
603
|
+
die "cannot order releases for pruning"
|
|
604
|
+
_prune_index=0
|
|
605
|
+
_prune_removed=0
|
|
606
|
+
_prune_kept=0
|
|
607
|
+
while IFS="$(printf '\t')" read -r _prune_mtime _prune_name; do
|
|
608
|
+
[ -n "$_prune_name" ] || continue
|
|
609
|
+
_prune_index=$((_prune_index + 1))
|
|
610
|
+
if [ "$_prune_index" -le "$_prune_keep" ]; then
|
|
611
|
+
_prune_kept=$((_prune_kept + 1))
|
|
612
|
+
continue
|
|
613
|
+
fi
|
|
614
|
+
_prune_target="$VALISE_RELEASES_DIR/$_prune_name"
|
|
615
|
+
# A release is only removed after it verifies against its own
|
|
616
|
+
# manifest. A corrupted release is kept, never deleted, so a damaged
|
|
617
|
+
# installation can still be inspected and repaired.
|
|
618
|
+
_prune_manifest="$_prune_target/.valise-manifest"
|
|
619
|
+
if [ ! -f "$_prune_manifest" ] || [ -L "$_prune_manifest" ]; then
|
|
620
|
+
info "keeping unverifiable release $_prune_name (no manifest snapshot)"
|
|
621
|
+
_prune_kept=$((_prune_kept + 1))
|
|
622
|
+
continue
|
|
623
|
+
fi
|
|
624
|
+
if ! prepare_manifest_rows "$_prune_manifest" ||
|
|
625
|
+
! validate_manifest "$_manifest_rows"; then
|
|
626
|
+
info "keeping unverifiable release $_prune_name (invalid manifest)"
|
|
627
|
+
_prune_kept=$((_prune_kept + 1))
|
|
628
|
+
continue
|
|
629
|
+
fi
|
|
630
|
+
_verify_quiet=1
|
|
631
|
+
if ! verify_release_dir "$_prune_target" "$_manifest_rows" "$_prune_name"; then
|
|
632
|
+
_verify_quiet=0
|
|
633
|
+
info "keeping unverifiable release $_prune_name (failed verification)"
|
|
634
|
+
_prune_kept=$((_prune_kept + 1))
|
|
635
|
+
continue
|
|
636
|
+
fi
|
|
637
|
+
_verify_quiet=0
|
|
638
|
+
acquire_lock
|
|
639
|
+
if remove_tree "$_prune_target"; then
|
|
640
|
+
_prune_removed=$((_prune_removed + 1))
|
|
641
|
+
info "removed release $_prune_name"
|
|
642
|
+
else
|
|
643
|
+
info "keeping release $_prune_name (could not be removed)"
|
|
644
|
+
_prune_kept=$((_prune_kept + 1))
|
|
645
|
+
fi
|
|
646
|
+
release_lock
|
|
647
|
+
done <"$_prune_sorted"
|
|
648
|
+
_prune_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
649
|
+
"$_prune_rm" -f "$_prune_sort" "$_prune_sorted" || die "cannot remove prune staging files"
|
|
650
|
+
if [ -n "$_prune_active" ]; then
|
|
651
|
+
info "prune kept the active release plus $_prune_kept other release(s); removed $_prune_removed"
|
|
652
|
+
else
|
|
653
|
+
info "prune kept $_prune_kept release(s); removed $_prune_removed (no active release)"
|
|
654
|
+
fi
|
|
655
|
+
;;
|
|
656
|
+
rollback)
|
|
657
|
+
shift
|
|
658
|
+
cmd_release_rollback "$@"
|
|
659
|
+
;;
|
|
660
|
+
verify)
|
|
661
|
+
if [ "$#" -gt 0 ]; then
|
|
662
|
+
shift
|
|
663
|
+
fi
|
|
664
|
+
cmd_verify_active "$@"
|
|
665
|
+
;;
|
|
666
|
+
*)
|
|
667
|
+
die "unknown release subcommand: $_subcmd (use list, use, rollback, prune, or verify)"
|
|
668
|
+
;;
|
|
669
|
+
esac
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
cmd_install() {
|
|
673
|
+
_install_integration=1
|
|
674
|
+
for _arg do
|
|
675
|
+
case "$_arg" in
|
|
676
|
+
--help|-h) usage_install; return 0 ;;
|
|
677
|
+
--no-integration) _install_integration=0 ;;
|
|
678
|
+
*) die "unknown install option: $_arg (usage: valise install [--no-integration])" ;;
|
|
679
|
+
esac
|
|
680
|
+
done
|
|
681
|
+
|
|
682
|
+
_manifest_source=$(locate_manifest) ||
|
|
683
|
+
die "cannot find manifest.tsv in the current directory (run valise install from a release tree built by valise build)"
|
|
684
|
+
_manifest_root=$(cd "$(path_parent "$_manifest_source")" && pwd -P) || die "cannot resolve manifest source root"
|
|
685
|
+
prepare_manifest_rows "$_manifest_source" || die "cannot normalize manifest.tsv"
|
|
686
|
+
_manifest="$_manifest_rows"
|
|
687
|
+
validate_manifest "$_manifest" || die "invalid manifest.tsv schema or row"
|
|
688
|
+
|
|
689
|
+
# Only a tree built by valise build is installable. The integration loads
|
|
690
|
+
# plans/<shell>.sh and runtime/fish/profile.fish, which only that shape
|
|
691
|
+
# carries; installing anything else would wire startup files to nothing.
|
|
692
|
+
_install_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
693
|
+
"$_install_awk" -F '\t' '$1 == "valise-release.conf" { found = 1 } END { exit found ? 0 : 1 }' "$_manifest" ||
|
|
694
|
+
die "manifest.tsv does not list valise-release.conf: only a release tree built by valise build can be installed"
|
|
695
|
+
[ "$(release_conf_value "$_manifest_root" format 2>/dev/null)" = 2 ] ||
|
|
696
|
+
die "valise-release.conf does not declare format=2: rebuild the release with this valise"
|
|
697
|
+
release_commands "$_manifest_root" >/dev/null ||
|
|
698
|
+
die "valise-release.conf lists an invalid command name: rebuild the release with valise build"
|
|
699
|
+
|
|
700
|
+
# Everything install will write must be writable before anything changes.
|
|
701
|
+
if [ "$_install_integration" -eq 1 ]; then
|
|
702
|
+
integration_preflight "$_manifest_root"
|
|
703
|
+
if ! _install_unmanaged=$(integration_unmanaged_report); then
|
|
704
|
+
info "warning: these lines load valise outside a managed block, so it may load twice; remove them:"
|
|
705
|
+
printf '%s\n' "$_install_unmanaged"
|
|
706
|
+
fi
|
|
707
|
+
fi
|
|
708
|
+
|
|
709
|
+
# Release identity is content-derived; the schema header is not an ID.
|
|
710
|
+
_release_id=$(file_sha256 "$_manifest") || die "no trusted SHA-256 utility available"
|
|
711
|
+
case "$_release_id" in
|
|
712
|
+
*[!0123456789abcdefABCDEF]*|'') die "invalid release ID derived from manifest" ;;
|
|
713
|
+
esac
|
|
714
|
+
[ "${#_release_id}" -eq 64 ] || die "invalid release ID derived from manifest"
|
|
715
|
+
|
|
716
|
+
_target_dir="$VALISE_RELEASES_DIR/$_release_id"
|
|
717
|
+
|
|
718
|
+
acquire_lock
|
|
719
|
+
_mkdir_path=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
720
|
+
_chmod_path=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
721
|
+
"$_mkdir_path" -p "$VALISE_XDG_CONFIG" "$VALISE_XDG_STATE" || die "cannot create valise configuration directories"
|
|
722
|
+
[ ! -L "$_target_dir" ] || die "release target must not be a symlink: $_release_id"
|
|
723
|
+
|
|
724
|
+
if [ -e "$_target_dir" ] && [ ! -d "$_target_dir" ]; then
|
|
725
|
+
die "release path is not a directory: $_target_dir"
|
|
726
|
+
fi
|
|
727
|
+
if [ -d "$_target_dir" ]; then
|
|
728
|
+
_target_manifest="$_target_dir/.valise-manifest"
|
|
729
|
+
if [ -e "$_target_manifest" ] || [ -L "$_target_manifest" ]; then
|
|
730
|
+
[ -f "$_target_manifest" ] || die "release manifest snapshot is not a regular file"
|
|
731
|
+
[ ! -L "$_target_manifest" ] || die "release manifest snapshot must not be a symlink"
|
|
732
|
+
prepare_manifest_rows "$_target_manifest" || die "cannot snapshot release manifest"
|
|
733
|
+
_target_manifest="$_manifest_rows"
|
|
734
|
+
validate_manifest "$_target_manifest" || die "invalid release manifest snapshot"
|
|
735
|
+
verify_release_dir "$_target_dir" "$_target_manifest" "$_release_id"
|
|
736
|
+
else
|
|
737
|
+
verify_release_dir "$_target_dir" "$_manifest" "$_release_id"
|
|
738
|
+
fi
|
|
739
|
+
info "release $_release_id already installed and verified"
|
|
740
|
+
else
|
|
741
|
+
info "installing release $_release_id..."
|
|
742
|
+
|
|
743
|
+
# Create target directories
|
|
744
|
+
"$_mkdir_path" -p "$VALISE_RELEASES_DIR" "$VALISE_XDG_CONFIG" "$VALISE_XDG_STATE"
|
|
745
|
+
|
|
746
|
+
# Stage into temp directory first
|
|
747
|
+
_stage=$(trusted_mktemp dir "${TMPDIR:-/tmp}/valise-install.XXXXXX") || die "cannot create staging directory"
|
|
748
|
+
_cp_path=$(trusted_utility_path cp) || die "no trusted cp utility available"
|
|
749
|
+
"$_cp_path" -p "$_manifest" "$_stage/.valise-manifest" || die "failed to stage release manifest"
|
|
750
|
+
"$_chmod_path" 0444 "$_stage/.valise-manifest" || die "failed to seal release manifest"
|
|
751
|
+
|
|
752
|
+
# Copy release files from manifest
|
|
753
|
+
# The manifest format is: destination source mode install_mode size sha256
|
|
754
|
+
while IFS="$(printf '\t')" read -r _dest _src _mode _imode _size _sha; do
|
|
755
|
+
[ -n "$_dest" ] || continue
|
|
756
|
+
case "$_dest" in \#*|valise-manifest-v2|destination) continue ;; esac
|
|
757
|
+
case "$_src" in
|
|
758
|
+
""|.|./*|*/.|*/./*|*//*|/*|*/../*|../*|*/..|..*) die "invalid manifest source path: $_src" ;;
|
|
759
|
+
esac
|
|
760
|
+
case "$_dest" in
|
|
761
|
+
""|.|./*|*/.|*/./*|*//*|/*|*/../*|../*|*/..|..*) die "invalid manifest destination path: $_dest" ;;
|
|
762
|
+
esac
|
|
763
|
+
_src_path="$_manifest_root/$_src"
|
|
764
|
+
_dest_path="$_stage/$_dest"
|
|
765
|
+
[ -f "$_src_path" ] || die "manifest source missing: $_src"
|
|
766
|
+
source_path_is_physical "$_manifest_root" "$_src" || die "manifest source path contains a symlink: $_src"
|
|
767
|
+
[ ! -L "$_src_path" ] || die "manifest source must not be a symlink: $_src"
|
|
768
|
+
_src_parent=$(cd "$(path_parent "$_src_path")" 2>/dev/null && pwd -P) || die "manifest source parent missing: $_src"
|
|
769
|
+
case "$_src_parent/" in
|
|
770
|
+
"$_manifest_root/"*) : ;;
|
|
771
|
+
*) die "manifest source escapes source root: $_src" ;;
|
|
772
|
+
esac
|
|
773
|
+
[ "$(file_mode "$_src_path")" = "$_mode" ] || die "manifest source mode mismatch: $_src"
|
|
774
|
+
[ "$(file_bytes "$_src_path")" = "$_size" ] || die "manifest source byte count mismatch: $_src"
|
|
775
|
+
case "$_mode:$_imode" in
|
|
776
|
+
0644:0444|0755:0555) : ;;
|
|
777
|
+
*) die "invalid immutable mode pair for $_dest" ;;
|
|
778
|
+
esac
|
|
779
|
+
[ -n "$_sha" ] || die "manifest digest missing: $_dest"
|
|
780
|
+
case "$_sha" in
|
|
781
|
+
*[!0123456789abcdefABCDEF]*) die "invalid manifest digest for $_dest" ;;
|
|
782
|
+
esac
|
|
783
|
+
[ "${#_sha}" -eq 64 ] || die "invalid manifest digest for $_dest"
|
|
784
|
+
_dest_parent=$(path_parent "$_dest_path")
|
|
785
|
+
"$_mkdir_path" -p "$_dest_parent"
|
|
786
|
+
"$_cp_path" -p "$_src_path" "$_dest_path" || die "failed to copy $_src"
|
|
787
|
+
"$_chmod_path" "$_imode" "$_dest_path" || die "failed to seal $_dest"
|
|
788
|
+
done < "${_manifest_parse:-$_manifest_rows}"
|
|
789
|
+
|
|
790
|
+
# Verify staged files against manifest digests
|
|
791
|
+
_verify_ok=1
|
|
792
|
+
while IFS="$(printf '\t')" read -r _dest _src _mode _imode _size _sha; do
|
|
793
|
+
[ -n "$_dest" ] || continue
|
|
794
|
+
case "$_dest" in \#*|valise-manifest-v2|destination) continue ;; esac
|
|
795
|
+
case "$_dest" in
|
|
796
|
+
""|.|./*|*/.|*/./*|*//*|/*|*/../*|../*|*/..|..*) die "invalid manifest destination path: $_dest" ;;
|
|
797
|
+
esac
|
|
798
|
+
_staged_file="$_stage/$_dest"
|
|
799
|
+
[ -f "$_staged_file" ] || die "staged manifest file missing: $_dest"
|
|
800
|
+
[ "$(file_mode "$_staged_file")" = "$_imode" ] || die "staged install mode mismatch: $_dest"
|
|
801
|
+
[ "$(file_bytes "$_staged_file")" = "$_size" ] || die "staged byte count mismatch: $_dest"
|
|
802
|
+
[ -n "$_sha" ] || die "manifest digest missing: $_dest"
|
|
803
|
+
case "$_sha" in
|
|
804
|
+
*[!0123456789abcdefABCDEF]*) die "invalid manifest digest for $_dest" ;;
|
|
805
|
+
esac
|
|
806
|
+
[ "${#_sha}" -eq 64 ] || die "invalid manifest digest for $_dest"
|
|
807
|
+
_actual=$(file_sha256 "$_staged_file") || die "no trusted SHA-256 utility available"
|
|
808
|
+
_expected=$(printf '%s' "$_sha" | lower_hex) || die "no trusted tr utility available"
|
|
809
|
+
if [ "$_actual" != "$_expected" ]; then
|
|
810
|
+
die "digest mismatch for $_dest: expected $_sha got $_actual"
|
|
811
|
+
fi
|
|
812
|
+
done < "${_manifest_parse:-$_manifest_rows}"
|
|
813
|
+
|
|
814
|
+
# Seal all release directories; files already received their declared
|
|
815
|
+
# immutable mode while being staged. Never publish an incompletely sealed
|
|
816
|
+
# tree.
|
|
817
|
+
seal_tree "$_stage" || die "failed to seal release directories"
|
|
818
|
+
|
|
819
|
+
# Publish through a temporary sibling, then atomically rename the
|
|
820
|
+
# complete immutable tree into place. The EXIT/signal cleanup removes any
|
|
821
|
+
# incomplete sibling before it can poison this release ID.
|
|
822
|
+
_publish_tmp="$VALISE_RELEASES_DIR/.valise-release.$_release_id.$$"
|
|
823
|
+
[ ! -e "$_publish_tmp" ] && [ ! -L "$_publish_tmp" ] || die "release publication temporary path already exists"
|
|
824
|
+
"$_cp_path" -rp "$_stage" "$_publish_tmp" || die "failed to publish release"
|
|
825
|
+
seal_tree "$_publish_tmp" || die "failed to seal published release"
|
|
826
|
+
verify_release_dir "$_publish_tmp" "$_manifest" "$_release_id"
|
|
827
|
+
[ ! -e "$_target_dir" ] && [ ! -L "$_target_dir" ] || die "release target appeared during publication"
|
|
828
|
+
_mv_path=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
829
|
+
"$_mv_path" "$_publish_tmp" "$_target_dir" || die "failed to publish release"
|
|
830
|
+
_publish_tmp=""
|
|
831
|
+
[ -d "$_target_dir" ] || die "published release directory is missing"
|
|
832
|
+
verify_release_dir "$_target_dir" "$_manifest" "$_release_id"
|
|
833
|
+
remove_tree "$_stage" || die "failed to remove staging directory"
|
|
834
|
+
_stage=""
|
|
835
|
+
info "release $_release_id installed"
|
|
836
|
+
fi
|
|
837
|
+
|
|
838
|
+
# Update active symlink atomically without clobbering a non-symlink path.
|
|
839
|
+
if [ ! -L "$VALISE_ACTIVE_LINK" ] || [ "$(readlink_target "$VALISE_ACTIVE_LINK" 2>/dev/null)" != "$_target_dir" ]; then
|
|
840
|
+
if [ -e "$VALISE_ACTIVE_LINK" ] && [ ! -L "$VALISE_ACTIVE_LINK" ]; then
|
|
841
|
+
die "active path exists and is not a symlink: $VALISE_ACTIVE_LINK"
|
|
842
|
+
fi
|
|
843
|
+
# From here a failure puts the previous active release back.
|
|
844
|
+
install_txn_begin
|
|
845
|
+
_install_txn=1
|
|
846
|
+
switch_active_link "$_target_dir"
|
|
847
|
+
info "active release set to $_release_id"
|
|
848
|
+
fi
|
|
849
|
+
|
|
850
|
+
# Deploy the Fish conf.d hook when Fish is available. The hook loads the
|
|
851
|
+
# release's Fish runtime when it has one and does nothing otherwise.
|
|
852
|
+
_fish_path=$(trusted_utility_path fish 2>/dev/null || :)
|
|
853
|
+
if [ "$_install_integration" -eq 1 ] && [ -n "$_fish_path" ]; then
|
|
854
|
+
_fish_conf_dir="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d"
|
|
855
|
+
_fish_hook="$_fish_conf_dir/valise.fish"
|
|
856
|
+
_mkdir_path=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
857
|
+
"$_mkdir_path" -p "$_fish_conf_dir" || die "cannot create Fish configuration directory"
|
|
858
|
+
[ ! -L "$_fish_hook" ] || die "Fish hook path must not be a symlink: $_fish_hook"
|
|
859
|
+
if [ -d "$_fish_hook" ]; then
|
|
860
|
+
die "Fish hook path is a directory: $_fish_hook"
|
|
861
|
+
fi
|
|
862
|
+
# Use the platform mktemp in the destination directory. Its exclusive
|
|
863
|
+
# creation makes a pre-created predictable symlink unable to redirect the
|
|
864
|
+
# shell redirection below into an attacker-selected victim.
|
|
865
|
+
_hook_tmp=$(trusted_mktemp_at "$_fish_conf_dir" file "$_fish_conf_dir/.valise-hook.XXXXXX") || die "cannot create Fish hook staging file"
|
|
866
|
+
[ -f "$_hook_tmp" ] && [ ! -L "$_hook_tmp" ] || die "Fish hook staging path is unsafe"
|
|
867
|
+
print_fish_integration >"$_hook_tmp" || die "cannot write Fish hook"
|
|
868
|
+
_chmod_path=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
869
|
+
"$_chmod_path" 0644 "$_hook_tmp" || die "cannot set Fish hook mode"
|
|
870
|
+
_cat_path=$(trusted_utility_path cat) || die "no trusted cat utility available"
|
|
871
|
+
_hook_expected=$("$_cat_path" "$_hook_tmp") || die "cannot read Fish hook staging file"
|
|
872
|
+
_hook_cmp=$(trusted_utility_path cmp) || die "no trusted cmp utility available"
|
|
873
|
+
if [ -f "$_fish_hook" ] && [ "$(file_mode "$_fish_hook")" = 0644 ] &&
|
|
874
|
+
"$_hook_cmp" -s "$_hook_tmp" "$_fish_hook"; then
|
|
875
|
+
# An identical hook stays in place, so its inode and mtime change only
|
|
876
|
+
# when its content does.
|
|
877
|
+
_rm_path=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
878
|
+
"$_rm_path" -f "$_hook_tmp" || die "cannot remove Fish hook staging file"
|
|
879
|
+
_hook_tmp=""
|
|
880
|
+
info "Fish hook current: $_fish_hook"
|
|
881
|
+
else
|
|
882
|
+
_mv_path=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
883
|
+
# mv -f uses rename(2) which atomically replaces a regular file
|
|
884
|
+
# destination on both BSD and GNU systems. The hook path was verified
|
|
885
|
+
# non-symlink above, so no explicit removal is needed — mv -f alone
|
|
886
|
+
# preserves atomicity.
|
|
887
|
+
"$_mv_path" -f "$_hook_tmp" "$_fish_hook" || die "cannot publish Fish hook"
|
|
888
|
+
_hook_tmp=""
|
|
889
|
+
[ -f "$_fish_hook" ] || die "Fish hook was not published"
|
|
890
|
+
[ "$(file_mode "$_fish_hook")" = 0644 ] || die "Fish hook mode mismatch"
|
|
891
|
+
_hook_actual=$("$_cat_path" "$_fish_hook") || die "cannot read published Fish hook"
|
|
892
|
+
[ "$_hook_actual" = "$_hook_expected" ] || die "published Fish hook contents mismatch"
|
|
893
|
+
_txn_changed="$_txn_changed $_fish_hook"
|
|
894
|
+
info "Fish hook deployed: $_fish_hook"
|
|
895
|
+
fi
|
|
896
|
+
fi
|
|
897
|
+
|
|
898
|
+
if [ "$_install_integration" -eq 1 ]; then
|
|
899
|
+
write_posix_integration
|
|
900
|
+
reconcile_commands "$_target_dir"
|
|
901
|
+
fi
|
|
902
|
+
|
|
903
|
+
_install_txn=0
|
|
904
|
+
release_lock
|
|
905
|
+
info "installation complete"
|
|
906
|
+
if [ "$_install_integration" -eq 1 ]; then
|
|
907
|
+
info "run 'valise uninstall' to remove the integration, the commands, and the releases"
|
|
908
|
+
else
|
|
909
|
+
info "run 'valise init' to print the shell integration snippet for manual setup"
|
|
910
|
+
fi
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
cmd_make_manifest() {
|
|
914
|
+
_mm_tree=""
|
|
915
|
+
_mm_out=""
|
|
916
|
+
_mm_want_out=0
|
|
917
|
+
for _mm_arg do
|
|
918
|
+
case "$_mm_arg" in
|
|
919
|
+
--output) _mm_want_out=1 ;;
|
|
920
|
+
--output=*)
|
|
921
|
+
_mm_out=${_mm_arg#--output=}
|
|
922
|
+
[ -n "$_mm_out" ] || die "usage: valise make-manifest <tree> [--output <file>]"
|
|
923
|
+
;;
|
|
924
|
+
--help|-h) printf '%s\n' "usage: valise make-manifest <tree> [--output <file>]"; return 0 ;;
|
|
925
|
+
--*) die "unknown make-manifest option: $_mm_arg" ;;
|
|
926
|
+
*)
|
|
927
|
+
if [ "$_mm_want_out" -eq 1 ]; then
|
|
928
|
+
_mm_out=$_mm_arg
|
|
929
|
+
_mm_want_out=0
|
|
930
|
+
elif [ -n "$_mm_tree" ]; then
|
|
931
|
+
die "usage: valise make-manifest <tree> [--output <file>]"
|
|
932
|
+
else
|
|
933
|
+
_mm_tree=$_mm_arg
|
|
934
|
+
fi
|
|
935
|
+
;;
|
|
936
|
+
esac
|
|
937
|
+
done
|
|
938
|
+
[ "$_mm_want_out" -eq 0 ] || die "usage: valise make-manifest <tree> [--output <file>]"
|
|
939
|
+
[ -n "$_mm_tree" ] || die "usage: valise make-manifest <tree> [--output <file>]"
|
|
940
|
+
[ -d "$_mm_tree" ] || die "manifest tree is not a directory: $_mm_tree"
|
|
941
|
+
_mm_root=$(cd "$_mm_tree" && pwd -P) || die "cannot resolve manifest tree: $_mm_tree"
|
|
942
|
+
_mm_default_out="$_mm_root/manifest.tsv"
|
|
943
|
+
if [ -z "$_mm_out" ]; then
|
|
944
|
+
_mm_out="$_mm_default_out"
|
|
945
|
+
fi
|
|
946
|
+
case "$_mm_out" in
|
|
947
|
+
/*) : ;;
|
|
948
|
+
*)
|
|
949
|
+
while :; do
|
|
950
|
+
case "$_mm_out" in
|
|
951
|
+
./*) _mm_out=${_mm_out#./} ;;
|
|
952
|
+
*) break ;;
|
|
953
|
+
esac
|
|
954
|
+
done
|
|
955
|
+
_mm_cwd=$(pwd -P) || die "cannot resolve working directory"
|
|
956
|
+
_mm_out="$_mm_cwd/$_mm_out"
|
|
957
|
+
;;
|
|
958
|
+
esac
|
|
959
|
+
_mm_out=$(path_normalize "$_mm_out") || die "cannot normalize manifest output path"
|
|
960
|
+
_mm_out_dir=$(path_parent "$_mm_out") || die "cannot resolve manifest parent"
|
|
961
|
+
_mm_mkdir_path=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
962
|
+
"$_mm_mkdir_path" -p "$_mm_out_dir" || die "cannot create manifest directory: $_mm_out_dir"
|
|
963
|
+
# Resolve the parent physically: find reports physical paths, and /tmp is a
|
|
964
|
+
# symlink to /private/tmp on macOS. A lexical comparison would include the
|
|
965
|
+
# manifest in itself on the second run.
|
|
966
|
+
_mm_out_dir=$(cd "$_mm_out_dir" && pwd -P) || die "cannot resolve manifest parent: $_mm_out_dir"
|
|
967
|
+
_mm_out="$_mm_out_dir/${_mm_out##*/}"
|
|
968
|
+
if [ -e "$_mm_out" ] || [ -L "$_mm_out" ]; then
|
|
969
|
+
[ -f "$_mm_out" ] && [ ! -L "$_mm_out" ] ||
|
|
970
|
+
die "manifest output is not a regular file: $_mm_out"
|
|
971
|
+
fi
|
|
972
|
+
_mm_find_path=$(trusted_utility_path find) || die "no trusted find utility available"
|
|
973
|
+
_mm_sort_path=$(trusted_utility_path sort) || die "no trusted sort utility available"
|
|
974
|
+
# Symlinks never enter a sealed release: find without -L reports them as
|
|
975
|
+
# -type l, so any symlink anywhere under the tree fails closed here.
|
|
976
|
+
_mm_links=$("$_mm_find_path" "$_mm_root" -type l -print) || die "cannot scan manifest tree: $_mm_root"
|
|
977
|
+
[ -z "$_mm_links" ] || die "manifest tree must not contain symlinks: $_mm_root"
|
|
978
|
+
_mm_list=$("$_mm_find_path" "$_mm_root" -type f -print) || die "cannot list manifest tree: $_mm_root"
|
|
979
|
+
_mm_tmp=$(trusted_mktemp_at "$_mm_out_dir" file "$_mm_out_dir/.valise-manifest.XXXXXX") || die "cannot stage manifest"
|
|
980
|
+
_mm_tab=$(printf '\t')
|
|
981
|
+
_mm_nl='
|
|
982
|
+
'
|
|
983
|
+
_mm_count=0
|
|
984
|
+
printf '%s\n' "valise-manifest-v2" >"$_mm_tmp" || die "cannot write manifest"
|
|
985
|
+
printf 'destination\tsource\tsource_mode\tinstall_mode\tbytes\tsha256\n' >>"$_mm_tmp" || die "cannot write manifest"
|
|
986
|
+
_mm_sorted=$(printf '%s\n' "$_mm_list" | LC_ALL=C "$_mm_sort_path") || die "cannot sort manifest tree listing"
|
|
987
|
+
while IFS= read -r _mm_full || [ -n "$_mm_full" ]; do
|
|
988
|
+
[ -n "$_mm_full" ] || continue
|
|
989
|
+
# The manifest describes the tree payload, never itself: both the
|
|
990
|
+
# requested output and the default output path stay out of the rows so
|
|
991
|
+
# consecutive runs over an unchanged tree are byte-identical.
|
|
992
|
+
[ "$_mm_full" != "$_mm_out" ] || continue
|
|
993
|
+
[ "$_mm_full" != "$_mm_default_out" ] || continue
|
|
994
|
+
[ "$_mm_full" != "$_mm_tmp" ] || continue
|
|
995
|
+
_mm_rel=${_mm_full#"$_mm_root"/}
|
|
996
|
+
# Same name rules as validate_manifest: the schema charset plus no
|
|
997
|
+
# tab/newline (row delimiters) and no dot segments. Anything else
|
|
998
|
+
# fails here naming the file instead of at validation time.
|
|
999
|
+
case "$_mm_rel" in
|
|
1000
|
+
*"$_mm_nl"*|*"$_mm_tab"*|*[!A-Za-z0-9._/-]*|"#"*|.|..|./*|../*|*/.|*/..|*/./*|*/../*) die "manifest tree file has an unrepresentable name: $_mm_full" ;;
|
|
1001
|
+
esac
|
|
1002
|
+
_mm_mode=$(file_mode "$_mm_full") || die "cannot read file mode: $_mm_full"
|
|
1003
|
+
case "$_mm_mode" in
|
|
1004
|
+
0644) _mm_imode=0444 ;;
|
|
1005
|
+
0755) _mm_imode=0555 ;;
|
|
1006
|
+
*) die "manifest tree file has unsupported mode $_mm_mode (normalize to 0644 or 0755): $_mm_full" ;;
|
|
1007
|
+
esac
|
|
1008
|
+
_mm_bytes=$(file_bytes "$_mm_full") || die "cannot read file size: $_mm_full"
|
|
1009
|
+
_mm_sha=$(file_sha256 "$_mm_full") || die "no trusted SHA-256 utility available"
|
|
1010
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$_mm_rel" "$_mm_rel" "$_mm_mode" "$_mm_imode" "$_mm_bytes" "$_mm_sha" >>"$_mm_tmp" || die "cannot write manifest"
|
|
1011
|
+
_mm_count=$((_mm_count + 1))
|
|
1012
|
+
done <<_MM_SORTED_EOF
|
|
1013
|
+
$_mm_sorted
|
|
1014
|
+
_MM_SORTED_EOF
|
|
1015
|
+
[ "$_mm_count" -gt 0 ] || die "manifest tree contains no files: $_mm_root"
|
|
1016
|
+
validate_manifest "$_mm_tmp" || die "generated manifest failed validation"
|
|
1017
|
+
_mm_mv_path=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
1018
|
+
"$_mm_mv_path" -f "$_mm_tmp" "$_mm_out" || die "cannot publish manifest: $_mm_out"
|
|
1019
|
+
_mm_chmod_path=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
1020
|
+
"$_mm_chmod_path" 0644 "$_mm_out" || die "cannot set manifest mode: $_mm_out"
|
|
1021
|
+
_mm_tmp=""
|
|
1022
|
+
info "manifest written: $_mm_out ($_mm_count files)"
|
|
1023
|
+
}
|