summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml4
-rw-r--r--Containerfile4
-rw-r--r--Justfile6
-rw-r--r--README.md19
-rwxr-xr-xbuild_files/build.sh2
-rwxr-xr-xbuild_files/install-instruments.sh182
-rw-r--r--image-template.env7
-rw-r--r--system_files/usr/lib/systemd/system/aerocore-instruments-setup.service18
-rwxr-xr-xsystem_files/usr/libexec/aerocore-instruments-setup123
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 \
diff --git a/Justfile b/Justfile
index 5b5cc02..5e73752 100644
--- a/Justfile
+++ b/Justfile
@@ -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
diff --git a/README.md b/README.md
index 0fc3ba4..3fc11c5 100644
--- a/README.md
+++ b/README.md
@@ -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)"