fix(install): ship portable host activation
- commit
- 48635dde1f13bd5f5e82d87a6c8472359f4e2228
- parent
- f8679be7a4c84a5f3ee0774fb08a1307f1b06973
- parent
- 209456b72ebf505e371aacc21ed4135104f5465c
- author
- FelineStateMachine <dami.etoile@gmail.com>
@@ -183,9 +183,12 @@ Portal, then use **client -> reset enrollment** and confirm the displayed host.
183183 The reset is intentionally explicit and controller reachable; importing a new184184 invitation never silently replaces a working enrollment.185185 186-Provision a dedicated AMD host with the187-[fresh Bazzite host runbook](docs/fresh-bazzite-host.md). The runbook also188-defines the temporary `slate` stand-in used for protocol and daemon extraction.186+Activate an installed dedicated AMD host with the portable, package-owned187+[Sigil host activation guide](docs/sigil-host-activation.md). It uses the188+current gaming user, dynamically discovered hardware, and installed package189+assets without assuming a maintainer hostname or checkout path. The190+[fresh Bazzite host runbook](docs/fresh-bazzite-host.md) remains the engineering191+and hardware-acceptance log, including the temporary `slate` stand-in.189192 The backward-compatible [appliance status v1 contract](docs/sigil-appliance-status-v1.md)190193 and explicit [v2 contract](docs/sigil-appliance-status-v2.md) define the local,191194 redacted interface intended for the Decky management surface.@@ -217,7 +220,7 @@ scripts/stage-bazzite-release.sh \
217220 The stager atomically updates the user-owned `current` symlink only after both218221 binaries, their dynamic libraries, and bounded startup commands validate. Host219222 identity, hardware-specific configuration, and service activation remain220-separate gates in the Bazzite runbook.223+separate gates in the package-owned activation guide.221224 222225 The thin binary stager is only for unmanaged development layouts. It refuses223226 activation when package-managed service, audio, rollback, or udev links follow@@ -255,13 +258,13 @@ The GitHub workflow and package builder never receive the Minisign secret.
255258 256259 The package contains the primary `sigil` host executable, its byte-identical257260 `sigil-host` compatibility copy, the `sigil-probe` diagnostic, installer and258-rollback tools, service/audio/udev assets, license, checksums, and build259-provenance. It cannot contain the source tree, `.env` files, identities, host260-configuration, or test evidence. Its release ID is the SHA-256 of the complete261-installed-file checksum manifest. Identical inputs produce a byte-identical262-archive. The manifest marks product binaries as built from clean `HEAD`;263-caller-supplied development binaries are explicitly marked as having264-unverified provenance.261+rollback tools, service/audio/udev assets, the portable activation guide,262+license, checksums, and build provenance. It cannot contain the source tree,263+`.env` files, identities, host configuration, or test evidence. Its release ID264+is the SHA-256 of the complete installed-file checksum manifest. Identical265+inputs produce a byte-identical archive. The manifest marks product binaries266+as built from clean `HEAD`; caller-supplied development binaries are explicitly267+marked as having unverified provenance.265268 266269 During the legacy rollback window, `sigil-host.service` deliberately starts267270 the byte-identical `sigil-host` compatibility filename. Interactive and@@ -1,4 +1,10 @@
1-# Fresh Bazzite AMD host setup1+# Fresh Bazzite AMD host engineering setup2+3+This document is the maintainer lab log and hardware-acceptance source. It4+intentionally contains historical machine names, checkout paths, evidence, and5+development workflows. A user activating an installed package should follow6+the portable [Sigil host activation guide](sigil-host-activation.md) shipped in7+that package instead.28 39 This runbook takes a dedicated AMD x86_64 machine from a fresh Bazzite install410 to a remotely managed Sigil host. It also defines how `slate` is used as@@ -0,0 +1,338 @@
1+# Activate an installed Sigil host2+3+This is the portable post-install path for a dedicated Bazzite or SteamOS-like4+AMD gaming host. Run it as the gaming user that owns the Gamescope session, not5+as root. The commands use only `$HOME` and `$USER`; no hostname or account name6+is assumed.7+8+The package installer deliberately stops before these steps. Identity creation,9+system input permissions, PipeWire restart, hardware configuration, and service10+enablement each remain visible operator decisions. Re-running a package install11+or rollback preserves the identity and `host.toml` created here.12+13+## 1. Grant the gaming user access to only uinput14+15+The package stages its virtual-input udev rule but does not write `/etc` or16+change group membership. Install that exact package asset together with the two17+rules that bound `/dev/uinput` to a dedicated group:18+19+```bash20+set -euo pipefail21+22+current="$HOME/.local/libexec/sigil-spark/current"23+remote_input_rule="$current/assets/70-sigil-remote-input.rules"24+early_uinput_rule="$current/assets/72-sigil-uinput.rules"25+final_uinput_rule="$current/assets/99-sigil-uinput.rules"26+test -x "$current/sigil"27+for rule in "$remote_input_rule" "$early_uinput_rule" "$final_uinput_rule"; do28+ if test ! -f "$rule" || test -L "$rule"; then29+ echo "unsafe or missing package rule: $rule" >&230+ exit 131+ fi32+done33+34+getent group sigil-uinput >/dev/null || sudo groupadd --system sigil-uinput35+sudo usermod --append --groups sigil-uinput "$USER"36+sudo install -o root -g root -m 0644 \37+ "$remote_input_rule" /etc/udev/rules.d/70-sigil-remote-input.rules38+sudo install -o root -g root -m 0644 \39+ "$early_uinput_rule" /etc/udev/rules.d/72-sigil-uinput.rules40+sudo install -o root -g root -m 0644 \41+ "$final_uinput_rule" /etc/udev/rules.d/99-sigil-uinput.rules42+sudo modprobe uinput43+sudo udevadm control --reload-rules44+sudo udevadm trigger --action=add --subsystem-match=misc --sysname-match=uinput45+```46+47+Reboot, or fully sign out and back in, before continuing. A new login is48+required for the user service to inherit the `sigil-uinput` group.49+50+After that login, fail closed unless the device has the expected Linux uinput51+identity, ownership, mode, and no extended ACL:52+53+```bash54+set -euo pipefail55+56+uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"57+test -n "$uinput_gid"58+id -G | tr ' ' '\n' | grep -Fx "$uinput_gid"59+test ! -L /dev/uinput60+test -c /dev/uinput && test -r /dev/uinput && test -w /dev/uinput61+test "$(stat -Lc '%u:%g:%a:%t:%T' /dev/uinput)" = "0:$uinput_gid:660:a:df"62+if getfacl -cp /dev/uinput | grep -Eq '^(user|group):[^:]|^mask:'; then63+ echo '/dev/uinput has an extended access ACL' >&264+ exit 165+fi66+```67+68+Do not add this account to the broad `input` group and do not run Sigil as69+root. Access to uinput is equivalent to local controller, keyboard, and pointer70+control.71+72+## 2. Create the host identity once73+74+```bash75+set -euo pipefail76+umask 07777+78+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"79+identity="$HOME/.local/share/sigil-spark/identity/host.key"80+install -d -m 0700 "$(dirname "$identity")" \81+ "$HOME/.config/sigil-spark" "$HOME/.local/state/sigil-spark"82+if test ! -e "$identity" && test ! -L "$identity"; then83+ "$sigil" identity init --output "$identity"84+fi85+if test ! -f "$identity" || test -L "$identity"; then86+ echo "unsafe host identity: $identity" >&287+ exit 188+fi89+chmod 0600 "$identity"90+"$sigil" identity show --identity "$identity"91+```92+93+Never copy this seed to a client or rotate it during an upgrade. Portal pairs94+to the host identity represented by this file.95+96+## 3. Select the exact AMD render node and GstVA H.264 factory97+98+Sigil binds the encoder factory to its inspected read-only `device-path`; a99+factory name alone is not enough on a multi-GPU machine. The following chooses100+automatically only when there is one usable AMD node/factory pair. Set101+`RENDER_NODE` and `VA_ENCODER` explicitly before running it when the printed102+inventory is intentionally ambiguous.103+104+```bash105+set -euo pipefail106+107+gst_inspect_path="$(command -v gst-inspect-1.0)"108+timeout_path="$(command -v timeout)"109+inspect_max_bytes=1048576110+inspection_dir="$(mktemp -d "${TMPDIR:-/tmp}/sigil-activation-inspect.XXXXXX")"111+chmod 0700 "$inspection_dir"112+trap 'rm -rf -- "$inspection_dir"' EXIT INT TERM HUP113+114+bounded_gst_inspect() {115+ local output_path116+ local output_size117+ output_path="$(mktemp "$inspection_dir/gst-inspect.XXXXXX")"118+ if ! "$timeout_path" --signal=TERM --kill-after=2s 5s \119+ "$gst_inspect_path" "$@" 2>/dev/null \120+ | head -c "$((inspect_max_bytes + 1))" >"$output_path"121+ then122+ rm -f -- "$output_path"123+ return 1124+ fi125+ output_size="$(wc -c <"$output_path" | tr -d '[:space:]')"126+ if test "$output_size" -gt "$inspect_max_bytes"; then127+ rm -f -- "$output_path"128+ return 1129+ fi130+ cat "$output_path"131+ rm -f -- "$output_path"132+}133+134+mapfile -t amd_nodes < <(135+ for sysfs_node in /sys/class/drm/renderD*; do136+ test -e "$sysfs_node/device/driver" || continue137+ test "$(basename "$(readlink -f "$sysfs_node/device/driver")")" = amdgpu \138+ || continue139+ node="/dev/dri/$(basename "$sysfs_node")"140+ test -c "$node" && test -r "$node" && test -w "$node" && printf '%s\n' "$node"141+ done142+)143+printf 'AMD render node: %s\n' "${amd_nodes[@]}"144+if test -n "${RENDER_NODE:-}"; then145+ render_node="$RENDER_NODE"146+else147+ test "${#amd_nodes[@]}" -eq 1148+ render_node="${amd_nodes[0]}"149+fi150+printf '%s\n' "${amd_nodes[@]}" | grep -Fx "$render_node"151+152+registry="$(bounded_gst_inspect)"153+mapfile -t va_factories < <(154+ awk '$2 ~ /^va(renderD[0-9]+)?h264(lp)?enc:$/ {155+ sub(/:$/, "", $2); print $2156+ }' <<<"$registry" | sort -u157+)158+matching_factories=()159+for factory in "${va_factories[@]}"; do160+ inspection="$(bounded_gst_inspect "$factory")" || continue161+ grep -Fq "Default: \"$render_node\"" <<<"$inspection" || continue162+ grep -Eq '\([0-9]+\): cbr([[:space:]]|$)' <<<"$inspection" || continue163+ matching_factories+=("$factory")164+done165+printf 'matching CBR GstVA factory: %s\n' "${matching_factories[@]}"166+if test -n "${VA_ENCODER:-}"; then167+ va_encoder="$VA_ENCODER"168+else169+ test "${#matching_factories[@]}" -eq 1170+ va_encoder="${matching_factories[0]}"171+fi172+printf '%s\n' "${matching_factories[@]}" | grep -Fx "$va_encoder"173+174+printf 'export SIGIL_RENDER_NODE=%q\n' "$render_node"175+printf 'export SIGIL_VA_ENCODER=%q\n' "$va_encoder"176+177+rm -rf -- "$inspection_dir"178+trap - EXIT INT TERM HUP179+```180+181+Keep the two printed export commands for the next step. If automatic selection182+fails, inspect every printed node and factory and rerun with an exact pair, for183+example `RENDER_NODE=/dev/dri/renderD129 VA_ENCODER=varenderD129h264enc`.184+Never substitute a software encoder just to make preflight pass.185+186+## 4. Restart the package-owned audio sink187+188+The package already linked its PipeWire drop-in into the gaming user's config.189+Restart only the PulseAudio-compatible PipeWire service during provisioning,190+then select the persistent headless sink:191+192+```bash193+set -euo pipefail194+195+test -f "$HOME/.config/pipewire/pipewire-pulse.conf.d/50-sigil-spark-audio.conf"196+systemctl --user restart pipewire-pulse.service197+audio_sink_id="$(wpctl status -n | awk '198+ $0 ~ /sigil_spark/ {199+ for (field = 1; field <= NF; field++) {200+ if ($field ~ /^[0-9]+\.$/) {201+ gsub("\\.", "", $field); print $field; exit202+ }203+ }204+ }205+')"206+test "$audio_sink_id" -gt 0207+wpctl set-default "$audio_sink_id"208+pactl get-default-sink | grep -Fx sigil_spark209+```210+211+This restart can interrupt existing local playback. Perform it before starting212+a game.213+214+## 5. Write and validate the hardware configuration215+216+First run the two `export` commands printed by step 3 in this shell. Width and217+height are intentionally omitted so Sigil follows the bounded native Gamescope218+mode instead of assuming one panel resolution.219+220+```bash221+set -euo pipefail222+: "${SIGIL_RENDER_NODE:?run the render-node exports from step 3}"223+: "${SIGIL_VA_ENCODER:?run the encoder exports from step 3}"224+225+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"226+uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"227+config="$HOME/.config/sigil-spark/host.toml"228+ffmpeg_path="$(command -v ffmpeg)"229+pw_dump_path="$(command -v pw-dump)"230+gst_launch_path="$(command -v gst-launch-1.0)"231+gst_inspect_path="$(command -v gst-inspect-1.0)"232+test -n "$uinput_gid"233+234+umask 077235+set +e236+set -o noclobber237+exec 3>"$config"238+create_status=$?239+set +o noclobber240+set -e241+if test "$create_status" -ne 0; then242+ echo "refusing to replace existing host configuration: $config" >&2243+ exit 1244+fi245+cat >&3 <<EOF246+identity_path = "$HOME/.local/share/sigil-spark/identity/host.key"247+state_path = "$HOME/.local/state/sigil-spark/runtime"248+source = "gamescope-pipewire"249+framerate = 60250+codec = "h264"251+input_mode = "uinput"252+ffmpeg_path = "$ffmpeg_path"253+254+[uinput]255+device_path = "/dev/uinput"256+expected_owner_uid = 0257+expected_group_gid = $uinput_gid258+expected_mode = 0o660259+260+[gamescope_pipewire]261+node_name = "gamescope"262+media_class = "Video/Source"263+xwayland_display = ":0"264+pw_dump_path = "$pw_dump_path"265+gst_launch_path = "$gst_launch_path"266+gst_inspect_path = "$gst_inspect_path"267+encoder_backend = "external-gst-launch"268+vaapi_encoder = "$SIGIL_VA_ENCODER"269+vaapi_render_node = "$SIGIL_RENDER_NODE"270+rate_control = "cbr"271+bitrate_kbps = 12000272+273+[audio]274+node_name = "sigil_spark"275+media_class = "Audio/Sink"276+bitrate_bps = 96000277+EOF278+exec 3>&-279+test "$(stat -Lc '%a:%U' "$config")" = "600:$(id -un)"280+281+"$sigil" config check --config "$config"282+"$sigil" capture probe \283+ --source gamescope-pipewire --config "$config" --frames 300284+```285+286+Both commands must exit zero. The capture probe must report 300 encoded frames,287+zero post-encode drops, and the actual resolved dimensions. Do not enable the288+service after a failed preflight or by weakening the device/factory checks.289+290+## 6. Start, inspect, and only then enable Sigil291+292+```bash293+set -euo pipefail294+295+systemctl --user daemon-reload296+systemctl --user start sigil-host.service297+systemctl --user is-active --quiet sigil-host.service298+invocation="$(systemctl --user show -p InvocationID --value sigil-host.service)"299+test -n "$invocation"300+ready=false301+for _attempt in $(seq 1 60); do302+ if journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" \303+ _SYSTEMD_USER_UNIT=sigil-host.service --no-pager -o cat \304+ | grep -Fxq status=ready305+ then306+ ready=true307+ break308+ fi309+ sleep 1310+done311+test "$ready" = true312+systemctl --user status sigil-host.service --no-pager313+journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" --no-pager -o cat314+315+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"316+"$sigil" appliance status \317+ --config "$HOME/.config/sigil-spark/host.toml" --json318+319+systemctl --user enable sigil-host.service320+```321+322+The exact service invocation must report `status=ready` before enablement. The323+unit runs in the gaming user's ordinary user manager and retries safely if its324+configured Gamescope or PipeWire source is not ready yet; no root daemon or325+interactive desktop capture portal is involved.326+327+Create a one-time, short-lived invitation only after Portal shows its peer ID:328+329+```bash330+"$HOME/.local/libexec/sigil-spark/current/sigil" invitation create \331+ --config "$HOME/.config/sigil-spark/host.toml" \332+ --peer '<PORTAL_IROH_PEER_ID>' \333+ --expires-in-seconds 900 --pointer-keyboard --gamepad \334+ --output "$HOME/sigil.goq-invite"335+```336+337+Move that owner-only file to Portal and confirm the displayed host and grants.338+After the one-time enrollment, ordinary startup remains **PIN -> tap -> play**.@@ -66,7 +66,10 @@ if ! diff -u \
6666 release/SHA256SUMS \6767 release/assets/50-sigil-spark-audio.conf \6868 release/assets/70-sigil-remote-input.rules \69+ release/assets/72-sigil-uinput.rules \70+ release/assets/99-sigil-uinput.rules \6971 release/assets/sigil-host.service \72+ release/docs/sigil-host-activation.md \7073 release/release-manifest.json \7174 release/sigil \7275 release/sigil-host \@@ -82,7 +85,10 @@ if ! diff -u \
8285 LICENSE \8386 assets/50-sigil-spark-audio.conf \8487 assets/70-sigil-remote-input.rules \88+ assets/72-sigil-uinput.rules \89+ assets/99-sigil-uinput.rules \8590 assets/sigil-host.service \91+ docs/sigil-host-activation.md \8692 release-manifest.json \8793 sigil \8894 sigil-host \@@ -465,4 +471,5 @@ printf 'pipewire_restart=not-attempted\n'
465471 printf 'service_activation=not-attempted\n'466472 printf 'system_asset=%s\n' "$asset_dir/70-sigil-remote-input.rules"467473 printf '%s\n' \468- 'next: complete the explicit identity, hardware config, uinput-group/udev, PipeWire restart, and service-enable gates from docs/fresh-bazzite-host.md'474+ "activation_guide=$install_root/current/docs/sigil-host-activation.md" \475+ "next: follow $install_root/current/docs/sigil-host-activation.md as the dedicated gaming user"@@ -133,6 +133,9 @@ fi
133133 [[ -f "$script_dir/sigil-host.service" ]] || die "systemd user unit is missing"134134 [[ -f "$script_dir/50-sigil-spark-audio.conf" ]] || die "PipeWire audio drop-in is missing"135135 [[ -f "$script_dir/70-sigil-remote-input.rules" ]] || die "udev rule is missing"136+[[ -f "$script_dir/72-sigil-uinput.rules" ]] || die "early uinput rule is missing"137+[[ -f "$script_dir/99-sigil-uinput.rules" ]] || die "final uinput rule is missing"138+[[ -f "$repo_dir/docs/sigil-host-activation.md" ]] || die "host activation guide is missing"136139 command -v git >/dev/null 2>&1 || die "git is required"137140 command -v python3 >/dev/null 2>&1 || die "python3 is required for deterministic archives"138141 command -v tar >/dev/null 2>&1 || die "tar is required"@@ -214,7 +217,9 @@ fi
214217 215218 payload="$temp_root/payload"216219 release_tree="$payload/release"217-install -d -m 0700 "$payload" "$release_tree" "$release_tree/assets" "$release_tree/tools"220+install -d -m 0700 \221+ "$payload" "$release_tree" "$release_tree/assets" "$release_tree/docs" \222+ "$release_tree/tools"218223 install -m 0755 "$host_binary" "$release_tree/sigil"219224 # Retain one byte-identical compatibility executable for existing automation.220225 install -m 0755 "$host_binary" "$release_tree/sigil-host"@@ -226,6 +231,12 @@ install -m 0600 "$script_dir/50-sigil-spark-audio.conf" \
226231 "$release_tree/assets/50-sigil-spark-audio.conf"227232 install -m 0644 "$script_dir/70-sigil-remote-input.rules" \228233 "$release_tree/assets/70-sigil-remote-input.rules"234+install -m 0644 "$script_dir/72-sigil-uinput.rules" \235+ "$release_tree/assets/72-sigil-uinput.rules"236+install -m 0644 "$script_dir/99-sigil-uinput.rules" \237+ "$release_tree/assets/99-sigil-uinput.rules"238+install -m 0644 "$repo_dir/docs/sigil-host-activation.md" \239+ "$release_tree/docs/sigil-host-activation.md"229240 install -m 0644 "$repo_dir/LICENSE" "$release_tree/LICENSE"230241 install -m 0755 "$script_dir/install-bazzite-package.sh" "$payload/install-bazzite-package.sh"231242 @@ -298,7 +309,10 @@ for relative in \
298309 sigil-probe \299310 assets/50-sigil-spark-audio.conf \300311 assets/70-sigil-remote-input.rules \312+ assets/72-sigil-uinput.rules \313+ assets/99-sigil-uinput.rules \301314 assets/sigil-host.service \315+ docs/sigil-host-activation.md \302316 tools/rollback-bazzite-release.sh \303317 LICENSE \304318 release-manifest.json@@ -39,7 +39,8 @@ make_payload() {
3939 true_binary="$(readlink -f /usr/bin/true 2>/dev/null || readlink -f /bin/true)"4040 [[ -x "$true_binary" ]]4141 42- install -d -m 0700 "$root" "$release/assets" "$release/tools"42+ install -d -m 0700 \43+ "$root" "$release/assets" "$release/docs" "$release/tools"4344 install -m 0755 "$true_binary" "$release/sigil"4445 install -m 0755 "$true_binary" "$release/sigil-host"4546 install -m 0755 "$true_binary" "$release/sigil-probe"@@ -47,10 +48,19 @@ make_payload() {
4748 printf '[Service]\nExecStart=/fixture serve\n' >"$release/assets/sigil-host.service"4849 printf 'audio fixture\n' >"$release/assets/50-sigil-spark-audio.conf"4950 printf 'udev fixture\n' >"$release/assets/70-sigil-remote-input.rules"51+ printf 'early uinput fixture\n' >"$release/assets/72-sigil-uinput.rules"52+ printf 'final uinput fixture\n' >"$release/assets/99-sigil-uinput.rules"53+ printf '# Activation fixture %s\n' "$tag" \54+ >"$release/docs/sigil-host-activation.md"5055 printf 'MIT\n' >"$release/LICENSE"5156 chmod 0600 "$release/assets/50-sigil-spark-audio.conf"52- chmod 0644 "$release/assets/70-sigil-remote-input.rules" \53- "$release/assets/sigil-host.service" "$release/LICENSE"57+ chmod 0644 \58+ "$release/assets/70-sigil-remote-input.rules" \59+ "$release/assets/72-sigil-uinput.rules" \60+ "$release/assets/99-sigil-uinput.rules" \61+ "$release/assets/sigil-host.service" \62+ "$release/docs/sigil-host-activation.md" \63+ "$release/LICENSE"5464 5565 python3 - "$release/release-manifest.json" "$tag" "$commit" "$asset_name" <<'PY'5666 import json@@ -88,7 +98,10 @@ PY
8898 sigil sigil-host sigil-probe \8999 assets/50-sigil-spark-audio.conf \90100 assets/70-sigil-remote-input.rules \101+ assets/72-sigil-uinput.rules \102+ assets/99-sigil-uinput.rules \91103 assets/sigil-host.service \104+ docs/sigil-host-activation.md \92105 tools/rollback-bazzite-release.sh \93106 LICENSE release-manifest.json94107 do@@ -147,6 +160,14 @@ run_installer "$payload_a" >"$temp_root/install-a.log"
147160 install_root="$test_home/.local/libexec/sigil-spark"148161 [[ "$(basename -- "$(readlink -f "$install_root/current")")" == "$release_a" ]]149162 [[ ! -e "$install_root/previous" && ! -L "$install_root/previous" ]]163+grep -Fqx '# Activation fixture v0.1.0' \164+ "$install_root/current/docs/sigil-host-activation.md"165+grep -Fq "activation_guide=$install_root/current/docs/sigil-host-activation.md" \166+ "$temp_root/install-a.log"167+if grep -Fq 'fresh-bazzite-host.md' "$temp_root/install-a.log"; then168+ printf 'FAIL: package installer delegated activation to the lab runbook\n' >&2169+ exit 1170+fi150171 151172 run_installer "$payload_a" >"$temp_root/repeat-a.log"152173 [[ "$(basename -- "$(readlink -f "$install_root/current")")" == "$release_a" ]]@@ -164,6 +185,8 @@ config_sha="$(sha256sum "$config" | awk '{print $1}')"
164185 run_installer "$payload_b" >"$temp_root/install-b.log"165186 [[ "$(basename -- "$(readlink -f "$install_root/current")")" == "$release_b" ]]166187 [[ "$(basename -- "$(readlink -f "$install_root/previous")")" == "$release_a" ]]188+grep -Fqx '# Activation fixture v0.1.1' \189+ "$install_root/current/docs/sigil-host-activation.md"167190 [[ "$(sha256sum "$identity" | awk '{print $1}')" == "$identity_sha" ]]168191 [[ "$(sha256sum "$config" | awk '{print $1}')" == "$config_sha" ]]169192 @@ -27,6 +27,9 @@ for path in \
2727 scripts/sigil-host.service \2828 scripts/50-sigil-spark-audio.conf \2929 scripts/70-sigil-remote-input.rules \30+ scripts/72-sigil-uinput.rules \31+ scripts/99-sigil-uinput.rules \32+ docs/sigil-host-activation.md \3033 crates/sigil-host/Cargo.toml \3134 Cargo.lock rust-toolchain.toml LICENSE3235 do@@ -0,0 +1,68 @@
1+2+3+set -euo pipefail4+5+script_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"6+repo_dir="$(CDPATH='' cd -- "$script_dir/../.." && pwd -P)"7+guide="$repo_dir/docs/sigil-host-activation.md"8+installer="$repo_dir/scripts/install-bazzite-package.sh"9+bootstrap="$repo_dir/website/install-sigil"10+temp_parent="${TMPDIR:-/tmp}"11+temp_parent="${temp_parent%/}"12+temp_root="$(mktemp -d "$temp_parent/sigil-activation-guide.XXXXXX")"13+14+cleanup() {15+ local status=$?16+ trap - EXIT INT TERM HUP17+ case "$temp_root" in18+ "$temp_parent"/sigil-activation-guide.??????) rm -rf -- "$temp_root" ;;19+ esac20+ exit "$status"21+}22+trap cleanup EXIT INT TERM HUP23+24+fail() {25+ printf 'FAIL: %s\n' "$1" >&226+ exit 127+}28+29+[[ -f "$guide" && ! -L "$guide" ]] || fail 'the activation guide is missing or unsafe'30+31+awk '32+ /^```bash$/ { inside = 1; next }33+ /^```$/ && inside { inside = 0; print ""; next }34+ inside { print }35+' "$guide" >"$temp_root/guide-code.sh"36+bash -n "$temp_root/guide-code.sh"37+38+if grep -Eq 'tank@|/Users/dami|Developer/sigil-spark|scripts/' "$guide"; then39+ fail 'the product activation guide contains a maintainer or source-checkout path'40+fi41+if grep -Eq '/dev/dri/renderD128|vaapi_encoder = "vah264enc"' "$guide"; then42+ fail 'the activation guide pins the original test host GPU contract'43+fi44+45+grep -Fq 'refusing to replace existing host configuration' "$guide" \46+ || fail 'the activation guide can overwrite an existing host configuration'47+grep -Fq 'set -o noclobber' "$guide" \48+ || fail 'the activation guide does not create host.toml exclusively'49+grep -Fq -- '--kill-after=2s 5s' "$guide" \50+ || fail 'the activation guide does not time-bound GStreamer inspection'51+grep -Fq 'inspect_max_bytes=1048576' "$guide" \52+ || fail 'the activation guide does not size-bound GStreamer inspection'53+grep -Fq 'current/assets/72-sigil-uinput.rules' "$guide" \54+ || fail 'the activation guide does not use its packaged early uinput rule'55+grep -Fq 'current/assets/99-sigil-uinput.rules' "$guide" \56+ || fail 'the activation guide does not use its packaged final uinput rule'57+grep -Fq 'native Gamescope' "$guide" \58+ || fail 'the activation guide does not preserve dynamic native resolution'59+60+for output_script in "$installer" "$bootstrap"; do61+ grep -Fq 'current/docs/sigil-host-activation.md' "$output_script" \62+ || fail "$(basename "$output_script") does not print the persistent guide path"63+ if grep -Fq 'fresh-bazzite-host.md' "$output_script"; then64+ fail "$(basename "$output_script") still delegates activation to the lab runbook"65+ fi66+done67+68+printf 'sigil_host_activation_guide_tests=ok\n'@@ -71,7 +71,10 @@ release = {
7171 "sigil-probe": b"fixture probe\n",7272 "assets/50-sigil-spark-audio.conf": b"audio\n",7373 "assets/70-sigil-remote-input.rules": b"udev\n",74+ "assets/72-sigil-uinput.rules": b"early uinput\n",75+ "assets/99-sigil-uinput.rules": b"final uinput\n",7476 "assets/sigil-host.service": b"[Service]\nExecStart=/fixture\n",77+ "docs/sigil-host-activation.md": b"# Activation fixture\n",7578 "tools/rollback-bazzite-release.sh": b"#!/usr/bin/env bash\nexit 0\n",7679 "LICENSE": b"MIT\n",7780 "release-manifest.json": (json.dumps(manifest, sort_keys=True, indent=2) + "\n").encode(),@@ -92,7 +95,12 @@ package_sums = sums(payload)
9295 files = {"payload/PACKAGE-SHA256SUMS": package_sums}9396 files.update({f"payload/{name}": data for name, data in payload.items()})9497 files.update({f"payload/release/{name}": data for name, data in release.items()})95-dirs = ["payload/release", "payload/release/assets", "payload/release/tools"]98+dirs = [99+ "payload/release",100+ "payload/release/assets",101+ "payload/release/docs",102+ "payload/release/tools",103+]96104 if variant == "unexpected":97105 files["payload/unexpected"] = b"unexpected\n"98106 @@ -618,6 +618,11 @@ done
618618 self.assertEqual(self.minisign_marker.read_text(encoding="utf-8"), "invoked\n")619619 self.assertFalse(self.forbidden_marker.exists())620620 self.assertIn(f"Sigil runtime {RELEASE_TAG} is installed.", result.stdout)621+ self.assertIn(622+ ".local/libexec/sigil-spark/current/docs/sigil-host-activation.md",623+ result.stdout,624+ )625+ self.assertNotIn("fresh Bazzite runbook", result.stdout)621626 self.assertEqual(list(self.tmp_dir.glob("goq-sigil-install.*")), [])622627 623628 @@ -133,6 +133,7 @@ source_commit = sys.argv[4]
133133 expected_dirs = {134134 "payload/release",135135 "payload/release/assets",136+ "payload/release/docs",136137 "payload/release/tools",137138 }138139 release_files = {@@ -140,7 +141,10 @@ release_files = {
140141 "SHA256SUMS",141142 "assets/50-sigil-spark-audio.conf",142143 "assets/70-sigil-remote-input.rules",144+ "assets/72-sigil-uinput.rules",145+ "assets/99-sigil-uinput.rules",143146 "assets/sigil-host.service",147+ "docs/sigil-host-activation.md",144148 "release-manifest.json",145149 "sigil",146150 "sigil-host",@@ -162,5 +162,5 @@ payload_path="$unpack_path/payload"
162162 "$payload_path/stage-this-release.sh"163163 printf '%s\n' \164164 "Sigil runtime $release_tag is installed." \165- "Continue with the hardware and identity gates in the fresh Bazzite runbook." \165+ "Continue with $HOME/.local/libexec/sigil-spark/current/docs/sigil-host-activation.md." \166166 "The installer did not start or restart the active Sigil service."