Skip to content
Open
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
38 changes: 33 additions & 5 deletions src/cosh-ng/cosh-ng.spec.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
%define anolis_release 2
%global debug_package %{nil}
%global _libexecdir_cosh %{_libexecdir}/anolisa/cosh-ng
%define cosh_replacement_ready ( [ -x "%{_bindir}/cosh" ] && rpm -q --qf '%%{NAME}\\n' -f "%{_bindir}/cosh" 2>/dev/null | awk '$0 == "copilot-shell" { found=1 } END { exit found ? 0 : 1 }' )

Name: cosh-ng
Version: @VERSION@
Expand Down Expand Up @@ -98,13 +99,40 @@ install -m 0644 component.toml %{buildroot}%{_datadir}/anolisa/components/cosh-n
%{_libexecdir_cosh}/cosh-shell
%{_datadir}/anolisa/components/cosh-ng/component.toml

%preun
if [ "$1" -eq 0 ]; then
if ! passwd_entries=$(getent passwd); then
echo "error: cannot uninstall cosh-ng: failed to enumerate passwd entries with getent." >&2
exit 1
fi
if [ -z "$passwd_entries" ]; then
echo "error: cannot uninstall cosh-ng: getent returned no passwd entries." >&2
exit 1
fi
if ! users=$(printf '%s\n' "$passwd_entries" | awk -F: '$7 == "%{_bindir}/cosh" { print $1 }'); then
echo "error: cannot uninstall cosh-ng: failed to inspect passwd login shells." >&2
exit 1
fi
if [ -n "$users" ] && ! %{cosh_replacement_ready}; then
echo "error: cannot uninstall cosh-ng while users still use %{_bindir}/cosh as their login shell:" >&2
printf '%s\n' "$users" >&2
exit 1
fi
fi

%post -p <lua>
nl = '\n'
cosh = '%{_bindir}/cosh'..nl
f = io.open('/etc/shells', 'a+')
local nl = '\n'
local cosh = '%{_bindir}/cosh'
local f = io.open('/etc/shells', 'a+')
if f then
local shells = nl..f:read('*all')..nl
if not shells:find(nl..cosh) then f:write(cosh) end
f:seek('set')
local shells = f:read('*all') or ''
local normalized = nl..shells
if shells ~= '' and shells:sub(-1) ~= nl then normalized = normalized..nl end
if not normalized:find(nl..cosh..nl, 1, true) then
local separator = (shells ~= '' and shells:sub(-1) ~= nl) and nl or ''
f:write(separator, cosh, nl)
end
f:close()
end

Expand Down
11 changes: 11 additions & 0 deletions src/cosh-ng/scripts/run-test-gates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,21 @@ run_raw_packaging() {
bash tests/test-package-raw.sh
}

run_rpm_packaging() {
if ! command -v shellcheck >/dev/null 2>&1; then
echo "shellcheck is required by the rpm packaging gate" >&2
return 1
fi
shellcheck tests/test-package-rpm.sh
bash tests/test-package-rpm.sh
}

case "${1:-all}" in
fast)
scripts/check-test-inventory.sh
crates/cosh-shell/scripts/check-layout.sh
run_raw_packaging
run_rpm_packaging
cargo test --locked --workspace --exclude cosh-core --exclude cosh-shell
run_canonical_units cosh-core cosh-core
run_canonical_units cosh-shell cosh-shell 1
Expand All @@ -141,6 +151,7 @@ case "${1:-all}" in
scripts/check-test-inventory.sh
crates/cosh-shell/scripts/check-layout.sh
run_raw_packaging
run_rpm_packaging
cargo test --locked --workspace --exclude cosh-core --exclude cosh-shell
run_canonical_units cosh-core cosh-core
run_core_integrations
Expand Down
185 changes: 185 additions & 0 deletions src/cosh-ng/tests/test-package-rpm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
# Exercise the RPM spec scriptlets without building the RPM: the %post
# /etc/shells registration through the real RPM Lua interpreter and the
# %preun erase guard through fixture-backed bash runs.
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SPEC="$ROOT/cosh-ng.spec.in"
TMP="$(mktemp -d /tmp/cosh-ng-rpm-scriptlet-test.XXXXXX)"
trap 'rm -rf "$TMP"' EXIT

# --- structural anchors: the lifecycle sections must stay in the spec ---
grep -q '^%preun$' "$SPEC"
grep -q '^%define cosh_replacement_ready ' "$SPEC"
grep -q '^%post -p <lua>$' "$SPEC"
# the extraction below slices on section boundaries, so the sections must
# keep their order: %preun, then %post, then %postun
awk '
/^%preun$/ { a = NR }
/^%post -p <lua>$/ { b = NR }
/^%postun/ { c = NR }
END { exit !(a && b && c && a < b && b < c) }
' "$SPEC"

# --- %post registration matrix through the real RPM Lua interpreter ---
SHELLS="$TMP/shells"
COSH="$TMP/cosh"

post_script() {
awk '/^%post -p <lua>$/{f=1;next} /^%/{f=0} f' "$SPEC" |
sed -e "s|/etc/shells|$SHELLS|g" -e "s|%{_bindir}/cosh|$COSH|g"
}

run_post() {
rpm --eval "%{lua:$(post_script)}" >/dev/null
}

expect_shells() {
local name="$1"
printf '%s' "$2" > "$TMP/expected"
if ! cmp -s "$TMP/expected" "$SHELLS"; then
echo "ERROR: %post case '$name' produced unexpected bytes:" >&2
od -c "$SHELLS" >&2
exit 1
fi
}

