Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Regression: formulas that template a config file from `post_install_steps`
# installed with the placeholder still in it.
#
# The `inreplace` step was not implemented, and neither were the `{{etc}}` and
# `{{user}}` tokens its steps depend on. Affected formulas shipped a config
# naming a user that does not exist — `change_this`, `@@HOMEBREW-UNBOUND-USER@@`
# — behind an install that reported success.
#
# The step edits live user configuration, so most of this guards the refusals
# rather than the happy path:
# - an unresolved `{{user}}` must never be written into a config; with no
# USER in the environment the step declines instead;
# - there is no regex engine here. Only the `^<literal>.*` shape upstream
# actually uses is honoured, behind a strict metacharacter allowlist;
# anything richer reaches the fallback rather than being approximated,
# because a near-miss silently corrupts the file;
# - an unmet `if_exists` guard skips the step rather than inventing config;
# - the write is atomic, so a crash mid-write cannot truncate the original.
#
# Nothing drives these steps offline without standing up a live tap, so the
# behaviour is pinned by colocated inline unit tests (`lib_tests`). This script
# asserts the load-bearing code and its tests are present, then builds and runs
# that binary. ~45s, matching its siblings; no network.

set -euo pipefail

ROOT=$(cd "$(dirname "$0")/../.." && pwd)
cd "$ROOT"

STEPS="$ROOT/src/core/post_install_steps.zig"

# 1. The step and the two tokens its steps cannot work without.
if ! grep -qE '\.\{ "inreplace", \.inreplace \}' "$STEPS"; then
echo "FAIL: the inreplace step is no longer registered in step_map" >&2
exit 1
fi
for tok in etc user; do
if ! grep -qE "\.\{ \"$tok\", \." "$STEPS"; then
echo "FAIL: template {{$tok}} is no longer resolved" >&2
exit 1
fi
done

# 2. The refusals. Each of these turns a silent config corruption into a loud
# partial skip, so losing one is worse than losing the feature.
if ! grep -q 'unresolved template' "$STEPS"; then
echo "FAIL: inreplace no longer refuses an unresolved template — would write {{user}} into a config" >&2
exit 1
fi
if ! grep -q 'fn anchoredLineLiteral' "$STEPS"; then
echo "FAIL: the regexp allowlist is gone — richer patterns would be approximated" >&2
exit 1
fi
if ! grep -q 'fn guardsSatisfied' "$STEPS"; then
echo "FAIL: guards are no longer evaluated — inreplace would run on absent config" >&2
exit 1
fi
if ! grep -q 'atomicReplaceFile' "$STEPS"; then
echo "FAIL: inreplace no longer writes atomically" >&2
exit 1
fi

# 3. Behavioural guards. A deleted test block would let the run below go green
# vacuously, so assert each is present first.
LITERAL_TEST="execute substitutes a literal inreplace and resolves the invoking user"
GUARD_TEST="execute skips an inreplace whose if_exists guard is unmet"
REGEXP_TEST="execute rewrites a whole line for the anchored regexp form"
RICH_TEST="execute refuses a regexp outside the anchored-line subset"
CLASS_TEST="execute refuses an anchored regexp whose literal hides metacharacters"
NOUSER_TEST="execute refuses an inreplace whose user token cannot be resolved"

for t in "$LITERAL_TEST" "$GUARD_TEST" "$REGEXP_TEST" "$RICH_TEST" "$CLASS_TEST" "$NOUSER_TEST"; do
if ! grep -Rqs -- "$t" "$STEPS"; then
echo "FAIL: guard test missing from post_install_steps.zig: $t" >&2
exit 1
fi
done

if ! zig build test-bin >/dev/null 2>&1; then
echo "FAIL: could not build the test binaries (zig build test-bin)" >&2
exit 1
fi

out=$("$ROOT/zig-out/test-bin/lib_tests" 2>&1 || true)
for t in "$LITERAL_TEST" "$GUARD_TEST" "$REGEXP_TEST" "$RICH_TEST" "$CLASS_TEST" "$NOUSER_TEST"; do
line=$(printf '%s\n' "$out" | grep -F -- "$t" || true)
if [[ -z "$line" ]]; then
echo "FAIL: guard test did not run: $t" >&2
exit 1
fi
if [[ "$line" != *OK ]]; then
echo "FAIL: $t: $line" >&2
exit 1
fi
done

