Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6cb58c4c3
|
||
|
|
5742a2841f
|
||
|
|
70cbcb558c
|
No files matched your search
@@ -1,118 +0,0 @@
|
|||||||
name: Install test comment
|
|
||||||
|
|
||||||
# Posts the install test's numbers on the pull request as one comment, kept
|
|
||||||
# up to date on every run, next to quattro's latest numbers. GitHub runs
|
|
||||||
# workflow_run workflows from the default branch, with write access even for
|
|
||||||
# pull requests from forks, so this job never checks out or runs pull request
|
|
||||||
# code: it only reads the finished run's artifact, as data.
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Install test"]
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment:
|
|
||||||
if: github.event.workflow_run.event == 'pull_request'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
|
||||||
RUN_URL: ${{ github.event.workflow_run.html_url }}
|
|
||||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
|
||||||
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
|
|
||||||
steps:
|
|
||||||
# A rerun keeps earlier attempts' artifacts under the same run, so take
|
|
||||||
# exactly the attempt that finished (and the baseline's latest attempt).
|
|
||||||
- name: Fetch this run's numbers and quattro's latest
|
|
||||||
env:
|
|
||||||
ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
|
|
||||||
run: |
|
|
||||||
gh run download "$RUN_ID" -R "$REPO" -n "install-test-$RUN_ID-$ATTEMPT" -D pr || true
|
|
||||||
base=$(gh run list -R "$REPO" --workflow install-test.yml --branch quattro --event push \
|
|
||||||
--status success -L 1 --json databaseId -q '.[0].databaseId')
|
|
||||||
if [ -n "$base" ]; then
|
|
||||||
base_attempt=$(gh api "repos/$REPO/actions/runs/$base" -q .run_attempt)
|
|
||||||
gh run download "$base" -R "$REPO" -n "install-test-$base-$base_attempt" -D base || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Comment on the pull request
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
marker='<!-- omarchy-install-test -->'
|
|
||||||
|
|
||||||
# The number comes from the pull request's own run, so trust it only
|
|
||||||
# when that pull request's head is the commit this run tested.
|
|
||||||
pr=$({ find pr -name pr-number -exec cat {} + 2>/dev/null || true; } | head -1 | tr -dc 0-9)
|
|
||||||
[ -n "$pr" ] || { echo "No pull request number in the artifact"; exit 0; }
|
|
||||||
[ "$(gh api "repos/$REPO/pulls/$pr" -q .head.sha)" = "$HEAD_SHA" ] ||
|
|
||||||
{ echo "PR #$pr has moved past $HEAD_SHA; not commenting"; exit 0; }
|
|
||||||
|
|
||||||
# Every lookup prints nothing rather than failing, so a missing baseline (none
|
|
||||||
# yet, or expired) shows as "–" instead of ending the script under set -e.
|
|
||||||
timing() { { find "$1" -name omarchy-install-timing.json 2>/dev/null || true; } | head -1; }
|
|
||||||
install_s() { [ -n "$1" ] || return 0; jq -r '.finished_at - .started_at' "$1" 2>/dev/null || true; }
|
|
||||||
# "Startup finished in … = 1min 2.3s" or "= 12.104s" -> seconds
|
|
||||||
boot_s() {
|
|
||||||
local f
|
|
||||||
f=$({ find "$1" -path "$2" 2>/dev/null || true; } | head -1)
|
|
||||||
[ -n "$f" ] || return 0
|
|
||||||
head -1 "$f" | sed -n 's/.*= *//p' |
|
|
||||||
awk '{t=0; for (i=1;i<=NF;i++) { if ($i ~ /min$/) t+=60*$i; else if ($i ~ /ms$/) t+=$i/1000; else t+=$i } printf "%.3f", t}'
|
|
||||||
}
|
|
||||||
cell() { [ -n "$1" ] && awk -v x="$1" 'BEGIN{printf "%.1f s", x}' || echo "–"; }
|
|
||||||
change() {
|
|
||||||
[ -n "$1" ] && [ -n "$2" ] || { echo "–"; return; }
|
|
||||||
awk -v a="$1" -v b="$2" 'BEGIN{d=a-b; printf "%s%.1f s", (d>=0?"+":"−"), (d<0?-d:d)}'
|
|
||||||
}
|
|
||||||
|
|
||||||
pt=$(timing pr); bt=$(timing base)
|
|
||||||
rows=(
|
|
||||||
"Install|$(install_s "$pt")|$(install_s "$bt")"
|
|
||||||
"First boot|$(boot_s pr '*/first-boot-systemd-analyze.txt')|$(boot_s base '*/first-boot-systemd-analyze.txt')"
|
|
||||||
"Everyday boot|$(boot_s pr '*-boot/systemd-analyze.txt')|$(boot_s base '*-boot/systemd-analyze.txt')"
|
|
||||||
)
|
|
||||||
|
|
||||||
status="passed"; [ "$CONCLUSION" = success ] || status="**${CONCLUSION}**"
|
|
||||||
{
|
|
||||||
echo "$marker"
|
|
||||||
echo "### Install test $status"
|
|
||||||
echo
|
|
||||||
echo "| | This PR | quattro | Change |"
|
|
||||||
echo "| --- | ---: | ---: | ---: |"
|
|
||||||
for row in "${rows[@]}"; do
|
|
||||||
IFS='|' read -r name this base_v <<<"$row"
|
|
||||||
echo "| $name | $(cell "$this") | $(cell "$base_v") | $(change "$this" "$base_v") |"
|
|
||||||
done
|
|
||||||
if [ -n "$pt" ]; then
|
|
||||||
echo
|
|
||||||
echo "<details><summary>Install phases</summary>"
|
|
||||||
echo
|
|
||||||
echo "| Phase | This PR | quattro |"
|
|
||||||
echo "| --- | ---: | ---: |"
|
|
||||||
# Phase names come from the pull request's installer: keep them to plain text.
|
|
||||||
jq -r --slurpfile b <( [ -n "$bt" ] && cat "$bt" || echo '{"phases":[]}' ) '
|
|
||||||
($b[0].phases | map({key: .name, value: .elapsed}) | from_entries) as $base
|
|
||||||
| .phases[]
|
|
||||||
| "| \(.name | gsub("[^A-Za-z0-9 +&/().-]"; "") | .[0:60]) | \(.elapsed * 10 | round / 10) s | \(($base[.name] // null) | if . == null then "–" else "\(. * 10 | round / 10) s" end) |"
|
|
||||||
' "$pt"
|
|
||||||
echo
|
|
||||||
echo "</details>"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
echo "[Run]($RUN_URL) · ${HEAD_SHA:0:8}"
|
|
||||||
} > comment.md
|
|
||||||
|
|
||||||
existing=$(gh api "repos/$REPO/issues/$pr/comments" --paginate \
|
|
||||||
-q ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | .id" | head -1)
|
|
||||||
if [ -n "$existing" ]; then
|
|
||||||
gh api -X PATCH "repos/$REPO/issues/comments/$existing" -F body=@comment.md >/dev/null
|
|
||||||
else
|
|
||||||
gh api "repos/$REPO/issues/$pr/comments" -F body=@comment.md >/dev/null
|
|
||||||
fi
|
|
||||||
cat comment.md
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
name: Install test
|
|
||||||
|
|
||||||
# Builds the ISO a pull request produces, installs it unattended in a KVM
|
|
||||||
# guest, boots the installed system again, and reports how long the install
|
|
||||||
# and both boots took. Every later change to the installer is measured
|
|
||||||
# against these numbers in its own PR.
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
# Each merge records quattro's numbers, which PR comments compare against.
|
|
||||||
push:
|
|
||||||
branches: [quattro]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: install-test-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
install:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
timeout-minutes: 75
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
|
||||||
|
|
||||||
# The hosted runner has far more disk than the build needs (145 GB,
|
|
||||||
# ~86 GB free); fail early and loudly if that ever changes.
|
|
||||||
- name: Check disk space
|
|
||||||
run: |
|
|
||||||
df -h /
|
|
||||||
avail_gb=$(df -BG --output=avail / | tail -1 | tr -dc 0-9)
|
|
||||||
[ "$avail_gb" -ge 40 ] || { echo "::error::only ${avail_gb} GB free, the ISO build and install test need ~40 GB"; exit 1; }
|
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
|
||||||
|
|
||||||
- name: Build the ISO
|
|
||||||
run: NO_BOOT_OFFER=1 ./bin/omarchy-iso-make
|
|
||||||
|
|
||||||
# The harness runs in an Arch container: Arch's current QEMU and OVMF
|
|
||||||
# (QEMU 11) instead of Ubuntu 24.04's QEMU 8.2, at the paths the
|
|
||||||
# harness expects, the same as on an Omarchy workstation.
|
|
||||||
- name: Install unattended, then boot the installed system
|
|
||||||
run: |
|
|
||||||
sudo chmod 0666 /dev/kvm
|
|
||||||
docker run --rm --device /dev/kvm -v "$PWD:/w" -w /w \
|
|
||||||
-e OMARCHY_INTEGRATION_DISK_CACHE=none \
|
|
||||||
archlinux:latest bash -euo pipefail -c '
|
|
||||||
pacman -Syu --noconfirm --needed qemu-system-x86 qemu-hw-display-virtio-gpu qemu-hw-display-virtio-vga qemu-img edk2-ovmf socat \
|
|
||||||
imagemagick tesseract tesseract-data-eng mtools dosfstools jq openssh openssl python curl >/dev/null
|
|
||||||
# test/integration installs its tools through Omarchy'"'"'s helper; they are installed above.
|
|
||||||
printf "#!/bin/sh\nexit 0\n" >/usr/local/bin/omarchy-pkg-add
|
|
||||||
chmod 0755 /usr/local/bin/omarchy-pkg-add
|
|
||||||
qemu-system-x86_64 --version | head -1
|
|
||||||
./test/integration release/*.iso boot --no-preview --memory 8192 --timeout 1800
|
|
||||||
'
|
|
||||||
|
|
||||||
# The harness ran as root in its container: test-runs and what QEMU
|
|
||||||
# wrote there (the serial logs) belong to root until this step.
|
|
||||||
- name: Hand the results to the runner's user
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
mkdir -p test-runs
|
|
||||||
sudo chown -R "$(id -u):$(id -g)" test-runs
|
|
||||||
|
|
||||||
- name: Report times
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
base=$(ls -d test-runs/*-integration 2>/dev/null | head -1)
|
|
||||||
[ -n "$base" ] || { echo "No test run to report" >> "$GITHUB_STEP_SUMMARY"; exit 0; }
|
|
||||||
{
|
|
||||||
echo "## Install test"
|
|
||||||
echo
|
|
||||||
if [ -f "$base/omarchy-install-timing.json" ]; then
|
|
||||||
jq -r '"**Install:** \((.finished_at - .started_at) * 10 | round / 10) s across \(.phases | length) phases"' "$base/omarchy-install-timing.json"
|
|
||||||
echo
|
|
||||||
echo "| Phase | Seconds |"
|
|
||||||
echo "| --- | ---: |"
|
|
||||||
jq -r '.phases[] | "| \(.name) | \(.elapsed * 10 | round / 10) |"' "$base/omarchy-install-timing.json"
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
if [ -f "$base/first-boot-systemd-analyze.txt" ]; then
|
|
||||||
echo "**First boot:** $(head -1 "$base/first-boot-systemd-analyze.txt")"
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
boot_dir=$(ls -d "$base"/runs/*-boot 2>/dev/null | tail -1)
|
|
||||||
if [ -n "$boot_dir" ] && [ -f "$boot_dir/systemd-analyze.txt" ]; then
|
|
||||||
echo "**Everyday boot:** $(head -1 "$boot_dir/systemd-analyze.txt")"
|
|
||||||
echo
|
|
||||||
echo "System state: $(jq -r .system_state "$boot_dir/boot-timing.json" 2>/dev/null)"
|
|
||||||
fi
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|
||||||
# The comment workflow runs from quattro and reads only this artifact;
|
|
||||||
# it needs the PR number, which a workflow_run event omits for forks.
|
|
||||||
- name: Record the pull request
|
|
||||||
if: always() && github.event_name == 'pull_request'
|
|
||||||
run: echo "${{ github.event.pull_request.number }}" > test-runs/pr-number
|
|
||||||
|
|
||||||
- name: Upload test artifacts
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
||||||
with:
|
|
||||||
name: install-test-${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
path: |
|
|
||||||
test-runs/pr-number
|
|
||||||
test-runs/*/omarchy-install-timing.json
|
|
||||||
test-runs/*/omarchy-install.log
|
|
||||||
test-runs/*/first-boot-systemd-analyze.txt
|
|
||||||
test-runs/*/runs/**/*.png
|
|
||||||
test-runs/*/runs/**/*.txt
|
|
||||||
test-runs/*/runs/**/*.json
|
|
||||||
test-runs/*/runs/**/*.log
|
|
||||||
if-no-files-found: ignore
|
|
||||||
retention-days: 14
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Unit tests
|
|
||||||
|
|
||||||
# The fast, VM-free tests under test/unit/: cidata autoinstall loading and the
|
|
||||||
# orchestrator's phases. Seconds per run, no ISO build; the install test
|
|
||||||
# covers the rest.
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches: [quattro]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
unit:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
|
||||||
|
|
||||||
# The tests run where the code they cover runs: on Arch, the same as
|
|
||||||
# the live ISO and an Omarchy workstation. The container boots systemd
|
|
||||||
# because the keyboard tests ask the real localectl for its keymaps, and
|
|
||||||
# the tests run as an ordinary user because some of them check what
|
|
||||||
# happens when a file cannot be written.
|
|
||||||
#
|
|
||||||
# A privileged container shares the host's consoles and devices, so the
|
|
||||||
# units that would claim them are masked before systemd starts: without
|
|
||||||
# that, its getty, udev and vconsole setup take over the host's.
|
|
||||||
- name: Run ./test/all in an Arch container
|
|
||||||
run: |
|
|
||||||
docker run -d --name unit --privileged --cgroupns=host \
|
|
||||||
-v /sys/fs/cgroup:/sys/fs/cgroup:rw --tmpfs /run --tmpfs /tmp:exec \
|
|
||||||
-v "$PWD:/src:ro" archlinux:latest bash -c '
|
|
||||||
systemctl mask getty.target console-getty.service \
|
|
||||||
systemd-udev-trigger.service systemd-udevd.service \
|
|
||||||
systemd-vconsole-setup.service
|
|
||||||
exec /usr/lib/systemd/systemd
|
|
||||||
'
|
|
||||||
docker exec unit bash -euo pipefail -c '
|
|
||||||
for i in $(seq 1 60); do
|
|
||||||
state=$(systemctl is-system-running 2>/dev/null || true)
|
|
||||||
case $state in running | degraded) break ;; esac
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "systemd: $state"
|
|
||||||
pacman -Syu --noconfirm --needed python parted >/dev/null
|
|
||||||
useradd -m tester
|
|
||||||
cp -r /src /home/tester/omarchy-iso
|
|
||||||
chown -R tester /home/tester/omarchy-iso
|
|
||||||
cd /home/tester/omarchy-iso
|
|
||||||
setpriv --reuid=tester --regid=tester --init-groups ./test/all
|
|
||||||
'
|
|
||||||
@@ -28,20 +28,6 @@ Despite the local folder name, the first argument is the Omarchy source checkout
|
|||||||
|
|
||||||
Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror.
|
Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror.
|
||||||
|
|
||||||
### Regional builds
|
|
||||||
|
|
||||||
`--region cn` adds China bootstrap defaults independently of the channel. The default, `--region global`, preserves the existing package set, mirrors, cache paths, and artifact names. Supported country profiles use lowercase ISO 3166-1 alpha-2 codes; currently only `cn` is implemented.
|
|
||||||
|
|
||||||
For an unpublished regional runtime, use matching local source and package-recipe checkouts:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./bin/omarchy-iso-make --region cn --no-boot-offer --local-source /path/to/omarchy /path/to/omarchy-pkgs
|
|
||||||
```
|
|
||||||
|
|
||||||
China builds keep the channel mirror first, append the USTC Arch mirror, and add ArchLinuxCN with its verified keyring. These are a practical bridge until Omarchy runs its own mirror in China; USTC is rolling, not a matching stable/RC snapshot, so fallback version compatibility is not guaranteed. The region architecture also stages optional new-user defaults before user creation, including deferred setup, without touching existing homes. The China profile currently ships repository defaults only; language, timezone, and input-method defaults come as separate changes. See `docs/regions.md` in the companion runtime checkout.
|
|
||||||
|
|
||||||
Non-global cache and ISO names gain a `-cn` suffix. A runtime without the selected profile fails the build. These builds require an x86_64 Arch container environment; packaging checks on another architecture do not substitute for a fresh build, offline installation, and graphical acceptance run.
|
|
||||||
|
|
||||||
## Autoinstall
|
## Autoinstall
|
||||||
|
|
||||||
The shipped ISO installs itself with no keyboard when it finds its configuration on a second drive. Attach a drive labeled `cidata` alongside the ISO and the installer copies the config off it and skips the configurator; with no such drive, nothing changes and the wizard runs as usual. No rebuild, no extra boot entry.
|
The shipped ISO installs itself with no keyboard when it finds its configuration on a second drive. Attach a drive labeled `cidata` alongside the ISO and the installer copies the config off it and skips the configurator; with no such drive, nothing changes and the wizard runs as usual. No rebuild, no extra boot entry.
|
||||||
@@ -157,3 +143,11 @@ Run `./bin/omarchy-iso-upload [release/omarchy.iso]`. This requires rclone confi
|
|||||||
## Full release of the ISO
|
## Full release of the ISO
|
||||||
|
|
||||||
Run `./bin/omarchy-iso-release VERSION` to create, test, sign, and upload the ISO in one flow. Add `--rc` to release an RC build instead.
|
Run `./bin/omarchy-iso-release VERSION` to create, test, sign, and upload the ISO in one flow. Add `--rc` to release an RC build instead.
|
||||||
|
|
||||||
|
## Building the UTM SE appliance
|
||||||
|
|
||||||
|
`./bin/omarchy-utm-make` builds Omarchy for the App Store version of UTM on iPad and iPhone: an aarch64 disk with Arch Linux ARM underneath, wrapped as a `.utm` bundle and zipped into `release/Omarchy-<version>-aarch64-utm-se.utm.zip` beside its `.sha256`. Everything Omarchy ships from its own repository comes from the aarch64 builds at [ZacharyZhang-NY/omarchy-pkgs-aarch64](https://github.com/ZacharyZhang-NY/omarchy-pkgs-aarch64), which `builder/pacman-utm-se.conf` names; the runtime is the `omarchy-dev` and `omarchy-settings-dev` pair built there from the `utm-se` branch of omarchy, as local-source ISOs do.
|
||||||
|
|
||||||
|
The build runs in a privileged `linux/arm64` container: it partitions a 32 GiB sparse disk, lays out the same Btrfs subvolumes the ISO does, installs `builder/utm-se.packages` and then the Omarchy base set minus `builder/utm-se-drop.packages`, puts Limine on the UEFI fallback path, runs `omarchy-apply-system --defer-provisioning`, stages first-boot provisioning with the arm64 Node tarball, builds the boot entries, and seals the disk with an empty machine-id. The host needs Docker with a kernel that offers btrfs and loop devices (with colima: `colima ssh -- sudo modprobe btrfs`), `qemu` for the UEFI variable store template, `zip`, and `shasum`.
|
||||||
|
|
||||||
|
The bundle boots with 2 cores, 2048 MiB, a `virtio-gpu-pci` display at 1280x800, VirtFS sharing, and the shared clipboard. The first boot asks for the keyboard, user, and password, the way a deferred-provisioning ISO install does.
|
||||||
+8
-36
@@ -6,17 +6,8 @@ NO_CACHE=""
|
|||||||
KEEP_PKG_CACHE=""
|
KEEP_PKG_CACHE=""
|
||||||
LOCAL_OMARCHY_PATH=""
|
LOCAL_OMARCHY_PATH=""
|
||||||
LOCAL_PKGS_PATH=""
|
LOCAL_PKGS_PATH=""
|
||||||
OMARCHY_REGION="${OMARCHY_REGION:-global}"
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--region)
|
|
||||||
OMARCHY_REGION="${2:-}"
|
|
||||||
if [[ $OMARCHY_REGION != "global" && $OMARCHY_REGION != "cn" ]]; then
|
|
||||||
echo "Error: --region requires global or cn (ISO 3166-1 alpha-2)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--no-cache)
|
--no-cache)
|
||||||
NO_CACHE=1
|
NO_CACHE=1
|
||||||
shift
|
shift
|
||||||
@@ -66,17 +57,12 @@ while [[ $# -gt 0 ]]; do
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1"
|
echo "Unknown option: $1"
|
||||||
echo "Usage: $0 [--no-cache] [--keep-pkg-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--region global|cn] [--local-source <omarchy-checkout> <pkgs-checkout>]"
|
echo "Usage: $0 [--no-cache] [--keep-pkg-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--local-source <omarchy-checkout> <pkgs-checkout>]"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ $OMARCHY_REGION != "global" && $OMARCHY_REGION != "cn" ]]; then
|
|
||||||
echo "Error: unsupported region '$OMARCHY_REGION'. Expected global or cn." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n $LOCAL_OMARCHY_PATH ]]; then
|
if [[ -n $LOCAL_OMARCHY_PATH ]]; then
|
||||||
[[ -d $LOCAL_OMARCHY_PATH ]] || { echo "Error: Omarchy checkout not found at $LOCAL_OMARCHY_PATH" >&2; exit 1; }
|
[[ -d $LOCAL_OMARCHY_PATH ]] || { echo "Error: Omarchy checkout not found at $LOCAL_OMARCHY_PATH" >&2; exit 1; }
|
||||||
[[ -d $LOCAL_PKGS_PATH ]] || { echo "Error: pkgs checkout not found at $LOCAL_PKGS_PATH" >&2; exit 1; }
|
[[ -d $LOCAL_PKGS_PATH ]] || { echo "Error: pkgs checkout not found at $LOCAL_PKGS_PATH" >&2; exit 1; }
|
||||||
@@ -130,10 +116,6 @@ BUILD_SCRIPT="builder/build-iso.sh"
|
|||||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||||
BUILD_RELEASE_PATH="$BUILD_ROOT/release"
|
BUILD_RELEASE_PATH="$BUILD_ROOT/release"
|
||||||
mkdir -p "$BUILD_RELEASE_PATH"
|
mkdir -p "$BUILD_RELEASE_PATH"
|
||||||
# Each build writes into its own directory, so concurrent builds (such as a
|
|
||||||
# global and a regional one) never pick up and rename each other's ISO.
|
|
||||||
BUILD_OUT=$(mktemp -d "$BUILD_RELEASE_PATH/.build-XXXXXX")
|
|
||||||
trap 'rm -rf "$BUILD_OUT"' EXIT
|
|
||||||
|
|
||||||
OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
|
OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
|
||||||
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
|
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
|
||||||
@@ -143,11 +125,10 @@ DOCKER_ARGS=(
|
|||||||
--privileged
|
--privileged
|
||||||
-e "OMARCHY_ISO_REF=$OMARCHY_ISO_REF"
|
-e "OMARCHY_ISO_REF=$OMARCHY_ISO_REF"
|
||||||
-e "OMARCHY_MIRROR=$OMARCHY_MIRROR"
|
-e "OMARCHY_MIRROR=$OMARCHY_MIRROR"
|
||||||
-e "OMARCHY_REGION=$OMARCHY_REGION"
|
|
||||||
-e "OMARCHY_INSTALL_DEBUG=${OMARCHY_INSTALL_DEBUG:-}"
|
-e "OMARCHY_INSTALL_DEBUG=${OMARCHY_INSTALL_DEBUG:-}"
|
||||||
-e "HOST_UID=$(id -u)"
|
-e "HOST_UID=$(id -u)"
|
||||||
-e "HOST_GID=$(id -g)"
|
-e "HOST_GID=$(id -g)"
|
||||||
-v "$BUILD_OUT/:/out/"
|
-v "$BUILD_RELEASE_PATH/:/out/"
|
||||||
-v "$BUILD_ROOT/$BUILD_SCRIPT:/$BUILD_SCRIPT:ro"
|
-v "$BUILD_ROOT/$BUILD_SCRIPT:/$BUILD_SCRIPT:ro"
|
||||||
-v "$BUILD_ROOT/archiso:/archiso:ro"
|
-v "$BUILD_ROOT/archiso:/archiso:ro"
|
||||||
-v "$BUILD_ROOT/builder:/builder:ro"
|
-v "$BUILD_ROOT/builder:/builder:ro"
|
||||||
@@ -165,14 +146,10 @@ if [[ -d /var/cache/pacman/pkg ]]; then
|
|||||||
DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg")
|
DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Channels and regions must not share an offline mirror cache. Keep the
|
# Mount the build cache directory where packages are downloaded. Channels use
|
||||||
# existing global cache path and artifact suffix unchanged.
|
# different package snapshots and must never share an offline mirror cache.
|
||||||
region_suffix=""
|
|
||||||
if [[ $OMARCHY_REGION != "global" ]]; then
|
|
||||||
region_suffix="-$OMARCHY_REGION"
|
|
||||||
fi
|
|
||||||
if [[ -z "$NO_CACHE" ]]; then
|
if [[ -z "$NO_CACHE" ]]; then
|
||||||
OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${OMARCHY_MIRROR}${region_suffix}/airootfs/var/cache/omarchy"
|
OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${OMARCHY_MIRROR}/airootfs/var/cache/omarchy"
|
||||||
mkdir -p "$OFFLINE_REPO_BUILD_CACHE_DIR"
|
mkdir -p "$OFFLINE_REPO_BUILD_CACHE_DIR"
|
||||||
DOCKER_ARGS+=(-v "$OFFLINE_REPO_BUILD_CACHE_DIR:/var/cache/airootfs/var/cache/omarchy")
|
DOCKER_ARGS+=(-v "$OFFLINE_REPO_BUILD_CACHE_DIR:/var/cache/airootfs/var/cache/omarchy")
|
||||||
else
|
else
|
||||||
@@ -191,14 +168,9 @@ fi
|
|||||||
|
|
||||||
"${DOCKER[@]}" run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
|
"${DOCKER[@]}" run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
|
||||||
|
|
||||||
built_isos=("$BUILD_OUT"/*.iso)
|
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*.iso | head -n1)
|
||||||
if (( ${#built_isos[@]} != 1 )) || [[ ! -f ${built_isos[0]} ]]; then
|
iso_ref="${latest_iso%.*}-$OMARCHY_ISO_REF.iso"
|
||||||
echo "Error: expected exactly one ISO in $BUILD_OUT" >&2
|
mv -f "$latest_iso" "$iso_ref"
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
built_iso_name=$(basename "${built_isos[0]}" .iso)
|
|
||||||
iso_ref="$BUILD_RELEASE_PATH/$built_iso_name-${OMARCHY_ISO_REF}${region_suffix}.iso"
|
|
||||||
mv -f "${built_isos[0]}" "$iso_ref"
|
|
||||||
|
|
||||||
if [[ -z "${NO_BOOT_OFFER:-}" ]]; then
|
if [[ -z "${NO_BOOT_OFFER:-}" ]]; then
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -614,7 +614,7 @@ drive_configurator() {
|
|||||||
log "Driving the installer wizard"
|
log "Driving the installer wizard"
|
||||||
|
|
||||||
# A greeter (animated logo + tagline) opens the installer; Return begins.
|
# A greeter (animated logo + tagline) opens the installer; Return begins.
|
||||||
wait_for_screen "Linux by DHH" 300
|
wait_for_screen "Opinionated" 300
|
||||||
capture_console "success-installer-00-greeter"
|
capture_console "success-installer-00-greeter"
|
||||||
press ret
|
press ret
|
||||||
|
|
||||||
@@ -731,7 +731,7 @@ drive_provision_owner() {
|
|||||||
# layout — UK leaves every letter identical to US, so the typed password (sent
|
# layout — UK leaves every letter identical to US, so the typed password (sent
|
||||||
# as raw keycodes) still matches the literal one the harness pipes to sudo.
|
# as raw keycodes) still matches the literal one the harness pipes to sudo.
|
||||||
# A greeter (animated logo + tagline) opens first boot; Return begins.
|
# A greeter (animated logo + tagline) opens first boot; Return begins.
|
||||||
wait_for_screen "Linux by DHH" 600
|
wait_for_screen "Opinionated" 600
|
||||||
capture_console "success-provision-00-greeter"
|
capture_console "success-provision-00-greeter"
|
||||||
press ret
|
press ret
|
||||||
|
|
||||||
@@ -806,7 +806,7 @@ wait_for_install() {
|
|||||||
# Deferred-provisioning installs skip the celebration and reboot on their own
|
# Deferred-provisioning installs skip the celebration and reboot on their own
|
||||||
# (no Reboot Now prompt); the first-boot greeter tagline appearing means the
|
# (no Reboot Now prompt); the first-boot greeter tagline appearing means the
|
||||||
# install finished and rebooted into setup.
|
# install finished and rebooted into setup.
|
||||||
if $PROVISION && grep -qi "Linux by DHH" <<<"$text"; then
|
if $PROVISION && grep -qi "Opinionated" <<<"$text"; then
|
||||||
log "Install finished and auto-rebooted into first-boot setup."
|
log "Install finished and auto-rebooted into first-boot setup."
|
||||||
capture_console "success-installer-15-autoreboot"
|
capture_console "success-installer-15-autoreboot"
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 <omarchy-utm-se.qcow2|Omarchy.utm> [--hvf]" >&2
|
||||||
|
echo "Boots the appliance disk under qemu-system-aarch64 with the devices the UTM bundle declares." >&2
|
||||||
|
echo "TCG emulation is the default, the closest stand-in for UTM SE; --hvf uses Apple's hypervisor instead." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
(( $# >= 1 )) || usage
|
||||||
|
disk="$1"
|
||||||
|
shift
|
||||||
|
accel=tcg
|
||||||
|
cpu=cortex-a72
|
||||||
|
if (( $# > 0 )); then
|
||||||
|
[[ $1 == "--hvf" ]] || usage
|
||||||
|
accel=hvf
|
||||||
|
cpu=host
|
||||||
|
fi
|
||||||
|
if [[ -d $disk ]]; then
|
||||||
|
disk=$(ls "$disk"/Data/*.qcow2 | head -n1)
|
||||||
|
fi
|
||||||
|
[[ -f $disk ]] || { echo "Error: $disk is not a disk image" >&2; exit 1; }
|
||||||
|
|
||||||
|
firmware=""
|
||||||
|
for candidate in "$(brew --prefix 2>/dev/null)/share/qemu" /usr/share/qemu /usr/share/edk2/aarch64; do
|
||||||
|
if [[ -f $candidate/edk2-aarch64-code.fd ]]; then
|
||||||
|
firmware="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ -n $firmware ]] || { echo "Error: no edk2-aarch64-code.fd found; install qemu" >&2; exit 1; }
|
||||||
|
|
||||||
|
run_dir="${TMPDIR:-/tmp}/omarchy-utm-boot"
|
||||||
|
mkdir -p "$run_dir"
|
||||||
|
vars="$run_dir/efi_vars.fd"
|
||||||
|
[[ -f $vars ]] || cp "$firmware/edk2-arm-vars.fd" "$vars"
|
||||||
|
echo "QMP socket: $run_dir/qmp.sock serial log: $run_dir/serial.log ssh: localhost:2222"
|
||||||
|
|
||||||
|
exec qemu-system-aarch64 \
|
||||||
|
-machine virt -cpu "$cpu" -accel "$accel" -smp 2 -m 2048 \
|
||||||
|
-drive if=pflash,format=raw,readonly=on,file="$firmware/edk2-aarch64-code.fd" \
|
||||||
|
-drive if=pflash,format=raw,file="$vars" \
|
||||||
|
-drive file="$disk",format=qcow2,if=none,id=drive0 \
|
||||||
|
-device virtio-blk-pci,drive=drive0 \
|
||||||
|
-device virtio-gpu-pci -device virtio-keyboard-pci -device virtio-mouse-pci \
|
||||||
|
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 -device virtio-net-pci,netdev=net0 \
|
||||||
|
-device virtio-rng-pci \
|
||||||
|
-serial file:"$run_dir/serial.log" \
|
||||||
|
-qmp unix:"$run_dir/qmp.sock",server,nowait \
|
||||||
|
-display default
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BUILD_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
RELEASE_DIR="$BUILD_ROOT/release"
|
||||||
|
CACHE_DIR="$HOME/.cache/omarchy/utm-se"
|
||||||
|
BUILDER_IMAGE="menci/archlinuxarm:base"
|
||||||
|
|
||||||
|
if (( $# > 0 )); then
|
||||||
|
echo "Usage: $0" >&2
|
||||||
|
echo "Builds the Omarchy appliance for UTM SE into $RELEASE_DIR: a zipped .utm bundle and its sha256." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOCKER=(docker)
|
||||||
|
if ! docker version &>/dev/null; then
|
||||||
|
DOCKER=(sudo docker)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The build partitions a loop device and mounts btrfs inside the container, so the engine's kernel must offer both.
|
||||||
|
if ! "${DOCKER[@]}" run --rm --privileged --platform linux/arm64 "$BUILDER_IMAGE" \
|
||||||
|
sh -c 'grep -q btrfs /proc/filesystems && losetup -f >/dev/null'; then
|
||||||
|
echo "Error: the Docker engine's kernel needs btrfs and loop devices (colima: colima ssh -- sudo modprobe btrfs)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
efi_vars=""
|
||||||
|
for candidate in "$(brew --prefix 2>/dev/null)/share/qemu/edk2-arm-vars.fd" /usr/share/qemu/edk2-arm-vars.fd /usr/share/edk2/aarch64/QEMU_VARS.fd; do
|
||||||
|
if [[ -f $candidate ]]; then
|
||||||
|
efi_vars="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -z $efi_vars ]]; then
|
||||||
|
echo "Error: no UEFI variable store template found; install qemu" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$RELEASE_DIR" "$CACHE_DIR/pkg"
|
||||||
|
rm -f "$RELEASE_DIR/omarchy-utm-se.qcow2" "$RELEASE_DIR/omarchy-utm-se.version"
|
||||||
|
|
||||||
|
"${DOCKER[@]}" run --rm --privileged --platform linux/arm64 \
|
||||||
|
-v "$BUILD_ROOT/builder:/builder:ro" \
|
||||||
|
-v "$RELEASE_DIR:/out" \
|
||||||
|
-v "$CACHE_DIR/pkg:/var/cache/pacman/pkg" \
|
||||||
|
"$BUILDER_IMAGE" bash /builder/build-utm.sh
|
||||||
|
|
||||||
|
version=$(cat "$RELEASE_DIR/omarchy-utm-se.version")
|
||||||
|
name="Omarchy-$version-aarch64-utm-se"
|
||||||
|
bundle="$RELEASE_DIR/$name/Omarchy.utm"
|
||||||
|
vm_uuid=$(uuidgen | tr '[:lower:]' '[:upper:]')
|
||||||
|
disk_uuid=$(uuidgen | tr '[:lower:]' '[:upper:]')
|
||||||
|
mac=$(printf '02:%02X:%02X:%02X:%02X:%02X' $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256)))
|
||||||
|
|
||||||
|
rm -rf "$RELEASE_DIR/$name"
|
||||||
|
mkdir -p "$bundle/Data"
|
||||||
|
mv "$RELEASE_DIR/omarchy-utm-se.qcow2" "$bundle/Data/$disk_uuid.qcow2"
|
||||||
|
cp "$efi_vars" "$bundle/Data/efi_vars.fd"
|
||||||
|
"$BUILD_ROOT/builder/utm/render" "$vm_uuid" "$disk_uuid" "$mac" "$version" >"$bundle/config.plist"
|
||||||
|
|
||||||
|
rm -f "$RELEASE_DIR/$name.utm.zip"
|
||||||
|
(cd "$RELEASE_DIR/$name" && zip -qr "../$name.utm.zip" Omarchy.utm)
|
||||||
|
(cd "$RELEASE_DIR" && shasum -a 256 "$name.utm.zip" >"$name.utm.zip.sha256")
|
||||||
|
rm -rf "$RELEASE_DIR/$name" "$RELEASE_DIR/omarchy-utm-se.version"
|
||||||
|
echo "Built $RELEASE_DIR/$name.utm.zip"
|
||||||
+2
-44
@@ -4,11 +4,6 @@ set -e
|
|||||||
|
|
||||||
OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
|
OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
|
||||||
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
|
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
|
||||||
OMARCHY_REGION="${OMARCHY_REGION:-global}"
|
|
||||||
case "$OMARCHY_REGION" in
|
|
||||||
global|cn) ;;
|
|
||||||
*) echo "Unsupported region: $OMARCHY_REGION" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Edge, dev, and local-source ISOs install the dev packages explicitly. Those
|
# Edge, dev, and local-source ISOs install the dev packages explicitly. Those
|
||||||
# package recipes track the quattro branch. This avoids relying on pacman's
|
# package recipes track the quattro branch. This avoids relying on pacman's
|
||||||
@@ -72,7 +67,6 @@ cp -r /configs/* "$build_cache_dir/"
|
|||||||
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
|
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
|
||||||
echo "$OMARCHY_MIRROR" > "$build_cache_dir/airootfs/root/omarchy_mirror"
|
echo "$OMARCHY_MIRROR" > "$build_cache_dir/airootfs/root/omarchy_mirror"
|
||||||
echo "$OMARCHY_ISO_REF" > "$build_cache_dir/airootfs/root/omarchy_iso_ref"
|
echo "$OMARCHY_ISO_REF" > "$build_cache_dir/airootfs/root/omarchy_iso_ref"
|
||||||
echo "$OMARCHY_REGION" > "$build_cache_dir/airootfs/root/omarchy_region"
|
|
||||||
cat > "$build_cache_dir/airootfs/usr/share/omarchy-iso/package-targets" <<EOF
|
cat > "$build_cache_dir/airootfs/usr/share/omarchy-iso/package-targets" <<EOF
|
||||||
OMARCHY_RUNTIME_PACKAGE=$OMARCHY_RUNTIME_PACKAGE
|
OMARCHY_RUNTIME_PACKAGE=$OMARCHY_RUNTIME_PACKAGE
|
||||||
OMARCHY_SETTINGS_PACKAGE=$OMARCHY_SETTINGS_PACKAGE
|
OMARCHY_SETTINGS_PACKAGE=$OMARCHY_SETTINGS_PACKAGE
|
||||||
@@ -86,7 +80,6 @@ if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then
|
|||||||
echo "built_at=$(date -Is)"
|
echo "built_at=$(date -Is)"
|
||||||
echo "ref=$OMARCHY_ISO_REF"
|
echo "ref=$OMARCHY_ISO_REF"
|
||||||
echo "mirror=$OMARCHY_MIRROR"
|
echo "mirror=$OMARCHY_MIRROR"
|
||||||
echo "region=$OMARCHY_REGION"
|
|
||||||
echo "runtime_package=$OMARCHY_RUNTIME_PACKAGE"
|
echo "runtime_package=$OMARCHY_RUNTIME_PACKAGE"
|
||||||
echo "settings_package=$OMARCHY_SETTINGS_PACKAGE"
|
echo "settings_package=$OMARCHY_SETTINGS_PACKAGE"
|
||||||
echo "nvim_package=$OMARCHY_NVIM_PACKAGE"
|
echo "nvim_package=$OMARCHY_NVIM_PACKAGE"
|
||||||
@@ -146,7 +139,6 @@ sed -i -E '/^(linux|broadcom-wl)$/d' "$build_cache_dir/packages.x86_64"
|
|||||||
# already in the mirror and we filter them out below. Without it, pacman -Syw
|
# already in the mirror and we filter them out below. Without it, pacman -Syw
|
||||||
# pulls the published omarchy* from the network mirror like any other package.
|
# pulls the published omarchy* from the network mirror like any other package.
|
||||||
if [[ -d /omarchy-source ]]; then
|
if [[ -d /omarchy-source ]]; then
|
||||||
runtime_root=/omarchy-source
|
|
||||||
base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages)
|
base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages)
|
||||||
setup_form=/omarchy-source/install/provisioning/setup-form.sh
|
setup_form=/omarchy-source/install/provisioning/setup-form.sh
|
||||||
else
|
else
|
||||||
@@ -163,14 +155,6 @@ else
|
|||||||
fi
|
fi
|
||||||
mkdir -p /tmp/omarchy-pkglists
|
mkdir -p /tmp/omarchy-pkglists
|
||||||
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/omarchy-base.packages usr/share/omarchy/install/omarchy-other.packages
|
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/omarchy-base.packages usr/share/omarchy/install/omarchy-other.packages
|
||||||
runtime_root=/tmp/omarchy-pkglists/usr/share/omarchy
|
|
||||||
if [[ $OMARCHY_REGION != "global" ]]; then
|
|
||||||
if ! bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists \
|
|
||||||
"usr/share/omarchy/default/regions/$OMARCHY_REGION" usr/share/omarchy/bin/omarchy-apply-pacman; then
|
|
||||||
echo "ERROR: runtime lacks region support; publish a matching runtime or use --local-source." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
base_pkg_lists=(/tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-base.packages /tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-other.packages)
|
base_pkg_lists=(/tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-base.packages /tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-other.packages)
|
||||||
# Extracted on its own, tolerating a miss: bsdtar exits non-zero for a member
|
# Extracted on its own, tolerating a miss: bsdtar exits non-zero for a member
|
||||||
# it can't find, so asking for this alongside the package lists would abort the
|
# it can't find, so asking for this alongside the package lists would abort the
|
||||||
@@ -184,15 +168,6 @@ mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
|
|||||||
cp "${base_pkg_lists[0]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
|
cp "${base_pkg_lists[0]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
|
||||||
cp "${base_pkg_lists[1]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages"
|
cp "${base_pkg_lists[1]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages"
|
||||||
|
|
||||||
# Work on writable copies, leaving the mounted runtime and ISO configs intact.
|
|
||||||
online_config=/tmp/omarchy-pacman-online.conf
|
|
||||||
cp "/configs/pacman-online-${OMARCHY_MIRROR}.conf" "$online_config"
|
|
||||||
bash /builder/prepare-region.sh "$OMARCHY_REGION" "$runtime_root" "$build_cache_dir/airootfs" "$online_config"
|
|
||||||
base_pkg_lists=(
|
|
||||||
"$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
|
|
||||||
"$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages"
|
|
||||||
)
|
|
||||||
|
|
||||||
# The configurator's setup form comes from the runtime this ISO bundles, so the
|
# The configurator's setup form comes from the runtime this ISO bundles, so the
|
||||||
# installer and the first-boot setup that finishes a deferred install can never
|
# installer and the first-boot setup that finishes a deferred install can never
|
||||||
# disagree. A runtime predating the split ships no such file, which would leave
|
# disagree. A runtime predating the split ships no such file, which would leave
|
||||||
@@ -225,23 +200,6 @@ mapfile -t all_packages < <(
|
|||||||
} | sort -u
|
} | sort -u
|
||||||
)
|
)
|
||||||
|
|
||||||
# Arch dropped the prebuilt broadcom-wl on 2026-09-02 and rebuilt broadcom-wl-dkms
|
|
||||||
# with replaces=(broadcom-wl). A replaces entry only helps upgrades of an already
|
|
||||||
# installed package; as an explicit pacman target the old name now fails with
|
|
||||||
# "target not found". Published Omarchy runtime packages that predate the rename
|
|
||||||
# still list it in omarchy-other.packages, so map it here until every channel
|
|
||||||
# ships a runtime that names broadcom-wl-dkms itself.
|
|
||||||
# arch-mact2 dropped apple-bcm-firmware on 2026-09-16 in favour of
|
|
||||||
# apple-bcm-firmware-fetcher, which does the same job (pull the T2 Wi-Fi and
|
|
||||||
# Bluetooth firmware off the macOS volume) but only conflicts with the old name
|
|
||||||
# rather than replacing it, so pacman cannot follow the rename on its own.
|
|
||||||
mapfile -t all_packages < <(
|
|
||||||
printf '%s\n' "${all_packages[@]}" |
|
|
||||||
sed -e 's/^broadcom-wl$/broadcom-wl-dkms/' \
|
|
||||||
-e 's/^apple-bcm-firmware$/apple-bcm-firmware-fetcher/' |
|
|
||||||
sort -u
|
|
||||||
)
|
|
||||||
|
|
||||||
# With --local-source we already built these omarchy* packages directly into
|
# With --local-source we already built these omarchy* packages directly into
|
||||||
# the mirror; strip them from the pacman -Syw list so it doesn't try to fetch
|
# the mirror; strip them from the pacman -Syw list so it doesn't try to fetch
|
||||||
# the published versions on top.
|
# the published versions on top.
|
||||||
@@ -257,7 +215,7 @@ fi
|
|||||||
|
|
||||||
mkdir -p /tmp/offlinedb
|
mkdir -p /tmp/offlinedb
|
||||||
download_offline_packages() {
|
download_offline_packages() {
|
||||||
pacman --config "$online_config" --noconfirm -Syw \
|
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw \
|
||||||
"${all_packages[@]}" --cachedir "$offline_mirror_dir/" --dbpath /tmp/offlinedb --needed
|
"${all_packages[@]}" --cachedir "$offline_mirror_dir/" --dbpath /tmp/offlinedb --needed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +233,7 @@ fi
|
|||||||
# newest version of every cached package name) removes packages that have left
|
# newest version of every cached package name) removes packages that have left
|
||||||
# the lists or dependency closure, such as an old Electron major version.
|
# the lists or dependency closure, such as an old Electron major version.
|
||||||
if ! resolved_package_files="$(
|
if ! resolved_package_files="$(
|
||||||
pacman --config "$online_config" --noconfirm \
|
pacman --config "/configs/pacman-online-${OMARCHY_MIRROR}.conf" --noconfirm \
|
||||||
--dbpath /tmp/offlinedb -S --print --print-format '%f' "${all_packages[@]}"
|
--dbpath /tmp/offlinedb -S --print --print-format '%f' "${all_packages[@]}"
|
||||||
)"; then
|
)"; then
|
||||||
echo "ERROR: could not resolve the package files required by the offline mirror" >&2
|
echo "ERROR: could not resolve the package files required by the offline mirror" >&2
|
||||||
|
|||||||
Executable
+205
@@ -0,0 +1,205 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Builds the aarch64 Omarchy disk for UTM SE inside a privileged Arch Linux ARM container.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BUILDER=/builder
|
||||||
|
OUT=/out
|
||||||
|
WORK=/work
|
||||||
|
TARGET=$WORK/target
|
||||||
|
IMAGE=$WORK/omarchy.img
|
||||||
|
DISK_SIZE=32G
|
||||||
|
NODE_DIST_URL="https://nodejs.org/dist/latest"
|
||||||
|
PROVISIONING_DIR=var/lib/omarchy/provisioning
|
||||||
|
loop=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
umount -R "$TARGET" 2>/dev/null || true
|
||||||
|
[[ -n $loop ]] && losetup -d "$loop" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
packages() {
|
||||||
|
grep -hv '^#\|^$' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker's /dev is static, so the partition nodes the kernel scanned have to be made by hand.
|
||||||
|
partition() {
|
||||||
|
local node="${loop}p$1"
|
||||||
|
[[ -b $node ]] || mknod "$node" b $(tr ':' ' ' <"/sys/block/${loop#/dev/}/${loop#/dev/}p$1/dev")
|
||||||
|
echo "$node"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The ISO defers the boot-image hooks the same way: /dev/null masks on the side pacman reads them from.
|
||||||
|
deferred_hooks=(60-mkinitcpio-remove.hook 60-limine-mkinitcpio-remove-pre.hook 80-limine-efi-deploy.hook 90-limine-mkinitcpio-remove-post.hook 90-mkinitcpio-install.hook)
|
||||||
|
|
||||||
|
mask_hooks() {
|
||||||
|
local dir="$1" name path
|
||||||
|
shift
|
||||||
|
mkdir -p "$dir"
|
||||||
|
for name in "$@"; do
|
||||||
|
path="$dir/$name"
|
||||||
|
if [[ -e $path || -L $path ]]; then
|
||||||
|
mv "$path" "$path.omarchy-backup"
|
||||||
|
fi
|
||||||
|
ln -s /dev/null "$path"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
unmask_hooks() {
|
||||||
|
local dir="$1" name path
|
||||||
|
shift
|
||||||
|
for name in "$@"; do
|
||||||
|
path="$dir/$name"
|
||||||
|
rm "$path"
|
||||||
|
if [[ -e $path.omarchy-backup ]]; then
|
||||||
|
mv "$path.omarchy-backup" "$path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
chroot_env=(
|
||||||
|
env --unset=XDG_RUNTIME_DIR
|
||||||
|
OMARCHY_PATH=/usr/share/omarchy
|
||||||
|
OMARCHY_INSTALL=/usr/share/omarchy/install
|
||||||
|
OMARCHY_MIRROR=stable
|
||||||
|
OMARCHY_RUNTIME_PACKAGE=omarchy-dev
|
||||||
|
OMARCHY_SETTINGS_PACKAGE=omarchy-settings-dev
|
||||||
|
OMARCHY_NVIM_PACKAGE=omarchy-nvim
|
||||||
|
OMARCHY_INSTALL_LOG_FILE=/var/log/omarchy-install.log
|
||||||
|
OMARCHY_LOG_TO_STDOUT=1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "==> Preparing the build container"
|
||||||
|
pacman-key --init
|
||||||
|
pacman-key --populate archlinuxarm
|
||||||
|
install -m 0644 "$BUILDER/mirrorlist-utm-se" /etc/pacman.d/mirrorlist
|
||||||
|
pacman -Syu --noconfirm arch-install-scripts btrfs-progs dosfstools gptfdisk psmisc qemu-img
|
||||||
|
|
||||||
|
echo "==> Partitioning a $DISK_SIZE disk"
|
||||||
|
mkdir -p "$WORK" "$TARGET"
|
||||||
|
rm -f "$IMAGE"
|
||||||
|
truncate -s "$DISK_SIZE" "$IMAGE"
|
||||||
|
sgdisk --zap-all --new=1:0:+1G --typecode=1:ef00 --change-name=1:OMARCHY_ESP \
|
||||||
|
--new=2:0:0 --typecode=2:8305 --change-name=2:OMARCHY "$IMAGE" >/dev/null
|
||||||
|
loop=$(losetup --find --show --partscan "$IMAGE")
|
||||||
|
esp=$(partition 1)
|
||||||
|
root=$(partition 2)
|
||||||
|
|
||||||
|
mkfs.vfat -F32 -n OMARCHY_ESP "$esp" >/dev/null
|
||||||
|
mkfs.btrfs -f -L OMARCHY "$root" >/dev/null
|
||||||
|
mount "$root" "$TARGET"
|
||||||
|
for subvolume in @ @home @log @pkg; do
|
||||||
|
btrfs subvolume create "$TARGET/$subvolume" >/dev/null
|
||||||
|
done
|
||||||
|
umount "$TARGET"
|
||||||
|
mount -o noatime,compress=zstd,subvol=@ "$root" "$TARGET"
|
||||||
|
mkdir -p "$TARGET/home" "$TARGET/var/log" "$TARGET/var/cache/pacman/pkg" "$TARGET/boot"
|
||||||
|
mount -o noatime,compress=zstd,subvol=@home "$root" "$TARGET/home"
|
||||||
|
mount -o noatime,compress=zstd,subvol=@log "$root" "$TARGET/var/log"
|
||||||
|
mount -o noatime,compress=zstd,subvol=@pkg "$root" "$TARGET/var/cache/pacman/pkg"
|
||||||
|
mount "$esp" "$TARGET/boot"
|
||||||
|
|
||||||
|
echo "==> Installing the base system and Omarchy"
|
||||||
|
# pacstrap reads hooks from this container's /etc/pacman.d/hooks; the UKI is built once by limine-update at the end.
|
||||||
|
mask_hooks /etc/pacman.d/hooks "${deferred_hooks[@]}"
|
||||||
|
pacstrap -c -K -C "$BUILDER/pacman-utm-se.conf" "$TARGET" $(packages "$BUILDER/utm-se.packages")
|
||||||
|
base_list="$TARGET/usr/share/omarchy/install/omarchy-base.packages"
|
||||||
|
pacstrap -c -C "$BUILDER/pacman-utm-se.conf" "$TARGET" \
|
||||||
|
$(bash "$TARGET/usr/bin/omarchy-pkg-list" "$base_list" | grep -vxF -f <(packages "$BUILDER/utm-se-drop.packages"))
|
||||||
|
|
||||||
|
echo "==> Configuring the system"
|
||||||
|
# genfstab falls back to the hand-made loop device names here, so the table is written from the UUIDs directly.
|
||||||
|
root_uuid=$(blkid -s UUID -o value "$root")
|
||||||
|
esp_uuid=$(blkid -s UUID -o value "$esp")
|
||||||
|
cat >"$TARGET/etc/fstab" <<FSTAB
|
||||||
|
UUID=$root_uuid / btrfs rw,noatime,compress=zstd,subvol=/@ 0 0
|
||||||
|
UUID=$root_uuid /home btrfs rw,noatime,compress=zstd,subvol=/@home 0 0
|
||||||
|
UUID=$root_uuid /var/log btrfs rw,noatime,compress=zstd,subvol=/@log 0 0
|
||||||
|
UUID=$root_uuid /var/cache/pacman/pkg btrfs rw,noatime,compress=zstd,subvol=/@pkg 0 0
|
||||||
|
UUID=$esp_uuid /boot vfat rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro 0 2
|
||||||
|
FSTAB
|
||||||
|
install -m 0644 "$BUILDER/pacman-utm-se.conf" "$TARGET/etc/pacman.conf"
|
||||||
|
install -m 0644 "$BUILDER/mirrorlist-utm-se" "$TARGET/etc/pacman.d/mirrorlist"
|
||||||
|
echo "en_US.UTF-8 UTF-8" >"$TARGET/etc/locale.gen"
|
||||||
|
arch-chroot "$TARGET" locale-gen >/dev/null
|
||||||
|
echo "LANG=en_US.UTF-8" >"$TARGET/etc/locale.conf"
|
||||||
|
printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$TARGET/etc/vconsole.conf"
|
||||||
|
echo omarchy >"$TARGET/etc/hostname"
|
||||||
|
ln -sf /usr/share/zoneinfo/UTC "$TARGET/etc/localtime"
|
||||||
|
install -m 0644 "$BUILDER/utm/monitors.lua" "$TARGET/etc/skel/.config/hypr/monitors.lua"
|
||||||
|
|
||||||
|
echo "==> Installing Limine on the fallback boot path"
|
||||||
|
# The virt machine's firmware puts the console on the serial port; tty0 keeps boot messages on the display too.
|
||||||
|
cmdline="root=UUID=$root_uuid rootflags=subvol=@ rw console=ttyAMA0 console=tty0"
|
||||||
|
install -D -m 0644 "$TARGET/usr/share/limine/BOOTAA64.EFI" "$TARGET/boot/EFI/BOOT/BOOTAA64.EFI"
|
||||||
|
mkdir -p "$TARGET/etc/kernel"
|
||||||
|
cat >"$TARGET/etc/pacman.d/hooks/99-omarchy-limine.hook" <<'HOOK'
|
||||||
|
[Trigger]
|
||||||
|
Operation = Upgrade
|
||||||
|
Type = Package
|
||||||
|
Target = limine
|
||||||
|
|
||||||
|
[Action]
|
||||||
|
Description = Deploying Omarchy Limine after upgrade...
|
||||||
|
When = PostTransaction
|
||||||
|
Exec = /bin/sh -c "/usr/bin/cp /usr/share/limine/BOOTAA64.EFI /boot/EFI/BOOT/BOOTAA64.EFI"
|
||||||
|
HOOK
|
||||||
|
sed "s|@@CMDLINE@@|$cmdline|" "$TARGET/usr/share/omarchy/default/limine/default.conf" >"$TARGET/etc/default/limine"
|
||||||
|
echo "$cmdline" >"$TARGET/etc/kernel/cmdline"
|
||||||
|
cp "$TARGET/usr/share/omarchy/default/limine/limine.conf" "$TARGET/boot/limine.conf"
|
||||||
|
|
||||||
|
echo "==> Running Omarchy system setup"
|
||||||
|
mask_hooks "$TARGET/etc/pacman.d/hooks" 90-mkinitcpio-install.hook
|
||||||
|
arch-chroot "$TARGET" "${chroot_env[@]}" omarchy-apply-system --defer-provisioning --first-install
|
||||||
|
unmask_hooks "$TARGET/etc/pacman.d/hooks" 90-mkinitcpio-install.hook
|
||||||
|
|
||||||
|
echo "==> Staging first-boot provisioning"
|
||||||
|
provisioning="$TARGET/$PROVISIONING_DIR"
|
||||||
|
install -d -m 0755 "$provisioning" "$provisioning/packages"
|
||||||
|
shasums=$(curl -fsSL "$NODE_DIST_URL/SHASUMS256.txt")
|
||||||
|
node_file=$(awk '/linux-arm64\.tar\.gz$/ { print $2 }' <<<"$shasums")
|
||||||
|
curl -fsSL "$NODE_DIST_URL/$node_file" -o "$provisioning/packages/$node_file"
|
||||||
|
awk -v file="$provisioning/packages/$node_file" '/linux-arm64\.tar\.gz$/ { print $1 " " file }' <<<"$shasums" | sha256sum -c - >/dev/null
|
||||||
|
touch "$provisioning/pending"
|
||||||
|
install -m 0644 "$TARGET/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service" \
|
||||||
|
"$TARGET/etc/systemd/system/omarchy-provision-owner.service"
|
||||||
|
mkdir -p "$TARGET/etc/systemd/system/multi-user.target.wants"
|
||||||
|
ln -sf /etc/systemd/system/omarchy-provision-owner.service \
|
||||||
|
"$TARGET/etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service"
|
||||||
|
|
||||||
|
echo "==> Building the boot entries"
|
||||||
|
arch-chroot "$TARGET" limine-update
|
||||||
|
arch-chroot "$TARGET" btrfs quota disable / >/dev/null 2>&1 || true
|
||||||
|
# limine-update swallows a kernel's failure, so the UKI and its entry are checked directly.
|
||||||
|
compgen -G "$TARGET/boot/EFI/Linux/*.efi" >/dev/null
|
||||||
|
grep -q 'EFI/Linux/' "$TARGET/boot/limine.conf"
|
||||||
|
|
||||||
|
echo "==> Sealing the image"
|
||||||
|
version=$(arch-chroot "$TARGET" pacman -Q omarchy-dev | cut -d' ' -f2)
|
||||||
|
: >"$TARGET/etc/machine-id"
|
||||||
|
rm -f "$TARGET"/etc/ssh/ssh_host_* "$TARGET/root/.bash_history"
|
||||||
|
rm -rf "$TARGET"/var/cache/pacman/pkg/*
|
||||||
|
# Freed blocks still hold old bytes; trimming zeroes them so they compress to nothing.
|
||||||
|
fstrim "$TARGET" && fstrim "$TARGET/boot"
|
||||||
|
|
||||||
|
echo "==> Checking the sealed image"
|
||||||
|
[[ ! -s $TARGET/etc/machine-id ]]
|
||||||
|
[[ -z $(ls -A "$TARGET/var/cache/pacman/pkg") ]]
|
||||||
|
[[ -f $provisioning/pending && -f $provisioning/packages/$node_file ]]
|
||||||
|
[[ -L $TARGET/etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service ]]
|
||||||
|
[[ -f $TARGET/boot/EFI/BOOT/BOOTAA64.EFI && -f $TARGET/etc/snapper/configs/root ]]
|
||||||
|
[[ -f $TARGET/etc/pacman.d/hooks/90-mkinitcpio-install.hook && ! -L $TARGET/etc/pacman.d/hooks/90-mkinitcpio-install.hook ]]
|
||||||
|
grep -q "root=UUID=$root_uuid" "$TARGET/etc/kernel/cmdline"
|
||||||
|
# Keyring setup leaves gpg-agents holding files on the target; their sockets died with the chroot's /run, so kill by holder.
|
||||||
|
fuser -km "$TARGET" >/dev/null 2>&1 || true
|
||||||
|
if ! umount -R "$TARGET"; then
|
||||||
|
fuser -vm "$TARGET" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
losetup -d "$loop"
|
||||||
|
loop=""
|
||||||
|
|
||||||
|
echo "==> Writing the compressed disk"
|
||||||
|
qemu-img convert -c -O qcow2 -o compression_type=zstd,cluster_size=2M "$IMAGE" "$OUT/omarchy-utm-se.qcow2"
|
||||||
|
echo "$version" >"$OUT/omarchy-utm-se.version"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Arch Linux ARM mirrors: the geo mirror first, then fallbacks pacman tries when one stalls.
|
||||||
|
Server = http://fl.us.mirror.archlinuxarm.org/$arch/$repo
|
||||||
|
Server = http://ca.us.mirror.archlinuxarm.org/$arch/$repo
|
||||||
|
Server = http://dk.mirror.archlinuxarm.org/$arch/$repo
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# pacman configuration of the UTM SE appliance: Arch Linux ARM plus the aarch64 builds of Omarchy's own packages.
|
||||||
|
[options]
|
||||||
|
HoldPkg = pacman glibc
|
||||||
|
Architecture = auto
|
||||||
|
CheckSpace
|
||||||
|
ParallelDownloads = 5
|
||||||
|
SigLevel = Required DatabaseOptional
|
||||||
|
LocalFileSigLevel = Optional
|
||||||
|
# Developer documentation does not fit a bundle that GitHub caps at 2 GiB.
|
||||||
|
NoExtract = usr/share/doc/* usr/share/gtk-doc/*
|
||||||
|
|
||||||
|
[core]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
[alarm]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
[aur]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
[omarchy-aarch64]
|
||||||
|
SigLevel = Optional
|
||||||
|
Server = https://github.com/ZacharyZhang-NY/omarchy-pkgs-aarch64/releases/download/stable
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Add the selected runtime's regional payload to the ISO. Repository defaults
|
|
||||||
# stay in the runtime package, not in a second set of ISO-specific templates.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
region="$1"
|
|
||||||
runtime="$2"
|
|
||||||
iso_root="$3"
|
|
||||||
online_config="$4"
|
|
||||||
|
|
||||||
case "$region" in
|
|
||||||
global) exit 0 ;;
|
|
||||||
cn) ;;
|
|
||||||
*) echo "Unsupported region: $region" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
profile="$runtime/default/regions/$region"
|
|
||||||
if [[ ! -f $runtime/bin/omarchy-apply-pacman || ! -f $profile/packages ||
|
|
||||||
! -f $profile/pacman/pacman.conf.append || ! -f $profile/pacman/mirrorlist.append ]]; then
|
|
||||||
echo "Runtime does not support region '$region'; publish a matching runtime or use --local-source." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This is the target package list used both for download resolution and by the
|
|
||||||
# installer. Merely putting extra packages in the cache would not install them.
|
|
||||||
payload="$iso_root/usr/share/omarchy-iso"
|
|
||||||
cp -a "$profile" "$payload/region"
|
|
||||||
printf '\n' >> "$payload/omarchy-base.packages"
|
|
||||||
cat "$profile/packages" >> "$payload/omarchy-base.packages"
|
|
||||||
|
|
||||||
# Only the community repository is needed for regional package downloads.
|
|
||||||
# Keep the channel's build mirrors intact; target mirrors are restored later.
|
|
||||||
printf '\n' >> "$online_config"
|
|
||||||
cat "$profile/pacman/pacman.conf.append" >> "$online_config"
|
|
||||||
|
|
||||||
# The keyring package must be signed by a key already trusted by Arch. Do not
|
|
||||||
# bootstrap trust from an unchecked keyserver response or disable signatures.
|
|
||||||
# https://github.com/archlinuxcn/repo#usage
|
|
||||||
pacman --config "$online_config" --noconfirm -Sy --needed archlinuxcn-keyring
|
|
||||||
pacman-key --populate archlinuxcn
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Base packages the appliance leaves out: apps and toolchains too heavy for an interpreted guest, tools with no aarch64 build, and helpers for hardware the guest has not.
|
||||||
|
aether
|
||||||
|
asdcontrol
|
||||||
|
clang
|
||||||
|
docker-buildx
|
||||||
|
docker-compose
|
||||||
|
gpu-screen-recorder
|
||||||
|
hyprland-preview-share-picker
|
||||||
|
kdenlive
|
||||||
|
libreoffice-fresh
|
||||||
|
llvm
|
||||||
|
localsend
|
||||||
|
moonlight-qt
|
||||||
|
xournalpp
|
||||||
|
yay
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Installed before the Omarchy base set: the aarch64 kernel, keyrings, boot stack, guest tools, and the dev packages built from the utm-se checkout.
|
||||||
|
archlinuxarm-keyring
|
||||||
|
base
|
||||||
|
efibootmgr
|
||||||
|
limine
|
||||||
|
linux-aarch64
|
||||||
|
lua51
|
||||||
|
luarocks
|
||||||
|
omarchy-dev
|
||||||
|
omarchy-keyring
|
||||||
|
omarchy-settings-dev
|
||||||
|
openssh
|
||||||
|
pipewire
|
||||||
|
qemu-guest-agent
|
||||||
|
snapper
|
||||||
|
spice-vdagent
|
||||||
|
sudo
|
||||||
|
zram-generator
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Backend</key>
|
||||||
|
<string>QEMU</string>
|
||||||
|
<key>ConfigurationVersion</key>
|
||||||
|
<integer>4</integer>
|
||||||
|
<key>Information</key>
|
||||||
|
<dict>
|
||||||
|
<key>Name</key>
|
||||||
|
<string>Omarchy</string>
|
||||||
|
<key>UUID</key>
|
||||||
|
<string>@@VM_UUID@@</string>
|
||||||
|
<key>IconCustom</key>
|
||||||
|
<false/>
|
||||||
|
<key>Icon</key>
|
||||||
|
<string>arch-linux</string>
|
||||||
|
<key>Notes</key>
|
||||||
|
<string>Omarchy @@VERSION@@ for UTM SE (aarch64). The first boot asks for your keyboard, user, and password. Option acts as SUPER.</string>
|
||||||
|
</dict>
|
||||||
|
<key>System</key>
|
||||||
|
<dict>
|
||||||
|
<key>Architecture</key>
|
||||||
|
<string>aarch64</string>
|
||||||
|
<key>Target</key>
|
||||||
|
<string>virt</string>
|
||||||
|
<key>CPU</key>
|
||||||
|
<string>default</string>
|
||||||
|
<key>CPUFlagsAdd</key>
|
||||||
|
<array/>
|
||||||
|
<key>CPUFlagsRemove</key>
|
||||||
|
<array/>
|
||||||
|
<key>CPUCount</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
<key>ForceMulticore</key>
|
||||||
|
<false/>
|
||||||
|
<key>MemorySize</key>
|
||||||
|
<integer>2048</integer>
|
||||||
|
<key>JITCacheSize</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
</dict>
|
||||||
|
<key>QEMU</key>
|
||||||
|
<dict>
|
||||||
|
<key>DebugLog</key>
|
||||||
|
<false/>
|
||||||
|
<key>UEFIBoot</key>
|
||||||
|
<true/>
|
||||||
|
<key>RNGDevice</key>
|
||||||
|
<true/>
|
||||||
|
<key>BalloonDevice</key>
|
||||||
|
<false/>
|
||||||
|
<key>TPMDevice</key>
|
||||||
|
<false/>
|
||||||
|
<key>Hypervisor</key>
|
||||||
|
<false/>
|
||||||
|
<key>RTCLocalTime</key>
|
||||||
|
<false/>
|
||||||
|
<key>PS2Controller</key>
|
||||||
|
<false/>
|
||||||
|
<key>AdditionalArguments</key>
|
||||||
|
<array/>
|
||||||
|
</dict>
|
||||||
|
<key>Input</key>
|
||||||
|
<dict>
|
||||||
|
<key>UsbBusSupport</key>
|
||||||
|
<string>3.0</string>
|
||||||
|
<key>UsbSharing</key>
|
||||||
|
<false/>
|
||||||
|
<key>MaximumUsbShare</key>
|
||||||
|
<integer>3</integer>
|
||||||
|
</dict>
|
||||||
|
<key>Sharing</key>
|
||||||
|
<dict>
|
||||||
|
<key>DirectoryShareMode</key>
|
||||||
|
<string>VirtFS</string>
|
||||||
|
<key>DirectoryShareReadOnly</key>
|
||||||
|
<false/>
|
||||||
|
<key>ClipboardSharing</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>Display</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>Hardware</key>
|
||||||
|
<string>virtio-gpu-pci</string>
|
||||||
|
<key>DynamicResolution</key>
|
||||||
|
<false/>
|
||||||
|
<key>NativeResolution</key>
|
||||||
|
<false/>
|
||||||
|
<key>UpscalingFilter</key>
|
||||||
|
<string>Linear</string>
|
||||||
|
<key>DownscalingFilter</key>
|
||||||
|
<string>Linear</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>Drive</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>Identifier</key>
|
||||||
|
<string>@@DISK_UUID@@</string>
|
||||||
|
<key>ImageName</key>
|
||||||
|
<string>@@DISK_UUID@@.qcow2</string>
|
||||||
|
<key>ImageType</key>
|
||||||
|
<string>Disk</string>
|
||||||
|
<key>Interface</key>
|
||||||
|
<string>VirtIO</string>
|
||||||
|
<key>InterfaceVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>ReadOnly</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>Network</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>Mode</key>
|
||||||
|
<string>Shared</string>
|
||||||
|
<key>Hardware</key>
|
||||||
|
<string>virtio-net-pci</string>
|
||||||
|
<key>MacAddress</key>
|
||||||
|
<string>@@MAC@@</string>
|
||||||
|
<key>IsolateFromHost</key>
|
||||||
|
<false/>
|
||||||
|
<key>PortForward</key>
|
||||||
|
<array/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>Serial</key>
|
||||||
|
<array/>
|
||||||
|
<key>Sound</key>
|
||||||
|
<array/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- UTM scales the guest's 1280x800 display to the device, so Hyprland and GTK draw at scale 1.
|
||||||
|
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1 })
|
||||||
|
hl.env("GDK_SCALE", "1")
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Renders the UTM config.plist template: render <vm-uuid> <disk-uuid> <mac> <version>
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
(( $# == 4 )) || { echo "Usage: $0 <vm-uuid> <disk-uuid> <mac> <version>" >&2; exit 1; }
|
||||||
|
|
||||||
|
sed -e "s/@@VM_UUID@@/$1/" -e "s/@@DISK_UUID@@/$2/g" -e "s/@@MAC@@/$3/" -e "s/@@VERSION@@/$4/" "$(dirname "$0")/config.plist"
|
||||||
@@ -415,7 +415,7 @@ detect_kernel() {
|
|||||||
if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then
|
if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then
|
||||||
echo "linux-t2"
|
echo "linux-t2"
|
||||||
else
|
else
|
||||||
echo "linux-omarchy"
|
echo "linux"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ CLEAR="${CSI}2J${CSI}H"
|
|||||||
CLEAR_LINE="${CSI}2K"
|
CLEAR_LINE="${CSI}2K"
|
||||||
CLEAR_TO_END="${CSI}J"
|
CLEAR_TO_END="${CSI}J"
|
||||||
GREEN="${CSI}32m"
|
GREEN="${CSI}32m"
|
||||||
|
WHITE="${CSI}37m"
|
||||||
RED="${CSI}31m"
|
RED="${CSI}31m"
|
||||||
|
DARK="${CSI}90m"
|
||||||
|
|
||||||
# The dashboard owns the final reboot prompt now, so keep the gum confirm
|
# The dashboard owns the final reboot prompt now, so keep the gum confirm
|
||||||
# styling that used to come from install/helpers/presentation.sh.
|
# styling that used to come from install/helpers/presentation.sh.
|
||||||
@@ -51,22 +53,6 @@ export GUM_CONFIRM_UNSELECTED_BACKGROUND="${GUM_CONFIRM_UNSELECTED_BACKGROUND:-0
|
|||||||
child_pid=""
|
child_pid=""
|
||||||
child_pgid=""
|
child_pgid=""
|
||||||
|
|
||||||
whimsy=(
|
|
||||||
"delivering delightful defaults"
|
|
||||||
"saving setup struggles"
|
|
||||||
"picking polished packages"
|
|
||||||
"curating cohesive colors"
|
|
||||||
"preparing pleasant pixels"
|
|
||||||
"hyping happy hyprland"
|
|
||||||
"integrating intuitive interfaces"
|
|
||||||
"bootstrapping beautiful boxes"
|
|
||||||
"crafting cozy commands"
|
|
||||||
"making machines marvelous"
|
|
||||||
"polishing pretty pixels"
|
|
||||||
"tuning tasteful terminals"
|
|
||||||
"weaving wonderful workflows"
|
|
||||||
)
|
|
||||||
|
|
||||||
tips=(
|
tips=(
|
||||||
"Super + Space opens the Omarchy menu for apps, settings, and more"
|
"Super + Space opens the Omarchy menu for apps, settings, and more"
|
||||||
"Super + K shows all the key bindings"
|
"Super + K shows all the key bindings"
|
||||||
@@ -129,58 +115,10 @@ trap 'interrupt 143' TERM
|
|||||||
term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; }
|
term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; }
|
||||||
term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; }
|
term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; }
|
||||||
|
|
||||||
# Read the actual character dimensions instead of assuming an 8 x 16 font.
|
repeat() {
|
||||||
# Linux VTs expose the font through KDFONTOP; desktop terminals may expose
|
local char="$1" count="$2" out="" i
|
||||||
# pixels in their window size or answer the standard cell-size query.
|
for ((i=0; i<count; i++)); do out+="$char"; done
|
||||||
term_cell_size() {
|
printf '%s' "$out"
|
||||||
python3 - "$TTY_PATH" <<'PY'
|
|
||||||
import fcntl
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import select
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
import termios
|
|
||||||
import time
|
|
||||||
import tty
|
|
||||||
|
|
||||||
with open(sys.argv[1], "r+b", buffering=0) as terminal:
|
|
||||||
fd = terminal.fileno()
|
|
||||||
rows, cols, width, height = struct.unpack(
|
|
||||||
"HHHH", fcntl.ioctl(fd, termios.TIOCGWINSZ, bytes(8)))
|
|
||||||
if rows and cols and width and height:
|
|
||||||
print(round(width / cols), round(height / rows))
|
|
||||||
sys.exit()
|
|
||||||
try:
|
|
||||||
font = bytearray(struct.pack("@5IP", 1, 0, 0, 0, 0, 0))
|
|
||||||
fcntl.ioctl(fd, 0x4B72, font) # KDFONTOP / KD_FONT_OP_GET, no glyph data
|
|
||||||
_, _, width, height, _ = struct.unpack_from("@5I", font)
|
|
||||||
if width and height:
|
|
||||||
print(width, height)
|
|
||||||
sys.exit()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
settings = termios.tcgetattr(fd)
|
|
||||||
try:
|
|
||||||
tty.setcbreak(fd, termios.TCSANOW)
|
|
||||||
os.write(fd, b"\x1b[16t")
|
|
||||||
response = b""
|
|
||||||
deadline = time.monotonic() + 0.15
|
|
||||||
while select.select([fd], [], [], max(0, deadline - time.monotonic()))[0]:
|
|
||||||
response += os.read(fd, 64)
|
|
||||||
match = re.search(rb"\x1b\[6;(\d+);(\d+)t", response)
|
|
||||||
if match:
|
|
||||||
height, width = map(int, match.groups())
|
|
||||||
if width and height:
|
|
||||||
print(width, height)
|
|
||||||
sys.exit()
|
|
||||||
if time.monotonic() >= deadline:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
termios.tcsetattr(fd, termios.TCSANOW, settings)
|
|
||||||
print(8, 16) # Conventional console font when dimensions aren't available.
|
|
||||||
PY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logo_width() {
|
logo_width() {
|
||||||
@@ -213,16 +151,22 @@ LOGO_HEIGHT="$(logo_height)"
|
|||||||
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
|
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
|
||||||
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
|
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
|
||||||
CONTENT_WIDTH="$LOGO_WIDTH"
|
CONTENT_WIDTH="$LOGO_WIDTH"
|
||||||
|
# Rows the progress screen occupies: logo, blank, title, blank, bar, blank, tip.
|
||||||
|
PROGRESS_HEIGHT=$((LOGO_HEIGHT + 6))
|
||||||
TOP_ROW=1
|
TOP_ROW=1
|
||||||
|
DYNAMIC_ROW=$((LOGO_HEIGHT + 3))
|
||||||
LAST_SIZE=""
|
LAST_SIZE=""
|
||||||
LAST_PM=10
|
LAST_PM=10
|
||||||
# Wordmark, blank, title, and the three rows gum confirm uses.
|
# The finish screen mirrors the greeter's frame — logo, blank, one line, gap,
|
||||||
|
# one button — so the last thing the installer shows matches the first. It runs
|
||||||
|
# one row taller: `gum confirm` always paints prompt/blank/button and clears all
|
||||||
|
# three, so the button cannot sit closer than two rows under a title we drew.
|
||||||
FINISH_HEIGHT=$((LOGO_HEIGHT + 5))
|
FINISH_HEIGHT=$((LOGO_HEIGHT + 5))
|
||||||
FINISH_TOP_ROW=2
|
FINISH_TOP_ROW=2
|
||||||
|
|
||||||
# ── Install progress ─────────────────────────────────────────────────────────
|
# ── Install progress ─────────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# Position = max(floor, work), both non-decreasing, in per-mille of the drawing.
|
# Position = max(floor, work), both non-decreasing, in per-mille of the bar.
|
||||||
#
|
#
|
||||||
# floor lo + (span-1) * t / (t+tau), on time since this process saw the
|
# floor lo + (span-1) * t / (t+tau), on time since this process saw the
|
||||||
# phase. Asymptotic, so it approaches the band end without reaching it.
|
# phase. Asymptotic, so it approaches the band end without reaching it.
|
||||||
@@ -245,8 +189,8 @@ fi
|
|||||||
# A wrong denominator is worse than none: it never triggers the fallback.
|
# A wrong denominator is worse than none: it never triggers the fallback.
|
||||||
(( PKG_TOTAL < 300 || PKG_TOTAL > 5000 )) && PKG_TOTAL=0
|
(( PKG_TOTAL < 300 || PKG_TOTAL > 5000 )) && PKG_TOTAL=0
|
||||||
|
|
||||||
PKG_BONUS_MAX_PM=100 # cap on how far progress may run ahead of the count
|
PKG_BONUS_MAX_PM=100 # cap on how far the bar may run ahead of the count
|
||||||
PKG_TAIL_PM=40 # band held back so stalled progress has somewhere to go
|
PKG_TAIL_PM=40 # band held back so a stalled bar has somewhere to go
|
||||||
PKG_FLOOR_TAU=900 # with a work signal, time is only a backstop until the
|
PKG_FLOOR_TAU=900 # with a work signal, time is only a backstop until the
|
||||||
# first package, so slow it until it cannot outrun data
|
# first package, so slow it until it cannot outrun data
|
||||||
|
|
||||||
@@ -319,161 +263,13 @@ render_logo() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# The icon uses half-block glyphs, scaled to the terminal's actual cell aspect
|
progress_bar() {
|
||||||
# ratio so its square silhouette stays square. No graphics protocol is needed.
|
local pm="$1" width="${2:-34}" filled empty
|
||||||
# Coordinates follow the 15 x 15 grid of Omarchy's icon.svg. Each segment
|
(( pm < 0 )) && pm=0
|
||||||
# paints the full stroke width, starting at the bottom middle of the outer ring.
|
(( pm > 1000 )) && pm=1000
|
||||||
ICON_WIDTH=30
|
filled=$(( pm * width / 1000 ))
|
||||||
ICON_HEIGHT=15
|
empty=$(( width - filled ))
|
||||||
CELL_WIDTH=8
|
printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET"
|
||||||
CELL_HEIGHT=16
|
|
||||||
ICON_LEFT=1
|
|
||||||
ICON_STEPS=0
|
|
||||||
ICON_DRAWN=0
|
|
||||||
DISPLAY_PM=0
|
|
||||||
ICON_FRAME=""
|
|
||||||
declare -a ICON_ORDER=() ICON_TRACE=() ICON_FILL=()
|
|
||||||
|
|
||||||
icon_segment() {
|
|
||||||
local scale="$1" x1="$2" y1="$3" x2="$4" y2="$5"
|
|
||||||
local x y from to
|
|
||||||
if (( y1 == y2 )); then
|
|
||||||
from=$(( (x1 * ICON_WIDTH + 14) / 15 ))
|
|
||||||
to=$(( ((x2 + 1) * ICON_WIDTH + 14) / 15 ))
|
|
||||||
if (( x2 < x1 )); then
|
|
||||||
from=$(( ((x1 + 1) * ICON_WIDTH + 14) / 15 - 1 ))
|
|
||||||
to=$(( (x2 * ICON_WIDTH + 14) / 15 - 1 ))
|
|
||||||
fi
|
|
||||||
for ((x=from; x!=to; x+=(x2 >= x1 ? 1 : -1))); do
|
|
||||||
for ((y=y1 * scale; y<(y1 + 1) * scale; y++)); do
|
|
||||||
icon_pixel "$((y * ICON_WIDTH + x))"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
else
|
|
||||||
from=$(( y1 * scale ))
|
|
||||||
to=$(( (y2 + 1) * scale ))
|
|
||||||
if (( y2 < y1 )); then
|
|
||||||
from=$(( (y1 + 1) * scale - 1 ))
|
|
||||||
to=$(( y2 * scale - 1 ))
|
|
||||||
fi
|
|
||||||
for ((y=from; y!=to; y+=(y2 >= y1 ? 1 : -1))); do
|
|
||||||
for ((x=(x1 * ICON_WIDTH + 14) / 15; x<((x1 + 1) * ICON_WIDTH + 14) / 15; x++)); do
|
|
||||||
icon_pixel "$((y * ICON_WIDTH + x))"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
icon_pixel() {
|
|
||||||
local pixel="$1"
|
|
||||||
if [[ ! -v ICON_TRACE[pixel] ]]; then
|
|
||||||
ICON_ORDER+=("$pixel")
|
|
||||||
ICON_TRACE[pixel]=${#ICON_ORDER[@]}
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
build_icon() {
|
|
||||||
local scale="$1" x y row
|
|
||||||
local -a mask=(
|
|
||||||
111111111111111
|
|
||||||
100000010000001
|
|
||||||
101111110001101
|
|
||||||
101000000000101
|
|
||||||
101000000000101
|
|
||||||
101000000000101
|
|
||||||
101000000000101
|
|
||||||
111000000000101
|
|
||||||
101000000000101
|
|
||||||
101000000000101
|
|
||||||
101000000000101
|
|
||||||
101000000000101
|
|
||||||
101111111111101
|
|
||||||
100000010000001
|
|
||||||
111111110111111
|
|
||||||
)
|
|
||||||
ICON_WIDTH=$(( (15 * scale * CELL_HEIGHT + CELL_WIDTH) / (2 * CELL_WIDTH) ))
|
|
||||||
ICON_HEIGHT=$(((15 * scale + 1) / 2))
|
|
||||||
ICON_ORDER=() ICON_TRACE=() ICON_FILL=()
|
|
||||||
ICON_DRAWN=0
|
|
||||||
# Start at the bottom middle, follow the outer left edge to the top cross,
|
|
||||||
# then descend into the inner shape. Fill its two bridges as we reach them.
|
|
||||||
icon_segment "$scale" 7 14 0 14
|
|
||||||
icon_segment "$scale" 0 14 0 0
|
|
||||||
icon_segment "$scale" 0 0 7 0
|
|
||||||
icon_segment "$scale" 7 0 7 2
|
|
||||||
icon_segment "$scale" 7 2 2 2
|
|
||||||
icon_segment "$scale" 2 2 2 7
|
|
||||||
icon_segment "$scale" 2 7 0 7
|
|
||||||
icon_segment "$scale" 2 7 2 12
|
|
||||||
icon_segment "$scale" 2 12 7 12
|
|
||||||
icon_segment "$scale" 7 12 7 14
|
|
||||||
icon_segment "$scale" 7 12 12 12
|
|
||||||
icon_segment "$scale" 12 12 12 2
|
|
||||||
icon_segment "$scale" 12 2 11 2
|
|
||||||
# Finish from the top cross: right along the outside, down, then left.
|
|
||||||
icon_segment "$scale" 7 0 14 0
|
|
||||||
icon_segment "$scale" 14 0 14 14
|
|
||||||
icon_segment "$scale" 14 14 9 14
|
|
||||||
ICON_STEPS=${#ICON_ORDER[@]}
|
|
||||||
for ((y=0; y<15 * scale; y++)); do
|
|
||||||
row=${mask[y / scale]}
|
|
||||||
for ((x=0; x<ICON_WIDTH; x++)); do
|
|
||||||
ICON_FILL[y * ICON_WIDTH + x]=${row:x * 15 / ICON_WIDTH:1}
|
|
||||||
done
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Append one cell with independently colored upper/lower pixels. This also
|
|
||||||
# handles a green tracing tip sharing a cell with the still-dark outline.
|
|
||||||
icon_cell() {
|
|
||||||
local pixel="$1" drawn="$2" solid="$3" upper=0 lower=0 step
|
|
||||||
if (( solid )); then
|
|
||||||
(( ICON_FILL[pixel] )) && upper=32
|
|
||||||
(( ${ICON_FILL[pixel + ICON_WIDTH]:-0} )) && lower=32
|
|
||||||
else
|
|
||||||
step=${ICON_TRACE[pixel]:-0}
|
|
||||||
(( step > 0 )) && upper=90
|
|
||||||
(( step > 0 && step <= drawn )) && upper=32
|
|
||||||
step=${ICON_TRACE[pixel + ICON_WIDTH]:-0}
|
|
||||||
(( step > 0 )) && lower=90
|
|
||||||
(( step > 0 && step <= drawn )) && lower=32
|
|
||||||
fi
|
|
||||||
if (( upper == 0 && lower == 0 )); then
|
|
||||||
ICON_FRAME+="${RESET} "
|
|
||||||
elif (( upper == lower )); then
|
|
||||||
ICON_FRAME+="${CSI}${upper};49m█"
|
|
||||||
elif (( lower == 0 )); then
|
|
||||||
ICON_FRAME+="${CSI}${upper};49m▀"
|
|
||||||
elif (( upper == 0 )); then
|
|
||||||
ICON_FRAME+="${CSI}${lower};49m▄"
|
|
||||||
else
|
|
||||||
ICON_FRAME+="${CSI}${upper};$((lower + 10))m▀"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
render_icon() {
|
|
||||||
local pm="$1" full="${2:-0}" drawn i pixel row col
|
|
||||||
drawn=$((pm * ICON_STEPS / 1000))
|
|
||||||
ICON_FRAME=""
|
|
||||||
if (( full || pm == 1000 )); then
|
|
||||||
for ((row=0; row<ICON_HEIGHT; row++)); do
|
|
||||||
ICON_FRAME+="${CSI}$((TOP_ROW + row));${ICON_LEFT}H"
|
|
||||||
for ((col=0; col<ICON_WIDTH; col++)); do
|
|
||||||
icon_cell "$((row * 2 * ICON_WIDTH + col))" "$drawn" "$((pm == 1000))"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
else
|
|
||||||
# Between layout changes, repaint only newly traced pixels. This keeps the
|
|
||||||
# animation cheap even while pacstrap saturates the disk.
|
|
||||||
for ((i=ICON_DRAWN; i<drawn; i++)); do
|
|
||||||
pixel=${ICON_ORDER[i]}
|
|
||||||
row=$((pixel / ICON_WIDTH / 2)) col=$((pixel % ICON_WIDTH))
|
|
||||||
ICON_FRAME+="${CSI}$((TOP_ROW + row));$((ICON_LEFT + col))H"
|
|
||||||
icon_cell "$((row * 2 * ICON_WIDTH + col))" "$drawn" 0
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
printf '%s%s' "$ICON_FRAME" "$RESET"
|
|
||||||
ICON_DRAWN=$drawn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Packages libalpm has finished installing. local/ holds one directory per
|
# Packages libalpm has finished installing. local/ holds one directory per
|
||||||
@@ -494,7 +290,7 @@ count_packages() {
|
|||||||
|
|
||||||
# Sets PROGRESS_PM. Never call in a command substitution: POS, the phase timer
|
# Sets PROGRESS_PM. Never call in a command substitution: POS, the phase timer
|
||||||
# and the package counters must survive between frames. Print nothing — output
|
# and the package counters must survive between frames. Print nothing — output
|
||||||
# here would land on the TTY between the cursor escape and the drawing.
|
# here would land on the TTY between the cursor escape and the bar.
|
||||||
install_progress() {
|
install_progress() {
|
||||||
local phase finished index total target
|
local phase finished index total target
|
||||||
local lo hi tau span t pos work gap n pkg_signal=0
|
local lo hi tau span t pos work gap n pkg_signal=0
|
||||||
@@ -581,7 +377,7 @@ install_progress() {
|
|||||||
PKG_DB_DIR="$target/var/lib/pacman/local"
|
PKG_DB_DIR="$target/var/lib/pacman/local"
|
||||||
count_packages
|
count_packages
|
||||||
# Baseline on first sight: a target that already holds packages would
|
# Baseline on first sight: a target that already holds packages would
|
||||||
# otherwise pin progress at the band ceiling for the whole phase.
|
# otherwise pin the bar at the band ceiling for the whole phase.
|
||||||
(( PKG_BASE < 0 )) && PKG_BASE=$PKG_COUNT
|
(( PKG_BASE < 0 )) && PKG_BASE=$PKG_COUNT
|
||||||
n=$(( PKG_COUNT - PKG_BASE ))
|
n=$(( PKG_COUNT - PKG_BASE ))
|
||||||
(( n < 0 )) && n=0
|
(( n < 0 )) && n=0
|
||||||
@@ -605,20 +401,17 @@ install_progress() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep the original loading messages on their own clock, so progress jumps
|
# One tip per 8 seconds, in order. On elapsed time, not a frame counter: a frame
|
||||||
# cannot skip phrases and animation frame rate does not change their cadence.
|
# is 0.5s of sleep plus a render, which stretches under pacstrap I/O — on the
|
||||||
current_whimsy() {
|
# machine where the tips, not the bar, are carrying liveness.
|
||||||
printf '%s' "${whimsy[$(( (NOW - DASH_T0) / 4 % ${#whimsy[@]} ))]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# One tip per 8 seconds, in order. Use elapsed time, since rendering can stretch
|
|
||||||
# under pacstrap I/O, when the tips may be the most visible sign of activity.
|
|
||||||
current_tip() {
|
current_tip() {
|
||||||
printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}"
|
printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Vertically center a block of the given height, echoing the row it starts on.
|
# Vertically center a block of the given height, echoing the row it starts on.
|
||||||
# Center both the progress icon and the final wordmark on the console.
|
# Both screens here center the way the configurator's greeter centers its
|
||||||
|
# welcome, so the install opens, runs, and closes on the same composition
|
||||||
|
# rather than stacking each frame from row one.
|
||||||
centered_top_row() {
|
centered_top_row() {
|
||||||
local height="$1" rows top
|
local height="$1" rows top
|
||||||
rows="$(term_size)"
|
rows="$(term_size)"
|
||||||
@@ -630,60 +423,42 @@ centered_top_row() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
measure_progress_layout() {
|
measure_progress_layout() {
|
||||||
local rows cols scale=1
|
|
||||||
LAST_SIZE="$(term_size)"
|
LAST_SIZE="$(term_size)"
|
||||||
read -r rows cols <<<"$LAST_SIZE"
|
TOP_ROW="$(centered_top_row "$PROGRESS_HEIGHT")"
|
||||||
read -r CELL_WIDTH CELL_HEIGHT < <(term_cell_size 2>/dev/null || printf '8 16\n')
|
DYNAMIC_ROW=$(( TOP_ROW + LOGO_HEIGHT + 1 ))
|
||||||
(( rows >= 34 && cols >= 49 )) && scale=2
|
|
||||||
(( rows >= 50 && cols >= 64 )) && scale=3
|
|
||||||
while (( scale > 1 && (15 * scale * CELL_HEIGHT + CELL_WIDTH) / (2 * CELL_WIDTH) > cols - 4 )); do
|
|
||||||
scale=$((scale - 1))
|
|
||||||
done
|
|
||||||
build_icon "$scale"
|
|
||||||
TOP_ROW="$(centered_top_row "$((ICON_HEIGHT + 6))")"
|
|
||||||
ICON_LEFT=$(( (cols - ICON_WIDTH) / 2 + 1 ))
|
|
||||||
(( ICON_LEFT < 1 )) && ICON_LEFT=1
|
|
||||||
PROGRESS_TEXT_WIDTH=$((cols - 2))
|
|
||||||
(( PROGRESS_TEXT_WIDTH > CONTENT_WIDTH )) && PROGRESS_TEXT_WIDTH=$CONTENT_WIDTH
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_static() {
|
render_static() {
|
||||||
measure_progress_layout
|
measure_progress_layout
|
||||||
printf '%s' "$CLEAR"
|
printf '%s%s%d;1H' "$CLEAR" "$CSI" "$TOP_ROW"
|
||||||
render_icon "$DISPLAY_PM" 1
|
render_logo
|
||||||
}
|
blank_line
|
||||||
|
|
||||||
update_progress() {
|
|
||||||
set_now
|
|
||||||
install_progress
|
|
||||||
local pm=$PROGRESS_PM
|
|
||||||
[[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM
|
|
||||||
# Only the child's successful exit can complete the drawing.
|
|
||||||
(( pm > 999 )) && pm=999
|
|
||||||
# POS is monotone by construction; this is a guard, not the mechanism.
|
|
||||||
(( pm < LAST_PM )) && pm=$LAST_PM
|
|
||||||
LAST_PM=$pm
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_dynamic() {
|
render_dynamic() {
|
||||||
local step message tip
|
local pm
|
||||||
# The framebuffer can settle late in VMs. Rebuild at the new size, preserving
|
# The console can still resize under us (the framebuffer settles late in VMs).
|
||||||
# progress, instead of leaving the drawing at the old screen coordinates.
|
# Horizontal padding is recomputed every frame anyway, but the logo is drawn
|
||||||
|
# once, so redraw the static half at the new center rather than leave the
|
||||||
|
# centered layout stranded against a stale geometry.
|
||||||
[[ "$(term_size)" == "$LAST_SIZE" ]] || render_static
|
[[ "$(term_size)" == "$LAST_SIZE" ]] || render_static
|
||||||
# Ease phase jumps over several frames, with the percentage following the
|
set_now
|
||||||
# same position as the trace. The measured progress still never goes back.
|
install_progress
|
||||||
step=$(( (LAST_PM - DISPLAY_PM + 3) / 4 ))
|
pm=$PROGRESS_PM
|
||||||
DISPLAY_PM=$((DISPLAY_PM + step))
|
[[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM
|
||||||
render_icon "$DISPLAY_PM"
|
(( pm > 1000 )) && pm=1000
|
||||||
|
# POS is monotone by construction; this is a guard, not the mechanism.
|
||||||
|
(( pm < LAST_PM )) && pm=$LAST_PM
|
||||||
|
LAST_PM=$pm
|
||||||
|
|
||||||
message="$(truncate_for_width "$(current_whimsy)" "$PROGRESS_TEXT_WIDTH")"
|
printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW"
|
||||||
tip="$(truncate_for_width "Tip: $(current_tip)" "$PROGRESS_TEXT_WIDTH")"
|
center "Installing Omarchy" "$CONTENT_WIDTH"
|
||||||
printf '%s%d;1H' "$CSI" "$((TOP_ROW + ICON_HEIGHT + 1))"
|
|
||||||
center "${GREEN}$((DISPLAY_PM / 10))%${RESET}" "$PROGRESS_TEXT_WIDTH"
|
|
||||||
center "${GREEN}${message}${RESET}" "$PROGRESS_TEXT_WIDTH"
|
|
||||||
blank_line
|
blank_line
|
||||||
center "${DIM}${tip}${RESET}" "$PROGRESS_TEXT_WIDTH"
|
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) ""
|
||||||
|
progress_bar "$pm" 34
|
||||||
|
printf '\n'
|
||||||
|
blank_line
|
||||||
|
center "${DIM}Tip:${RESET} ${GREEN}$(current_tip)${RESET}" "$CONTENT_WIDTH"
|
||||||
printf '%s' "$CLEAR_TO_END"
|
printf '%s' "$CLEAR_TO_END"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,11 +493,13 @@ install_duration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_finish() {
|
render_finish() {
|
||||||
local duration effect_canvas_width title="Installed Omarchy"
|
local duration effect_canvas_width
|
||||||
duration="$(install_duration || true)"
|
duration="$(install_duration || true)"
|
||||||
[[ -n $duration ]] && title+=" in ${duration}"
|
duration="${duration:-Complete}"
|
||||||
# ttfx centers its text two columns right of plain centering. A canvas two
|
# ttfx centers its text two columns right of plain centering, so hand it a
|
||||||
# columns narrower aligns the animation with the title and fallback logo.
|
# canvas two columns narrower and the animated logo lands exactly on the
|
||||||
|
# static one left_padding already drew. Otherwise the logo jumps a column
|
||||||
|
# sideways when the effect starts and again when it ends.
|
||||||
effect_canvas_width="$(term_cols)"
|
effect_canvas_width="$(term_cols)"
|
||||||
(( effect_canvas_width > 2 )) && effect_canvas_width=$((effect_canvas_width - 2))
|
(( effect_canvas_width > 2 )) && effect_canvas_width=$((effect_canvas_width - 2))
|
||||||
|
|
||||||
@@ -731,21 +508,23 @@ render_finish() {
|
|||||||
FINISH_TOP_ROW="$(centered_top_row "$FINISH_HEIGHT")"
|
FINISH_TOP_ROW="$(centered_top_row "$FINISH_HEIGHT")"
|
||||||
|
|
||||||
{
|
{
|
||||||
# Leave the wordmark area blank: laseretch reveals it for the first time.
|
printf '%s%s%s%d;1H' "$SHOW_CURSOR" "$CLEAR" "$CSI" "$FINISH_TOP_ROW"
|
||||||
printf '%s%s%s%d;1H' "$HIDE_CURSOR" "$CLEAR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 1))"
|
render_logo
|
||||||
center "$title" "$CONTENT_WIDTH"
|
blank_line
|
||||||
|
center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH"
|
||||||
blank_line
|
blank_line
|
||||||
} >"$TTY_PATH"
|
} >"$TTY_PATH"
|
||||||
|
|
||||||
if [[ -f $LOGO_PATH ]]; then
|
if [[ -f $LOGO_PATH ]]; then
|
||||||
# Reuse mode restores a saved cursor before moving up over its input. Save
|
# Reuse mode restores a saved cursor before moving up over its input. Save
|
||||||
# this anchor so the effect occupies the reserved wordmark rows.
|
# this anchor so the effect redraws the same logo rows shown above.
|
||||||
printf '%s%d;1H\0337' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT))" >"$TTY_PATH"
|
printf '%s%d;1H\0337' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT))" >"$TTY_PATH"
|
||||||
# Laseretch runs ~4s at 260fps, so the 8s cap is a stuck-process guard rather
|
# Laseretch runs ~4s at 260fps, so the 8s cap is a stuck-process guard rather
|
||||||
# than a trim. --xterm-colors keeps the whole effect in the 256-index palette
|
# than a trim. --xterm-colors keeps the whole effect in the 256-index palette
|
||||||
# the way the greeter does, since the console reduces indexed codes well and
|
# the way the greeter does, since the console reduces indexed codes well and
|
||||||
# truecolor ones badly. It settles on indexed 2, the Tokyo Night green used
|
# truecolor ones badly, and it settles on indexed 2 — the green gum already
|
||||||
# throughout the installer. The laser and sparks keep their own hot palette.
|
# drew the logo in above, so the send-off ends on the frame it started from
|
||||||
|
# instead of recoloring it. The laser and sparks keep their own hot palette.
|
||||||
timeout 8s ttfx -i "$LOGO_PATH" \
|
timeout 8s ttfx -i "$LOGO_PATH" \
|
||||||
--canvas-width "$effect_canvas_width" \
|
--canvas-width "$effect_canvas_width" \
|
||||||
--anchor-text c \
|
--anchor-text c \
|
||||||
@@ -754,14 +533,9 @@ render_finish() {
|
|||||||
--xterm-colors \
|
--xterm-colors \
|
||||||
laseretch \
|
laseretch \
|
||||||
--final-gradient-stops 2 \
|
--final-gradient-stops 2 \
|
||||||
>"$TTY_PATH" 2>/dev/null || {
|
>"$TTY_PATH" 2>/dev/null || dashboard_log "logo effect skipped (ttfx exited $?)"
|
||||||
dashboard_log "logo effect skipped (ttfx exited $?)"
|
printf '%s%d;1H' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH"
|
||||||
{ printf '%s%d;1H' "$CSI" "$FINISH_TOP_ROW"; render_logo; } >"$TTY_PATH"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{ printf '%s%d;1H' "$CSI" "$FINISH_TOP_ROW"; render_logo; } >"$TTY_PATH"
|
|
||||||
fi
|
fi
|
||||||
printf '%s%s%d;1H' "$SHOW_CURSOR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reboot_prompt() {
|
reboot_prompt() {
|
||||||
@@ -1031,13 +805,9 @@ PHASE_T0=$NOW
|
|||||||
PKG_LAST_CHANGE=$NOW
|
PKG_LAST_CHANGE=$NOW
|
||||||
launch_child
|
launch_child
|
||||||
|
|
||||||
frame=0
|
|
||||||
while jobs -pr | grep -qx "$child_pid"; do
|
while jobs -pr | grep -qx "$child_pid"; do
|
||||||
# Keep package counting/state reads at 2 Hz; animate the trace at 10 Hz.
|
|
||||||
(( frame % 5 == 0 )) && update_progress
|
|
||||||
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
||||||
frame=$((frame + 1))
|
sleep 0.5
|
||||||
sleep 0.1
|
|
||||||
done
|
done
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
@@ -1050,14 +820,8 @@ child_pgid=""
|
|||||||
dashboard_log "installer child exited with status $status"
|
dashboard_log "installer child exited with status $status"
|
||||||
|
|
||||||
if (( status == 0 )); then
|
if (( status == 0 )); then
|
||||||
# Finish the trace even when a short-lived child exits between state polls.
|
# finished_at lands microseconds before the child exits, so the 0.5s poll
|
||||||
LAST_PM=1000
|
# rarely renders a full bar before the finish screen replaces it.
|
||||||
PHASE_NAME="Installation complete"
|
|
||||||
while (( DISPLAY_PM < 999 )); do
|
|
||||||
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
DISPLAY_PM=1000
|
|
||||||
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
||||||
sleep 0.4
|
sleep 0.4
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -50,13 +50,6 @@ class InstallContext:
|
|||||||
user_configuration = json.loads(config_path.read_text())
|
user_configuration = json.loads(config_path.read_text())
|
||||||
omarchy_install = user_configuration.get("omarchy_install") or _default_omarchy_install(user_configuration)
|
omarchy_install = user_configuration.get("omarchy_install") or _default_omarchy_install(user_configuration)
|
||||||
|
|
||||||
# Autoinstall configs may omit kernels, which makes archinstall default
|
|
||||||
# to stock linux. Apply the same hardware default as the configurator,
|
|
||||||
# while honoring an explicit selection (including storage-only configs).
|
|
||||||
if not user_configuration.get("kernels"):
|
|
||||||
kernel = (omarchy_install.get("storage") or {}).get("kernel") or _default_kernel()
|
|
||||||
user_configuration["kernels"] = [kernel]
|
|
||||||
|
|
||||||
# Deferred provisioning: the whole system installs but user creation is deferred to
|
# Deferred provisioning: the whole system installs but user creation is deferred to
|
||||||
# first boot. Selected by the configurator (omarchy_install.defer_provisioning) or by
|
# first boot. Selected by the configurator (omarchy_install.defer_provisioning) or by
|
||||||
# an `defer-provisioning` marker file on an autoinstall drive, which also replaces the
|
# an `defer-provisioning` marker file on an autoinstall drive, which also replaces the
|
||||||
@@ -182,18 +175,6 @@ def _inject_provisioning_encryption_password(arch_configuration: dict, user_cred
|
|||||||
user_credentials["encryption_password"] = password
|
user_credentials["encryption_password"] = password
|
||||||
|
|
||||||
|
|
||||||
def _default_kernel(pci_devices: Path = Path("/sys/bus/pci/devices")) -> str:
|
|
||||||
for device in pci_devices.glob("*"):
|
|
||||||
try:
|
|
||||||
vendor = (device / "vendor").read_text().strip().lower()
|
|
||||||
device_id = (device / "device").read_text().strip().lower()
|
|
||||||
except OSError:
|
|
||||||
continue
|
|
||||||
if vendor == "0x106b" and device_id in {"0x1801", "0x1802"}:
|
|
||||||
return "linux-t2"
|
|
||||||
return "linux-omarchy"
|
|
||||||
|
|
||||||
|
|
||||||
def _default_omarchy_install(user_configuration: dict) -> dict[str, Any]:
|
def _default_omarchy_install(user_configuration: dict) -> dict[str, Any]:
|
||||||
disk_config = user_configuration.get("disk_config", {})
|
disk_config = user_configuration.get("disk_config", {})
|
||||||
mode = "protected" if disk_config.get("config_type") == "pre_mounted_config" else "full_disk"
|
mode = "protected" if disk_config.get("config_type") == "pre_mounted_config" else "full_disk"
|
||||||
|
|||||||
@@ -39,36 +39,6 @@ from .keyboard import configure_keyboard
|
|||||||
from .ui import error, info
|
from .ui import error, info
|
||||||
|
|
||||||
|
|
||||||
ISO_REGION_FILE = Path("/root/omarchy_region")
|
|
||||||
ISO_REGION_PAYLOAD = Path("/usr/share/omarchy-iso/region")
|
|
||||||
|
|
||||||
|
|
||||||
def _iso_region() -> str:
|
|
||||||
region = ISO_REGION_FILE.read_text().strip() if ISO_REGION_FILE.exists() else "global"
|
|
||||||
if region not in {"global", "cn"}:
|
|
||||||
raise RuntimeError(f"Unsupported ISO region: {region}")
|
|
||||||
return region
|
|
||||||
|
|
||||||
|
|
||||||
def _stage_region_defaults(target: Path) -> None:
|
|
||||||
"""Seed the target before useradd, including deferred first-boot users.
|
|
||||||
|
|
||||||
Region is a bootstrap default, not a locale or a user-level policy.
|
|
||||||
Existing home directories are never rewritten by this step.
|
|
||||||
"""
|
|
||||||
region = _iso_region()
|
|
||||||
if region != "global":
|
|
||||||
if not ISO_REGION_PAYLOAD.is_dir():
|
|
||||||
raise RuntimeError(f"Missing ISO payload for region: {region}")
|
|
||||||
skel = ISO_REGION_PAYLOAD / "skel"
|
|
||||||
if skel.is_dir():
|
|
||||||
shutil.copytree(skel, target / "etc/skel", dirs_exist_ok=True)
|
|
||||||
marker = target / "etc/omarchy/region"
|
|
||||||
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
marker.write_text(region + "\n")
|
|
||||||
marker.chmod(0o644)
|
|
||||||
|
|
||||||
|
|
||||||
# Package targets are written by builder/build-iso.sh. Stable ISOs use the
|
# Package targets are written by builder/build-iso.sh. Stable ISOs use the
|
||||||
# stable package names, while dev/local-source ISOs install the dev package
|
# stable package names, while dev/local-source ISOs install the dev package
|
||||||
# names explicitly instead of relying on provides=omarchy resolution.
|
# names explicitly instead of relying on provides=omarchy resolution.
|
||||||
@@ -299,11 +269,6 @@ def arch_install_system(ctx: InstallContext) -> None:
|
|||||||
pacman_config=config.pacman_config,
|
pacman_config=config.pacman_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Headers are optional kernel dependencies. Install them before any
|
|
||||||
# DKMS package so generic installs have them too, and module builds
|
|
||||||
# use the target kernel instead of the live ISO's kernel.
|
|
||||||
installer.add_additional_packages([f"{kernel}-headers" for kernel in config.kernels])
|
|
||||||
|
|
||||||
if not configure_keyboard(installer.target, kb_layout):
|
if not configure_keyboard(installer.target, kb_layout):
|
||||||
error(f"Invalid keyboard language specified: {kb_layout}")
|
error(f"Invalid keyboard language specified: {kb_layout}")
|
||||||
|
|
||||||
@@ -317,7 +282,6 @@ def arch_install_system(ctx: InstallContext) -> None:
|
|||||||
_install_early_packages(installer)
|
_install_early_packages(installer)
|
||||||
_configure_limine_boot(ctx, installer, config)
|
_configure_limine_boot(ctx, installer, config)
|
||||||
|
|
||||||
_stage_region_defaults(ctx.target)
|
|
||||||
info("› creating user (with /etc/skel populated)")
|
info("› creating user (with /etc/skel populated)")
|
||||||
if config.auth_config and config.auth_config.users:
|
if config.auth_config and config.auth_config.users:
|
||||||
installer.create_users(config.auth_config.users)
|
installer.create_users(config.auth_config.users)
|
||||||
@@ -826,7 +790,7 @@ def verify_protected_mounts(ctx: InstallContext) -> None:
|
|||||||
esp_mp.mkdir(parents=True, exist_ok=True)
|
esp_mp.mkdir(parents=True, exist_ok=True)
|
||||||
subprocess.run(["mount", esp_dev, str(esp_mp)], check=True)
|
subprocess.run(["mount", esp_dev, str(esp_mp)], check=True)
|
||||||
|
|
||||||
info(f"› protected target verified: kernel={storage.get('kernel', 'linux-omarchy')} esp={boot['esp_mount']}")
|
info(f"› protected target verified: kernel={storage.get('kernel', 'linux')} esp={boot['esp_mount']}")
|
||||||
|
|
||||||
|
|
||||||
def _is_mountpoint(path: Path) -> bool:
|
def _is_mountpoint(path: Path) -> bool:
|
||||||
@@ -1166,13 +1130,6 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
|
|||||||
|
|
||||||
|
|
||||||
def run_system_finalizer(ctx: InstallContext) -> None:
|
def run_system_finalizer(ctx: InstallContext) -> None:
|
||||||
# The runtime package transaction has installed the regional keyring from
|
|
||||||
# the ISO. Initialize the target's trust database before enabling the online
|
|
||||||
# community repository; the build container's GPG state is not inherited.
|
|
||||||
if _iso_region() == "cn":
|
|
||||||
_run_target_setup_command(ctx, ["pacman-key", "--init"])
|
|
||||||
_run_target_setup_command(ctx, ["pacman-key", "--populate", "archlinux", "archlinuxcn"])
|
|
||||||
|
|
||||||
if ctx.defer_provisioning:
|
if ctx.defer_provisioning:
|
||||||
cmd = ["/usr/bin/omarchy-apply-system", "--defer-provisioning", "--first-install"]
|
cmd = ["/usr/bin/omarchy-apply-system", "--defer-provisioning", "--first-install"]
|
||||||
else:
|
else:
|
||||||
@@ -1687,7 +1644,6 @@ def _tailscale_authkey(path: Path) -> str:
|
|||||||
|
|
||||||
def validate_boot(ctx: InstallContext) -> None:
|
def validate_boot(ctx: InstallContext) -> None:
|
||||||
_assert_boot_hooks_restored(ctx)
|
_assert_boot_hooks_restored(ctx)
|
||||||
_validate_kernel_headers(ctx)
|
|
||||||
|
|
||||||
boot = _boot_intent(ctx)
|
boot = _boot_intent(ctx)
|
||||||
storage = _storage_intent(ctx)
|
storage = _storage_intent(ctx)
|
||||||
@@ -1710,7 +1666,7 @@ def validate_boot(ctx: InstallContext) -> None:
|
|||||||
default_limine = ctx.target / "etc" / "default" / "limine"
|
default_limine = ctx.target / "etc" / "default" / "limine"
|
||||||
config_text = _limine_combined_config_text(ctx, default_limine.read_text())
|
config_text = _limine_combined_config_text(ctx, default_limine.read_text())
|
||||||
uki_prefix = _limine_setting(config_text, "CUSTOM_UKI_NAME", "omarchy") or "omarchy"
|
uki_prefix = _limine_setting(config_text, "CUSTOM_UKI_NAME", "omarchy") or "omarchy"
|
||||||
kernel = storage.get("kernel") or (ctx.user_configuration.get("kernels") or ["linux-omarchy"])[0]
|
kernel = storage.get("kernel") or (ctx.user_configuration.get("kernels") or ["linux"])[0]
|
||||||
|
|
||||||
if arch.has_uefi():
|
if arch.has_uefi():
|
||||||
limine_binary = esp_mount / boot.get("esp_path", "/EFI/limine").lstrip("/") / boot.get("efi_binary", "limine_x64.efi")
|
limine_binary = esp_mount / boot.get("esp_path", "/EFI/limine").lstrip("/") / boot.get("efi_binary", "limine_x64.efi")
|
||||||
@@ -1786,19 +1742,6 @@ def _assert_boot_hooks_restored(ctx: InstallContext) -> None:
|
|||||||
raise RuntimeError(f"{path} is missing — future kernel updates would ship no UKI")
|
raise RuntimeError(f"{path} is missing — future kernel updates would ship no UKI")
|
||||||
|
|
||||||
|
|
||||||
def _validate_kernel_headers(ctx: InstallContext) -> None:
|
|
||||||
kernels = sorted((ctx.target / "usr/lib/modules").glob("*/pkgbase"))
|
|
||||||
if not kernels:
|
|
||||||
raise RuntimeError("no installed kernel found in target")
|
|
||||||
for pkgbase in kernels:
|
|
||||||
release = pkgbase.parent.name
|
|
||||||
header_release = pkgbase.parent / "build/include/config/kernel.release"
|
|
||||||
if not header_release.is_file():
|
|
||||||
raise RuntimeError(f"{pkgbase.read_text().strip()} ({release}) has no kernel headers")
|
|
||||||
if header_release.read_text().strip() != release:
|
|
||||||
raise RuntimeError(f"{pkgbase.read_text().strip()} headers do not match kernel {release}")
|
|
||||||
|
|
||||||
|
|
||||||
# Every kernel package leaves its pkgbase next to its modules, which is also
|
# Every kernel package leaves its pkgbase next to its modules, which is also
|
||||||
# the name limine-mkinitcpio-hook builds the UKI under.
|
# the name limine-mkinitcpio-hook builds the UKI under.
|
||||||
def _installed_kernels(ctx: InstallContext) -> list[str]:
|
def _installed_kernels(ctx: InstallContext) -> list[str]:
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ ISO="$OMARCHY_INTEGRATION_ISO"
|
|||||||
SSH_PORT="${OMARCHY_INTEGRATION_SSH_PORT:-2322}"
|
SSH_PORT="${OMARCHY_INTEGRATION_SSH_PORT:-2322}"
|
||||||
MEMORY="${OMARCHY_INTEGRATION_MEMORY:-8192}"
|
MEMORY="${OMARCHY_INTEGRATION_MEMORY:-8192}"
|
||||||
INSTALL_TIMEOUT="${OMARCHY_INTEGRATION_INSTALL_TIMEOUT:-2400}"
|
INSTALL_TIMEOUT="${OMARCHY_INTEGRATION_INSTALL_TIMEOUT:-2400}"
|
||||||
# cache=none keeps the host page cache out of the guest's disk writes, so
|
|
||||||
# install times measure the installer rather than the host (CI sets it).
|
|
||||||
DISK_CACHE="${OMARCHY_INTEGRATION_DISK_CACHE:-}"
|
|
||||||
NO_PREVIEW="${OMARCHY_INTEGRATION_NO_PREVIEW:-false}"
|
NO_PREVIEW="${OMARCHY_INTEGRATION_NO_PREVIEW:-false}"
|
||||||
BOOT_TIMEOUT=600
|
BOOT_TIMEOUT=600
|
||||||
|
|
||||||
@@ -169,7 +166,7 @@ start_vm() {
|
|||||||
-m "$MEMORY" \
|
-m "$MEMORY" \
|
||||||
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
|
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
|
||||||
-drive if=pflash,format=raw,file="$ACTIVE_OVMF" \
|
-drive if=pflash,format=raw,file="$ACTIVE_OVMF" \
|
||||||
-drive file="$disk",format=qcow2,if=none,id=drive0${DISK_CACHE:+,cache=$DISK_CACHE} \
|
-drive file="$disk",format=qcow2,if=none,id=drive0 \
|
||||||
-device virtio-blk-pci,drive=drive0,bootindex=1 \
|
-device virtio-blk-pci,drive=drive0,bootindex=1 \
|
||||||
-device virtio-vga \
|
-device virtio-vga \
|
||||||
-display none \
|
-display none \
|
||||||
@@ -285,12 +282,8 @@ ssh_sudo() {
|
|||||||
ssh_guest "echo $GUEST_PASSWORD | sudo -S -p '' bash -c $(printf %q "$1")"
|
ssh_guest "echo $GUEST_PASSWORD | sudo -S -p '' bash -c $(printf %q "$1")"
|
||||||
}
|
}
|
||||||
|
|
||||||
# The deadline is wall-clock: under QEMU user networking every attempt can
|
|
||||||
# spend its whole ConnectTimeout waiting for a banner, so counting only the
|
|
||||||
# sleeps let a "300 s" wait run for ten minutes or more, silently.
|
|
||||||
wait_for_ssh() {
|
wait_for_ssh() {
|
||||||
local timeout="$1" failure_name="${2:-failure-ssh-timeout}"
|
local timeout="$1" failure_name="${2:-failure-ssh-timeout}" waited=0
|
||||||
local started=$SECONDS next_note=30 progress_name
|
|
||||||
|
|
||||||
while ! ssh_guest true 2>/dev/null; do
|
while ! ssh_guest true 2>/dev/null; do
|
||||||
if ! vm_running; then
|
if ! vm_running; then
|
||||||
@@ -298,20 +291,14 @@ wait_for_ssh() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ((SECONDS - started >= timeout)); then
|
if ((waited >= timeout)); then
|
||||||
capture_console "$failure_name"
|
capture_console "$failure_name"
|
||||||
echo "Timed out after ${timeout}s waiting for SSH" >&2
|
echo "Timed out after ${timeout}s waiting for SSH" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ((SECONDS - started >= next_note)); then
|
sleep 5
|
||||||
printf -v progress_name 'waiting-ssh-%04ds' "$((SECONDS - started))"
|
((waited += 5))
|
||||||
capture_console "$progress_name"
|
|
||||||
echo " ... waiting for SSH ($((SECONDS - started))s)"
|
|
||||||
((next_note += 30))
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +403,7 @@ EOF
|
|||||||
"efi_binary": "limine_x64.efi",
|
"efi_binary": "limine_x64.efi",
|
||||||
"enable_fallback": true
|
"enable_fallback": true
|
||||||
},
|
},
|
||||||
"storage": { "kernel": "linux-omarchy" }
|
"storage": { "kernel": "linux" }
|
||||||
},
|
},
|
||||||
"disk_config": {
|
"disk_config": {
|
||||||
"config_type": "default_layout",
|
"config_type": "default_layout",
|
||||||
@@ -461,7 +448,7 @@ EOF
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hostname": "$GUEST_HOSTNAME",
|
"hostname": "$GUEST_HOSTNAME",
|
||||||
"kernels": [ "linux-omarchy" ],
|
"kernels": [ "linux" ],
|
||||||
"network_config": { "type": "iso" },
|
"network_config": { "type": "iso" },
|
||||||
"ntp": true,
|
"ntp": true,
|
||||||
"parallel_downloads": 8,
|
"parallel_downloads": 8,
|
||||||
@@ -569,28 +556,16 @@ install_phase() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# A line every 30 s so a slow runner never looks hung; a screenshot of
|
|
||||||
# the installer's dashboard every 2 min, which is enough to see progress.
|
|
||||||
if ((waited % 120 == 0)); then
|
if ((waited % 120 == 0)); then
|
||||||
printf -v progress_name 'success-install-progress-%04ds' "$waited"
|
printf -v progress_name 'success-install-progress-%04ds' "$waited"
|
||||||
capture_console "$progress_name"
|
capture_console "$progress_name"
|
||||||
|
echo " ... installing (${waited}s)"
|
||||||
fi
|
fi
|
||||||
((waited % 30 == 0)) && echo " ... installing (${waited}s)"
|
|
||||||
|
|
||||||
sleep 10
|
sleep 10
|
||||||
((waited += 10))
|
((waited += 10))
|
||||||
done
|
done
|
||||||
|
|
||||||
# Keep the installer's own clock, its log and the first boot's numbers next
|
|
||||||
# to the base image while the system is reachable, so CI can report them
|
|
||||||
# without attaching the disk.
|
|
||||||
ssh_sudo "cat /var/log/omarchy-install-timing.json" >"$BASE_DIR/omarchy-install-timing.json" 2>/dev/null || true
|
|
||||||
ssh_sudo "cat /var/log/omarchy-install.log" >"$BASE_DIR/omarchy-install.log" 2>/dev/null || true
|
|
||||||
# SSH can answer before startup finishes, and systemd-analyze refuses until
|
|
||||||
# it has; wait (bounded) so the first-boot numbers are final.
|
|
||||||
ssh_guest "timeout 120 systemctl is-system-running --wait >/dev/null; systemd-analyze" \
|
|
||||||
>"$BASE_DIR/first-boot-systemd-analyze.txt" 2>/dev/null || true
|
|
||||||
|
|
||||||
log "Installed system is up. Saving base image."
|
log "Installed system is up. Saving base image."
|
||||||
stop_vm
|
stop_vm
|
||||||
mv "$BASE_DISK.building" "$BASE_DISK"
|
mv "$BASE_DISK.building" "$BASE_DISK"
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Everyday boot: boots the installed system a second time and records how
|
|
||||||
# long it takes to answer SSH and what systemd-analyze reports, then asserts
|
|
||||||
# that systemd finished starting with no failed units. CI reports these next
|
|
||||||
# to the install time, so a change that slows booting shows up in its PR.
|
|
||||||
|
|
||||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
||||||
|
|
||||||
base_image_ready || { echo "No base image; run this through ./test/integration" >&2; exit 1; }
|
|
||||||
|
|
||||||
log "Booting the installed system"
|
|
||||||
boot_started=$(date +%s%N)
|
|
||||||
start_vm_from_base
|
|
||||||
if ! wait_for_ssh 300 failure-boot-ssh-timeout; then
|
|
||||||
# The console screenshot is saved; the serial log shows what the firmware,
|
|
||||||
# loader and kernel printed, which a screenshot of a hung boot often lacks.
|
|
||||||
echo "--- last lines of the serial console ---" >&2
|
|
||||||
tail -n 40 "$RUN_DIR/serial.log" >&2 2>/dev/null || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
boot_ms=$((($(date +%s%N) - boot_started) / 1000000))
|
|
||||||
# Coarse: wait_for_ssh polls every 2 s. systemd-analyze below has the exact
|
|
||||||
# firmware, loader, kernel and userspace split.
|
|
||||||
log "SSH answered within ${boot_ms} ms of power-on"
|
|
||||||
|
|
||||||
# --wait returns once startup has finished, so systemd-analyze has final
|
|
||||||
# numbers and a unit that fails late in boot is still counted. It exits
|
|
||||||
# non-zero for "degraded"; keep the answer so the diagnostics below are still
|
|
||||||
# collected and the assertion reports it.
|
|
||||||
state=$(ssh_guest "timeout 120 systemctl is-system-running --wait" 2>/dev/null | tr -d '\r' || true)
|
|
||||||
ssh_guest "systemd-analyze" >"$RUN_DIR/systemd-analyze.txt" 2>/dev/null || true
|
|
||||||
ssh_guest "systemd-analyze blame --no-pager | head -n 20" >"$RUN_DIR/systemd-analyze-blame.txt" 2>/dev/null || true
|
|
||||||
# A query that fails must not read as "no failed units".
|
|
||||||
ssh_guest "systemctl --failed --no-legend --plain" >"$RUN_DIR/failed-units.txt" 2>/dev/null ||
|
|
||||||
echo "could not query failed units over SSH" >"$RUN_DIR/failed-units.txt"
|
|
||||||
printf '{"boot_to_ssh_ms": %d, "system_state": "%s"}\n' "$boot_ms" "$state" >"$RUN_DIR/boot-timing.json"
|
|
||||||
|
|
||||||
check "systemd reports the system running (got: ${state:-nothing})" test "$state" = running
|
|
||||||
check "no failed units" test ! -s "$RUN_DIR/failed-units.txt"
|
|
||||||
capture_console "success-boot"
|
|
||||||
|
|
||||||
stop_vm
|
|
||||||
finish
|
|
||||||
@@ -165,7 +165,7 @@ drive_first_boot() {
|
|||||||
# first-boot greeter on its own. A numeric default_entry pointing at a
|
# first-boot greeter on its own. A numeric default_entry pointing at a
|
||||||
# moved entry parks Limine at the menu instead — catch that explicitly,
|
# moved entry parks Limine at the menu instead — catch that explicitly,
|
||||||
# then boot the Omarchy entry by hand so the rest of the flow still runs.
|
# then boot the Omarchy entry by hand so the rest of the flow still runs.
|
||||||
if wait_for_screen "Linux by DHH" 120 2>/dev/null; then
|
if wait_for_screen "Opinionated" 120 2>/dev/null; then
|
||||||
printf 'ok - %s\n' "the machine boots into first-boot setup unattended"
|
printf 'ok - %s\n' "the machine boots into first-boot setup unattended"
|
||||||
else
|
else
|
||||||
if ocr_screen | grep -qi "Omarchy Bootloader"; then
|
if ocr_screen | grep -qi "Omarchy Bootloader"; then
|
||||||
@@ -175,7 +175,7 @@ drive_first_boot() {
|
|||||||
log "Selecting the Omarchy entry manually to continue the run"
|
log "Selecting the Omarchy entry manually to continue the run"
|
||||||
press down; sleep 1; press down; sleep 1; press down; sleep 1
|
press down; sleep 1; press down; sleep 1; press down; sleep 1
|
||||||
press ret
|
press ret
|
||||||
wait_for_screen "Linux by DHH" 600
|
wait_for_screen "Opinionated" 600
|
||||||
else
|
else
|
||||||
echo "Neither the greeter nor the Limine menu appeared" >&2
|
echo "Neither the greeter nor the Limine menu appeared" >&2
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
"""Fresh installs use the Omarchy kernel, except for T2 Macs."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import types
|
|
||||||
import unittest
|
|
||||||
from contextlib import ExitStack
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
sys.path.insert(0, str(ROOT / "configs/airootfs/usr/share/omarchy-iso"))
|
|
||||||
sys.modules.setdefault(
|
|
||||||
"orchestrator.archinstall_adapter",
|
|
||||||
types.ModuleType("orchestrator.archinstall_adapter"),
|
|
||||||
)
|
|
||||||
from orchestrator import context, phases_impl # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
class KernelSelectionTest(unittest.TestCase):
|
|
||||||
def test_hardware_detection(self):
|
|
||||||
# Run the configurator's actual probe without starting its disk wizard.
|
|
||||||
configurator = (ROOT / "configs/airootfs/root/configurator").read_text()
|
|
||||||
probe = re.search(r"^detect_kernel\(\) \{\n.*?^\}", configurator, re.M | re.S)
|
|
||||||
self.assertIsNotNone(probe)
|
|
||||||
cases = [
|
|
||||||
("00:02.0 VGA compatible controller [0300]: Intel [8086:b080]", "linux-omarchy"),
|
|
||||||
("00:01.0 Ethernet controller [0200]: Apple [106b:2001]", "linux-omarchy"),
|
|
||||||
("04:00.0 Mass storage controller [0180]: Apple [106b:1801]", "linux-t2"),
|
|
||||||
("04:00.0 Mass storage controller [0180]: Apple [106b:1802]", "linux-t2"),
|
|
||||||
("", "linux-omarchy"),
|
|
||||||
]
|
|
||||||
for devices, expected in cases:
|
|
||||||
with self.subTest(devices=devices):
|
|
||||||
result = subprocess.run(
|
|
||||||
["bash", "-c", 'lspci() { printf "%s\\n" "$TEST_PCI"; }\n'
|
|
||||||
+ probe.group() + "\ndetect_kernel"],
|
|
||||||
env={**os.environ, "TEST_PCI": devices},
|
|
||||||
check=True, capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
self.assertEqual(result.stdout.strip(), expected)
|
|
||||||
|
|
||||||
def test_unattended_kernel_default_uses_pci_hardware(self):
|
|
||||||
for vendor, device, expected in [
|
|
||||||
("0x8086", "0xb080", "linux-omarchy"),
|
|
||||||
("0x106b", "0x2001", "linux-omarchy"),
|
|
||||||
("0x106b", "0x1801", "linux-t2"),
|
|
||||||
("0x106b", "0x1802", "linux-t2"),
|
|
||||||
]:
|
|
||||||
with self.subTest(device=device), tempfile.TemporaryDirectory() as tmp:
|
|
||||||
pci = Path(tmp)
|
|
||||||
slot = pci / "0000:00:00.0"
|
|
||||||
slot.mkdir()
|
|
||||||
(slot / "vendor").write_text(vendor + "\n")
|
|
||||||
(slot / "device").write_text(device + "\n")
|
|
||||||
self.assertEqual(context._default_kernel(pci), expected)
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
|
||||||
self.assertEqual(context._default_kernel(Path(tmp)), "linux-omarchy")
|
|
||||||
|
|
||||||
def test_context_passes_effective_kernels_to_archinstall(self):
|
|
||||||
for config, default, expected in [
|
|
||||||
({}, "linux-omarchy", ["linux-omarchy"]),
|
|
||||||
({"kernels": []}, "linux-omarchy", ["linux-omarchy"]),
|
|
||||||
({"kernels": None}, "linux-t2", ["linux-t2"]),
|
|
||||||
({}, "linux-t2", ["linux-t2"]),
|
|
||||||
({"kernels": ["linux-lts"]}, "linux-omarchy", ["linux-lts"]),
|
|
||||||
({"kernels": ["linux-omarchy", "linux-lts"]}, "linux-t2", ["linux-omarchy", "linux-lts"]),
|
|
||||||
({"omarchy_install": {"storage": {"kernel": "linux-t2"}}}, "linux-omarchy", ["linux-t2"]),
|
|
||||||
]:
|
|
||||||
with self.subTest(config=config, default=default), tempfile.TemporaryDirectory() as tmp:
|
|
||||||
root = Path(tmp)
|
|
||||||
config_path = root / "config.json"
|
|
||||||
creds_path = root / "creds.json"
|
|
||||||
config_path.write_text(json.dumps(config))
|
|
||||||
creds_path.write_text(json.dumps({"users": [{"username": "test"}]}))
|
|
||||||
with mock.patch.dict(os.environ, {
|
|
||||||
"OMARCHY_INSTALL_CONFIG": str(config_path),
|
|
||||||
"OMARCHY_INSTALL_CREDS": str(creds_path),
|
|
||||||
"OMARCHY_INSTALL_STATE_DIR": str(root / "state"),
|
|
||||||
}, clear=True), mock.patch.object(context, "_default_kernel", return_value=default):
|
|
||||||
ctx = context.InstallContext.from_env()
|
|
||||||
self.assertEqual(ctx.user_configuration["kernels"], expected)
|
|
||||||
self.assertEqual(json.loads(ctx.arch_config_path.read_text())["kernels"], expected)
|
|
||||||
|
|
||||||
def test_headers_install_before_dkms_packages(self):
|
|
||||||
for kernels in (["linux-omarchy"], ["linux-t2"], ["linux-omarchy", "linux-lts"]):
|
|
||||||
for fail_headers in (False, True):
|
|
||||||
with self.subTest(kernels=kernels, fail_headers=fail_headers), ExitStack() as stack:
|
|
||||||
events = []
|
|
||||||
installer = mock.MagicMock()
|
|
||||||
installer.minimal_installation.side_effect = lambda **kwargs: events.append("base")
|
|
||||||
installer.create_users.side_effect = lambda users: events.append("user")
|
|
||||||
|
|
||||||
def install_packages(packages):
|
|
||||||
events.append(packages)
|
|
||||||
if fail_headers:
|
|
||||||
raise RuntimeError("header install failed")
|
|
||||||
|
|
||||||
installer.add_additional_packages.side_effect = install_packages
|
|
||||||
config = types.SimpleNamespace(
|
|
||||||
kernels=kernels, locale_config=None, mirror_config=None,
|
|
||||||
swap=None, auth_config=types.SimpleNamespace(users=["alice"]), app_config=None, timezone=None,
|
|
||||||
ntp=False, hostname="test", pacman_config="/etc/pacman.conf",
|
|
||||||
)
|
|
||||||
ctx = types.SimpleNamespace(
|
|
||||||
state={"arch_config_handler": types.SimpleNamespace(config=config), "mirror_handler": None},
|
|
||||||
target=Path("/unused"), tailscale_authkey_path=None,
|
|
||||||
)
|
|
||||||
for name in ("_mount_offline_package_cache", "_mask_mkinitcpio_pacman_hooks",
|
|
||||||
"_configure_limine_boot", "_write_pre_mounted_fstab"):
|
|
||||||
stack.enter_context(mock.patch.object(phases_impl, name))
|
|
||||||
unmask = stack.enter_context(mock.patch.object(phases_impl, "_unmask_mkinitcpio_pacman_hooks"))
|
|
||||||
unmount = stack.enter_context(mock.patch.object(phases_impl, "_unmount_offline_package_cache"))
|
|
||||||
stack.enter_context(mock.patch.object(phases_impl, "configure_keyboard", return_value=True))
|
|
||||||
stack.enter_context(mock.patch.object(phases_impl, "_install_early_packages", side_effect=lambda inst: events.append("early")))
|
|
||||||
stack.enter_context(mock.patch.object(phases_impl, "_stage_region_defaults", side_effect=lambda target: events.append("region")))
|
|
||||||
stack.enter_context(mock.patch.object(phases_impl, "_runtime_package_list", return_value=["omarchy"]))
|
|
||||||
stack.enter_context(mock.patch.object(phases_impl.arch, "is_pre_mount", return_value=True, create=True))
|
|
||||||
stack.enter_context(mock.patch.object(phases_impl.arch, "root_user", return_value=None, create=True))
|
|
||||||
opened = stack.enter_context(mock.patch.object(phases_impl.arch, "open_installer", create=True))
|
|
||||||
opened.return_value.__enter__.return_value = installer
|
|
||||||
if fail_headers:
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "header install failed"):
|
|
||||||
phases_impl.arch_install_system(ctx)
|
|
||||||
else:
|
|
||||||
phases_impl.arch_install_system(ctx)
|
|
||||||
expected = ["base", [f"{kernel}-headers" for kernel in kernels]]
|
|
||||||
if not fail_headers:
|
|
||||||
expected += ["early", "region", "user", ["omarchy"]]
|
|
||||||
self.assertEqual(events, expected)
|
|
||||||
unmask.assert_called_once_with(ctx)
|
|
||||||
unmount.assert_called_once_with(ctx)
|
|
||||||
|
|
||||||
def test_validation_rejects_missing_or_mismatched_headers(self):
|
|
||||||
for kernel in ("linux-omarchy", "linux-t2"):
|
|
||||||
with self.subTest(kernel=kernel), tempfile.TemporaryDirectory() as tmp:
|
|
||||||
target = Path(tmp)
|
|
||||||
ctx = types.SimpleNamespace(target=target)
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "no installed kernel"):
|
|
||||||
phases_impl._validate_kernel_headers(ctx)
|
|
||||||
modules = target / "usr/lib/modules/7.2-test"
|
|
||||||
modules.mkdir(parents=True)
|
|
||||||
(modules / "pkgbase").write_text(kernel + "\n")
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "no kernel headers"):
|
|
||||||
phases_impl._validate_kernel_headers(ctx)
|
|
||||||
header_release = modules / "build/include/config/kernel.release"
|
|
||||||
header_release.parent.mkdir(parents=True)
|
|
||||||
header_release.write_text("7.2-stock\n")
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "headers do not match"):
|
|
||||||
phases_impl._validate_kernel_headers(ctx)
|
|
||||||
header_release.write_text("7.2-test\n")
|
|
||||||
phases_impl._validate_kernel_headers(ctx)
|
|
||||||
|
|
||||||
def test_boot_validation_uses_default_or_explicit_kernel(self):
|
|
||||||
for storage, configuration, expected in [
|
|
||||||
({}, {}, "linux-omarchy"),
|
|
||||||
({}, {"kernels": ["linux-t2"]}, "linux-t2"),
|
|
||||||
({"kernel": "linux-t2"}, {"kernels": ["linux-omarchy"]}, "linux-t2"),
|
|
||||||
({}, {"kernels": ["linux-lts"]}, "linux-lts"),
|
|
||||||
]:
|
|
||||||
with self.subTest(kernel=expected, storage=storage), tempfile.TemporaryDirectory() as tmp:
|
|
||||||
target = Path(tmp)
|
|
||||||
files = {
|
|
||||||
f"usr/lib/modules/7.2-test/pkgbase": expected + "\n",
|
|
||||||
"usr/lib/modules/7.2-test/build/include/config/kernel.release": "7.2-test\n",
|
|
||||||
"boot/limine.conf": "/Omarchy\n",
|
|
||||||
"etc/kernel/cmdline": "root=UUID=test\n",
|
|
||||||
"etc/default/limine": "CUSTOM_UKI_NAME=omarchy\n",
|
|
||||||
"boot/EFI/limine/limine_x64.efi": "bootloader",
|
|
||||||
f"boot/EFI/Linux/omarchy_{expected}.efi": "UKI",
|
|
||||||
}
|
|
||||||
for name, content in files.items():
|
|
||||||
path = target / name
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(content)
|
|
||||||
ctx = types.SimpleNamespace(
|
|
||||||
target=target, omarchy_install={"storage": storage},
|
|
||||||
user_configuration=configuration, encrypt=False,
|
|
||||||
is_protected=False, defer_provisioning=False,
|
|
||||||
)
|
|
||||||
with mock.patch.object(phases_impl, "_assert_boot_hooks_restored"), \
|
|
||||||
mock.patch.object(phases_impl.arch, "has_uefi", return_value=True, create=True), \
|
|
||||||
mock.patch.object(phases_impl, "_read_efibootmgr", return_value={
|
|
||||||
"entries": {"0001": "Limine\tHD(1,GPT,test)"},
|
|
||||||
}):
|
|
||||||
phases_impl.validate_boot(ctx)
|
|
||||||
(target / f"boot/EFI/Linux/omarchy_{expected}.efi").unlink()
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "missing or empty"):
|
|
||||||
phases_impl.validate_boot(ctx)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
"""Region bootstrap contracts without Docker, pacman, or a running guest."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import types
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
sys.path.insert(0, str(ROOT / "configs/airootfs/usr/share/omarchy-iso"))
|
|
||||||
sys.modules.setdefault("orchestrator.archinstall_adapter", types.ModuleType("orchestrator.archinstall_adapter"))
|
|
||||||
from orchestrator import phases_impl
|
|
||||||
|
|
||||||
|
|
||||||
class RegionTargetTest(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
tmp = tempfile.TemporaryDirectory()
|
|
||||||
self.addCleanup(tmp.cleanup)
|
|
||||||
self.root = Path(tmp.name)
|
|
||||||
self.target = self.root / "target"
|
|
||||||
self.marker = self.root / "omarchy_region"
|
|
||||||
self.payload = self.root / "payload"
|
|
||||||
for name, value in (("ISO_REGION_FILE", self.marker), ("ISO_REGION_PAYLOAD", self.payload)):
|
|
||||||
patch = mock.patch.object(phases_impl, name, value)
|
|
||||||
patch.start()
|
|
||||||
self.addCleanup(patch.stop)
|
|
||||||
|
|
||||||
def test_global_does_not_need_a_profile_or_change_user_defaults(self):
|
|
||||||
phases_impl._stage_region_defaults(self.target)
|
|
||||||
self.assertEqual((self.target / "etc/omarchy/region").read_text(), "global\n")
|
|
||||||
self.assertFalse((self.target / "etc/skel").exists())
|
|
||||||
|
|
||||||
def test_cn_seeds_skel_without_touching_existing_users_or_locale(self):
|
|
||||||
self.marker.write_text("cn\n")
|
|
||||||
profile = self.payload / "skel/.config/example/defaults"
|
|
||||||
profile.parent.mkdir(parents=True)
|
|
||||||
profile.write_text("regional defaults\n")
|
|
||||||
existing = self.target / "home/alice/.config/example/defaults"
|
|
||||||
existing.parent.mkdir(parents=True)
|
|
||||||
existing.write_text("personal settings\n")
|
|
||||||
skel = self.target / "etc/skel"
|
|
||||||
skel.mkdir(parents=True)
|
|
||||||
(skel / ".bashrc").write_text("original\n")
|
|
||||||
(self.target / "etc/locale.conf").write_text("LANG=en_US.UTF-8\n")
|
|
||||||
|
|
||||||
phases_impl._stage_region_defaults(self.target)
|
|
||||||
self.assertEqual((skel / ".config/example/defaults").read_text(), "regional defaults\n")
|
|
||||||
self.assertEqual((skel / ".bashrc").read_text(), "original\n")
|
|
||||||
self.assertEqual(existing.read_text(), "personal settings\n")
|
|
||||||
self.assertEqual((self.target / "etc/locale.conf").read_text(), "LANG=en_US.UTF-8\n")
|
|
||||||
self.assertEqual((self.target / "etc/omarchy/region").read_text(), "cn\n")
|
|
||||||
|
|
||||||
def test_bad_region_and_missing_payload_fail_before_target_changes(self):
|
|
||||||
for region in ("CN", "chn", "zz", "../cn", "", "cn"):
|
|
||||||
with self.subTest(region=region):
|
|
||||||
self.marker.write_text(region)
|
|
||||||
with self.assertRaises(RuntimeError):
|
|
||||||
phases_impl._stage_region_defaults(self.target)
|
|
||||||
self.assertFalse(self.target.exists())
|
|
||||||
|
|
||||||
def test_cn_keyring_is_initialized_before_finalization_for_both_install_modes(self):
|
|
||||||
self.marker.write_text("cn\n")
|
|
||||||
for deferred in (False, True):
|
|
||||||
ctx = types.SimpleNamespace(target=self.target, username="alice", defer_provisioning=deferred)
|
|
||||||
with mock.patch.object(phases_impl, "_run_target_setup_command") as run, \
|
|
||||||
mock.patch.object(phases_impl, "_mask_mkinitcpio_pacman_hooks"), \
|
|
||||||
mock.patch.object(phases_impl, "_unmask_mkinitcpio_pacman_hooks"):
|
|
||||||
phases_impl.run_system_finalizer(ctx)
|
|
||||||
commands = [call.args[1] for call in run.call_args_list]
|
|
||||||
self.assertEqual(commands[:2], [["pacman-key", "--init"], ["pacman-key", "--populate", "archlinux", "archlinuxcn"]])
|
|
||||||
expected = ["--defer-provisioning"] if deferred else ["--install-user", "alice"]
|
|
||||||
self.assertEqual(commands[2], ["/usr/bin/omarchy-apply-system", *expected, "--first-install"])
|
|
||||||
|
|
||||||
def test_keyring_failure_prevents_enabling_online_repositories(self):
|
|
||||||
self.marker.write_text("cn\n")
|
|
||||||
ctx = types.SimpleNamespace(target=self.target, username="alice", defer_provisioning=False)
|
|
||||||
with mock.patch.object(phases_impl, "_run_target_setup_command", side_effect=RuntimeError("keyring failed")) as run:
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "keyring failed"):
|
|
||||||
phases_impl.run_system_finalizer(ctx)
|
|
||||||
self.assertEqual(run.call_count, 1)
|
|
||||||
|
|
||||||
|
|
||||||
class RegionBuildTest(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
tmp = tempfile.TemporaryDirectory()
|
|
||||||
self.addCleanup(tmp.cleanup)
|
|
||||||
self.root = Path(tmp.name)
|
|
||||||
self.runtime = self.root / "runtime"
|
|
||||||
self.profile = self.runtime / "default/regions/cn"
|
|
||||||
(self.profile / "pacman").mkdir(parents=True)
|
|
||||||
(self.runtime / "bin").mkdir()
|
|
||||||
(self.runtime / "bin/omarchy-apply-pacman").touch()
|
|
||||||
(self.profile / "packages").write_text("archlinuxcn-keyring\nexample-regional-package\n")
|
|
||||||
(self.profile / "pacman/pacman.conf.append").write_text("[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n")
|
|
||||||
(self.profile / "pacman/mirrorlist.append").write_text("Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch\n")
|
|
||||||
self.iso = self.root / "iso"
|
|
||||||
self.payload = self.iso / "usr/share/omarchy-iso"
|
|
||||||
self.payload.mkdir(parents=True)
|
|
||||||
self.packages = self.payload / "omarchy-base.packages"
|
|
||||||
self.packages.write_text("base\n")
|
|
||||||
self.config = self.root / "pacman.conf"
|
|
||||||
shutil.copyfile(ROOT / "configs/pacman-online-stable.conf", self.config)
|
|
||||||
self.original_config = self.config.read_bytes()
|
|
||||||
self.bin = self.root / "bin"
|
|
||||||
self.bin.mkdir()
|
|
||||||
self.log = self.root / "commands"
|
|
||||||
for command in ("pacman", "pacman-key"):
|
|
||||||
stub = self.bin / command
|
|
||||||
stub.write_text('#!/bin/bash\nprintf "%s %s\\n" "${0##*/}" "$*" >> "$TEST_LOG"\nif [[ ${0##*/} == pacman ]]; then exit "${TEST_PACMAN_STATUS:-0}"; fi\n')
|
|
||||||
stub.chmod(0o755)
|
|
||||||
|
|
||||||
def prepare(self, region, **env):
|
|
||||||
return subprocess.run(
|
|
||||||
["bash", str(ROOT / "builder/prepare-region.sh"), region, str(self.runtime), str(self.iso), str(self.config)],
|
|
||||||
env={**os.environ, "PATH": f"{self.bin}:{os.environ['PATH']}", "TEST_LOG": str(self.log), **env},
|
|
||||||
capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_cn_adds_real_install_targets_and_verifies_keyring_first(self):
|
|
||||||
result = self.prepare("cn")
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
self.assertEqual(self.packages.read_text(), "base\n\narchlinuxcn-keyring\nexample-regional-package\n")
|
|
||||||
self.assertEqual(self.config.read_bytes(), self.original_config + b"\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n")
|
|
||||||
self.assertEqual((self.payload / "region/packages").read_text(), "archlinuxcn-keyring\nexample-regional-package\n")
|
|
||||||
self.assertEqual(self.log.read_text().splitlines(), [f"pacman --config {self.config} --noconfirm -Sy --needed archlinuxcn-keyring", "pacman-key --populate archlinuxcn"])
|
|
||||||
with mock.patch.object(phases_impl, "Path", return_value=self.packages), \
|
|
||||||
mock.patch.object(phases_impl, "_package_targets", return_value={"runtime": "omarchy-dev", "settings": "omarchy-settings-dev", "nvim": "omarchy-nvim"}):
|
|
||||||
self.assertEqual(phases_impl._runtime_package_list(None), ["omarchy-dev", "base", "archlinuxcn-keyring", "example-regional-package"])
|
|
||||||
|
|
||||||
def test_global_is_unchanged_without_region_support_in_runtime(self):
|
|
||||||
shutil.rmtree(self.runtime)
|
|
||||||
result = self.prepare("global")
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
self.assertEqual(self.packages.read_text(), "base\n")
|
|
||||||
self.assertEqual(self.config.read_bytes(), self.original_config)
|
|
||||||
self.assertFalse(self.log.exists())
|
|
||||||
self.assertFalse((self.payload / "region").exists())
|
|
||||||
|
|
||||||
def test_missing_runtime_support_fails_before_package_operations(self):
|
|
||||||
(self.runtime / "bin/omarchy-apply-pacman").unlink()
|
|
||||||
result = self.prepare("cn")
|
|
||||||
self.assertNotEqual(result.returncode, 0)
|
|
||||||
self.assertIn("--local-source", result.stderr)
|
|
||||||
self.assertFalse(self.log.exists())
|
|
||||||
self.assertEqual(self.config.read_bytes(), self.original_config)
|
|
||||||
|
|
||||||
def test_profile_without_user_defaults_is_supported(self):
|
|
||||||
result = self.prepare("cn")
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
self.assertFalse((self.payload / "region/skel").exists())
|
|
||||||
|
|
||||||
def test_untrusted_keyring_aborts_without_importing_keys(self):
|
|
||||||
result = self.prepare("cn", TEST_PACMAN_STATUS="1")
|
|
||||||
self.assertNotEqual(result.returncode, 0)
|
|
||||||
self.assertNotIn("pacman-key", self.log.read_text())
|
|
||||||
|
|
||||||
|
|
||||||
class RegionMakeTest(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
tmp = tempfile.TemporaryDirectory()
|
|
||||||
self.addCleanup(tmp.cleanup)
|
|
||||||
self.root = Path(tmp.name)
|
|
||||||
(self.root / "bin").mkdir()
|
|
||||||
shutil.copyfile(ROOT / "bin/omarchy-iso-make", self.root / "bin/omarchy-iso-make")
|
|
||||||
for directory in ("configs/airootfs/root", "configs/airootfs/usr/local/bin", "stubs"):
|
|
||||||
(self.root / directory).mkdir(parents=True)
|
|
||||||
(self.root / "configs/profiledef.sh").touch()
|
|
||||||
self.log = self.root / "docker-args"
|
|
||||||
stubs = {
|
|
||||||
"git": '#!/bin/bash\nexit 0\n',
|
|
||||||
"sudo": '#!/bin/bash\necho "unexpected sudo" >&2\nexit 99\n',
|
|
||||||
"docker": '''#!/bin/bash
|
|
||||||
if [[ $1 == version ]]; then exit 0; fi
|
|
||||||
printf '%s\\n' "$@" > "$TEST_LOG"
|
|
||||||
for arg in "$@"; do
|
|
||||||
if [[ $arg == *:/out/ ]]; then touch "${arg%:/out/}/omarchy-test-x86_64.iso"; fi
|
|
||||||
done
|
|
||||||
''',
|
|
||||||
}
|
|
||||||
for name, content in stubs.items():
|
|
||||||
stub = self.root / "stubs" / name
|
|
||||||
stub.write_text(content)
|
|
||||||
stub.chmod(0o755)
|
|
||||||
|
|
||||||
def make(self, *args):
|
|
||||||
env = {key: value for key, value in os.environ.items() if not key.startswith("OMARCHY_")}
|
|
||||||
return subprocess.run(
|
|
||||||
["bash", "bin/omarchy-iso-make", "--keep-pkg-cache", "--no-boot-offer", *args],
|
|
||||||
cwd=self.root, env={**env, "HOME": str(self.root), "PATH": f"{self.root / 'stubs'}:{env['PATH']}", "TEST_LOG": str(self.log), "TEST_ROOT": str(self.root)},
|
|
||||||
capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_region_is_independent_of_channel_and_names_cache_and_artifact(self):
|
|
||||||
for flags, channel, ref in (([], "stable", "quattro"), (["--rc"], "rc", "rc"), (["--edge"], "edge", "edge"), (["--dev"], "edge", "dev")):
|
|
||||||
with self.subTest(channel=ref):
|
|
||||||
release = self.root / "release"
|
|
||||||
if release.exists():
|
|
||||||
shutil.rmtree(release)
|
|
||||||
result = self.make(*flags, "--region", "cn")
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
args = self.log.read_text().splitlines()
|
|
||||||
self.assertIn("OMARCHY_REGION=cn", args)
|
|
||||||
self.assertIn(f"OMARCHY_MIRROR={channel}", args)
|
|
||||||
self.assertIn(f"OMARCHY_ISO_REF={ref}", args)
|
|
||||||
self.assertIn(f"{self.root}/.cache/omarchy/iso_{channel}-cn/airootfs/var/cache/omarchy:/var/cache/airootfs/var/cache/omarchy", args)
|
|
||||||
self.assertTrue((release / f"omarchy-test-x86_64-{ref}-cn.iso").exists())
|
|
||||||
|
|
||||||
def test_default_preserves_existing_global_cache_and_name(self):
|
|
||||||
result = self.make()
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
self.assertIn("OMARCHY_REGION=global", self.log.read_text().splitlines())
|
|
||||||
self.assertIn("/iso_stable/", self.log.read_text())
|
|
||||||
self.assertTrue((self.root / "release/omarchy-test-x86_64-quattro.iso").exists())
|
|
||||||
|
|
||||||
def test_build_renames_only_its_own_iso(self):
|
|
||||||
release = self.root / "release"
|
|
||||||
release.mkdir()
|
|
||||||
other = release / "omarchy-other-x86_64.iso"
|
|
||||||
other.touch()
|
|
||||||
result = self.make("--region", "cn")
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
self.assertTrue(other.exists())
|
|
||||||
self.assertTrue((release / "omarchy-test-x86_64-quattro-cn.iso").exists())
|
|
||||||
self.assertEqual(list(release.glob(".build-*")), [])
|
|
||||||
|
|
||||||
def test_local_source_keeps_region_and_mounts_both_checkouts(self):
|
|
||||||
for name in ("runtime", "packages"):
|
|
||||||
(self.root / name).mkdir()
|
|
||||||
result = self.make("--region", "cn", "--local-source", str(self.root / "runtime"), str(self.root / "packages"))
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
args = self.log.read_text().splitlines()
|
|
||||||
self.assertIn("OMARCHY_REGION=cn", args)
|
|
||||||
self.assertIn("OMARCHY_ISO_REF=local", args)
|
|
||||||
self.assertIn(f"{self.root}/runtime:/omarchy-source:ro", args)
|
|
||||||
self.assertIn(f"{self.root}/packages:/omarchy-pkgs:ro", args)
|
|
||||||
self.assertTrue((self.root / "release/omarchy-test-x86_64-local-cn.iso").exists())
|
|
||||||
|
|
||||||
def test_invalid_regions_fail_without_starting_build(self):
|
|
||||||
for flags in (("--region",), ("--region", "chn"), ("--region", "CN"), ("--region", "zz"), ("--region", "../cn")):
|
|
||||||
with self.subTest(flags=flags):
|
|
||||||
result = self.make(*flags)
|
|
||||||
self.assertNotEqual(result.returncode, 0)
|
|
||||||
self.assertFalse(self.log.exists())
|
|
||||||
self.assertFalse((self.root / "release").exists())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Unit tests for the UTM SE appliance builder's static inputs: the config.plist
|
||||||
|
# template must be a valid plist once omarchy-utm-make substitutes its
|
||||||
|
# placeholders, and the package lists must name real packages.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
printf 'ok - %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
local description="$1"
|
||||||
|
local detail="${2:-}"
|
||||||
|
|
||||||
|
[[ -n $detail ]] && printf '%s\n' "$detail" >&2
|
||||||
|
printf 'not ok - %s\n' "$description" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
template="$ROOT/builder/utm/config.plist"
|
||||||
|
render="$ROOT/builder/utm/render"
|
||||||
|
|
||||||
|
placeholders=$(grep -o '@@[A-Z_]*@@' "$template" | sort -u | tr '\n' ' ')
|
||||||
|
[[ $placeholders == "@@DISK_UUID@@ @@MAC@@ @@VERSION@@ @@VM_UUID@@ " ]] ||
|
||||||
|
fail "the template carries exactly the placeholders the build fills" "$placeholders"
|
||||||
|
pass "the template carries exactly the placeholders the build fills"
|
||||||
|
|
||||||
|
rendered=$("$render" 11111111-2222-3333-4444-555555555555 66666666-7777-8888-9999-000000000000 02:00:00:00:00:01 4.0.2)
|
||||||
|
python3 - "$rendered" <<'PY' || fail "the rendered config.plist parses, names the disk it ships, and declares nothing iOS rejects"
|
||||||
|
import plistlib, sys
|
||||||
|
config = plistlib.loads(sys.argv[1].encode())
|
||||||
|
assert "@@" not in sys.argv[1]
|
||||||
|
assert config["ConfigurationVersion"] == 4
|
||||||
|
assert config["Information"]["UUID"] == "11111111-2222-3333-4444-555555555555"
|
||||||
|
assert config["Drive"][0]["Identifier"] == "66666666-7777-8888-9999-000000000000"
|
||||||
|
assert config["Drive"][0]["ImageName"] == "66666666-7777-8888-9999-000000000000.qcow2"
|
||||||
|
assert config["Network"][0]["MacAddress"] == "02:00:00:00:00:01"
|
||||||
|
assert "4.0.2" in config["Information"]["Notes"]
|
||||||
|
assert config["System"]["Architecture"] == "aarch64"
|
||||||
|
assert config["QEMU"]["Hypervisor"] is False
|
||||||
|
assert config["System"]["MemorySize"] == 2048
|
||||||
|
# UTM on iOS has no Ptty serial mode and decodes strictly, so the bundle ships no serial device.
|
||||||
|
assert config["Serial"] == []
|
||||||
|
PY
|
||||||
|
pass "the rendered config.plist parses, names the disk it ships, and declares nothing iOS rejects"
|
||||||
|
|
||||||
|
base=""
|
||||||
|
for candidate in "$ROOT/../omarchy" "$ROOT/../omacom/omarchy"; do
|
||||||
|
if [[ -f $candidate/install/omarchy-base.packages ]]; then
|
||||||
|
base="$candidate/install/omarchy-base.packages"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -z $base ]]; then
|
||||||
|
pass "no omarchy checkout beside this one; skipping the drop list check"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
missing=$(grep -v '^#\|^$' "$ROOT/builder/utm-se-drop.packages" | grep -vxF -f <(sed -e 's/[[:space:]]*#.*$//' -e '/^$/d' "$base") || true)
|
||||||
|
[[ -z $missing ]] || fail "every dropped package is one the base list ships" "$missing"
|
||||||
|
pass "every dropped package is one the base list ships"
|
||||||
Reference in new issue
Block a user