run_post_case() {
local name="$1"
local initial="$2"
local expected="$3"

if [ "$initial" = "<missing>" ]; then
rm -f "$SHELLS"
else
printf '%s' "$initial" > "$SHELLS"
fi
run_post
expect_shells "$name (install)" "$expected"
run_post
expect_shells "$name (reinstall)" "$expected"
}

if command -v rpm >/dev/null 2>&1 && rpm --eval '%{lua:print("ok")}' >/dev/null 2>&1; then
# the shared predicate must survive macro expansion with a queryformat
# that emits a real newline (%%{NAME} folds to %{NAME}, \\n folds to \n)
predicate_line="$(sed -n 's/^%define cosh_replacement_ready //p' "$SPEC")"
expanded="$(rpm --define "cosh_replacement_ready $predicate_line" \
--eval '%{cosh_replacement_ready}')"
case "$expanded" in
*"--qf '%{NAME}\n' -f"*) : ;;
*)
echo "ERROR: cosh_replacement_ready expanded unexpectedly: $expanded" >&2
exit 1
;;
esac

run_post_case "missing file" "<missing>" "$COSH"$'\n'
run_post_case "empty file" "" "$COSH"$'\n'
run_post_case "missing trailing newline" \
$'/bin/sh\n/bin/bash' \
$'/bin/sh\n/bin/bash\n'"$COSH"$'\n'
run_post_case "existing trailing newline" \
$'/usr/bin/bash\n' \
$'/usr/bin/bash\n'"$COSH"$'\n'
run_post_case "existing exact registration" \
$'/usr/bin/bash\n'"$COSH"$'\n/usr/bin/zsh\n' \
$'/usr/bin/bash\n'"$COSH"$'\n/usr/bin/zsh\n'
run_post_case "duplicate registrations preserved" \
$'/usr/bin/bash\n'"$COSH"$'\n'"$COSH"$'\n' \
$'/usr/bin/bash\n'"$COSH"$'\n'"$COSH"$'\n'
run_post_case "substring is not a registration" \
$'/usr/bin/bash\n'"$COSH"$'-backup\n' \
$'/usr/bin/bash\n'"$COSH"$'-backup\n'"$COSH"$'\n'

# registration stays fail-open when the shells file cannot be opened
rm -f "$SHELLS"
rpm --eval "%{lua:io.open = function() return nil, 'Read-only file system' end
$(post_script)}" >/dev/null
if [ -e "$SHELLS" ]; then
echo "ERROR: fail-open %post unexpectedly touched the shells file" >&2
exit 1
fi
else
echo "SKIP: rpm lua interpreter unavailable; %post matrix not exercised" >&2
fi

# --- %preun erase guard matrix through fixture-backed bash runs ---
STUB="$TMP/stub-bin"
install -d -m 0755 "$STUB"
GUARD_COSH="$STUB/cosh"

PREDICATE="$(sed -n 's/^%define cosh_replacement_ready //p' "$SPEC")"
PREUN_RAW="$(awk '/^%preun$/{f=1;next} /^%post/{f=0} f' "$SPEC")"
PREUN="${PREUN_RAW//'%{cosh_replacement_ready}'/$PREDICATE}"
PREUN="${PREUN//'%{_bindir}'/$STUB}"
PREUN="${PREUN//%%/%}"

write_stub() {
printf '%s\n' "#!/usr/bin/env bash" "$2" > "$STUB/$1"
chmod 0755 "$STUB/$1"
}

run_preun() {
local action="$1"
PATH="$STUB:/usr/bin:/bin" bash -c "$PREUN" cosh-preun "$action"
}

expect_preun() {
local name="$1"
local action="$2"
local expected_status="$3"
local status=0

run_preun "$action" >"$TMP/preun.out" 2>"$TMP/preun.err" || status=$?
if [ "$status" -ne "$expected_status" ]; then
echo "ERROR: %preun case '$name' exited $status, expected $expected_status:" >&2
cat "$TMP/preun.err" >&2
exit 1
fi
}

write_stub getent "printf '%s\n' 'coshuser:x:1000:1000::/home/coshuser:$GUARD_COSH'"
write_stub rpm "printf '%s\n' cosh-ng"
write_stub cosh ":"

expect_preun "erase with cosh login-shell user" 0 1
grep -Fq coshuser "$TMP/preun.err"
grep -Fq "$GUARD_COSH" "$TMP/preun.err"

expect_preun "upgrade never blocks" 1 0

write_stub getent "printf '%s\n' 'root:x:0:0:root:/root:/usr/bin/bash'"
expect_preun "erase without cosh users" 0 0

write_stub getent "exit 2"
expect_preun "failed passwd enumeration" 0 1
expect_preun "upgrade with broken enumeration" 1 0

write_stub getent "exit 0"
expect_preun "empty passwd enumeration" 0 1

write_stub getent "printf '%s\n' 'root:x:0:0:root:/root:/usr/bin/bash'"
write_stub awk "exit 3"
expect_preun "failed passwd filter" 0 1
rm -f "$STUB/awk"

write_stub getent "printf '%s\n' 'coshuser:x:1000:1000::/home/coshuser:$GUARD_COSH'"
write_stub rpm "exit 1"
expect_preun "failed replacement lookup" 0 1

write_stub rpm "printf '%s\n' cosh-ng unexpected-shell"
expect_preun "unexpected replacement owner" 0 1

write_stub rpm "printf '%s\n' cosh-ng copilot-shell"
chmod 0644 "$GUARD_COSH"
expect_preun "non-executable replacement" 0 1

chmod 0755 "$GUARD_COSH"
expect_preun "atomic provider swap" 0 0

rm -f "$GUARD_COSH"
expect_preun "upgrade without launcher" 1 0

echo "cosh-ng rpm scriptlet tests passed"
Loading