Fix: Re-run verified installer inputs
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
# * setup-identity and install-hardware are dropped. They exist only to
|
# * setup-identity and install-hardware are dropped. They exist only to
|
||||||
# consume interview answers -- git identity, NVIDIA, Secure Boot, firmware
|
# consume interview answers -- git identity, NVIDIA, Secure Boot, firmware
|
||||||
# -- and every one of those is a first-run decision.
|
# -- and every one of those is a first-run decision.
|
||||||
# * install-packages runs only when the package lists actually changed.
|
# * install-packages runs only when its tracked installation inputs changed.
|
||||||
# * Migrations always run rather than baseline. See the migrations block.
|
# * Migrations always run rather than baseline. See the migrations block.
|
||||||
#
|
#
|
||||||
# Everything else is shared on purpose: the sudo keepalive, the per-stage
|
# Everything else is shared on purpose: the sudo keepalive, the per-stage
|
||||||
@@ -64,12 +64,15 @@ done
|
|||||||
|
|
||||||
source "$PANAMA_PATH/bin/ascii"
|
source "$PANAMA_PATH/bin/ascii"
|
||||||
|
|
||||||
# ── Have the package lists changed? ──────────────────────────────────────────
|
# ── Have the installation inputs changed? ───────────────────────────────────
|
||||||
#
|
#
|
||||||
# install-packages is the slow stage -- a dnf metadata refresh, a Flathub
|
# install-packages is the slow stage -- a dnf metadata refresh, a Flathub
|
||||||
# round-trip, and a transaction that resolves to "nothing to do" almost every
|
# round-trip, and a transaction that resolves to "nothing to do" almost every
|
||||||
# time. On an upgrade it is worth running only when the lists it reads actually
|
# time. On an upgrade it is worth running only when its package lists or
|
||||||
# changed, so this hashes them and remembers the result.
|
# reviewed installer trust inputs changed, so this hashes them and remembers
|
||||||
|
# the result. The framed, sorted stream includes top-level package files, the
|
||||||
|
# package-stage adapter, the provenance helper, and regular provenance files;
|
||||||
|
# both relative paths and bytes are part of the state.
|
||||||
#
|
#
|
||||||
# A content hash rather than a git range, because Panama is developed in place:
|
# A content hash rather than a git range, because Panama is developed in place:
|
||||||
# a package added to a list and not yet committed must still install. A range
|
# a package added to a list and not yet committed must still install. A range
|
||||||
@@ -86,8 +89,21 @@ STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
|||||||
PACKAGES_HASH="$STATE_DIR/packages-hash"
|
PACKAGES_HASH="$STATE_DIR/packages-hash"
|
||||||
|
|
||||||
hash_packages() {
|
hash_packages() {
|
||||||
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -exec sha256sum {} + \
|
local file relative size
|
||||||
| sort | sha256sum | cut -d' ' -f1
|
|
||||||
|
{
|
||||||
|
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -print0
|
||||||
|
printf '%s\0' \
|
||||||
|
"$PANAMA_PATH/setup/scripts/install-packages" \
|
||||||
|
"$PANAMA_PATH/setup/lib/artifact-provenance"
|
||||||
|
find "$PANAMA_PATH/setup/provenance" -type f -print0
|
||||||
|
} | LC_ALL=C sort -z | while IFS= read -r -d '' file; do
|
||||||
|
relative="${file#"$PANAMA_PATH"/}"
|
||||||
|
size="$(wc -c <"$file")"
|
||||||
|
printf '%s\0%s\0' "$relative" "$size"
|
||||||
|
cat -- "$file"
|
||||||
|
printf '\0'
|
||||||
|
done | sha256sum | cut -d' ' -f1
|
||||||
}
|
}
|
||||||
|
|
||||||
packages_needed() {
|
packages_needed() {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
|
|||||||
[[ -f "$extension/package.json" ]] || continue
|
[[ -f "$extension/package.json" ]] || continue
|
||||||
name="$(basename "$extension")"
|
name="$(basename "$extension")"
|
||||||
|
|
||||||
# Skip a build that would produce what is already there. `npm install`
|
# Skip a build that would produce what is already there. `npm ci`
|
||||||
# alone takes long enough to be worth not repeating on every re-run of
|
# alone takes long enough to be worth not repeating on every re-run of
|
||||||
# a stage that is otherwise nearly instant.
|
# a stage that is otherwise nearly instant.
|
||||||
built="$vicinae_data_dir/extensions/$name"
|
built="$vicinae_data_dir/extensions/$name"
|
||||||
@@ -91,7 +91,7 @@ if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'Building Vicinae extension %s\n' "$name"
|
printf 'Building Vicinae extension %s\n' "$name"
|
||||||
if ! (cd "$extension" && npm install --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
|
if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
|
||||||
printf 'Vicinae extension %s did not build; skipping\n' "$name" >&2
|
printf 'Vicinae extension %s did not build; skipping\n' "$name" >&2
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -116,6 +116,41 @@ grep -q '/etc/profile.d/nvm.sh' "$stage" \
|
|||||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
||||||
|| note 'the extension node_modules is not gitignored'
|
|| note 'the extension node_modules is not gitignored'
|
||||||
|
|
||||||
|
# npm must honour the committed dependency graph. This disposable fixture
|
||||||
|
# simulates npm rejecting a mismatched lockfile, which must leave that lockfile
|
||||||
|
# untouched and keep the launcher stage nonfatal.
|
||||||
|
fixture_root="$(mktemp -d -t panama-vicinae-lock.XXXXXX)"
|
||||||
|
trap 'rm -rf -- "$fixture_root"' EXIT
|
||||||
|
mkdir -p "$fixture_root/config/local/share/vicinae/scripts" \
|
||||||
|
"$fixture_root/config/local/share/vicinae/extensions/panama-search/src" \
|
||||||
|
"$fixture_root/bin"
|
||||||
|
printf '{"name":"panama-search","dependencies":{"left-pad":"1.3.0"}}\n' \
|
||||||
|
>"$fixture_root/config/local/share/vicinae/extensions/panama-search/package.json"
|
||||||
|
printf '{"lockfileVersion":3,"packages":{}}\n' \
|
||||||
|
>"$fixture_root/config/local/share/vicinae/extensions/panama-search/package-lock.json"
|
||||||
|
lockfile="$fixture_root/config/local/share/vicinae/extensions/panama-search/package-lock.json"
|
||||||
|
lock_before="$fixture_root/package-lock.before"
|
||||||
|
cp -- "$lockfile" "$lock_before"
|
||||||
|
cat >"$fixture_root/bin/npm" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" >>"${NPM_LOG:?}"
|
||||||
|
[[ "${1:-}" == ci ]] || exit 64
|
||||||
|
exit 1
|
||||||
|
EOF
|
||||||
|
chmod +x "$fixture_root/bin/npm"
|
||||||
|
stage_status=0
|
||||||
|
stage_output="$(PATH="$fixture_root/bin:$PATH" PANAMA_PATH="$fixture_root" \
|
||||||
|
VICINAE_DATA_DIR="$fixture_root/vicinae-data" NPM_LOG="$fixture_root/npm.log" \
|
||||||
|
bash "$stage" 2>&1)" || stage_status=$?
|
||||||
|
[[ "$stage_status" -eq 0 ]] \
|
||||||
|
|| note "the Vicinae stage returned $stage_status for a lockfile mismatch instead of remaining nonfatal"
|
||||||
|
[[ "$(<"$fixture_root/npm.log")" == 'ci --silent' ]] \
|
||||||
|
|| note 'the Vicinae extension dependency command was not npm ci --silent'
|
||||||
|
[[ "$stage_output" == *'Vicinae extension panama-search did not build; skipping'* ]] \
|
||||||
|
|| note 'a nonzero Vicinae extension dependency install did not fail the extension build'
|
||||||
|
cmp -s -- "$lock_before" "$lockfile" \
|
||||||
|
|| note 'a rejected Vicinae lockfile mismatch changed package-lock.json'
|
||||||
|
|
||||||
# ── Report ───────────────────────────────────────────────────────────────────
|
# ── Report ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (( ${#findings[@]} > 0 )); then
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
|||||||
@@ -128,6 +128,19 @@ if (( ${#unsafe_installers[@]} > 0 )); then
|
|||||||
fail 'replace each finding with a reviewed, verified installation path'
|
fail 'replace each finding with a reviewed, verified installation path'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Re-running install-packages must be keyed to every reviewed trust input it
|
||||||
|
# consumes. The update-command fixture proves each input changes the digest;
|
||||||
|
# this public-boundary guard keeps any of those inputs from being silently
|
||||||
|
# removed from the installer state definition.
|
||||||
|
for state_input in \
|
||||||
|
'setup/packages' \
|
||||||
|
'setup/scripts/install-packages' \
|
||||||
|
'setup/lib/artifact-provenance' \
|
||||||
|
'setup/provenance'; do
|
||||||
|
grep -Fq "$state_input" "$repo_dir/install" \
|
||||||
|
|| fail "packages hash does not name required state input: $state_input"
|
||||||
|
done
|
||||||
|
|
||||||
# This must be the only production file sourced by the contract.
|
# This must be the only production file sourced by the contract.
|
||||||
# shellcheck source=../../setup/lib/artifact-provenance
|
# shellcheck source=../../setup/lib/artifact-provenance
|
||||||
source "$repo_dir/setup/lib/artifact-provenance"
|
source "$repo_dir/setup/lib/artifact-provenance"
|
||||||
|
|||||||
@@ -46,11 +46,15 @@ build_fixture() {
|
|||||||
local root="$1" packages_rc="${2:-0}" trust_rc="${3:-0}"
|
local root="$1" packages_rc="${2:-0}" trust_rc="${3:-0}"
|
||||||
rm -rf "$root"
|
rm -rf "$root"
|
||||||
mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \
|
mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \
|
||||||
|
"$root/setup/lib" "$root/setup/provenance/keys" \
|
||||||
"$root/config/dot/quickshell/scripts"
|
"$root/config/dot/quickshell/scripts"
|
||||||
|
|
||||||
cp "$installer" "$root/install"
|
cp "$installer" "$root/install"
|
||||||
: >"$root/bin/ascii"
|
: >"$root/bin/ascii"
|
||||||
printf 'base-package\n' >"$root/setup/packages/base"
|
printf 'base-package\n' >"$root/setup/packages/core-packages"
|
||||||
|
printf 'artifact provenance\n' >"$root/setup/lib/artifact-provenance"
|
||||||
|
printf 'reviewed installer inputs\n' >"$root/setup/provenance/installers.conf"
|
||||||
|
printf 'reviewed key\n' >"$root/setup/provenance/keys/fixture.asc"
|
||||||
|
|
||||||
local stage
|
local stage
|
||||||
for stage in "${STAGE_NAMES[@]}"; do
|
for stage in "${STAGE_NAMES[@]}"; do
|
||||||
@@ -210,7 +214,7 @@ grep -qx 'install-packages' <<<"$ran_forced" \
|
|||||||
|| note '--packages did not force install-packages to run'
|
|| note '--packages did not force install-packages to run'
|
||||||
|
|
||||||
# A changed list brings the stage back.
|
# A changed list brings the stage back.
|
||||||
printf 'another-package\n' >>"$tmp/a/setup/packages/base"
|
printf 'another-package\n' >>"$tmp/a/setup/packages/core-packages"
|
||||||
install_status=0
|
install_status=0
|
||||||
ran_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
ran_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
||||||
[[ "$install_status" -eq 0 ]] \
|
[[ "$install_status" -eq 0 ]] \
|
||||||
@@ -218,6 +222,24 @@ ran_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
|||||||
grep -qx 'install-packages' <<<"$ran_changed" \
|
grep -qx 'install-packages' <<<"$ran_changed" \
|
||||||
|| note 'a changed package list did not bring install-packages back'
|
|| note 'a changed package list did not bring install-packages back'
|
||||||
|
|
||||||
|
# The package stage consumes more than package lists. Each reviewed input must
|
||||||
|
# independently invalidate a recorded package state so an upgrade cannot keep
|
||||||
|
# using a stale trust or installer policy.
|
||||||
|
for state_input in \
|
||||||
|
'setup/scripts/install-packages installer adapter' \
|
||||||
|
'setup/lib/artifact-provenance provenance helper' \
|
||||||
|
'setup/provenance/installers.conf installer provenance' \
|
||||||
|
'setup/provenance/keys/fixture.asc provenance key'; do
|
||||||
|
read -r relative label <<<"$state_input"
|
||||||
|
printf 'changed %s\n' "$label" >>"$tmp/a/$relative"
|
||||||
|
install_status=0
|
||||||
|
ran_input_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
||||||
|
[[ "$install_status" -eq 0 ]] \
|
||||||
|
|| note "install --upgrade failed after a $label change with status $install_status"
|
||||||
|
grep -qx 'install-packages' <<<"$ran_input_changed" \
|
||||||
|
|| note "a changed $label did not bring install-packages back"
|
||||||
|
done
|
||||||
|
|
||||||
# A failing stage must not record the hash, or the failure is hidden forever.
|
# A failing stage must not record the hash, or the failure is hidden forever.
|
||||||
build_fixture "$tmp/c" 1
|
build_fixture "$tmp/c" 1
|
||||||
install_status=0
|
install_status=0
|
||||||
|
|||||||
Reference in New Issue
Block a user