diff options
| -rw-r--r-- | .github/workflows/build.yml | 4 | ||||
| -rw-r--r-- | Containerfile | 4 | ||||
| -rw-r--r-- | Justfile | 6 | ||||
| -rw-r--r-- | README.md | 19 | ||||
| -rwxr-xr-x | build_files/build.sh | 2 | ||||
| -rwxr-xr-x | build_files/install-instruments.sh | 182 | ||||
| -rw-r--r-- | image-template.env | 7 | ||||
| -rw-r--r-- | system_files/usr/lib/systemd/system/aerocore-instruments-setup.service | 18 | ||||
| -rwxr-xr-x | system_files/usr/libexec/aerocore-instruments-setup | 123 |
9 files changed, 364 insertions, 1 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e72f937..49d2f81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,6 +97,10 @@ jobs: HOMEBREW_BOTTLE_DOMAIN: ${{ vars.HOMEBREW_BOTTLE_DOMAIN }} HOMEBREW_API_DOMAIN: ${{ vars.HOMEBREW_API_DOMAIN }} OSTREE_IMAGE_REF: ${{ vars.OSTREE_IMAGE_REF }} + INSTRUMENTS_ENABLED: ${{ vars.INSTRUMENTS_ENABLED || 'true' }} + INSTRUMENTS_RELEASE_REPOSITORY: ${{ vars.INSTRUMENTS_RELEASE_REPOSITORY || 'AeroCore-IO/booster-installer' }} + INSTRUMENTS_VERSION: ${{ vars.INSTRUMENTS_VERSION || 'latest' }} + INSTRUMENTS_RELEASE_API_BASE: ${{ vars.INSTRUMENTS_RELEASE_API_BASE || 'https://api.github.com' }} run: | sudo -E $(command -v just) build \ ${IMAGE_NAME} \ diff --git a/Containerfile b/Containerfile index 90bd7c7..c9377ca 100644 --- a/Containerfile +++ b/Containerfile @@ -36,6 +36,10 @@ ARG OSTREE_IMAGE_REF="" ARG FLATPAK_REMOTE_URL="" ARG HOMEBREW_BOTTLE_DOMAIN="" ARG HOMEBREW_API_DOMAIN="" +ARG INSTRUMENTS_ENABLED="true" +ARG INSTRUMENTS_RELEASE_REPOSITORY="AeroCore-IO/booster-installer" +ARG INSTRUMENTS_VERSION="latest" +ARG INSTRUMENTS_RELEASE_API_BASE="https://api.github.com" RUN --mount=type=bind,from=ctx,source=/,target=/ctx \ --mount=type=cache,dst=/var/cache \ @@ -12,6 +12,10 @@ export BASE_IMAGE := env_var_or_default("BASE_IMAGE", "ghcr.io/ublue-os/bazzite- export FLATPAK_REMOTE_URL := env_var_or_default("FLATPAK_REMOTE_URL", "") export HOMEBREW_BOTTLE_DOMAIN := env_var_or_default("HOMEBREW_BOTTLE_DOMAIN", "") export HOMEBREW_API_DOMAIN := env_var_or_default("HOMEBREW_API_DOMAIN", "") +export INSTRUMENTS_ENABLED := env_var_or_default("INSTRUMENTS_ENABLED", "true") +export INSTRUMENTS_RELEASE_REPOSITORY := env_var_or_default("INSTRUMENTS_RELEASE_REPOSITORY", "AeroCore-IO/booster-installer") +export INSTRUMENTS_VERSION := env_var_or_default("INSTRUMENTS_VERSION", "latest") +export INSTRUMENTS_RELEASE_API_BASE := env_var_or_default("INSTRUMENTS_RELEASE_API_BASE", "https://api.github.com") alias build-vm := build-qcow2 alias rebuild-vm := rebuild-qcow2 @@ -114,7 +118,7 @@ build $target_image=image_name $tag=default_tag: BUILD_ARGS=() LABELS=() - for arg in BASE_IMAGE FLATPAK_REMOTE_URL HOMEBREW_BOTTLE_DOMAIN HOMEBREW_API_DOMAIN OSTREE_IMAGE_REF; do + for arg in BASE_IMAGE FLATPAK_REMOTE_URL HOMEBREW_BOTTLE_DOMAIN HOMEBREW_API_DOMAIN OSTREE_IMAGE_REF INSTRUMENTS_ENABLED INSTRUMENTS_RELEASE_REPOSITORY INSTRUMENTS_VERSION INSTRUMENTS_RELEASE_API_BASE; do if [[ -n "${!arg:-}" ]]; then BUILD_ARGS+=("--build-arg" "${arg}=${!arg}") fi @@ -11,6 +11,25 @@ FROM ghcr.io/ublue-os/bazzite-deck:stable By focusing strictly on file overlays and configuration writing, this build intentionally avoids invasive operations like `dnf5 install/remove`, COPR enablement, akmods pulls, or kernel recompilations. This architectural choice keeps AeroCore OS completely immune to Fedora/COPR dependency drift during major upstream transition windows (e.g., Bazzite 43 to 44). +## AeroCore Booster integration + +Image builds download the selected AeroCore Booster release from +`AeroCore-IO/booster-installer`, verify its published SHA-256 checksum, and +store the complete release seed under `/usr/share/aerocore-booster-release`. +The release installer runs once for the first active graphical user and copies +the application into that user's XDG directories. After bootstrapping, updates +and uninstallation remain owned by the normal Instruments user-space installer. +The bootstrap completion marker is stored at +`/var/lib/aerocore/instruments-bootstrap-complete`, so uninstalling the user +application does not cause the image seed to reinstall it on the next boot. +Remove that marker and enable `aerocore-instruments-setup.service` to explicitly +bootstrap the application again. + +Set `INSTRUMENTS_VERSION` to a release tag for reproducible builds, or leave it +as `latest` for rolling development images. Set `INSTRUMENTS_ENABLED=false` to +build without Booster. The release repository and API base can be overridden +with `INSTRUMENTS_RELEASE_REPOSITORY` and `INSTRUMENTS_RELEASE_API_BASE`. + --- ## Key Features & Optimizations diff --git a/build_files/build.sh b/build_files/build.sh index f1709da..b89907a 100755 --- a/build_files/build.sh +++ b/build_files/build.sh @@ -32,3 +32,5 @@ if [[ -n "${HOMEBREW_API_DOMAIN:-}" ]]; then fi find /etc/environment.d /etc/skel/.config/environment.d -name 99-aerocore-mirrors.conf -type f -exec chmod 0644 {} + 2>/dev/null || true + +/ctx/install-instruments.sh diff --git a/build_files/install-instruments.sh b/build_files/install-instruments.sh new file mode 100755 index 0000000..f85bd3b --- /dev/null +++ b/build_files/install-instruments.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${INSTRUMENTS_ENABLED:=true}" +: "${INSTRUMENTS_RELEASE_REPOSITORY:=AeroCore-IO/booster-installer}" +: "${INSTRUMENTS_VERSION:=latest}" +: "${INSTRUMENTS_RELEASE_API_BASE:=https://api.github.com}" +: "${INSTRUMENTS_SEED_ROOT:=/usr/share/aerocore-booster-release}" + +if [[ "${INSTRUMENTS_ENABLED}" != "true" ]]; then + echo "AeroCore Booster image integration is disabled" + exit 0 +fi + +case "$(uname -m)" in + x86_64 | amd64) + asset_suffix="_linux-x86_64.tar.gz" + ;; + *) + echo "Unsupported AeroCore Booster image architecture: $(uname -m)" >&2 + exit 1 + ;; +esac + +for command in curl jq sha256sum tar; do + command -v "$command" >/dev/null 2>&1 || { + echo "Required command is unavailable: $command" >&2 + exit 1 + } +done + +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT + +github_curl() { + local args=( + --fail + --location + --retry 3 + --retry-delay 2 + --retry-max-time 60 + --silent + --show-error + ) + + if [[ -s /run/secrets/GITHUB_TOKEN ]]; then + args+=(--header "Authorization: Bearer $(</run/secrets/GITHUB_TOKEN)") + fi + + curl "${args[@]}" "$@" +} + +if [[ "${INSTRUMENTS_VERSION}" == "latest" ]]; then + release_endpoint="releases/latest" +else + release_endpoint="releases/tags/${INSTRUMENTS_VERSION}" +fi + +release_json="$workdir/release.json" +github_curl \ + "${INSTRUMENTS_RELEASE_API_BASE%/}/repos/${INSTRUMENTS_RELEASE_REPOSITORY}/${release_endpoint}" \ + --output "$release_json" + +release_tag="$(jq -er '.tag_name' "$release_json")" +mapfile -t payload_assets < <( + jq -er --arg suffix "$asset_suffix" ' + .assets[] + | select( + (.name | startswith("AeroCore-Booster-v")) + or (.name | startswith("instruments-ng_v")) + ) + | select(.name | endswith($suffix)) + | [.name, .browser_download_url] + | @tsv + ' "$release_json" +) + +if [[ "${#payload_assets[@]}" -ne 1 ]]; then + echo "Expected one x86_64 Booster tarball in release $release_tag, found ${#payload_assets[@]}" >&2 + exit 1 +fi + +IFS=$'\t' read -r asset_name asset_url <<<"${payload_assets[0]}" +checksum_name="${asset_name}.sha256" +checksum_url="$( + jq -er --arg name "$checksum_name" \ + '.assets[] | select(.name == $name) | .browser_download_url' \ + "$release_json" +)" + +archive="$workdir/$asset_name" +checksum_file="$workdir/$checksum_name" +github_curl "$asset_url" --output "$archive" +github_curl "$checksum_url" --output "$checksum_file" + +expected_checksum="$(awk 'NR == 1 { print $1 }' "$checksum_file")" +actual_checksum="$(sha256sum "$archive" | awk '{ print $1 }')" +if [[ ! "$expected_checksum" =~ ^[[:xdigit:]]{64}$ ]] || + [[ "${actual_checksum,,}" != "${expected_checksum,,}" ]]; then + echo "Checksum verification failed for $asset_name" >&2 + exit 1 +fi + +while IFS= read -r entry; do + case "$entry" in + /* | ../* | */../* | */..) + echo "Unsafe path in release archive: $entry" >&2 + exit 1 + ;; + esac +done < <(tar -tzf "$archive") + +extract_dir="$workdir/extracted" +install -d "$extract_dir" +tar --extract --gzip --file "$archive" --directory "$extract_dir" \ + --no-same-owner + +if [[ -d "$extract_dir/aerocore-booster-release" ]]; then + payload="$extract_dir/aerocore-booster-release" +elif [[ -d "$extract_dir/instruments-ng-release" ]]; then + payload="$extract_dir/instruments-ng-release" +else + echo "Release archive does not contain a supported payload root" >&2 + exit 1 +fi + +required_paths=( + install.sh + appimage/AppRun + appimage/AppRun.wrapped + sidecar/hhd-input-helper.cjs + sidecar/vendor/hhd-input-helper/instruments_hhd_input/__init__.py + runtime/avionics + runtime/cabin + runtime/wing + runtime/node/bin/node + python-wheelhouse + pkg/systemd/ac-avionics.service + pkg/systemd/ac-cabin.service + pkg/systemd/ac-wing.service + pkg/systemd/ac-instruments.service + scripts/check-runtime-health.cjs + manifest.yaml +) + +for path in "${required_paths[@]}"; do + if [[ ! -e "$payload/$path" ]]; then + echo "Required path is missing from Booster release: $path" >&2 + exit 1 + fi +done + +if [[ ! -f "$payload/pkg/aerocore-booster.sh" && + ! -f "$payload/pkg/instruments-ng.sh" ]]; then + echo "Booster launcher is missing from the release payload" >&2 + exit 1 +fi + +# Remove the first implementation's image-owned runtime when upgrading an +# existing AeroCore OS image. The release seed itself remains immutable. +rm -rf /usr/lib/aerocore-booster +rm -f /usr/bin/aerocore-booster +for unit in ac-avionics.service ac-cabin.service ac-wing.service \ + ac-instruments.service; do + rm -f "/usr/lib/systemd/system/$unit" +done + +rm -rf "$INSTRUMENTS_SEED_ROOT" +install -d -m 0755 "$INSTRUMENTS_SEED_ROOT" +cp -a "$payload/." "$INSTRUMENTS_SEED_ROOT/" + +cat >"$INSTRUMENTS_SEED_ROOT/image-release" <<EOF +RELEASE_TAG=$release_tag +ASSET_NAME=$asset_name +SHA256=$actual_checksum +EOF +chmod 0644 "$INSTRUMENTS_SEED_ROOT/image-release" + +systemctl enable aerocore-instruments-setup.service + +echo "Seeded AeroCore Booster $release_tag from $asset_name" diff --git a/image-template.env b/image-template.env index 76fb1cc..3035db5 100644 --- a/image-template.env +++ b/image-template.env @@ -22,3 +22,10 @@ BASE_IMAGE="ghcr.io/ublue-os/bazzite-deck:stable" FLATPAK_REMOTE_URL="" HOMEBREW_BOTTLE_DOMAIN="" HOMEBREW_API_DOMAIN="" + +# AeroCore Booster release embedded into the image. Use a concrete tag for +# reproducible production builds, or "latest" for rolling development images. +INSTRUMENTS_ENABLED="true" +INSTRUMENTS_RELEASE_REPOSITORY="AeroCore-IO/booster-installer" +INSTRUMENTS_VERSION="latest" +INSTRUMENTS_RELEASE_API_BASE="https://api.github.com" diff --git a/system_files/usr/lib/systemd/system/aerocore-instruments-setup.service b/system_files/usr/lib/systemd/system/aerocore-instruments-setup.service new file mode 100644 index 0000000..0c9cc45 --- /dev/null +++ b/system_files/usr/lib/systemd/system/aerocore-instruments-setup.service @@ -0,0 +1,18 @@ +[Unit] +Description=Configure AeroCore Booster for the graphical user +Documentation=https://github.com/AeroCore-IO/instruments-ng +After=systemd-user-sessions.service display-manager.service +Wants=systemd-user-sessions.service +ConditionPathExists=/usr/share/aerocore-booster-release/install.sh +ConditionPathExists=!/var/lib/aerocore/instruments-bootstrap-complete + +[Service] +Type=oneshot +Environment=AEROCORE_INSTRUMENTS_SETUP_WAIT_SECONDS=180 +ExecStart=/usr/libexec/aerocore-instruments-setup +Restart=on-failure +RestartSec=15 +TimeoutStartSec=360 + +[Install] +WantedBy=multi-user.target diff --git a/system_files/usr/libexec/aerocore-instruments-setup b/system_files/usr/libexec/aerocore-instruments-setup new file mode 100755 index 0000000..cdbe8cd --- /dev/null +++ b/system_files/usr/libexec/aerocore-instruments-setup @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${AEROCORE_INSTRUMENTS_SETUP_WAIT_SECONDS:=180}" + +seed_root="/usr/share/aerocore-booster-release" +state_dir="/var/lib/aerocore" +identity_file="$state_dir/instruments-user" +complete_marker="$state_dir/instruments-bootstrap-complete" +attempt_marker="$state_dir/instruments-bootstrap-attempted" + +find_graphical_user() { + local requested_uid="${1:-}" + local active seat session state type uid user + + while read -r session _; do + [[ -n "$session" ]] || continue + uid="$(loginctl show-session "$session" --property=User --value 2>/dev/null || true)" + if [[ ! "$uid" =~ ^[0-9]+$ ]] || ((uid < 1000)); then + continue + fi + [[ -z "$requested_uid" || "$uid" == "$requested_uid" ]] || continue + + active="$(loginctl show-session "$session" --property=Active --value 2>/dev/null || true)" + state="$(loginctl show-session "$session" --property=State --value 2>/dev/null || true)" + type="$(loginctl show-session "$session" --property=Type --value 2>/dev/null || true)" + seat="$(loginctl show-session "$session" --property=Seat --value 2>/dev/null || true)" + [[ "$active" == "yes" || "$state" == "active" || "$state" == "online" ]] || continue + + user="$(getent passwd "$uid" | cut -d: -f1)" + [[ -n "$user" && "$user" != "root" ]] || continue + if [[ "$type" == "x11" || "$type" == "wayland" ]]; then + printf '%s\n' "$user" + return 0 + fi + if [[ "$seat" == "seat0" ]] && pgrep -u "$uid" -x steam >/dev/null 2>&1; then + printf '%s\n' "$user" + return 0 + fi + done < <(loginctl list-sessions --no-legend --no-pager 2>/dev/null || true) + + return 1 +} + +if [[ -f "$complete_marker" ]]; then + echo "AeroCore Booster user bootstrap is already complete" + exit 0 +fi + +configured_uid="" +if [[ -f "$identity_file" ]]; then + configured_uid="$(awk -F= '$1 == "uid" { print $2; exit }' "$identity_file")" + if [[ ! "$configured_uid" =~ ^[0-9]+$ ]] || + ! getent passwd "$configured_uid" >/dev/null 2>&1; then + configured_uid="" + fi +fi + +deadline=$((SECONDS + AEROCORE_INSTRUMENTS_SETUP_WAIT_SECONDS)) +desktop_user="" +while ((SECONDS < deadline)); do + desktop_user="$(find_graphical_user "$configured_uid" || true)" + [[ -n "$desktop_user" ]] && break + sleep 2 +done + +if [[ -z "$desktop_user" ]]; then + echo "No active graphical user found within ${AEROCORE_INSTRUMENTS_SETUP_WAIT_SECONDS}s" >&2 + exit 1 +fi + +desktop_uid="$(id -u "$desktop_user")" +desktop_group="$(id -gn "$desktop_user")" +desktop_home="$(getent passwd "$desktop_user" | cut -d: -f6)" +if [[ -z "$desktop_home" || ! -d "$desktop_home" ]]; then + echo "Home directory is unavailable for $desktop_user: $desktop_home" >&2 + exit 1 +fi + +install -d -m 0755 "$state_dir" +printf 'user=%s\nuid=%s\n' "$desktop_user" "$desktop_uid" >"$identity_file" +chmod 0644 "$identity_file" + +existing_install=0 +for installed_path in \ + "$desktop_home/.local/share/aerocore-booster/manifest.yaml" \ + "$desktop_home/.local/share/aerocore-booster/appimage/AppRun" \ + "$desktop_home/.local/share/aerocore-booster/aerocore-booster" \ + "$desktop_home/.local/share/instruments-ng/manifest.yaml" \ + "$desktop_home/.local/share/instruments-ng/appimage/AppRun" \ + "$desktop_home/.local/share/instruments-ng/instruments-ng"; do + if [[ -e "$installed_path" ]]; then + existing_install=1 + break + fi +done + +if [[ "$existing_install" == "0" || -f "$attempt_marker" ]]; then + touch "$attempt_marker" + HOME="$desktop_home" \ + AEROCORE_INSTRUMENTS_USER="$desktop_user" \ + AEROCORE_INSTRUMENTS_GROUP="$desktop_group" \ + AEROCORE_INSTRUMENTS_UID="$desktop_uid" \ + AEROCORE_INSTRUMENTS_HOME="$desktop_home" \ + bash "$seed_root/install.sh" install +else + echo "Existing Instruments installation found for $desktop_user; preserving it" +fi + +cat >"$complete_marker" <<EOF +user=$desktop_user +uid=$desktop_uid +seed_release=$(awk -F= '$1 == "RELEASE_TAG" { print $2; exit }' "$seed_root/image-release") +EOF +chmod 0644 "$complete_marker" +rm -f "$attempt_marker" + +# Bootstrap is deliberately one-shot. Independent updates and uninstalls are +# owned by the user-space Instruments installer after this point. +systemctl disable aerocore-instruments-setup.service >/dev/null 2>&1 || true + +echo "AeroCore Booster user bootstrap completed for $desktop_user (uid $desktop_uid)" |