echo "OK: inreplace templates configs, refuses unresolved tokens and unsupported regexps"
95 changes: 95 additions & 0 deletions scripts/regressions/post-install-steps-keg-templates-and-remove.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Regression: a keg-only formula that publishes its launcher from
# `post_install_steps` installed with nothing on PATH.
#
# Two gaps, one symptom. The steps name their source with a `{{bin}}`-style
# template rather than a `base`, and those tokens were absent from
# `template_map`, so `expandTemplates` left them literal. The resulting path did
# not start with `/`, `stepSymlink` read that as a relative source and refused
# it, and the companion `remove` step was not implemented at all. Net effect for
# rustup: no `<prefix>/bin/rustup`, no completions, a stale `rustup-init` left
# behind, and a "post_install partially skipped" warning as the only clue.
#
# The `remove` half is a GUARDED delete — it retires a symlink identified by a
# substring of its target. That guard is the security-relevant part: without it
# the step becomes an unconditional formula-driven delete, so this script
# asserts the guard is still required rather than merely that removal happens.
#
# Nothing drives these steps offline without standing up a live tap, so the
# behaviour is pinned by colocated inline unit tests (`lib_tests`). This script
# asserts the load-bearing code and its tests are present, then builds and runs
# that binary. ~45s, matching its sibling silent-skip-post-install-steps.sh; no
# network.

set -euo pipefail

ROOT=$(cd "$(dirname "$0")/../.." && pwd)
cd "$ROOT"

STEPS="$ROOT/src/core/post_install_steps.zig"

# 1. The keg-relative templates. A formula's `{{bin}}` is inside the keg — not
# `<prefix>/bin`, which is what the same word means as a `base`. Losing any
# of these puts the token back on the "relative source" refusal path.
for tok in bin bash_completion zsh_completion fish_completion pwsh_completion; do
if ! grep -qE "\.\{ \"$tok\", \." "$STEPS"; then
echo "FAIL: template {{$tok}} is no longer resolved — its steps will be refused as relative" >&2
exit 1
fi
done

# 2. `remove` must be a native step, and `supportedStepType` must agree, or
# migrated formulas get miscounted as needing the Ruby fallback.
if ! grep -qE '\.\{ "remove", \.remove \}' "$STEPS"; then
echo "FAIL: the remove step is no longer registered in step_map" >&2
exit 1
fi

# 3. The guard on remove. An unconditional delete driven by formula data is a
# different and far worse thing than retiring one's own symlink.
if ! grep -q 'symlink_target_contains' "$STEPS"; then
echo "FAIL: remove no longer consults symlink_target_contains — unguarded delete" >&2
exit 1
fi
if ! grep -q 'readLinkAbsolute' "$STEPS"; then
echo "FAIL: remove no longer type-checks the path as a symlink before deleting" >&2
exit 1
fi

# 4. Behavioural guards. If a test block is deleted the run below goes green
# vacuously, so assert each is present first.
TEMPLATE_TEST="execute links keg-relative template paths into the prefix"
REMOVE_TEST="execute removes a symlink whose target matches the guard"
KEEP_FILE_TEST="execute leaves a regular file alone on remove"
KEEP_LINK_TEST="execute keeps a symlink whose target does not match the guard"
# The pre-existing base-form path shares stepSymlink; judged too so a fix to
# the template form cannot quietly break the form every other formula uses.
BASE_FORM_TEST="execute runs the filesystem tier natively and leaves the log clean"

for t in "$TEMPLATE_TEST" "$REMOVE_TEST" "$KEEP_FILE_TEST" "$KEEP_LINK_TEST" "$BASE_FORM_TEST"; do
if ! grep -Rqs -- "$t" "$STEPS"; then
echo "FAIL: guard test missing from post_install_steps.zig: $t" >&2
exit 1
fi
done

if ! zig build test-bin >/dev/null 2>&1; then
echo "FAIL: could not build the test binaries (zig build test-bin)" >&2
exit 1
fi

# One run of the suite, judged per guard line: a pass ends in "OK".
out=$("$ROOT/zig-out/test-bin/lib_tests" 2>&1 || true)
for t in "$TEMPLATE_TEST" "$REMOVE_TEST" "$KEEP_FILE_TEST" "$KEEP_LINK_TEST" "$BASE_FORM_TEST"; do
line=$(printf '%s\n' "$out" | grep -F -- "$t" || true)
if [[ -z "$line" ]]; then
echo "FAIL: guard test did not run: $t" >&2
exit 1
fi
if [[ "$line" != *OK ]]; then
echo "FAIL: $t: $line" >&2
exit 1
fi
done

echo "OK: keg-relative templates resolve, remove is guarded, base-form symlinks intact"
Loading
Loading