Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
411acd6d18
|
||
|
|
2c523afa6f
|
||
|
|
2a589e9b91
|
||
|
|
30f63b674e | ||
|
|
25cebcf3fd | ||
|
|
e8715fc888 | ||
|
|
75d00a2036 | ||
|
|
abe2d6a1b4 | ||
|
|
31cca48c14 | ||
|
|
abc1a7716d | ||
|
|
27b7e76c5a | ||
|
|
9dcfdb035a | ||
|
|
e121561851 | ||
|
|
1c912658aa | ||
|
|
8b17699997 | ||
|
|
595d38cb04 | ||
|
|
0c75c60e19 | ||
|
|
02ee068504 | ||
|
|
22f0b5acfb | ||
|
|
e3de148897 | ||
|
|
d2e37df6a4 | ||
|
|
81106d04e8 | ||
|
|
043e819d01 | ||
|
|
24d6573447 | ||
|
|
55d5f831d4 | ||
|
|
422d898033 | ||
|
|
39d65593df | ||
|
|
27b33e7f1c | ||
|
|
53a41664fa | ||
|
|
0d8d95a340 | ||
|
|
db8615e5ff | ||
|
|
304c64dab8 | ||
|
|
b7edf95ecb | ||
|
|
ed10c3661d | ||
|
|
02c803cb40 | ||
|
|
555093811f | ||
|
|
48471b25c1 | ||
|
|
45b31ae98c | ||
|
|
947a69ba91 | ||
|
|
8d069b0c03 | ||
|
|
a63d090407 | ||
|
|
8f3bdc1fb9 | ||
|
|
4d80dd8a2c | ||
|
|
22d02f0069 | ||
|
|
c1d5725837 | ||
|
|
073f7b2fc0 | ||
|
|
d02dee7e73 | ||
|
|
36b072ce76 | ||
|
|
fe133bd419 | ||
|
|
89a34f7885 | ||
|
|
37f71a0efb | ||
|
|
84149ef33b | ||
|
|
57df34ddfc | ||
|
|
6aef3008c1 | ||
|
|
1ad8872146 | ||
|
|
e7774eeeaf | ||
|
|
82d5693395 | ||
|
|
1ca5e5a7a3 | ||
|
|
86c07785cb | ||
|
|
9373d0e139 | ||
|
|
555c364221 | ||
|
|
7cfb7111a0 | ||
|
|
1daf1202d4 | ||
|
|
47426693de | ||
|
|
b507a20b7e | ||
|
|
447f6d2e58 | ||
|
|
a23f8d464d | ||
|
|
6f5ab0bcd1 | ||
|
|
2f5af06895 |
No files matched your search
@@ -0,0 +1,118 @@
|
||||
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
|
||||
@@ -0,0 +1,117 @@
|
||||
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
|
||||
@@ -0,0 +1,55 @@
|
||||
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
|
||||
'
|
||||
@@ -26,6 +26,8 @@ For local development, build the ISO from sibling checkouts:
|
||||
|
||||
Despite the local folder name, the first argument is the Omarchy source checkout (runtime commands, configs, setup scripts, themes, shell, migrations). The installer itself lives in this ISO repo.
|
||||
|
||||
The configurator asks for the language on its first screen: English or Simplified Chinese. Chinese reruns the installer in cage + foot, a console that draws Chinese, with Noto Sans Mono CJK unpacked from the offline mirror's `noto-fonts-cjk`. It opens on the keyboard screen, offers only keyboards with a US keymap (cage keeps a US layout) and preselects Chinese (Simplified, Pinyin); every later screen and the install progress read in Chinese. Esc there goes back to a bilingual language screen, where English returns to the VT, which applies the chosen keymap. Without a working display or the font, Chinese continues in English on the VT. Either way a Chinese install sets the target's `LANG` to `zh_CN.UTF-8` through archinstall's `locale_config`. The language never decides the region: that follows the timezone. The Chinese text lives in `OMARCHY_ZH` tables beside the English it replaces.
|
||||
|
||||
Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror.
|
||||
|
||||
## Autoinstall
|
||||
@@ -62,6 +64,8 @@ When `authorized_keys` is present, autoinstall installs it as the user's `~/.ssh
|
||||
|
||||
When `tailscale_authkey` is present (one key, blank lines and `#` comments ignored), the install adds the `tailscale` package from the ISO's bundled mirror — nothing is fetched from the network at install or boot — and stages the join for first boot: the key lands at `/etc/tailscale/authkey` (root-only), `tailscaled` is enabled, ufw allows traffic in on `tailscale0`, and a background unit runs `tailscale up` once the network is actually up, retrying until it succeeds without holding up the boot. After a successful join the key is deleted and the unit disables itself; until then both survive reboots, so a machine installed offline joins whenever it first gets connectivity. The node appears on the tailnet under the configured hostname. Use a reusable, pre-authorized (tagged) key so one drive image serves many machines — or an ephemeral key for disposable VMs.
|
||||
|
||||
The region follows `timezone`, exactly as in an interactive install: a timezone listed in one of the runtime's region profiles (`default/regions/*/timezones`, such as `Asia/Shanghai` for `cn`) selects that profile's mirrors and repositories, and anything else is global. Set `omarchy_install.region` in `user_configuration.json` to pick one explicitly, for example `"cn"` on a deferred-provisioning install, which carries `UTC` until first boot. Every ISO bundles every profile's packages, so a regional install still needs no network.
|
||||
|
||||
### Building the drive
|
||||
|
||||
```bash
|
||||
|
||||
+130
-30
@@ -13,7 +13,11 @@
|
||||
#
|
||||
# Usage: omarchy-iso-test [release/omarchy.iso] [options]
|
||||
#
|
||||
# --keyboard LABEL English (US/UK), Japanese, Japanese (US keyboard), Korean,
|
||||
# or Chinese (Simplified, Pinyin)/(Traditional, Zhuyin).
|
||||
# OMARCHY_PATH must be the source tree used to build the ISO.
|
||||
# --encrypt Drive the encrypted install flow (default: unencrypted)
|
||||
# --language LANG Pick en (default) or zh; zh keeps the preselected Pinyin keyboard
|
||||
# --provision Drive the deferred-provisioning install flow: no user during install, the
|
||||
# first boot runs omarchy-provision-owner and creates it there
|
||||
# --reuse-base Skip the install phase if a base image already exists
|
||||
@@ -39,6 +43,7 @@ MEMORY=8192
|
||||
INSTALL_TIMEOUT=2400
|
||||
BOOT_TIMEOUT=600
|
||||
ENCRYPT=false
|
||||
INSTALL_LANGUAGE=en
|
||||
PROVISION=false
|
||||
REUSE_BASE=false
|
||||
INSTALL_ONLY=false
|
||||
@@ -47,13 +52,16 @@ NO_PREVIEW=false
|
||||
SYNC_DIR=""
|
||||
SYNC_ALL=false
|
||||
|
||||
KEYBOARD_LABEL="English (US)"
|
||||
GUEST_USER="omarchy"
|
||||
GUEST_PASSWORD="omarchy"
|
||||
GUEST_HOSTNAME="omarchy-test"
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--keyboard) KEYBOARD_LABEL="$2"; shift ;;
|
||||
--encrypt) ENCRYPT=true ;;
|
||||
--language) INSTALL_LANGUAGE="$2"; shift ;;
|
||||
--provision) PROVISION=true ;;
|
||||
--reuse-base) REUSE_BASE=true ;;
|
||||
--install-only) INSTALL_ONLY=true ;;
|
||||
@@ -84,13 +92,42 @@ if [[ ! -f ${ISO:-} ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
omarchy-pkg-add qemu-full edk2-ovmf socat imagemagick tesseract tesseract-data-eng
|
||||
if [[ $INSTALL_LANGUAGE != "en" && $INSTALL_LANGUAGE != "zh" ]]; then
|
||||
echo "--language must be en or zh, not $INSTALL_LANGUAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ $INSTALL_LANGUAGE == "zh" && $KEYBOARD_LABEL != "English (US)" ]]; then
|
||||
echo "--language zh takes the preselected Pinyin keyboard; drop --keyboard" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$KEYBOARD_LABEL" in
|
||||
"English (US)" | "English (UK)" | "Japanese" | "Japanese (US keyboard)" | "Korean" | "Chinese (Simplified, Pinyin)" | "Chinese (Traditional, Zhuyin)") ;;
|
||||
*) echo "The VM harness cannot type credentials with keyboard: $KEYBOARD_LABEL" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
omarchy-pkg-add qemu-full edk2-ovmf socat imagemagick tesseract tesseract-data-eng tesseract-data-chi_sim
|
||||
|
||||
# After the language screen a Chinese install reads in Chinese.
|
||||
OCR_LANGS=eng
|
||||
[[ $INSTALL_LANGUAGE == "zh" ]] && OCR_LANGS=eng+chi_sim
|
||||
# lang_text <english> <chinese> picks the text a screen shows in this run.
|
||||
lang_text() {
|
||||
if [[ $INSTALL_LANGUAGE == "zh" ]]; then
|
||||
printf '%s' "$2"
|
||||
else
|
||||
printf '%s' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
|
||||
OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
|
||||
|
||||
BASE_NAME="$(basename "$ISO" .iso)"
|
||||
if [[ $KEYBOARD_LABEL != "English (US)" ]]; then
|
||||
BASE_NAME+="-$(printf '%s' "$KEYBOARD_LABEL" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')"
|
||||
fi
|
||||
$ENCRYPT && BASE_NAME+="-encrypted"
|
||||
[[ $INSTALL_LANGUAGE == "zh" ]] && BASE_NAME+="-zh"
|
||||
$PROVISION && BASE_NAME+="-provision"
|
||||
BASE_DIR="$REPO_DIR/test-runs/$BASE_NAME"
|
||||
RUN_DIR="$BASE_DIR/runs/$(date +%Y%m%d-%H%M%S)"
|
||||
@@ -240,7 +277,13 @@ ocr_screen() {
|
||||
screendump "$shot"
|
||||
[[ -s $shot ]] || return 0
|
||||
magick "$shot" -colorspace gray -negate -resize 150% "$prepped" 2>/dev/null || return 0
|
||||
tesseract "$prepped" - --psm 6 2>/dev/null || true
|
||||
# The logo throws tesseract off CJK, so Chinese runs also read below it, without spaces.
|
||||
if [[ $INSTALL_LANGUAGE == "zh" ]]; then
|
||||
magick "$shot" -gravity north -chop 0x35% -colorspace gray -negate -resize 150% "$prepped.text.png" 2>/dev/null || return 0
|
||||
{ tesseract "$prepped" - --psm 6 -l "$OCR_LANGS"; tesseract "$prepped.text.png" - --psm 6 -l "$OCR_LANGS"; } 2>/dev/null | sed 'p; s/ //g' || true
|
||||
else
|
||||
tesseract "$prepped" - --psm 6 -l "$OCR_LANGS" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Wait until the console shows the given text. Saves the offending screen as
|
||||
@@ -610,31 +653,65 @@ audio-panel|meta_l-ctrl-a|omarchy-keyboard-panel|omarchy.audio'
|
||||
|
||||
# ----------------------------------------------------- driving the installer
|
||||
|
||||
select_keyboard() {
|
||||
local label="$1" index direction="down"
|
||||
# gum choose uses arrows, not a searchable text field. Read the same ordered
|
||||
# choices the ISO vendors from the runtime rather than hard-code row numbers.
|
||||
index=$(OMARCHY_TEST_KEYBOARD="$label" python - <<'PYTHON'
|
||||
import os, re
|
||||
from pathlib import Path
|
||||
source = Path(os.environ.get("OMARCHY_PATH", "")) / "install/provisioning/setup-form.sh"
|
||||
if not source.is_file():
|
||||
raise SystemExit("Set OMARCHY_PATH to the source tree used to build this ISO")
|
||||
text = source.read_text()
|
||||
rows = re.search(r"OMARCHY_KEYBOARD_LAYOUTS=\$'(.*?)'\n", text, re.S).group(1).splitlines()
|
||||
labels = [row.split("|")[0] for row in rows]
|
||||
label = os.environ["OMARCHY_TEST_KEYBOARD"]
|
||||
if label not in labels:
|
||||
raise SystemExit(f"Keyboard choice is absent from the ISO source: {label}")
|
||||
print(labels.index(label) - labels.index("English (US)"))
|
||||
PYTHON
|
||||
) || return 1
|
||||
if (( index < 0 )); then
|
||||
direction="up"
|
||||
index=$((-index))
|
||||
fi
|
||||
for (( i = 0; i < index; i++ )); do
|
||||
press "$direction"
|
||||
sleep 0.1
|
||||
done
|
||||
}
|
||||
|
||||
drive_configurator() {
|
||||
log "Driving the installer wizard"
|
||||
|
||||
# A greeter (animated logo + tagline) opens the installer; Return begins.
|
||||
wait_for_screen "Opinionated" 300
|
||||
wait_for_screen "Linux by DHH" 300
|
||||
capture_console "success-installer-00-greeter"
|
||||
press ret
|
||||
|
||||
# Keyboard is the first screen. Ctrl+C there is the hidden entry into deferred provisioning
|
||||
# mode; a normal install just selects the layout.
|
||||
wait_for_screen "keyboard layout" 300
|
||||
# Language comes first: English on top, Chinese below.
|
||||
wait_for_screen "Select language" 300
|
||||
capture_console "success-installer-01-language"
|
||||
if [[ $INSTALL_LANGUAGE == "zh" ]]; then press end; else press home; fi
|
||||
press ret
|
||||
|
||||
# The keyboard follows, for Chinese in cage + foot; Ctrl+C there defers provisioning.
|
||||
wait_for_screen "$(lang_text "keyboard layout" "键盘布局")" 300
|
||||
capture_console "success-installer-01-keyboard-layout"
|
||||
|
||||
if $PROVISION; then
|
||||
press ctrl-c # arm "prepare for another owner"
|
||||
wait_for_screen "another owner" 30
|
||||
wait_for_screen "$(lang_text "another owner" "另一位")" 30
|
||||
capture_console "success-installer-02-prepare-confirm"
|
||||
press ret # "Yes, prepare for another owner" is the affirmative default
|
||||
|
||||
# Jumps straight to disk selection, then the overwrite confirm.
|
||||
wait_for_screen "install disk" 60
|
||||
wait_for_screen "$(lang_text "install disk" "安装磁盘")" 60
|
||||
capture_console "success-installer-03-prepare-install-disk"
|
||||
press ret
|
||||
|
||||
wait_for_screen "recovery possible" 60
|
||||
wait_for_screen "$(lang_text "recovery possible" "无法恢复")" 60
|
||||
capture_console "success-installer-04-prepare-disk-warning-encrypted"
|
||||
if ! $ENCRYPT; then
|
||||
press ctrl-c
|
||||
@@ -651,58 +728,70 @@ drive_configurator() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
press ret # English (US) is preselected
|
||||
if [[ $KEYBOARD_LABEL != "English (US)" ]]; then
|
||||
select_keyboard "$KEYBOARD_LABEL"
|
||||
capture_console "success-installer-01-selected-keyboard"
|
||||
fi
|
||||
press ret
|
||||
|
||||
# Normal flow: user step comes before disk selection.
|
||||
wait_for_screen "Username" 120
|
||||
wait_for_screen "$(lang_text "Username" "用户名")" 120
|
||||
type_text "$GUEST_USER"
|
||||
capture_console "success-installer-02-username"
|
||||
press ret
|
||||
|
||||
wait_for_screen "Password" 60
|
||||
wait_for_screen "$(lang_text "Password" "密码")" 60
|
||||
type_text "$GUEST_PASSWORD"
|
||||
capture_console "success-installer-03-password"
|
||||
press ret
|
||||
|
||||
wait_for_screen "Must match" 60
|
||||
wait_for_screen "$(lang_text "Must match" "确认")" 60
|
||||
type_text "$GUEST_PASSWORD"
|
||||
capture_console "success-installer-04-password-confirmation"
|
||||
press ret
|
||||
|
||||
wait_for_screen "Full name" 60
|
||||
wait_for_screen "$(lang_text "Full name" "姓名")" 60
|
||||
type_text "Omarchy Test"
|
||||
capture_console "success-installer-05-full-name"
|
||||
press ret
|
||||
|
||||
wait_for_screen "Email address" 60
|
||||
type_text "test@omarchy.org"
|
||||
wait_for_screen "$(lang_text "Email address" "邮箱")" 60
|
||||
type_text "test"
|
||||
if [[ $KEYBOARD_LABEL == "Japanese" ]]; then
|
||||
press bracket_left
|
||||
elif [[ $KEYBOARD_LABEL == "English (UK)" ]]; then
|
||||
press shift-apostrophe
|
||||
else
|
||||
press shift-2
|
||||
fi
|
||||
type_text "omarchy.org"
|
||||
capture_console "success-installer-06-email"
|
||||
press ret
|
||||
|
||||
wait_for_screen "Hostname" 60
|
||||
wait_for_screen "$(lang_text "Hostname" "主机名")" 60
|
||||
type_text "$GUEST_HOSTNAME"
|
||||
capture_console "success-installer-07-hostname"
|
||||
press ret
|
||||
|
||||
wait_for_screen "Timezone" 180 # tzupdate geo-guess can take a moment
|
||||
wait_for_screen "$(lang_text "Timezone" "时区")" 180 # tzupdate geo-guess can take a moment
|
||||
capture_console "success-installer-08-timezone"
|
||||
press ret
|
||||
|
||||
wait_for_screen "look right" 60
|
||||
wait_for_screen "$(lang_text "look right" "正确")" 60
|
||||
capture_console "success-installer-09-review"
|
||||
press ret
|
||||
|
||||
wait_for_screen "install disk" 60
|
||||
wait_for_screen "$(lang_text "install disk" "安装磁盘")" 60
|
||||
capture_console "success-installer-10-install-disk"
|
||||
press ret # the single virtio disk
|
||||
|
||||
wait_for_screen "installation mode\|recovery possible" 60
|
||||
if ocr_screen | grep -qi "installation mode"; then
|
||||
wait_for_screen "$(lang_text "installation mode\|recovery possible" "安装方式\|无法恢复")" 60
|
||||
if ocr_screen | grep -qi "$(lang_text "installation mode" "安装方式")"; then
|
||||
capture_console "success-installer-11-install-mode"
|
||||
press ret # "Full disk install" is preselected
|
||||
fi
|
||||
|
||||
wait_for_screen "recovery possible" 60
|
||||
wait_for_screen "$(lang_text "recovery possible" "无法恢复")" 60
|
||||
capture_console "success-installer-12-disk-warning-encrypted"
|
||||
if ! $ENCRYPT; then
|
||||
press ctrl-c # toggles the confirm to the unencrypted flow
|
||||
@@ -731,13 +820,17 @@ drive_provision_owner() {
|
||||
# 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.
|
||||
# A greeter (animated logo + tagline) opens first boot; Return begins.
|
||||
wait_for_screen "Opinionated" 600
|
||||
wait_for_screen "Linux by DHH" 600
|
||||
capture_console "success-provision-00-greeter"
|
||||
press ret
|
||||
|
||||
wait_for_screen "keyboard layout" 120
|
||||
press down
|
||||
capture_console "success-provision-01-keyboard-layout-uk"
|
||||
if [[ $KEYBOARD_LABEL == "English (US)" ]]; then
|
||||
press down # preserve the harness's default UK provisioning coverage
|
||||
else
|
||||
select_keyboard "$KEYBOARD_LABEL"
|
||||
fi
|
||||
capture_console "success-provision-01-keyboard-layout"
|
||||
press ret
|
||||
|
||||
wait_for_screen "Username" 120
|
||||
@@ -763,7 +856,13 @@ drive_provision_owner() {
|
||||
# Shift+2 as on US); send that keycode so the typed email is correct under
|
||||
# the new layout — itself further proof the deferred keymap took effect.
|
||||
type_text "test"
|
||||
press shift-apostrophe
|
||||
if [[ $KEYBOARD_LABEL == "English (US)" || $KEYBOARD_LABEL == "English (UK)" ]]; then
|
||||
press shift-apostrophe
|
||||
elif [[ $KEYBOARD_LABEL == "Japanese" ]]; then
|
||||
press bracket_left
|
||||
else
|
||||
press shift-2
|
||||
fi
|
||||
type_text "omarchy.org"
|
||||
capture_console "success-provision-04-email"
|
||||
press ret
|
||||
@@ -796,7 +895,8 @@ wait_for_install() {
|
||||
while true; do
|
||||
text=$(ocr_screen)
|
||||
|
||||
if grep -qi "Reboot Now" <<<"$text"; then
|
||||
# OCR cannot read the Chinese reboot button, so Chinese runs match the title above it.
|
||||
if grep -qi "$(lang_text "Reboot Now" "安装完成")" <<<"$text"; then
|
||||
log "Install finished. Confirming the reboot prompt."
|
||||
capture_console "success-installer-15-reboot"
|
||||
press ret
|
||||
@@ -806,13 +906,13 @@ wait_for_install() {
|
||||
# Deferred-provisioning installs skip the celebration and reboot on their own
|
||||
# (no Reboot Now prompt); the first-boot greeter tagline appearing means the
|
||||
# install finished and rebooted into setup.
|
||||
if $PROVISION && grep -qi "Opinionated" <<<"$text"; then
|
||||
if $PROVISION && grep -qi "Linux by DHH" <<<"$text"; then
|
||||
log "Install finished and auto-rebooted into first-boot setup."
|
||||
capture_console "success-installer-15-autoreboot"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -qi "installation stopped" <<<"$text"; then
|
||||
if grep -qi "$(lang_text "installation stopped" "安装已停止")" <<<"$text"; then
|
||||
capture_console "failure-installer-installation-stopped"
|
||||
echo "Install failed — screenshot saved to $RUN_DIR" >&2
|
||||
return 1
|
||||
|
||||
+61
-7
@@ -41,11 +41,14 @@ pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Sy omarchy-keyring
|
||||
pacman-key --populate omarchy
|
||||
|
||||
# Append the [omarchy] repo to the container's /etc/pacman.conf so subsequent
|
||||
# tools (notably makepkg in build-omarchy-packages.sh) can resolve omarchy-
|
||||
# only build deps like limine-snapper-sync and limine-mkinitcpio-hook.
|
||||
# Put OPR first in the container too so makepkg resolves the same overrides
|
||||
# as the online installer configuration.
|
||||
if ! grep -q '^\[omarchy\]' /etc/pacman.conf; then
|
||||
awk '/^\[omarchy\]/,/^$/' /configs/pacman-online-${OMARCHY_MIRROR}.conf >> /etc/pacman.conf
|
||||
opr_section=$(awk '/^\[omarchy\]/,/^$/' /configs/pacman-online-${OMARCHY_MIRROR}.conf)
|
||||
awk -v opr="$opr_section" '/^\[core\]/ { print opr; print "" } { print }' \
|
||||
/etc/pacman.conf >/tmp/pacman-opr-first.conf
|
||||
install -m 644 /tmp/pacman-opr-first.conf /etc/pacman.conf
|
||||
rm -f /tmp/pacman-opr-first.conf
|
||||
fi
|
||||
|
||||
# Build locations
|
||||
@@ -118,7 +121,8 @@ cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/"
|
||||
# The selected omarchy-settings package is needed here so its post_install hook
|
||||
# drops Omarchy's plymouthd.conf into /etc/plymouth before mkarchiso builds the
|
||||
# live initramfs.
|
||||
arch_packages=(linux-t2 git gum jq openssl plymouth ttfx tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted)
|
||||
# cage and foot give Chinese installs a console that draws Chinese.
|
||||
arch_packages=(linux-t2 git gum jq openssl plymouth ttfx tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted cage foot)
|
||||
printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.x86_64"
|
||||
|
||||
# The live ISO boots linux-t2 (see airootfs/etc/mkinitcpio.d/linux-t2.preset), so
|
||||
@@ -139,6 +143,7 @@ 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
|
||||
# pulls the published omarchy* from the network mirror like any other package.
|
||||
if [[ -d /omarchy-source ]]; then
|
||||
regions_source=/omarchy-source/default/regions
|
||||
base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages)
|
||||
setup_form=/omarchy-source/install/provisioning/setup-form.sh
|
||||
else
|
||||
@@ -161,6 +166,17 @@ else
|
||||
# build here (set -e) with a bare "Not found in archive" instead of the
|
||||
# actionable error below.
|
||||
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/provisioning/setup-form.sh 2>/dev/null || true
|
||||
# Region profiles live under default/, which the settings package ships,
|
||||
# not the runtime. Its own cache dir keeps the runtime lookup above from
|
||||
# ever matching it.
|
||||
settings_cache_dir="$bootstrap_cache_dir/settings"
|
||||
mkdir -p "$settings_cache_dir"
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw "$OMARCHY_SETTINGS_PACKAGE" --cachedir "$settings_cache_dir" --dbpath /tmp/offlinedb-bootstrap >/dev/null
|
||||
regions_source=$(find "$settings_cache_dir" -maxdepth 1 -type f -name "$OMARCHY_SETTINGS_PACKAGE-*.pkg.tar.zst" | sort | head -1)
|
||||
if [[ -z $regions_source ]]; then
|
||||
echo "ERROR: downloaded package for $OMARCHY_SETTINGS_PACKAGE not found in $settings_cache_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
setup_form=/tmp/omarchy-pkglists/usr/share/omarchy/install/provisioning/setup-form.sh
|
||||
fi
|
||||
|
||||
@@ -187,6 +203,24 @@ if [[ ! -f $setup_form ]]; then
|
||||
fi
|
||||
cp "$setup_form" "$build_cache_dir/airootfs/usr/share/omarchy-iso/setup-form.sh"
|
||||
|
||||
# Every region's packages ride in the offline mirror, and its community
|
||||
# repositories join a writable copy of the online config to download them.
|
||||
online_config=/tmp/omarchy-pacman-online.conf
|
||||
cp "/configs/pacman-online-${OMARCHY_MIRROR}.conf" "$online_config"
|
||||
bash /builder/prepare-regions.sh "$regions_source" \
|
||||
"$build_cache_dir/airootfs/usr/share/omarchy-iso" "$online_config"
|
||||
|
||||
# Profiles ship in the settings package, but the finalizer that applies them
|
||||
# ships in the runtime. A runtime without it would ignore the region marker,
|
||||
# leaving regional targets with the keyring but none of the repositories.
|
||||
if [[ -n ${omarchy_pkg:-} && -d $build_cache_dir/airootfs/usr/share/omarchy-iso/regions ]]; then
|
||||
runtime_listing=$(bsdtar -tf "$omarchy_pkg")
|
||||
if ! grep -qx 'usr/bin/omarchy-apply-pacman' <<<"$runtime_listing"; then
|
||||
echo "ERROR: $OMARCHY_SETTINGS_PACKAGE ships region profiles, but $OMARCHY_RUNTIME_PACKAGE lacks omarchy-apply-pacman to apply them." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Collect every package we want available in the offline mirror.
|
||||
declare -a all_packages
|
||||
mapfile -t all_packages < <(
|
||||
@@ -194,12 +228,32 @@ mapfile -t all_packages < <(
|
||||
cat "$build_cache_dir/packages.x86_64"
|
||||
grep -hv '^#\|^$' "${base_pkg_lists[@]}"
|
||||
grep -hv '^#\|^$' /builder/archinstall.packages
|
||||
# Downloaded but not installed: the installer adds a region's packages
|
||||
# only to targets in that region.
|
||||
grep -hsv '^#\|^$' "$build_cache_dir"/airootfs/usr/share/omarchy-iso/regions/*/packages || true
|
||||
# Always include the selected Omarchy packages so the target install can
|
||||
# find the runtime and companion packages in the offline mirror.
|
||||
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" "$OMARCHY_NVIM_PACKAGE"
|
||||
} | 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
|
||||
# the mirror; strip them from the pacman -Syw list so it doesn't try to fetch
|
||||
# the published versions on top.
|
||||
@@ -215,7 +269,7 @@ fi
|
||||
|
||||
mkdir -p /tmp/offlinedb
|
||||
download_offline_packages() {
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw \
|
||||
pacman --config "$online_config" --noconfirm -Syw \
|
||||
"${all_packages[@]}" --cachedir "$offline_mirror_dir/" --dbpath /tmp/offlinedb --needed
|
||||
}
|
||||
|
||||
@@ -233,7 +287,7 @@ fi
|
||||
# newest version of every cached package name) removes packages that have left
|
||||
# the lists or dependency closure, such as an old Electron major version.
|
||||
if ! resolved_package_files="$(
|
||||
pacman --config "/configs/pacman-online-${OMARCHY_MIRROR}.conf" --noconfirm \
|
||||
pacman --config "$online_config" --noconfirm \
|
||||
--dbpath /tmp/offlinedb -S --print --print-format '%f' "${all_packages[@]}"
|
||||
)"; then
|
||||
echo "ERROR: could not resolve the package files required by the offline mirror" >&2
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ship every region profile from the bundled runtime, so one ISO serves every
|
||||
# region. The installer picks a profile from the target's timezone; this only
|
||||
# stages the profiles and makes their packages downloadable. Repository
|
||||
# defaults stay in the runtime, not in a second set of ISO-specific templates.
|
||||
set -euo pipefail
|
||||
|
||||
regions_source="$1"
|
||||
iso_payload="$2"
|
||||
online_config="$3"
|
||||
|
||||
# The source is a checkout's default/regions, or the published settings
|
||||
# package that ships default/. One predating region profiles ships none; the
|
||||
# ISO then installs global targets only, exactly as before. Anything else that
|
||||
# fails here, such as an unreadable package, aborts the build.
|
||||
regions="$regions_source"
|
||||
if [[ -f $regions_source ]]; then
|
||||
member=usr/share/omarchy/default/regions
|
||||
listing=$(bsdtar -tf "$regions_source")
|
||||
if grep -q "^$member/" <<<"$listing"; then
|
||||
extracted=$(mktemp -d)
|
||||
trap 'rm -rf "$extracted"' EXIT
|
||||
bsdtar -xf "$regions_source" -C "$extracted" "$member"
|
||||
regions="$extracted/$member"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -d $regions ]]; then
|
||||
echo "Runtime ships no region profiles; every install will be global."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cp -a "$regions" "$iso_payload/regions"
|
||||
|
||||
for profile in "$regions"/*/; do
|
||||
# Only the community repositories are needed to download regional packages.
|
||||
# Keep the channel's build mirrors intact; target mirrors are set later by
|
||||
# the runtime's own pacman finalizer.
|
||||
printf '\n' >> "$online_config"
|
||||
cat "$profile/pacman/pacman.conf.append" >> "$online_config"
|
||||
|
||||
# Each keyring package must be signed by a key Arch already trusts. Do not
|
||||
# bootstrap trust from an unchecked keyserver response or disable signatures.
|
||||
# https://github.com/archlinuxcn/repo#usage
|
||||
mapfile -t keyring_packages < <(grep -hv '^#\|^$' "$profile/packages" | grep -- '-keyring$' || true)
|
||||
if (( ${#keyring_packages[@]} > 0 )); then
|
||||
pacman --config "$online_config" --noconfirm -Sy --needed "${keyring_packages[@]}"
|
||||
pacman-key --populate "${keyring_packages[@]%-keyring}"
|
||||
fi
|
||||
done
|
||||
@@ -10,7 +10,10 @@
|
||||
# - COLUMNS/LINES so gum picks up real terminal size
|
||||
set -euo pipefail
|
||||
|
||||
[[ $(tty) == /dev/tty1 ]] || exit 0
|
||||
[[ $(tty) == /dev/tty1 || -n ${OMARCHY_CJK_CONSOLE:-} ]] || exit 0
|
||||
if [[ -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
||||
touch "$XDG_RUNTIME_DIR/cjk-console-started"
|
||||
fi
|
||||
|
||||
export OMARCHY_MIRROR="$(cat /root/omarchy_mirror)"
|
||||
if [[ -f /root/omarchy_iso_ref ]]; then
|
||||
@@ -39,7 +42,8 @@ set_tokyo_night_colors() {
|
||||
echo -en "\033[0m"
|
||||
clear
|
||||
}
|
||||
set_tokyo_night_colors
|
||||
# foot sets the same palette from foot.ini; these escapes are VT-only.
|
||||
[[ -n ${OMARCHY_CJK_CONSOLE:-} ]] || set_tokyo_night_colors
|
||||
|
||||
mkdir -p /var/log
|
||||
touch "$OMARCHY_INSTALL_LOG_FILE"
|
||||
@@ -90,6 +94,16 @@ warm_offline_mirror &
|
||||
warm_pid=$!
|
||||
trap 'kill "$warm_pid" 2>/dev/null' EXIT
|
||||
|
||||
# The CJK console's font comes from the offline mirror's noto-fonts-cjk, not the live image.
|
||||
unpack_cjk_font() {
|
||||
local packages=(/var/cache/omarchy/mirror/offline/noto-fonts-cjk-*.pkg.tar.zst)
|
||||
if ((${#packages[@]} != 1)) || [[ ! -f ${packages[0]} ]]; then
|
||||
echo "Expected one noto-fonts-cjk package in the offline mirror, found: ${packages[*]}" >&2
|
||||
return 1
|
||||
fi
|
||||
bsdtar -xf "${packages[0]}" -C / usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc
|
||||
}
|
||||
|
||||
cd /root
|
||||
# Autoinstall: a cidata drive carrying the configurator's own output files
|
||||
# stands in for the wizard. omarchy-cidata-load copies them into /root and
|
||||
@@ -98,7 +112,43 @@ if /usr/local/bin/omarchy-cidata-load; then
|
||||
echo "Autoinstall configuration found on cidata drive; skipping the configurator."
|
||||
export OMARCHY_UI_INTERACTIVE=no
|
||||
else
|
||||
./configurator
|
||||
# Chinese moves to cage + foot, which draw Chinese; English moves back to the VT, which applies keymaps.
|
||||
cjk_relaunch=10
|
||||
vt_return=11
|
||||
if [[ -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
||||
export OMARCHY_VT_RETURN=$vt_return
|
||||
elif command -v cage >/dev/null; then
|
||||
export OMARCHY_CJK_RELAUNCH=$cjk_relaunch
|
||||
fi
|
||||
./configurator && status=0 || status=$?
|
||||
while ((status == cjk_relaunch)); do
|
||||
cjk_runtime=/run/omarchy-cjk-console
|
||||
mkdir -p -m 700 "$cjk_runtime"
|
||||
rm -f "$cjk_runtime/cjk-console-started" "$cjk_runtime/return-to-vt"
|
||||
if ! unpack_cjk_font; then
|
||||
echo "The Chinese font could not be unpacked from the offline mirror; continuing in English."
|
||||
unset OMARCHY_CJK_RELAUNCH
|
||||
OMARCHY_INSTALL_LANGUAGE=zh ./configurator && status=0 || status=$?
|
||||
continue
|
||||
fi
|
||||
cage_status=0
|
||||
env -u OMARCHY_CJK_RELAUNCH XDG_RUNTIME_DIR=$cjk_runtime OMARCHY_CJK_CONSOLE=1 OMARCHY_INSTALL_LANGUAGE=zh \
|
||||
LANG=C.UTF-8 WLR_RENDERER=pixman cage -- foot -c /usr/share/omarchy-iso/foot.ini "$0" || cage_status=$?
|
||||
if [[ -f $cjk_runtime/return-to-vt ]]; then
|
||||
OMARCHY_INSTALL_LANGUAGE=en ./configurator && status=0 || status=$?
|
||||
elif [[ -f $cjk_runtime/cjk-console-started ]]; then
|
||||
exit "$cage_status"
|
||||
else
|
||||
echo "The Chinese console could not start (cage exit $cage_status); continuing in English."
|
||||
unset OMARCHY_CJK_RELAUNCH
|
||||
OMARCHY_INSTALL_LANGUAGE=zh ./configurator && status=0 || status=$?
|
||||
fi
|
||||
done
|
||||
if [[ -n ${OMARCHY_CJK_CONSOLE:-} ]] && ((status == vt_return)); then
|
||||
touch "$XDG_RUNTIME_DIR/return-to-vt"
|
||||
exit 0
|
||||
fi
|
||||
((status == 0)) || exit "$status"
|
||||
fi
|
||||
|
||||
# Deferred-provisioning installs skip the celebration/reboot prompt and reboot on
|
||||
@@ -111,6 +161,11 @@ if [[ -f /root/defer-provisioning ]] ||
|
||||
export OMARCHY_UI_DEFER_PROVISIONING=yes
|
||||
fi
|
||||
|
||||
# Only Chinese reaches the install from the CJK console; English returns to the VT.
|
||||
if [[ -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
||||
export OMARCHY_INSTALL_LANG=zh
|
||||
fi
|
||||
|
||||
# The foreground dashboard is now the sole visible install UI owner. It starts
|
||||
# the actual installer as a non-interactive child, logs child output, waits for
|
||||
# completion, then renders the final installed-time/reboot prompt itself.
|
||||
|
||||
@@ -24,6 +24,72 @@ if [[ ! -r $SETUP_FORM ]]; then
|
||||
fi
|
||||
source "$SETUP_FORM"
|
||||
|
||||
# Chinese text for OMARCHY_INSTALL_LANG=zh; disk-step failures stay English for support logs.
|
||||
OMARCHY_ZH+=(
|
||||
["Aborted installation"]="安装已中止"
|
||||
["You can retry later by running: ./.automated_script.sh"]="稍后可运行 ./.automated_script.sh 重试"
|
||||
["Let's setup your machine..."]="开始设置这台机器……"
|
||||
["Press Ctrl+C to prepare this machine for another owner."]="按 Ctrl+C 为另一位使用者准备这台机器。"
|
||||
["This prepares the machine for another owner."]="这会为另一位使用者准备这台机器。"
|
||||
["The system installs now, but setup is delayed until first boot."]="系统现在安装,设置留到首次开机。"
|
||||
["Yes, prepare for another owner"]="是,为他人准备"
|
||||
["No, keep setting up"]="否,继续设置"
|
||||
["Prepare this machine for another owner?"]="为另一位使用者准备这台机器?"
|
||||
["Let's setup your user account..."]="开始设置用户账户……"
|
||||
["Field"]="项目"
|
||||
["Value"]="值"
|
||||
["Username"]="用户名"
|
||||
["Password"]="密码"
|
||||
["Full name"]="姓名"
|
||||
["Email address"]="邮箱"
|
||||
["Hostname"]="主机名"
|
||||
["Keyboard"]="键盘"
|
||||
["Region"]="区域"
|
||||
["[Skipped]"]="[已跳过]"
|
||||
["Yes"]="是"
|
||||
["No, change it"]="否,去修改"
|
||||
["Does this look right?"]="以上信息正确吗?"
|
||||
["Not enough free space on %s"]="%s 的空闲空间不足"
|
||||
["%s has %s of usable free space; Omarchy needs at least 32GB."]="%s 有 %s 可用空闲空间;Omarchy 至少需要 32GB。"
|
||||
["Open the partition tool to free at least 32GB on %s, then try again."]="打开分区工具,在 %s 上腾出至少 32GB,然后重试。"
|
||||
["Back"]="返回"
|
||||
["Open partition tool"]="打开分区工具"
|
||||
["Return to installation mode?"]="返回安装方式选择?"
|
||||
["Checking for BitLocker on %s"]="检查 %s 上的 BitLocker"
|
||||
["BitLocker signature detected on %s."]="在 %s 上发现 BitLocker 签名。"
|
||||
["Turn BitLocker off in Windows and wait for the drive to finish decrypting, then try again."]="在 Windows 中关闭 BitLocker,等磁盘解密完成后重试。"
|
||||
["Suspending BitLocker is not enough — the drive stays encrypted."]="只暂停 BitLocker 不够,磁盘仍是加密的。"
|
||||
["Aborted: BitLocker is enabled on this disk."]="已中止:这块磁盘开启了 BitLocker。"
|
||||
["Checking existing EFI partitions on %s"]="检查 %s 上已有的 EFI 分区"
|
||||
["Found a Windows ESP at %s — leaving it untouched."]="在 %s 发现 Windows ESP,保持不动。"
|
||||
["Omarchy will create its own dedicated EFI partition in free space."]="Omarchy 会在空闲空间中创建自己的 EFI 分区。"
|
||||
["A new EFI partition will be created in free space."]="会在空闲空间中新建 EFI 分区。"
|
||||
["Analyzing free space on %s"]="分析 %s 的空闲空间"
|
||||
["Install Omarchy in the %s of free space."]="在 %s 空闲空间中安装 Omarchy。"
|
||||
["Press Ctrl+C for unencrypted install."]="按 Ctrl+C 改为不加密安装。"
|
||||
["Yes, install"]="是,开始安装"
|
||||
["Yes, install without encryption"]="是,不加密安装"
|
||||
["Confirm installing on %s"]="确认安装到 %s"
|
||||
["Creating partitions on %s"]="在 %s 上创建分区"
|
||||
["Setting up LUKS2 on %s"]="在 %s 上设置 LUKS2 加密"
|
||||
["Creating Btrfs filesystem and subvolumes"]="创建 Btrfs 文件系统和子卷"
|
||||
["Let's select where to install Omarchy..."]="选择 Omarchy 的安装位置……"
|
||||
["Select install disk"]="选择安装磁盘"
|
||||
["Partition tool for %s"]="%s 的分区工具"
|
||||
["Create unallocated free space for Omarchy, then write changes and quit."]="为 Omarchy 腾出未分配空间,然后写入更改并退出。"
|
||||
["Do not create an Omarchy partition here — leave the target area as Free space."]="不要在这里创建 Omarchy 分区,目标区域保持为 Free space。"
|
||||
["Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists."]="提示:空闲空间安装至少需要 32GB 未分配空间;没有 ESP 时再加 2GB。"
|
||||
["Open cfdisk"]="打开 cfdisk"
|
||||
["Open partition tool for %s?"]="打开 %s 的分区工具?"
|
||||
["Full disk install"]="整盘安装"
|
||||
["Free space install (alongside existing data)"]="空闲空间安装(保留现有数据)"
|
||||
["Choose a different disk"]="换一块磁盘"
|
||||
["Let's select how to install Omarchy..."]="选择 Omarchy 的安装方式……"
|
||||
["Select installation mode on %s"]="选择 %s 上的安装方式"
|
||||
["Everything will be overwritten. There is no recovery possible."]="磁盘上的一切都会被覆盖,无法恢复。"
|
||||
["Confirm overwriting %s"]="确认覆盖 %s"
|
||||
)
|
||||
|
||||
# Partition creation helpers, shared with test/partition-numbering-test.sh so
|
||||
# the numbering logic can be exercised without a real disk.
|
||||
DISK_PARTITIONING="${DISK_PARTITIONING:-/usr/share/omarchy-iso/disk-partitioning.sh}"
|
||||
@@ -176,9 +242,9 @@ greeter() {
|
||||
}
|
||||
|
||||
abort() {
|
||||
gum style "${1:-Aborted installation}"
|
||||
gum style "${1:-$(omarchy_text "Aborted installation")}"
|
||||
echo
|
||||
gum style "You can retry later by running: ./.automated_script.sh"
|
||||
gum style "$(omarchy_text "You can retry later by running: ./.automated_script.sh")"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -203,6 +269,30 @@ notice() {
|
||||
|
||||
# STEP 1: KEYBOARD LAYOUT
|
||||
|
||||
# The target's LANG follows the language picked on the first screen.
|
||||
target_lang() {
|
||||
if [[ $language == "zh" ]]; then
|
||||
echo zh_CN.UTF-8
|
||||
else
|
||||
echo en_US.UTF-8
|
||||
fi
|
||||
}
|
||||
|
||||
# .automated_script.sh moves Chinese into the CJK console and English back to the VT, which applies keymaps.
|
||||
apply_language() {
|
||||
if [[ $language == "zh" && -n ${OMARCHY_CJK_RELAUNCH:-} ]]; then
|
||||
exit "$OMARCHY_CJK_RELAUNCH"
|
||||
fi
|
||||
if [[ $language == "en" && -n ${OMARCHY_VT_RETURN:-} ]]; then
|
||||
exit "$OMARCHY_VT_RETURN"
|
||||
fi
|
||||
if [[ $language == "zh" && -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
||||
export OMARCHY_INSTALL_LANG=zh
|
||||
else
|
||||
unset OMARCHY_INSTALL_LANG
|
||||
fi
|
||||
}
|
||||
|
||||
keyboard_form() {
|
||||
# Ctrl+C is the hidden entry into deferred provisioning, offered on every
|
||||
# visit: the user step unwinds back here and nothing is on disk yet. Callers
|
||||
@@ -212,11 +302,24 @@ keyboard_form() {
|
||||
while true; do
|
||||
clear_logo
|
||||
echo
|
||||
say "Let's setup your machine..."
|
||||
say --foreground 8 "Press Ctrl+C to prepare this machine for another owner."
|
||||
say "$(omarchy_text "Let's setup your machine...")"
|
||||
say --foreground 8 "$(omarchy_text "Press Ctrl+C to prepare this machine for another owner.")"
|
||||
echo
|
||||
|
||||
omarchy_prompt_keyboard && status=0 || status=$?
|
||||
status=0
|
||||
if ! $language_chosen; then
|
||||
omarchy_prompt_language && status=0 || status=$?
|
||||
((status == 0)) && apply_language
|
||||
fi
|
||||
# Esc, Ctrl+C or a later "No, change it" asks the language again.
|
||||
language_chosen=false
|
||||
if ((status == 0)); then
|
||||
local layouts=$OMARCHY_KEYBOARD_LAYOUTS selected="English (US)"
|
||||
[[ $language == "zh" ]] && selected="Chinese (Simplified, Pinyin)"
|
||||
# cage keeps its US layout, so the CJK console offers only US-keymap keyboards.
|
||||
[[ -n ${OMARCHY_CJK_CONSOLE:-} ]] && layouts=$(awk -F'|' '$2 == "us"' <<<"$layouts")
|
||||
OMARCHY_KEYBOARD_LAYOUTS=$layouts omarchy_prompt_keyboard "$selected" && status=0 || status=$?
|
||||
fi
|
||||
((status == 0)) && break
|
||||
|
||||
# Esc means "back", and nothing precedes the first screen, so re-ask.
|
||||
@@ -245,11 +348,11 @@ keyboard_form() {
|
||||
confirm_prepare_for_another_owner() {
|
||||
clear_logo
|
||||
echo
|
||||
say "This prepares the machine for another owner."
|
||||
say --foreground 8 "The system installs now, but setup is delayed until first boot."
|
||||
say "$(omarchy_text "This prepares the machine for another owner.")"
|
||||
say --foreground 8 "$(omarchy_text "The system installs now, but setup is delayed until first boot.")"
|
||||
echo
|
||||
gum confirm --affirmative "Yes, prepare for another owner" --negative "No, keep setting up" \
|
||||
"Prepare this machine for another owner?"
|
||||
gum confirm --affirmative "$(omarchy_text "Yes, prepare for another owner")" --negative "$(omarchy_text "No, keep setting up")" \
|
||||
"$(omarchy_text "Prepare this machine for another owner?")"
|
||||
}
|
||||
|
||||
# The user step. Deferred-provisioning installs skip it entirely — the machine's first owner
|
||||
@@ -257,7 +360,7 @@ confirm_prepare_for_another_owner() {
|
||||
# operator sets nothing user-specific.
|
||||
|
||||
user_form() {
|
||||
step "Let's setup your user account..."
|
||||
step "$(omarchy_text "Let's setup your user account...")"
|
||||
|
||||
# Each prompt reports 0 (set), OMARCHY_FORM_BACK (Esc), or OMARCHY_FORM_SIGNAL
|
||||
# (Ctrl+C). Both non-zero cases unwind to user_step, which decides what they
|
||||
@@ -290,19 +393,29 @@ user_step() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# The timezone selects the region (mirrors and repositories), using the
|
||||
# same lookup as the orchestrator. Global installs get no extra row.
|
||||
region=$(cd /usr/share/omarchy-iso 2>/dev/null && python -m orchestrator.region "$timezone") || region="global"
|
||||
region_row=""
|
||||
if [[ $region != "global" ]]; then
|
||||
region_row="\n$(omarchy_text Region),$region"
|
||||
fi
|
||||
|
||||
# Add manual padding since gum table -p doesn't respect padding
|
||||
echo -e "Field,Value
|
||||
Username,$username
|
||||
Password,$(printf "%${#password}s" | tr ' ' '*')
|
||||
Full name,${full_name:-[Skipped]}
|
||||
Email address,${email_address:-[Skipped]}
|
||||
Hostname,$hostname
|
||||
Timezone,$timezone
|
||||
Keyboard,$keyboard" |
|
||||
local skipped
|
||||
skipped=$(omarchy_text "[Skipped]")
|
||||
echo -e "$(omarchy_text Field),$(omarchy_text Value)
|
||||
$(omarchy_text Username),$username
|
||||
$(omarchy_text Password),$(printf "%${#password}s" | tr ' ' '*')
|
||||
$(omarchy_text "Full name"),${full_name:-$skipped}
|
||||
$(omarchy_text "Email address"),${email_address:-$skipped}
|
||||
$(omarchy_text Hostname),$hostname
|
||||
$(omarchy_text Timezone),$timezone$region_row
|
||||
$(omarchy_text Keyboard),\"$keyboard_label\"" |
|
||||
gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/"
|
||||
|
||||
echo
|
||||
if gum confirm --negative "No, change it" "Does this look right?"; then
|
||||
if gum confirm --affirmative "$(omarchy_text "Yes")" --negative "$(omarchy_text "No, change it")" "$(omarchy_text "Does this look right?")"; then
|
||||
break
|
||||
else
|
||||
keyboard_form
|
||||
@@ -415,7 +528,7 @@ detect_kernel() {
|
||||
if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then
|
||||
echo "linux-t2"
|
||||
else
|
||||
echo "linux"
|
||||
echo "linux-omarchy"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -504,11 +617,11 @@ print_dry_run_files() {
|
||||
# action so users have a way to actually fix the situation.
|
||||
not_enough_space() {
|
||||
local available="${1:-0}"
|
||||
step "Not enough free space on $disk"
|
||||
say --foreground 1 "$disk has $(to_gb $available) of usable free space; Omarchy needs at least 32GB."
|
||||
say "Open the partition tool to free at least 32GB on $disk, then try again."
|
||||
step "$(omarchy_text "Not enough free space on %s" "$disk")"
|
||||
say --foreground 1 "$(omarchy_text "%s has %s of usable free space; Omarchy needs at least 32GB." "$disk" "$(to_gb $available)")"
|
||||
say "$(omarchy_text "Open the partition tool to free at least 32GB on %s, then try again." "$disk")"
|
||||
echo
|
||||
if ! gum confirm --affirmative "Back" --negative "Open partition tool" "Return to installation mode?"; then
|
||||
if ! gum confirm --affirmative "$(omarchy_text "Back")" --negative "$(omarchy_text "Open partition tool")" "$(omarchy_text "Return to installation mode?")"; then
|
||||
open_partition_tool
|
||||
fi
|
||||
return 1
|
||||
@@ -518,14 +631,14 @@ not_enough_space() {
|
||||
# encryption confirm. Sets the globals run_partition_execute needs. Returns 1
|
||||
# to fall back to the install-mode picker.
|
||||
run_partition_decide() {
|
||||
step "Checking for BitLocker on $disk"
|
||||
step "$(omarchy_text "Checking for BitLocker on %s" "$disk")"
|
||||
local bl_part
|
||||
bl_part=$(detect_bitlocker || true)
|
||||
if [[ -n "$bl_part" ]]; then
|
||||
say --foreground 1 "BitLocker signature detected on $bl_part."
|
||||
say "Turn BitLocker off in Windows and wait for the drive to finish decrypting, then try again."
|
||||
say "Suspending BitLocker is not enough — the drive stays encrypted."
|
||||
abort "Aborted: BitLocker is enabled on this disk."
|
||||
say --foreground 1 "$(omarchy_text "BitLocker signature detected on %s." "$bl_part")"
|
||||
say "$(omarchy_text "Turn BitLocker off in Windows and wait for the drive to finish decrypting, then try again.")"
|
||||
say "$(omarchy_text "Suspending BitLocker is not enough — the drive stays encrypted.")"
|
||||
abort "$(omarchy_text "Aborted: BitLocker is enabled on this disk.")"
|
||||
fi
|
||||
|
||||
# Omarchy always creates its own dedicated ESP in free space and never
|
||||
@@ -534,17 +647,17 @@ run_partition_decide() {
|
||||
# Windows ESP is far too small for our Unified Kernel Images, and a shared
|
||||
# ESP forces the install unencrypted. A separate Linux ESP keeps Windows
|
||||
# and Omarchy fully isolated and keeps LUKS on the table.
|
||||
step "Checking existing EFI partitions on $disk"
|
||||
step "$(omarchy_text "Checking existing EFI partitions on %s" "$disk")"
|
||||
local detected_win_esp
|
||||
detected_win_esp=$(detect_windows_esp || true)
|
||||
if [[ -n "$detected_win_esp" ]]; then
|
||||
say "Found a Windows ESP at $detected_win_esp — leaving it untouched."
|
||||
say "Omarchy will create its own dedicated EFI partition in free space."
|
||||
say "$(omarchy_text "Found a Windows ESP at %s — leaving it untouched." "$detected_win_esp")"
|
||||
say "$(omarchy_text "Omarchy will create its own dedicated EFI partition in free space.")"
|
||||
else
|
||||
say "A new EFI partition will be created in free space."
|
||||
say "$(omarchy_text "A new EFI partition will be created in free space.")"
|
||||
fi
|
||||
|
||||
step "Analyzing free space on $disk"
|
||||
step "$(omarchy_text "Analyzing free space on %s" "$disk")"
|
||||
partprobe "$disk" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
@@ -619,21 +732,21 @@ run_partition_decide() {
|
||||
while true; do
|
||||
clear_logo
|
||||
echo
|
||||
say "Install Omarchy in the $(to_gb $INSTALL_MAX_B) of free space."
|
||||
say "$(omarchy_text "Install Omarchy in the %s of free space." "$(to_gb $INSTALL_MAX_B)")"
|
||||
|
||||
case $mode in
|
||||
encrypted)
|
||||
say --foreground 8 "Press Ctrl+C for unencrypted install."
|
||||
affirmative="Yes, install"
|
||||
say --foreground 8 "$(omarchy_text "Press Ctrl+C for unencrypted install.")"
|
||||
affirmative="$(omarchy_text "Yes, install")"
|
||||
;;
|
||||
unencrypted)
|
||||
affirmative="Yes, install without encryption"
|
||||
affirmative="$(omarchy_text "Yes, install without encryption")"
|
||||
;;
|
||||
esac
|
||||
echo
|
||||
|
||||
gum confirm --affirmative "$affirmative" --negative "No, change it" \
|
||||
"Confirm installing on $disk"
|
||||
gum confirm --affirmative "$affirmative" --negative "$(omarchy_text "No, change it")" \
|
||||
"$(omarchy_text "Confirm installing on %s" "$disk")"
|
||||
confirm_status=$?
|
||||
|
||||
case $confirm_status in
|
||||
@@ -691,7 +804,7 @@ run_partition_execute() {
|
||||
say "[dry] partition numbers are assigned by parted at creation, then read back"
|
||||
say "[dry] encrypted=$encrypt_installation kernel=$kernel_choice"
|
||||
else
|
||||
step "Creating partitions on $disk"
|
||||
step "$(omarchy_text "Creating partitions on %s" "$disk")"
|
||||
if $needs_mklabel; then
|
||||
disk_step "initializing GPT on $disk" parted --script "$disk" mklabel gpt
|
||||
partprobe "$disk" 2>/dev/null || true
|
||||
@@ -729,7 +842,7 @@ run_partition_execute() {
|
||||
disk_step "clearing stale signatures on $root_partition_device" wipefs -af "$root_partition_device"
|
||||
|
||||
if [[ "$encrypt_installation" == "true" ]]; then
|
||||
step "Setting up LUKS2 on $root_partition_device"
|
||||
step "$(omarchy_text "Setting up LUKS2 on %s" "$root_partition_device")"
|
||||
# Kept as pipes rather than routed through disk_step: the passphrase must
|
||||
# not become an argv the process table can show. Folding stderr into
|
||||
# stdout still lands any error in the install log.
|
||||
@@ -746,7 +859,7 @@ run_partition_execute() {
|
||||
root_mapper="$root_partition_device"
|
||||
fi
|
||||
|
||||
step "Creating Btrfs filesystem and subvolumes"
|
||||
step "$(omarchy_text "Creating Btrfs filesystem and subvolumes")"
|
||||
disk_step "creating the Btrfs filesystem on $root_mapper" \
|
||||
mkfs.btrfs -f -L OMARCHY "$root_mapper"
|
||||
wait_for_device "$root_mapper" || disk_abort_hook "Root device $root_mapper never appeared"
|
||||
@@ -800,6 +913,8 @@ run_partition_execute() {
|
||||
"bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
|
||||
"custom_commands": [],
|
||||
"omarchy_install": {
|
||||
"input_method": "${keyboard_input_method:-none}",
|
||||
"input_xkb_layout": "${keyboard_xkb_layout:-}",
|
||||
"mode": "protected",
|
||||
"target_mount": "/mnt",
|
||||
"boot": {
|
||||
@@ -832,7 +947,7 @@ run_partition_execute() {
|
||||
"locale_config": {
|
||||
"kb_layout": "$keyboard",
|
||||
"sys_enc": "UTF-8",
|
||||
"sys_lang": "en_US.UTF-8"
|
||||
"sys_lang": "$(target_lang)"
|
||||
},
|
||||
"mirror_config": {
|
||||
"custom_repositories": [],
|
||||
@@ -862,7 +977,7 @@ _EOF_
|
||||
}
|
||||
|
||||
disk_form() {
|
||||
step "Let's select where to install Omarchy..."
|
||||
step "$(omarchy_text "Let's select where to install Omarchy...")"
|
||||
|
||||
# Don't offer the install media as an option (Arch ISO mounts it here)
|
||||
local boot_source
|
||||
@@ -888,19 +1003,19 @@ disk_form() {
|
||||
fi
|
||||
done <<<"$available_disks"
|
||||
|
||||
selected_display=$(echo "$disk_options" | gum choose --header "Select install disk") || abort
|
||||
selected_display=$(echo "$disk_options" | gum choose --header "$(omarchy_text "Select install disk")") || abort
|
||||
disk=$(echo "$selected_display" | awk '{print $1}')
|
||||
}
|
||||
|
||||
# STEP 4: INSTALL MODE
|
||||
|
||||
open_partition_tool() {
|
||||
step "Partition tool for $disk"
|
||||
gum style "Create unallocated free space for Omarchy, then write changes and quit."
|
||||
gum style --foreground 8 "Do not create an Omarchy partition here — leave the target area as Free space."
|
||||
gum style --foreground 8 "Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists."
|
||||
step "$(omarchy_text "Partition tool for %s" "$disk")"
|
||||
gum style "$(omarchy_text "Create unallocated free space for Omarchy, then write changes and quit.")"
|
||||
gum style --foreground 8 "$(omarchy_text "Do not create an Omarchy partition here — leave the target area as Free space.")"
|
||||
gum style --foreground 8 "$(omarchy_text "Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists.")"
|
||||
echo
|
||||
gum confirm --affirmative "Open cfdisk" --negative "Back" "Open partition tool for $disk?" || return 0
|
||||
gum confirm --affirmative "$(omarchy_text "Open cfdisk")" --negative "$(omarchy_text "Back")" "$(omarchy_text "Open partition tool for %s?" "$disk")" || return 0
|
||||
|
||||
clear
|
||||
cfdisk "$disk" || true
|
||||
@@ -933,17 +1048,17 @@ requires_full_disk_install() {
|
||||
}
|
||||
|
||||
install_mode_form() {
|
||||
local choices=("Full disk install")
|
||||
local choices=("$(omarchy_text "Full disk install")")
|
||||
|
||||
# The free-space/protected path registers an EFI boot entry and is UEFI-only.
|
||||
# Full-disk installs still support BIOS through the orchestrator's BIOS branch.
|
||||
if [[ -d /sys/firmware/efi ]] && ! $full_disk_only; then
|
||||
choices+=("Free space install (alongside existing data)")
|
||||
choices+=("$(omarchy_text "Free space install (alongside existing data)")")
|
||||
fi
|
||||
choices+=("Choose a different disk")
|
||||
choices+=("$(omarchy_text "Choose a different disk")")
|
||||
|
||||
step "Let's select how to install Omarchy..."
|
||||
install_mode=$(gum choose --header "Select installation mode on $disk" "${choices[@]}") || abort
|
||||
step "$(omarchy_text "Let's select how to install Omarchy...")"
|
||||
install_mode=$(gum choose --header "$(omarchy_text "Select installation mode on %s" "$disk")" "${choices[@]}") || abort
|
||||
}
|
||||
|
||||
confirm_disk_overwrite() {
|
||||
@@ -953,17 +1068,17 @@ confirm_disk_overwrite() {
|
||||
while true; do
|
||||
clear_logo
|
||||
echo
|
||||
say "Everything will be overwritten. There is no recovery possible."
|
||||
say "$(omarchy_text "Everything will be overwritten. There is no recovery possible.")"
|
||||
|
||||
if [[ $mode == "encrypted" ]]; then
|
||||
say --foreground 8 "Press Ctrl+C for unencrypted install."
|
||||
affirmative="Yes, install"
|
||||
say --foreground 8 "$(omarchy_text "Press Ctrl+C for unencrypted install.")"
|
||||
affirmative="$(omarchy_text "Yes, install")"
|
||||
else
|
||||
affirmative="Yes, install without encryption"
|
||||
affirmative="$(omarchy_text "Yes, install without encryption")"
|
||||
fi
|
||||
|
||||
echo
|
||||
gum confirm --affirmative "$affirmative" --negative "No, change it" "Confirm overwriting ${disk}"
|
||||
gum confirm --affirmative "$affirmative" --negative "$(omarchy_text "No, change it")" "$(omarchy_text "Confirm overwriting %s" "$disk")"
|
||||
confirm_status=$?
|
||||
|
||||
case $confirm_status in
|
||||
@@ -1002,13 +1117,13 @@ select_installation() {
|
||||
# With no free-space option to offer, a full-disk install is the only
|
||||
# mode, so skip the picker and go straight to the overwrite confirm.
|
||||
if $full_disk_only; then
|
||||
install_mode="Full disk install"
|
||||
install_mode=$(omarchy_text "Full disk install")
|
||||
else
|
||||
install_mode_form
|
||||
fi
|
||||
|
||||
case "$install_mode" in
|
||||
"Full disk install")
|
||||
"$(omarchy_text "Full disk install")")
|
||||
if confirm_disk_overwrite; then
|
||||
install_target="full_disk"
|
||||
return 0
|
||||
@@ -1017,13 +1132,13 @@ select_installation() {
|
||||
# wants a different disk.
|
||||
$full_disk_only && disk_form
|
||||
;;
|
||||
"Free space install"*)
|
||||
"$(omarchy_text "Free space install (alongside existing data)")")
|
||||
if run_partition_decide; then
|
||||
install_target="free_space"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
"Choose a different disk")
|
||||
"$(omarchy_text "Choose a different disk")")
|
||||
disk_form
|
||||
;;
|
||||
esac
|
||||
@@ -1034,12 +1149,20 @@ select_installation() {
|
||||
|
||||
defer_provisioning=false
|
||||
install_target="full_disk"
|
||||
# A rerun arrives with the language chosen; otherwise English.
|
||||
language="${OMARCHY_INSTALL_LANGUAGE:-en}"
|
||||
language_chosen=false
|
||||
|
||||
# Let the live VT reach its settled width before the first screen so the logo
|
||||
# isn't measured against the transient 80-column boot console.
|
||||
wait_for_stable_terminal
|
||||
|
||||
greeter
|
||||
if [[ -n ${OMARCHY_INSTALL_LANGUAGE:-} ]]; then
|
||||
apply_language
|
||||
language_chosen=true
|
||||
else
|
||||
greeter
|
||||
fi
|
||||
|
||||
keyboard_form
|
||||
|
||||
@@ -1056,6 +1179,8 @@ if $defer_provisioning; then
|
||||
# their real layout in the OOBE, which re-keys the console and, on encrypted
|
||||
# installs, rebuilds the initramfs so the LUKS prompt matches.
|
||||
keyboard="us"
|
||||
keyboard_input_method="none"
|
||||
keyboard_xkb_layout=""
|
||||
username=""
|
||||
password=""
|
||||
password_hash=""
|
||||
@@ -1138,6 +1263,8 @@ cat <<-_EOF_ >user_configuration.json
|
||||
"bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
|
||||
"custom_commands": [],
|
||||
"omarchy_install": {
|
||||
"input_method": "${keyboard_input_method:-none}",
|
||||
"input_xkb_layout": "${keyboard_xkb_layout:-}",
|
||||
"mode": "full_disk",
|
||||
"defer_provisioning": $defer_provisioning,
|
||||
"target_mount": "/mnt",
|
||||
@@ -1221,7 +1348,7 @@ cat <<-_EOF_ >user_configuration.json
|
||||
"locale_config": {
|
||||
"kb_layout": "$keyboard",
|
||||
"sys_enc": "UTF-8",
|
||||
"sys_lang": "en_US.UTF-8"
|
||||
"sys_lang": "$(target_lang)"
|
||||
},
|
||||
"mirror_config": {
|
||||
"custom_repositories": [],
|
||||
|
||||
@@ -38,9 +38,7 @@ CLEAR="${CSI}2J${CSI}H"
|
||||
CLEAR_LINE="${CSI}2K"
|
||||
CLEAR_TO_END="${CSI}J"
|
||||
GREEN="${CSI}32m"
|
||||
WHITE="${CSI}37m"
|
||||
RED="${CSI}31m"
|
||||
DARK="${CSI}90m"
|
||||
|
||||
# The dashboard owns the final reboot prompt now, so keep the gum confirm
|
||||
# styling that used to come from install/helpers/presentation.sh.
|
||||
@@ -53,6 +51,22 @@ export GUM_CONFIRM_UNSELECTED_BACKGROUND="${GUM_CONFIRM_UNSELECTED_BACKGROUND:-0
|
||||
child_pid=""
|
||||
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=(
|
||||
"Super + Space opens the Omarchy menu for apps, settings, and more"
|
||||
"Super + K shows all the key bindings"
|
||||
@@ -74,6 +88,81 @@ tips=(
|
||||
"Keep the system fresh with Update in the Omarchy menu"
|
||||
)
|
||||
|
||||
# omarchy_text and its Chinese table come from the setup form the configurator uses.
|
||||
SETUP_FORM="${SETUP_FORM:-/usr/share/omarchy-iso/setup-form.sh}"
|
||||
[[ -r $SETUP_FORM ]] || SETUP_FORM="${OMARCHY_PATH:-/usr/share/omarchy}/install/provisioning/setup-form.sh"
|
||||
source "$SETUP_FORM"
|
||||
OMARCHY_ZH+=(
|
||||
["Tip: %s"]="提示:%s"
|
||||
["Installed Omarchy"]="Omarchy 安装完成"
|
||||
["Installed Omarchy in %s"]="Omarchy 安装完成,用时 %s"
|
||||
["Reboot Now"]="立即重启"
|
||||
["Last log lines:"]="日志末尾:"
|
||||
["Last target log lines:"]="目标系统日志末尾:"
|
||||
["Press Enter to continue…"]="按回车继续……"
|
||||
["No log uploader is available in this environment."]="这个环境没有日志上传工具。"
|
||||
["Install log: %s"]="安装日志:%s"
|
||||
["Target log: %s"]="目标系统日志:%s"
|
||||
["Omarchy installation stopped"]="Omarchy 安装已停止"
|
||||
["Installer exited with status %s"]="安装程序退出,状态码 %s"
|
||||
["Get help at https://omarchy.org/discord"]="求助:https://omarchy.org/discord"
|
||||
["What would you like to do?"]="接下来做什么?"
|
||||
["Upload log for support"]="上传日志求助"
|
||||
["View full log"]="查看完整日志"
|
||||
["Drop to shell"]="进入 shell"
|
||||
["Reboot"]="重启"
|
||||
["Power off"]="关机"
|
||||
["The install medium was misread"]="安装介质读取出错"
|
||||
["This package is intact on this ISO, so the download and the write were fine:"]="这个软件包在 ISO 中完好,下载和写入都没有问题:"
|
||||
["Try another USB port or another stick, and test this machine's memory"]="换一个 USB 口或换一个 U 盘,并检测这台机器的内存"
|
||||
["The install medium is damaged"]="安装介质已损坏"
|
||||
["This package no longer matches the checksum it was built with:"]="这个软件包和构建时的校验和不再一致:"
|
||||
["Re-download the ISO, verify it with sha256sum, and write the USB again"]="重新下载 ISO,用 sha256sum 校验,再重新写入 U 盘"
|
||||
["If a verified ISO fails here too, the USB stick itself is the problem"]="如果校验过的 ISO 在这里也失败,问题出在 U 盘本身"
|
||||
["The install medium is damaged or misread"]="安装介质已损坏或读取出错"
|
||||
["pacman rejected this package, read straight off this ISO:"]="pacman 拒绝了这个直接从 ISO 读出的软件包:"
|
||||
["Verify your download with sha256sum, then write the USB again"]="用 sha256sum 校验下载的文件,再重新写入 U 盘"
|
||||
["If the download verifies, the stick or this machine's memory is at fault"]="如果下载校验无误,问题出在 U 盘或这台机器的内存"
|
||||
)
|
||||
|
||||
if [[ ${OMARCHY_INSTALL_LANG:-} == "zh" ]]; then
|
||||
whimsy=(
|
||||
"备好贴心的默认设置"
|
||||
"省掉装机的折腾"
|
||||
"挑选打磨过的软件包"
|
||||
"调配协调的配色"
|
||||
"准备悦目的像素"
|
||||
"唤醒 Hyprland"
|
||||
"接好顺手的界面"
|
||||
"搭起漂亮的系统"
|
||||
"写好趁手的命令"
|
||||
"让机器焕然一新"
|
||||
"擦亮每一个像素"
|
||||
"调好终端的品味"
|
||||
"织出流畅的工作流"
|
||||
)
|
||||
tips=(
|
||||
"Super + Space 打开 Omarchy 菜单:应用、设置等"
|
||||
"Super + K 查看全部快捷键"
|
||||
"Super 是键盘上的 Windows 或 Command 键"
|
||||
"用 Xournal++ 给 PDF 签名"
|
||||
"用 LocalSend 和手机、电脑互传文件"
|
||||
"菜单 安装 > 网页应用 把任意网站变成应用"
|
||||
"Super + Return 打开终端,Super + Shift + Return 打开浏览器"
|
||||
"图片用 Pinta,视频用 Kdenlive,文档用 LibreOffice"
|
||||
"Super + Ctrl + Print 用 OCR 抓取屏幕文字"
|
||||
"Print 截屏,Alt + Print 录屏"
|
||||
"Super + Ctrl + R 设置提醒"
|
||||
"菜单 风格 > 主题 切换主题"
|
||||
"双击菜单栏让它透明"
|
||||
"菜单 安装 > Windows 运行完整的 Windows 虚拟机"
|
||||
"Super + Ctrl + V 打开剪贴板管理器"
|
||||
"Super + 1 到 0 切换工作区,加 Shift 带上窗口"
|
||||
"Super + Print 拾取屏幕上任意位置的颜色"
|
||||
"用菜单里的 更新 让系统保持最新"
|
||||
)
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
printf '%s%s' "$RESET" "$SHOW_CURSOR" >"$TTY_PATH" 2>/dev/null || true
|
||||
}
|
||||
@@ -115,10 +204,58 @@ trap 'interrupt 143' TERM
|
||||
term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; }
|
||||
term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; }
|
||||
|
||||
repeat() {
|
||||
local char="$1" count="$2" out="" i
|
||||
for ((i=0; i<count; i++)); do out+="$char"; done
|
||||
printf '%s' "$out"
|
||||
# Read the actual character dimensions instead of assuming an 8 x 16 font.
|
||||
# Linux VTs expose the font through KDFONTOP; desktop terminals may expose
|
||||
# pixels in their window size or answer the standard cell-size query.
|
||||
term_cell_size() {
|
||||
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() {
|
||||
@@ -151,22 +288,16 @@ LOGO_HEIGHT="$(logo_height)"
|
||||
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
|
||||
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
|
||||
CONTENT_WIDTH="$LOGO_WIDTH"
|
||||
# Rows the progress screen occupies: logo, blank, title, blank, bar, blank, tip.
|
||||
PROGRESS_HEIGHT=$((LOGO_HEIGHT + 6))
|
||||
TOP_ROW=1
|
||||
DYNAMIC_ROW=$((LOGO_HEIGHT + 3))
|
||||
LAST_SIZE=""
|
||||
LAST_PM=10
|
||||
# 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.
|
||||
# Wordmark, blank, title, and the three rows gum confirm uses.
|
||||
FINISH_HEIGHT=$((LOGO_HEIGHT + 5))
|
||||
FINISH_TOP_ROW=2
|
||||
|
||||
# ── Install progress ─────────────────────────────────────────────────────────
|
||||
#
|
||||
# Position = max(floor, work), both non-decreasing, in per-mille of the bar.
|
||||
# Position = max(floor, work), both non-decreasing, in per-mille of the drawing.
|
||||
#
|
||||
# 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.
|
||||
@@ -189,8 +320,8 @@ fi
|
||||
# A wrong denominator is worse than none: it never triggers the fallback.
|
||||
(( PKG_TOTAL < 300 || PKG_TOTAL > 5000 )) && PKG_TOTAL=0
|
||||
|
||||
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 a stalled bar has somewhere to go
|
||||
PKG_BONUS_MAX_PM=100 # cap on how far progress may run ahead of the count
|
||||
PKG_TAIL_PM=40 # band held back so stalled progress has somewhere to go
|
||||
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
|
||||
|
||||
@@ -230,8 +361,8 @@ left_padding() {
|
||||
|
||||
visible_len() {
|
||||
local text="$1"
|
||||
text="$(printf '%b' "$text" | sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g')"
|
||||
printf '%s' "${#text}"
|
||||
# wc -L counts display columns, so double-width CJK centers too.
|
||||
printf '%b' "$text" | sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g' | wc -L
|
||||
}
|
||||
|
||||
blank_line() { printf '\r%s\n' "$CLEAR_LINE"; }
|
||||
@@ -263,13 +394,161 @@ render_logo() {
|
||||
fi
|
||||
}
|
||||
|
||||
progress_bar() {
|
||||
local pm="$1" width="${2:-34}" filled empty
|
||||
(( pm < 0 )) && pm=0
|
||||
(( pm > 1000 )) && pm=1000
|
||||
filled=$(( pm * width / 1000 ))
|
||||
empty=$(( width - filled ))
|
||||
printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET"
|
||||
# The icon uses half-block glyphs, scaled to the terminal's actual cell aspect
|
||||
# ratio so its square silhouette stays square. No graphics protocol is needed.
|
||||
# Coordinates follow the 15 x 15 grid of Omarchy's icon.svg. Each segment
|
||||
# paints the full stroke width, starting at the bottom middle of the outer ring.
|
||||
ICON_WIDTH=30
|
||||
ICON_HEIGHT=15
|
||||
CELL_WIDTH=8
|
||||
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
|
||||
@@ -290,7 +569,7 @@ count_packages() {
|
||||
|
||||
# Sets PROGRESS_PM. Never call in a command substitution: POS, the phase timer
|
||||
# and the package counters must survive between frames. Print nothing — output
|
||||
# here would land on the TTY between the cursor escape and the bar.
|
||||
# here would land on the TTY between the cursor escape and the drawing.
|
||||
install_progress() {
|
||||
local phase finished index total target
|
||||
local lo hi tau span t pos work gap n pkg_signal=0
|
||||
@@ -377,7 +656,7 @@ install_progress() {
|
||||
PKG_DB_DIR="$target/var/lib/pacman/local"
|
||||
count_packages
|
||||
# Baseline on first sight: a target that already holds packages would
|
||||
# otherwise pin the bar at the band ceiling for the whole phase.
|
||||
# otherwise pin progress at the band ceiling for the whole phase.
|
||||
(( PKG_BASE < 0 )) && PKG_BASE=$PKG_COUNT
|
||||
n=$(( PKG_COUNT - PKG_BASE ))
|
||||
(( n < 0 )) && n=0
|
||||
@@ -401,17 +680,20 @@ install_progress() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# One tip per 8 seconds, in order. On elapsed time, not a frame counter: a frame
|
||||
# is 0.5s of sleep plus a render, which stretches under pacstrap I/O — on the
|
||||
# machine where the tips, not the bar, are carrying liveness.
|
||||
# Keep the original loading messages on their own clock, so progress jumps
|
||||
# cannot skip phrases and animation frame rate does not change their cadence.
|
||||
current_whimsy() {
|
||||
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() {
|
||||
printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}"
|
||||
}
|
||||
|
||||
# Vertically center a block of the given height, echoing the row it starts on.
|
||||
# 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.
|
||||
# Center both the progress icon and the final wordmark on the console.
|
||||
centered_top_row() {
|
||||
local height="$1" rows top
|
||||
rows="$(term_size)"
|
||||
@@ -423,42 +705,60 @@ centered_top_row() {
|
||||
}
|
||||
|
||||
measure_progress_layout() {
|
||||
local rows cols scale=1
|
||||
LAST_SIZE="$(term_size)"
|
||||
TOP_ROW="$(centered_top_row "$PROGRESS_HEIGHT")"
|
||||
DYNAMIC_ROW=$(( TOP_ROW + LOGO_HEIGHT + 1 ))
|
||||
read -r rows cols <<<"$LAST_SIZE"
|
||||
read -r CELL_WIDTH CELL_HEIGHT < <(term_cell_size 2>/dev/null || printf '8 16\n')
|
||||
(( 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() {
|
||||
measure_progress_layout
|
||||
printf '%s%s%d;1H' "$CLEAR" "$CSI" "$TOP_ROW"
|
||||
render_logo
|
||||
blank_line
|
||||
printf '%s' "$CLEAR"
|
||||
render_icon "$DISPLAY_PM" 1
|
||||
}
|
||||
|
||||
render_dynamic() {
|
||||
local pm
|
||||
# The console can still resize under us (the framebuffer settles late in VMs).
|
||||
# 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
|
||||
update_progress() {
|
||||
set_now
|
||||
install_progress
|
||||
pm=$PROGRESS_PM
|
||||
local pm=$PROGRESS_PM
|
||||
[[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM
|
||||
(( pm > 1000 )) && pm=1000
|
||||
# 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
|
||||
}
|
||||
|
||||
printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW"
|
||||
center "Installing Omarchy" "$CONTENT_WIDTH"
|
||||
render_dynamic() {
|
||||
local step message tip
|
||||
# The framebuffer can settle late in VMs. Rebuild at the new size, preserving
|
||||
# progress, instead of leaving the drawing at the old screen coordinates.
|
||||
[[ "$(term_size)" == "$LAST_SIZE" ]] || render_static
|
||||
# Ease phase jumps over several frames, with the percentage following the
|
||||
# same position as the trace. The measured progress still never goes back.
|
||||
step=$(( (LAST_PM - DISPLAY_PM + 3) / 4 ))
|
||||
DISPLAY_PM=$((DISPLAY_PM + step))
|
||||
render_icon "$DISPLAY_PM"
|
||||
|
||||
message="$(truncate_for_width "$(current_whimsy)" "$PROGRESS_TEXT_WIDTH")"
|
||||
tip="$(truncate_for_width "$(omarchy_text "Tip: %s" "$(current_tip)")" "$PROGRESS_TEXT_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
|
||||
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"
|
||||
center "${DIM}${tip}${RESET}" "$PROGRESS_TEXT_WIDTH"
|
||||
printf '%s' "$CLEAR_TO_END"
|
||||
}
|
||||
|
||||
@@ -493,13 +793,15 @@ install_duration() {
|
||||
}
|
||||
|
||||
render_finish() {
|
||||
local duration effect_canvas_width
|
||||
local duration effect_canvas_width title
|
||||
duration="$(install_duration || true)"
|
||||
duration="${duration:-Complete}"
|
||||
# ttfx centers its text two columns right of plain centering, so hand it a
|
||||
# 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.
|
||||
if [[ -n $duration ]]; then
|
||||
title=$(omarchy_text "Installed Omarchy in %s" "$duration")
|
||||
else
|
||||
title=$(omarchy_text "Installed Omarchy")
|
||||
fi
|
||||
# ttfx centers its text two columns right of plain centering. A canvas two
|
||||
# columns narrower aligns the animation with the title and fallback logo.
|
||||
effect_canvas_width="$(term_cols)"
|
||||
(( effect_canvas_width > 2 )) && effect_canvas_width=$((effect_canvas_width - 2))
|
||||
|
||||
@@ -508,23 +810,21 @@ render_finish() {
|
||||
FINISH_TOP_ROW="$(centered_top_row "$FINISH_HEIGHT")"
|
||||
|
||||
{
|
||||
printf '%s%s%s%d;1H' "$SHOW_CURSOR" "$CLEAR" "$CSI" "$FINISH_TOP_ROW"
|
||||
render_logo
|
||||
blank_line
|
||||
center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH"
|
||||
# Leave the wordmark area blank: laseretch reveals it for the first time.
|
||||
printf '%s%s%s%d;1H' "$HIDE_CURSOR" "$CLEAR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 1))"
|
||||
center "$title" "$CONTENT_WIDTH"
|
||||
blank_line
|
||||
} >"$TTY_PATH"
|
||||
|
||||
if [[ -f $LOGO_PATH ]]; then
|
||||
# Reuse mode restores a saved cursor before moving up over its input. Save
|
||||
# this anchor so the effect redraws the same logo rows shown above.
|
||||
# this anchor so the effect occupies the reserved wordmark rows.
|
||||
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
|
||||
# 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
|
||||
# truecolor ones badly, and it settles on indexed 2 — the green gum already
|
||||
# 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.
|
||||
# truecolor ones badly. It settles on indexed 2, the Tokyo Night green used
|
||||
# throughout the installer. The laser and sparks keep their own hot palette.
|
||||
timeout 8s ttfx -i "$LOGO_PATH" \
|
||||
--canvas-width "$effect_canvas_width" \
|
||||
--anchor-text c \
|
||||
@@ -533,13 +833,18 @@ render_finish() {
|
||||
--xterm-colors \
|
||||
laseretch \
|
||||
--final-gradient-stops 2 \
|
||||
>"$TTY_PATH" 2>/dev/null || dashboard_log "logo effect skipped (ttfx exited $?)"
|
||||
printf '%s%d;1H' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH"
|
||||
>"$TTY_PATH" 2>/dev/null || {
|
||||
dashboard_log "logo effect skipped (ttfx exited $?)"
|
||||
{ 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
|
||||
printf '%s%s%d;1H' "$SHOW_CURSOR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH"
|
||||
}
|
||||
|
||||
reboot_prompt() {
|
||||
local prompt_pad
|
||||
local prompt_pad label
|
||||
|
||||
# Nobody to confirm; the OMARCHY_UI_AUTO_REBOOT check at the call site still
|
||||
# applies, so setting both stops on the finish screen without a keyboard.
|
||||
@@ -550,24 +855,31 @@ reboot_prompt() {
|
||||
# gum allows. FINISH_HEIGHT budgets for the same three rows.
|
||||
printf '%s%d;1H%s' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 2))" "$CLEAR_LINE" >"$TTY_PATH"
|
||||
|
||||
prompt_pad=$(( ( $(term_cols) - 18 ) / 2 ))
|
||||
# gum pads the button label by four columns on each side.
|
||||
label=$(omarchy_text "Reboot Now")
|
||||
prompt_pad=$(( ( $(term_cols) - $(visible_len "$label") - 8 ) / 2 ))
|
||||
(( prompt_pad < 0 )) && prompt_pad=0
|
||||
gum confirm \
|
||||
--padding "0 0 0 $prompt_pad" \
|
||||
--show-help=false \
|
||||
--default \
|
||||
--affirmative "Reboot Now" \
|
||||
--affirmative "$label" \
|
||||
--negative "" \
|
||||
"" <"$TTY_PATH" >"$TTY_PATH" 2>&1
|
||||
}
|
||||
|
||||
# Columns, not characters: each CJK character takes two.
|
||||
truncate_for_width() {
|
||||
local text="$1" width="$2"
|
||||
if (( ${#text} > width )); then
|
||||
printf '%s…' "${text:0:$((width - 1))}"
|
||||
else
|
||||
if (( $(printf '%s' "$text" | wc -L) <= width )); then
|
||||
printf '%s' "$text"
|
||||
return
|
||||
fi
|
||||
text=${text:0:$((width - 1))}
|
||||
while (( $(printf '%s' "$text" | wc -L) > width - 1 )); do
|
||||
text=${text%?}
|
||||
done
|
||||
printf '%s…' "$text"
|
||||
}
|
||||
|
||||
render_failure_log_tail() {
|
||||
@@ -593,8 +905,8 @@ render_failure_log_tail() {
|
||||
fi
|
||||
[[ -f $tail_log ]] || return 0
|
||||
|
||||
label="Last log lines:"
|
||||
[[ $tail_log == /mnt/* ]] && label="Last target log lines:"
|
||||
label=$(omarchy_text "Last log lines:")
|
||||
[[ $tail_log == /mnt/* ]] && label=$(omarchy_text "Last target log lines:")
|
||||
line_at "$CONTENT_WIDTH" 2 "${DIM}${label}${RESET}"
|
||||
printf '\n'
|
||||
tail -n "$lines" "$tail_log" 2>/dev/null | while IFS= read -r line; do
|
||||
@@ -617,7 +929,7 @@ find_log_uploader() {
|
||||
}
|
||||
|
||||
prompt_enter() {
|
||||
printf '\nPress Enter to continue…' >"$TTY_PATH"
|
||||
printf '\n%s' "$(omarchy_text "Press Enter to continue…")" >"$TTY_PATH"
|
||||
IFS= read -r _ <"$TTY_PATH" || true
|
||||
}
|
||||
|
||||
@@ -628,9 +940,9 @@ upload_failure_log() {
|
||||
"$uploader" install >"$TTY_PATH" 2>&1 || true
|
||||
else
|
||||
{
|
||||
echo "No log uploader is available in this environment."
|
||||
echo "Install log: $LOG_FILE"
|
||||
[[ -f /mnt/var/log/omarchy-install.log ]] && echo "Target log: /mnt/var/log/omarchy-install.log"
|
||||
printf '%s\n' "$(omarchy_text "No log uploader is available in this environment.")"
|
||||
printf '%s\n' "$(omarchy_text "Install log: %s" "$LOG_FILE")"
|
||||
[[ -f /mnt/var/log/omarchy-install.log ]] && printf '%s\n' "$(omarchy_text "Target log: %s" /mnt/var/log/omarchy-install.log)"
|
||||
} >"$TTY_PATH"
|
||||
fi
|
||||
prompt_enter
|
||||
@@ -681,6 +993,8 @@ render_media_diagnosis() {
|
||||
blank_line
|
||||
continue
|
||||
fi
|
||||
# The support log keeps English; the screen shows the translation, and package names pass through.
|
||||
[[ ${OMARCHY_INSTALL_LANG:-} == "zh" ]] && line=${OMARCHY_ZH[$line]:-$line}
|
||||
if (( first )); then
|
||||
center "${RED}$(truncate_for_width "$line" "$CONTENT_WIDTH")${RESET}" "$CONTENT_WIDTH"
|
||||
first=0
|
||||
@@ -696,8 +1010,8 @@ render_failure() {
|
||||
blank_line
|
||||
render_logo
|
||||
blank_line
|
||||
center "${RED}Omarchy installation stopped${RESET}" "$CONTENT_WIDTH"
|
||||
center "Installer exited with status $status" "$CONTENT_WIDTH"
|
||||
center "${RED}$(omarchy_text "Omarchy installation stopped")${RESET}" "$CONTENT_WIDTH"
|
||||
center "$(omarchy_text "Installer exited with status %s" "$status")" "$CONTENT_WIDTH"
|
||||
|
||||
render_media_diagnosis "$media_diagnosis"
|
||||
|
||||
@@ -712,12 +1026,12 @@ render_failure() {
|
||||
blank_line
|
||||
render_failure_log_tail "$(media_diagnosis_rows "$media_diagnosis")"
|
||||
blank_line
|
||||
center "${DIM}Get help at https://omarchy.org/discord${RESET}" "$CONTENT_WIDTH"
|
||||
center "${DIM}$(omarchy_text "Get help at https://omarchy.org/discord")${RESET}" "$CONTENT_WIDTH"
|
||||
blank_line
|
||||
}
|
||||
|
||||
failure_menu() {
|
||||
local choice uploader
|
||||
local choice uploader actions shown i
|
||||
|
||||
[[ ${OMARCHY_UI_FAILURE_ACTION:-} == "exit" ]] && return 0
|
||||
# The failure screen and log tail have already rendered; exit with the
|
||||
@@ -727,26 +1041,20 @@ failure_menu() {
|
||||
while true; do
|
||||
uploader=""
|
||||
uploader=$(find_log_uploader || true)
|
||||
if [[ -n $uploader ]]; then
|
||||
choice=$(gum choose \
|
||||
--height 6 \
|
||||
--header "What would you like to do?" \
|
||||
"Upload log for support" \
|
||||
"View full log" \
|
||||
"Drop to shell" \
|
||||
"Reboot" \
|
||||
"Power off" \
|
||||
<"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell"
|
||||
else
|
||||
choice=$(gum choose \
|
||||
--height 5 \
|
||||
--header "What would you like to do?" \
|
||||
"View full log" \
|
||||
"Drop to shell" \
|
||||
"Reboot" \
|
||||
"Power off" \
|
||||
<"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell"
|
||||
fi
|
||||
actions=("View full log" "Drop to shell" "Reboot" "Power off")
|
||||
[[ -n $uploader ]] && actions=("Upload log for support" "${actions[@]}")
|
||||
shown=()
|
||||
for i in "${!actions[@]}"; do
|
||||
shown+=("$(omarchy_text "${actions[i]}")")
|
||||
done
|
||||
choice=$(gum choose \
|
||||
--height "$((${#actions[@]} + 1))" \
|
||||
--header "$(omarchy_text "What would you like to do?")" \
|
||||
"${shown[@]}" \
|
||||
<"$TTY_PATH" 2>"$TTY_PATH") || choice=""
|
||||
for i in "${!shown[@]}"; do
|
||||
[[ ${shown[i]} == "$choice" ]] && choice=${actions[i]}
|
||||
done
|
||||
|
||||
case "$choice" in
|
||||
"Upload log for support")
|
||||
@@ -805,9 +1113,13 @@ PHASE_T0=$NOW
|
||||
PKG_LAST_CHANGE=$NOW
|
||||
launch_child
|
||||
|
||||
frame=0
|
||||
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
|
||||
sleep 0.5
|
||||
frame=$((frame + 1))
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
set +e
|
||||
@@ -820,8 +1132,14 @@ child_pgid=""
|
||||
dashboard_log "installer child exited with status $status"
|
||||
|
||||
if (( status == 0 )); then
|
||||
# finished_at lands microseconds before the child exits, so the 0.5s poll
|
||||
# rarely renders a full bar before the finish screen replaces it.
|
||||
# Finish the trace even when a short-lived child exits between state polls.
|
||||
LAST_PM=1000
|
||||
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
|
||||
sleep 0.4
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# The Chinese installer's console: the VT's Tokyo Night palette, with Noto Sans Mono CJK from the offline mirror.
|
||||
[main]
|
||||
font=Noto Sans Mono CJK SC:size=13
|
||||
# Noto's tall default rows leave the summary too few lines for the logo.
|
||||
line-height=22px
|
||||
|
||||
[colors-dark]
|
||||
background=1a1b26
|
||||
foreground=a9b1d6
|
||||
regular0=1a1b26
|
||||
regular1=f7768e
|
||||
regular2=9ece6a
|
||||
regular3=e0af68
|
||||
regular4=7aa2f7
|
||||
regular5=bb9af7
|
||||
regular6=7dcfff
|
||||
regular7=a9b1d6
|
||||
bright0=414868
|
||||
bright1=f7768e
|
||||
bright2=9ece6a
|
||||
bright3=e0af68
|
||||
bright4=7aa2f7
|
||||
bright5=bb9af7
|
||||
bright6=7dcfff
|
||||
bright7=c0caf5
|
||||
|
||||
# cage draws no title bar, and a reserved one leaves a black strip.
|
||||
[csd]
|
||||
preferred=none
|
||||
@@ -13,7 +13,7 @@ The canonical call sequence (mirrored from archinstall.scripts.guided.py) is:
|
||||
# configs do their own mounting before the Installer context opens.
|
||||
if disk_config.config_type != DiskLayoutType.Pre_mount:
|
||||
inst.mount_ordered_layout()
|
||||
inst.sanity_check(offline=, skip_ntp=, skip_wkd=)
|
||||
sanity_check(inst)
|
||||
inst.generate_key_files() # encrypted only
|
||||
inst.set_mirrors(handler, mirror_config, on_target=False)
|
||||
inst.minimal_installation(...) # base + linux pacstrap
|
||||
@@ -214,6 +214,14 @@ def _method_accepts(method, name: str) -> bool:
|
||||
return name in (*positional, *kwonly)
|
||||
|
||||
|
||||
def sanity_check(installer) -> None:
|
||||
"""Archinstall 4.5 removed offline; retain it for older releases."""
|
||||
options = {"skip_ntp": True, "skip_wkd": True}
|
||||
if _method_accepts(installer.sanity_check, "offline"):
|
||||
options["offline"] = True
|
||||
installer.sanity_check(**options)
|
||||
|
||||
|
||||
def _method_accepts_users(method) -> bool:
|
||||
return _method_accepts(method, "users")
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .keyboard import validate_input_selection
|
||||
from .region import GLOBAL, resolve_region
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallContext:
|
||||
@@ -27,6 +30,7 @@ class InstallContext:
|
||||
arch_config_path: Path
|
||||
omarchy_install: dict[str, Any]
|
||||
defer_provisioning: bool = False
|
||||
region: str = GLOBAL
|
||||
|
||||
target: Path = Path("/mnt")
|
||||
omarchy_path: Path = Path("/usr/share/omarchy")
|
||||
@@ -50,6 +54,20 @@ class InstallContext:
|
||||
user_configuration = json.loads(config_path.read_text())
|
||||
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]
|
||||
|
||||
validate_input_selection(omarchy_install.get("input_method"), omarchy_install.get("input_xkb_layout", ""))
|
||||
|
||||
# Resolved before any disk is touched, so an autoinstall naming a
|
||||
# region this ISO lacks fails without wiping anything.
|
||||
region = resolve_region(omarchy_install, user_configuration.get("timezone"))
|
||||
omarchy_install["region"] = region
|
||||
|
||||
# Deferred provisioning: the whole system installs but user creation is deferred to
|
||||
# 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
|
||||
@@ -114,6 +132,7 @@ class InstallContext:
|
||||
arch_config_path=arch_config_path,
|
||||
omarchy_install=omarchy_install,
|
||||
defer_provisioning=defer_provisioning,
|
||||
region=region,
|
||||
state_dir=state_dir,
|
||||
)
|
||||
disk_config = user_configuration.get("disk_config", {})
|
||||
@@ -175,6 +194,18 @@ def _inject_provisioning_encryption_password(arch_configuration: dict, user_cred
|
||||
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]:
|
||||
disk_config = user_configuration.get("disk_config", {})
|
||||
mode = "protected" if disk_config.get("config_type") == "pre_mounted_config" else "full_disk"
|
||||
|
||||
@@ -16,21 +16,31 @@ from pathlib import Path
|
||||
from .command import capture
|
||||
|
||||
|
||||
def configure_keyboard(target: Path, language: str) -> bool:
|
||||
def configure_keyboard(target: Path, language: str, input_method: str | None = None, xkb_layout: str = "") -> bool:
|
||||
"""Write the console keymap into a mounted target without booting it.
|
||||
|
||||
Returns False for layouts localectl doesn't know, matching archinstall:
|
||||
warn and keep the default. Every layout the configurator offers is known;
|
||||
the guard is for the kb_layout an autoinstall drive can name freely.
|
||||
"""
|
||||
validate_input_selection(input_method, xkb_layout)
|
||||
if not language.strip():
|
||||
return True
|
||||
if input_method is None:
|
||||
return True
|
||||
language = "us"
|
||||
|
||||
# Input engines are separate from real console keymaps. The shared picker
|
||||
# uses US for Chinese/Korean and carries its preference in omarchy_install.
|
||||
if input_method is None:
|
||||
input_method = "mozc" if language == "jp106" else "none"
|
||||
|
||||
result = capture(["localectl", "--no-pager", "list-keymaps"])
|
||||
if result.returncode != 0:
|
||||
detail = result.stderr.strip() or "localectl returned an error"
|
||||
raise RuntimeError(f"Unable to list keyboard layouts: {detail}")
|
||||
if language.lower() not in {layout.lower() for layout in result.stdout.splitlines()}:
|
||||
if input_method != "none":
|
||||
raise ValueError(f"Unknown keyboard language: {language}")
|
||||
return False
|
||||
|
||||
# systemd-firstboot --force rewrites vconsole.conf wholesale, dropping the
|
||||
@@ -51,4 +61,15 @@ def configure_keyboard(target: Path, language: str) -> bool:
|
||||
if font:
|
||||
vconsole_path.write_text(vconsole_path.read_text() + font + "\n")
|
||||
|
||||
preference = target / "etc/omarchy/input-method"
|
||||
preference.parent.mkdir(parents=True, exist_ok=True)
|
||||
preference.write_text(f"INPUT_METHOD={input_method}\nXKB_LAYOUT={xkb_layout}\n")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_input_selection(input_method: str | None, xkb_layout: str) -> None:
|
||||
if input_method is not None and input_method not in {"none", "mozc", "hangul", "pinyin", "chewing"}:
|
||||
raise ValueError(f"Unknown input method: {input_method}")
|
||||
if xkb_layout not in {"", "kr"}:
|
||||
raise ValueError(f"Unknown input keyboard layout: {xkb_layout}")
|
||||
@@ -36,6 +36,7 @@ from . import archinstall_adapter as arch
|
||||
from .command import capture, capture_identifier, require_text
|
||||
from .context import InstallContext
|
||||
from .keyboard import configure_keyboard
|
||||
from .region import GLOBAL, region_keyrings, region_packages
|
||||
from .ui import error, info
|
||||
|
||||
|
||||
@@ -235,11 +236,7 @@ def arch_install_system(ctx: InstallContext) -> None:
|
||||
if not pre_mounted:
|
||||
installer.mount_ordered_layout()
|
||||
|
||||
installer.sanity_check(
|
||||
offline=True,
|
||||
skip_ntp=True,
|
||||
skip_wkd=True,
|
||||
)
|
||||
arch.sanity_check(installer)
|
||||
|
||||
if not pre_mounted and arch.is_encrypted(config):
|
||||
installer.generate_key_files()
|
||||
@@ -269,7 +266,16 @@ def arch_install_system(ctx: InstallContext) -> None:
|
||||
pacman_config=config.pacman_config,
|
||||
)
|
||||
|
||||
if not configure_keyboard(installer.target, kb_layout):
|
||||
# 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,
|
||||
ctx.omarchy_install.get("input_method"),
|
||||
ctx.omarchy_install.get("input_xkb_layout", ""),
|
||||
):
|
||||
error(f"Invalid keyboard language specified: {kb_layout}")
|
||||
|
||||
if config.mirror_config:
|
||||
@@ -293,6 +299,11 @@ def arch_install_system(ctx: InstallContext) -> None:
|
||||
info("› installing Omarchy runtime + omarchy-base.packages")
|
||||
installer.add_additional_packages(_runtime_package_list(ctx))
|
||||
|
||||
if region_pkgs := region_packages(ctx.region):
|
||||
info(f"› installing {ctx.region} region packages: {', '.join(region_pkgs)}")
|
||||
installer.add_additional_packages(region_pkgs)
|
||||
_write_region_marker(ctx)
|
||||
|
||||
# Tailscale is bundled in the offline mirror but only installed
|
||||
# when an autoinstall drive staged an auth key; must happen here,
|
||||
# while the mirror is still bind-mounted, not in the phase that
|
||||
@@ -723,6 +734,17 @@ def _unmask_mkinitcpio_pacman_hooks(
|
||||
info(f"warning: failed to restore pacman hook mask for {name}: {exc}")
|
||||
|
||||
|
||||
def _write_region_marker(ctx: InstallContext) -> None:
|
||||
"""Tell the runtime's pacman finalizer which profile to apply. Global
|
||||
installs write nothing, so they stay identical to a region-less install."""
|
||||
if ctx.region == GLOBAL:
|
||||
return
|
||||
marker = ctx.target / "etc/omarchy/region"
|
||||
marker.parent.mkdir(parents=True, exist_ok=True)
|
||||
marker.write_text(ctx.region + "\n")
|
||||
marker.chmod(0o644)
|
||||
|
||||
|
||||
def _runtime_package_list(ctx: InstallContext) -> list[str]:
|
||||
"""Selected Omarchy runtime package + every package in the ISO-bundled
|
||||
base package list that isn't already installed early."""
|
||||
@@ -790,7 +812,7 @@ def verify_protected_mounts(ctx: InstallContext) -> None:
|
||||
esp_mp.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(["mount", esp_dev, str(esp_mp)], check=True)
|
||||
|
||||
info(f"› protected target verified: kernel={storage.get('kernel', 'linux')} esp={boot['esp_mount']}")
|
||||
info(f"› protected target verified: kernel={storage.get('kernel', 'linux-omarchy')} esp={boot['esp_mount']}")
|
||||
|
||||
|
||||
def _is_mountpoint(path: Path) -> bool:
|
||||
@@ -1130,6 +1152,14 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
|
||||
|
||||
|
||||
def run_system_finalizer(ctx: InstallContext) -> None:
|
||||
# The region's keyring packages are installed by now, but the build
|
||||
# container's GPG state is not inherited. Initialize the target's trust
|
||||
# database with only this region's keys before the finalizer enables its
|
||||
# online repositories; global installs never trust them.
|
||||
if keyrings := region_keyrings(ctx.region):
|
||||
_run_target_setup_command(ctx, ["pacman-key", "--init"])
|
||||
_run_target_setup_command(ctx, ["pacman-key", "--populate", "archlinux", *keyrings])
|
||||
|
||||
if ctx.defer_provisioning:
|
||||
cmd = ["/usr/bin/omarchy-apply-system", "--defer-provisioning", "--first-install"]
|
||||
else:
|
||||
@@ -1644,6 +1674,7 @@ def _tailscale_authkey(path: Path) -> str:
|
||||
|
||||
def validate_boot(ctx: InstallContext) -> None:
|
||||
_assert_boot_hooks_restored(ctx)
|
||||
_validate_kernel_headers(ctx)
|
||||
|
||||
boot = _boot_intent(ctx)
|
||||
storage = _storage_intent(ctx)
|
||||
@@ -1666,7 +1697,7 @@ def validate_boot(ctx: InstallContext) -> None:
|
||||
default_limine = ctx.target / "etc" / "default" / "limine"
|
||||
config_text = _limine_combined_config_text(ctx, default_limine.read_text())
|
||||
uki_prefix = _limine_setting(config_text, "CUSTOM_UKI_NAME", "omarchy") or "omarchy"
|
||||
kernel = storage.get("kernel") or (ctx.user_configuration.get("kernels") or ["linux"])[0]
|
||||
kernel = storage.get("kernel") or (ctx.user_configuration.get("kernels") or ["linux-omarchy"])[0]
|
||||
|
||||
if arch.has_uefi():
|
||||
limine_binary = esp_mount / boot.get("esp_path", "/EFI/limine").lstrip("/") / boot.get("efi_binary", "limine_x64.efi")
|
||||
@@ -1742,6 +1773,19 @@ def _assert_boot_hooks_restored(ctx: InstallContext) -> None:
|
||||
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
|
||||
# the name limine-mkinitcpio-hook builds the UKI under.
|
||||
def _installed_kernels(ctx: InstallContext) -> list[str]:
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Region selection: which of the runtime's region profiles a target gets.
|
||||
|
||||
The builder copies every profile from the bundled runtime's default/regions/
|
||||
into REGIONS_DIR, and their packages into the offline mirror. Nobody picks a
|
||||
region by hand: the timezone the user already chose selects it, and an
|
||||
autoinstall config may name one explicitly in omarchy_install.region. Run as
|
||||
`python -m orchestrator.region <timezone>` to print the region the installer
|
||||
would use, which is how the configurator fills its summary.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REGIONS_DIR = Path("/usr/share/omarchy-iso/regions")
|
||||
GLOBAL = "global"
|
||||
|
||||
|
||||
def _entries(path: Path) -> list[str]:
|
||||
if not path.is_file():
|
||||
return []
|
||||
lines = (line.strip() for line in path.read_text().splitlines())
|
||||
return [line for line in lines if line and not line.startswith("#")]
|
||||
|
||||
|
||||
def region_for_timezone(timezone: str | None, regions_dir: Path | None = None) -> str:
|
||||
regions_dir = regions_dir or REGIONS_DIR
|
||||
if timezone and regions_dir.is_dir():
|
||||
for profile in sorted(regions_dir.iterdir()):
|
||||
if timezone in _entries(profile / "timezones"):
|
||||
return profile.name
|
||||
return GLOBAL
|
||||
|
||||
|
||||
def resolve_region(omarchy_install: dict, timezone: str | None, regions_dir: Path | None = None) -> str:
|
||||
regions_dir = regions_dir or REGIONS_DIR
|
||||
region = omarchy_install.get("region")
|
||||
if region is None or region == "":
|
||||
return region_for_timezone(timezone, regions_dir)
|
||||
# Profiles are named by ISO 3166-1 alpha-2 code; checking the shape first
|
||||
# keeps a path like ".." from passing the directory test.
|
||||
if not isinstance(region, str) or (
|
||||
region != GLOBAL and not (re.fullmatch(r"[a-z]{2}", region) and (regions_dir / region).is_dir())
|
||||
):
|
||||
raise RuntimeError(f"Unsupported region: {region!r}")
|
||||
return region
|
||||
|
||||
|
||||
def region_packages(region: str, regions_dir: Path | None = None) -> list[str]:
|
||||
if region == GLOBAL:
|
||||
return []
|
||||
return _entries((regions_dir or REGIONS_DIR) / region / "packages")
|
||||
|
||||
|
||||
def region_keyrings(region: str, regions_dir: Path | None = None) -> list[str]:
|
||||
"""Pacman keyrings the profile ships, named the Arch way: <name>-keyring
|
||||
installs /usr/share/pacman/keyrings/<name>.gpg."""
|
||||
return [
|
||||
package.removesuffix("-keyring")
|
||||
for package in region_packages(region, regions_dir)
|
||||
if package.endswith("-keyring")
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(region_for_timezone(sys.argv[1] if len(sys.argv) > 1 else None))
|
||||
@@ -13,6 +13,10 @@ ParallelDownloads = 5
|
||||
SigLevel = Required DatabaseOptional
|
||||
LocalFileSigLevel = Optional
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
|
||||
[core]
|
||||
Server = https://mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
@@ -25,10 +29,6 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Server = https://mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
|
||||
@@ -13,6 +13,10 @@ ParallelDownloads = 5
|
||||
SigLevel = Required DatabaseOptional
|
||||
LocalFileSigLevel = Optional
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/rc/$arch
|
||||
|
||||
[core]
|
||||
Server = https://rc-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
@@ -25,10 +29,6 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Server = https://rc-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/rc/$arch
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
|
||||
@@ -13,6 +13,10 @@ ParallelDownloads = 5
|
||||
SigLevel = Required DatabaseOptional
|
||||
LocalFileSigLevel = Optional
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/stable/$arch
|
||||
|
||||
[core]
|
||||
Server = https://stable-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
@@ -25,10 +29,6 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Server = https://stable-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/stable/$arch
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
|
||||
@@ -14,6 +14,9 @@ ISO="$OMARCHY_INTEGRATION_ISO"
|
||||
SSH_PORT="${OMARCHY_INTEGRATION_SSH_PORT:-2322}"
|
||||
MEMORY="${OMARCHY_INTEGRATION_MEMORY:-8192}"
|
||||
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}"
|
||||
BOOT_TIMEOUT=600
|
||||
|
||||
@@ -166,7 +169,7 @@ start_vm() {
|
||||
-m "$MEMORY" \
|
||||
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
|
||||
-drive if=pflash,format=raw,file="$ACTIVE_OVMF" \
|
||||
-drive file="$disk",format=qcow2,if=none,id=drive0 \
|
||||
-drive file="$disk",format=qcow2,if=none,id=drive0${DISK_CACHE:+,cache=$DISK_CACHE} \
|
||||
-device virtio-blk-pci,drive=drive0,bootindex=1 \
|
||||
-device virtio-vga \
|
||||
-display none \
|
||||
@@ -282,8 +285,12 @@ ssh_sudo() {
|
||||
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() {
|
||||
local timeout="$1" failure_name="${2:-failure-ssh-timeout}" waited=0
|
||||
local timeout="$1" failure_name="${2:-failure-ssh-timeout}"
|
||||
local started=$SECONDS next_note=30 progress_name
|
||||
|
||||
while ! ssh_guest true 2>/dev/null; do
|
||||
if ! vm_running; then
|
||||
@@ -291,14 +298,20 @@ wait_for_ssh() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ((waited >= timeout)); then
|
||||
if ((SECONDS - started >= timeout)); then
|
||||
capture_console "$failure_name"
|
||||
echo "Timed out after ${timeout}s waiting for SSH" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
((waited += 5))
|
||||
if ((SECONDS - started >= next_note)); then
|
||||
printf -v progress_name 'waiting-ssh-%04ds' "$((SECONDS - started))"
|
||||
capture_console "$progress_name"
|
||||
echo " ... waiting for SSH ($((SECONDS - started))s)"
|
||||
((next_note += 30))
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
@@ -403,7 +416,7 @@ EOF
|
||||
"efi_binary": "limine_x64.efi",
|
||||
"enable_fallback": true
|
||||
},
|
||||
"storage": { "kernel": "linux" }
|
||||
"storage": { "kernel": "linux-omarchy" }
|
||||
},
|
||||
"disk_config": {
|
||||
"config_type": "default_layout",
|
||||
@@ -448,7 +461,7 @@ EOF
|
||||
]
|
||||
},
|
||||
"hostname": "$GUEST_HOSTNAME",
|
||||
"kernels": [ "linux" ],
|
||||
"kernels": [ "linux-omarchy" ],
|
||||
"network_config": { "type": "iso" },
|
||||
"ntp": true,
|
||||
"parallel_downloads": 8,
|
||||
@@ -556,16 +569,28 @@ install_phase() {
|
||||
return 1
|
||||
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
|
||||
printf -v progress_name 'success-install-progress-%04ds' "$waited"
|
||||
capture_console "$progress_name"
|
||||
echo " ... installing (${waited}s)"
|
||||
fi
|
||||
((waited % 30 == 0)) && echo " ... installing (${waited}s)"
|
||||
|
||||
sleep 10
|
||||
((waited += 10))
|
||||
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."
|
||||
stop_vm
|
||||
mv "$BASE_DISK.building" "$BASE_DISK"
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/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
|
||||
# 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.
|
||||
if wait_for_screen "Opinionated" 120 2>/dev/null; then
|
||||
if wait_for_screen "Linux by DHH" 120 2>/dev/null; then
|
||||
printf 'ok - %s\n' "the machine boots into first-boot setup unattended"
|
||||
else
|
||||
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"
|
||||
press down; sleep 1; press down; sleep 1; press down; sleep 1
|
||||
press ret
|
||||
wait_for_screen "Opinionated" 600
|
||||
wait_for_screen "Linux by DHH" 600
|
||||
else
|
||||
echo "Neither the greeter nor the Limine menu appeared" >&2
|
||||
return 1
|
||||
|
||||
@@ -273,6 +273,12 @@ mkdir -p "$omarchy_share"
|
||||
printf 'logo bottom row%*s\n' 66 ''
|
||||
} >"$omarchy_share/logo.txt"
|
||||
|
||||
# The ISO vendors the runtime's setup form; the dashboard only needs its text lookup.
|
||||
cat >"$work/setup-form.sh" <<'FORM'
|
||||
declare -gA OMARCHY_ZH=()
|
||||
omarchy_text() { local text=$1; shift; printf -- "$text" "$@"; }
|
||||
FORM
|
||||
|
||||
run_dashboard() {
|
||||
local install_log="$1"
|
||||
|
||||
@@ -284,7 +290,7 @@ run_dashboard() {
|
||||
"error": "Pacstrap failed. See /var/log/archinstall.log"}]}
|
||||
STATE
|
||||
: >"$screen"
|
||||
script -qefc "stty rows 40 cols 120; PATH='$stubs:$PATH' OMARCHY_PATH='$omarchy_share' OMARCHY_UI_INTERACTIVE=no OMARCHY_UI_FAILURE_ACTION=exit OMARCHY_FAILURE_TAIL_LOG='$install_log' '$DASHBOARD' '$install_log' '$state_file' -- bash -c 'exit 1'" \
|
||||
script -qefc "stty rows 40 cols 120; PATH='$stubs:$PATH' OMARCHY_PATH='$omarchy_share' SETUP_FORM='$work/setup-form.sh' OMARCHY_UI_INTERACTIVE=no OMARCHY_UI_FAILURE_ACTION=exit OMARCHY_FAILURE_TAIL_LOG='$install_log' '$DASHBOARD' '$install_log' '$state_file' -- bash -c 'exit 1'" \
|
||||
"$screen" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Every installer text needs an OMARCHY_ZH entry, or a Chinese install shows it in English.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
SETUP_FORM="${OMARCHY_SOURCE:-$ROOT/../omarchy}/install/provisioning/setup-form.sh"
|
||||
|
||||
fail() {
|
||||
printf 'not ok - %s\n' "$1"
|
||||
[[ -z ${2:-} ]] || printf '%s\n' "$2" | sed 's/^/# /'
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ ! -r $SETUP_FORM ]]; then
|
||||
printf 'ok - # skip the setup form is not checked out next to this repo (set OMARCHY_SOURCE)\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
source "$SETUP_FORM"
|
||||
for file in configs/airootfs/root/configurator configs/airootfs/usr/local/bin/omarchy-install-dashboard; do
|
||||
eval "$(sed -n '/^OMARCHY_ZH+=(/,/^)/p' "$ROOT/$file")"
|
||||
done
|
||||
|
||||
mapfile -t texts < <(
|
||||
grep -ho 'omarchy_text "[^"$]*"' "$ROOT/configs/airootfs/root/configurator" "$ROOT/configs/airootfs/usr/local/bin/omarchy-install-dashboard" |
|
||||
sed 's/^omarchy_text "//; s/"$//' | sort -u
|
||||
)
|
||||
((${#texts[@]} > 50)) || fail "the installer routes its text through omarchy_text" "found ${#texts[@]}"
|
||||
for text in "${texts[@]}"; do
|
||||
[[ -n ${OMARCHY_ZH[$text]:-} ]] || fail "every installer text has a Chinese entry" "missing: $text"
|
||||
done
|
||||
printf 'ok - every installer and dashboard text has a Chinese entry\n'
|
||||
|
||||
for action in "View full log" "Drop to shell" "Reboot" "Power off" "Upload log for support"; do
|
||||
[[ -n ${OMARCHY_ZH[$action]:-} ]] || fail "labels passed through variables have Chinese entries" "missing: $action"
|
||||
done
|
||||
printf 'ok - labels passed through variables have Chinese entries\n'
|
||||
|
||||
mapfile -t diagnosis < <(grep -x ' *echo "[^"$]*"' "$ROOT/configs/airootfs/usr/local/bin/omarchy-install-diagnose-media" | sed 's/^ *echo "//; s/"$//')
|
||||
((${#diagnosis[@]} >= 10)) || fail "the media diagnosis sentences are found" "found ${#diagnosis[@]}"
|
||||
for line in "${diagnosis[@]}"; do
|
||||
[[ -n ${OMARCHY_ZH[$line]:-} ]] || fail "every media diagnosis sentence has a Chinese entry" "missing: $line"
|
||||
done
|
||||
printf 'ok - every media diagnosis sentence has a Chinese entry\n'
|
||||
|
||||
[[ $(OMARCHY_INSTALL_LANG=zh omarchy_text "Confirm overwriting %s" /dev/vda) == "确认覆盖 /dev/vda" ]] ||
|
||||
fail "Chinese text keeps its printf arguments"
|
||||
[[ $(omarchy_text "Confirm overwriting %s" /dev/vda) == "Confirm overwriting /dev/vda" ]] ||
|
||||
fail "English stays the default"
|
||||
printf 'ok - omarchy_text formats Chinese and English alike\n'
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
mkdir -p "$work/install/provisioning"
|
||||
cat > "$work/install/provisioning/setup-form.sh" <<'FORM'
|
||||
OMARCHY_KEYBOARD_LAYOUTS=$'Chinese (Simplified, Pinyin)|us|pinyin
|
||||
English (US)|us
|
||||
Japanese|jp106|mozc'
|
||||
FORM
|
||||
export OMARCHY_PATH="$work"
|
||||
eval "$(sed -n '/^select_keyboard() {$/,/^}$/p' "$ROOT/bin/omarchy-iso-test")"
|
||||
press() { pressed+=("$1"); }
|
||||
sleep() { :; }
|
||||
|
||||
pressed=()
|
||||
select_keyboard "Japanese"
|
||||
[[ ${pressed[*]} == "down" ]]
|
||||
pressed=()
|
||||
select_keyboard "Chinese (Simplified, Pinyin)"
|
||||
[[ ${pressed[*]} == "up" ]]
|
||||
pressed=()
|
||||
select_keyboard "English (US)"
|
||||
(( ${#pressed[@]} == 0 ))
|
||||
if (set -e; select_keyboard "Missing") 2>"$work/error"; then
|
||||
echo "Unknown keyboard unexpectedly succeeded" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'Keyboard choice is absent' "$work/error"
|
||||
echo "ok - VM keyboard choices are relative to the default and reject absent rows"
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Exercise the compatibility boundary without importing host archinstall."""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
import types
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
source = ROOT / "configs/airootfs/usr/share/omarchy-iso/orchestrator/archinstall_adapter.py"
|
||||
tree = ast.parse(source.read_text())
|
||||
namespace = {}
|
||||
functions = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name in {"_method_accepts", "sanity_check"}]
|
||||
exec(compile(ast.Module(body=functions, type_ignores=[]), str(source), "exec"), namespace)
|
||||
|
||||
|
||||
class SanityCheckTest(unittest.TestCase):
|
||||
def test_legacy_archinstall_stays_offline(self):
|
||||
calls = []
|
||||
def check(offline=False, skip_ntp=False, skip_wkd=False):
|
||||
calls.append((offline, skip_ntp, skip_wkd))
|
||||
namespace["sanity_check"](types.SimpleNamespace(sanity_check=check))
|
||||
self.assertEqual(calls, [(True, True, True)])
|
||||
|
||||
def test_archinstall_45_does_not_receive_removed_argument(self):
|
||||
calls = []
|
||||
class Installer:
|
||||
def sanity_check(self, skip_ntp=False, skip_wkd=False):
|
||||
calls.append((skip_ntp, skip_wkd))
|
||||
namespace["sanity_check"](Installer())
|
||||
self.assertEqual(calls, [(True, True)])
|
||||
@@ -0,0 +1,197 @@
|
||||
"""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")
|
||||
|
||||
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=None, 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, omarchy_install={},
|
||||
region="global",
|
||||
)
|
||||
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, "_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))
|
||||
stack.enter_context(mock.patch.object(phases_impl.arch, "sanity_check", 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", ["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,6 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -17,6 +18,7 @@ from orchestrator import keyboard as KEYBOARD # noqa: E402
|
||||
# wherever this checkout can see a runtime, and skip the coverage test rather
|
||||
# than fail when none is around (a bare CI checkout of just this repo).
|
||||
SETUP_FORM_CANDIDATES = (
|
||||
Path(os.environ.get("OMARCHY_PATH", "/nonexistent")) / "install/provisioning/setup-form.sh",
|
||||
Path("/omarchy-source/install/provisioning/setup-form.sh"),
|
||||
ROOT.parent / "omarchy/install/provisioning/setup-form.sh",
|
||||
Path("/usr/share/omarchy/install/provisioning/setup-form.sh"),
|
||||
@@ -31,7 +33,7 @@ def supported_keymaps():
|
||||
r"OMARCHY_KEYBOARD_LAYOUTS=\$'(.*?)'\n", candidate.read_text(), re.DOTALL
|
||||
)
|
||||
assert block, f"no layout list in {candidate}"
|
||||
return [line.split("|", 1)[1] for line in block.group(1).splitlines()]
|
||||
return [line.split("|")[1] for line in block.group(1).splitlines()]
|
||||
return None
|
||||
|
||||
|
||||
@@ -79,6 +81,36 @@ class KeyboardConfigurationTest(unittest.TestCase):
|
||||
"KEYMAP=us\nFONT=default8x16\n",
|
||||
)
|
||||
|
||||
def test_input_choice_survives_while_console_keymap_stays_real(self):
|
||||
for method, keymap, layout in (("mozc", "jp106", ""), ("mozc", "us", ""), ("hangul", "us", "kr"), ("pinyin", "us", ""), ("chewing", "us", "")):
|
||||
with self.subTest(method=method), tempfile.TemporaryDirectory() as directory:
|
||||
target = self.target(directory)
|
||||
self.assertTrue(KEYBOARD.configure_keyboard(target, keymap, method, layout))
|
||||
self.assertIn(f"KEYMAP={keymap}", (target / "etc/vconsole.conf").read_text())
|
||||
if keymap == "jp106":
|
||||
self.assertIn("XKBLAYOUT=jp", (target / "etc/vconsole.conf").read_text())
|
||||
self.assertEqual((target / "etc/omarchy/input-method").read_text(), f"INPUT_METHOD={method}\nXKB_LAYOUT={layout}\n")
|
||||
|
||||
def test_input_choice_with_no_console_map_defaults_to_us(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
target = self.target(directory)
|
||||
self.assertTrue(KEYBOARD.configure_keyboard(target, "", "pinyin"))
|
||||
self.assertIn("INPUT_METHOD=pinyin", (target / "etc/omarchy/input-method").read_text())
|
||||
|
||||
def test_unknown_keyboard_does_not_silently_drop_an_input_choice(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
target = self.target(directory)
|
||||
with self.assertRaises(ValueError):
|
||||
KEYBOARD.configure_keyboard(target, "unknown", "pinyin")
|
||||
|
||||
def test_invalid_input_preferences_are_rejected_without_writes(self):
|
||||
for method, layout in (("unknown", ""), ("hangul", "kr\nKEYMAP=ru")):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
target = self.target(directory)
|
||||
with self.assertRaises(ValueError):
|
||||
KEYBOARD.configure_keyboard(target, "us", method, layout)
|
||||
self.assertFalse((target / "etc/omarchy/input-method").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,236 @@
|
||||
"""The target's region comes from its timezone, using the runtime's profiles."""
|
||||
|
||||
import json
|
||||
import os
|
||||
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, region # noqa: E402
|
||||
|
||||
|
||||
def write_profiles(root: Path) -> Path:
|
||||
regions = root / "regions"
|
||||
cn = regions / "cn"
|
||||
(cn / "pacman").mkdir(parents=True)
|
||||
(cn / "timezones").write_text("# China\nAsia/Shanghai\n\n Asia/Urumqi \n")
|
||||
(cn / "packages").write_text("# keyring first\narchlinuxcn-keyring\nextra-tool\n")
|
||||
(cn / "pacman/pacman.conf.append").write_text("[archlinuxcn]\nServer = https://example.invalid/$arch\n")
|
||||
for channel in ("stable", "rc", "edge"):
|
||||
(cn / f"pacman/mirrorlist-{channel}.prepend").write_text(f"Server = https://{channel}.example.invalid/$repo/os/$arch\n")
|
||||
return regions
|
||||
|
||||
|
||||
class RegionSelectionTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.root = Path(tmp.name)
|
||||
self.regions = write_profiles(self.root)
|
||||
patcher = mock.patch.object(region, "REGIONS_DIR", self.regions)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_timezone_selects_region(self):
|
||||
for timezone, expected in [
|
||||
("Asia/Shanghai", "cn"),
|
||||
("Asia/Urumqi", "cn"),
|
||||
("Asia/Hong_Kong", "global"),
|
||||
("America/New_York", "global"),
|
||||
("UTC", "global"),
|
||||
("# China", "global"),
|
||||
("", "global"),
|
||||
(None, "global"),
|
||||
]:
|
||||
with self.subTest(timezone=timezone):
|
||||
self.assertEqual(region.region_for_timezone(timezone), expected)
|
||||
|
||||
def test_iso_without_profiles_is_global(self):
|
||||
self.assertEqual(region.region_for_timezone("Asia/Shanghai", self.root / "missing"), "global")
|
||||
self.assertEqual(region.resolve_region({}, "Asia/Shanghai", self.root / "missing"), "global")
|
||||
|
||||
def test_autoinstall_region_overrides_timezone(self):
|
||||
self.assertEqual(region.resolve_region({"region": "cn"}, "UTC"), "cn")
|
||||
self.assertEqual(region.resolve_region({"region": "global"}, "Asia/Shanghai"), "global")
|
||||
self.assertEqual(region.resolve_region({}, "Asia/Shanghai"), "cn")
|
||||
for bad in ("us", "CN", "..", "cn/../cn", True, 1, ["cn"]):
|
||||
with self.subTest(region=bad), self.assertRaises(RuntimeError):
|
||||
region.resolve_region({"region": bad}, "UTC")
|
||||
|
||||
def test_profile_packages_and_keyrings(self):
|
||||
self.assertEqual(region.region_packages("cn"), ["archlinuxcn-keyring", "extra-tool"])
|
||||
self.assertEqual(region.region_keyrings("cn"), ["archlinuxcn"])
|
||||
self.assertEqual(region.region_packages("global"), [])
|
||||
self.assertEqual(region.region_keyrings("global"), [])
|
||||
|
||||
def from_env(self, config: dict) -> context.InstallContext:
|
||||
config_path = self.root / "config.json"
|
||||
creds_path = self.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(self.root / "state"),
|
||||
}, clear=True), mock.patch.object(context, "_default_kernel", return_value="linux-omarchy"):
|
||||
return context.InstallContext.from_env()
|
||||
|
||||
def test_context_resolves_region_before_install(self):
|
||||
self.assertEqual(self.from_env({"timezone": "Asia/Shanghai"}).region, "cn")
|
||||
self.assertEqual(self.from_env({"timezone": "Europe/Berlin"}).region, "global")
|
||||
# Deferred installs carry UTC until first boot, so they are global
|
||||
# unless the autoinstall config names a region.
|
||||
self.assertEqual(self.from_env({"timezone": "UTC"}).region, "global")
|
||||
self.assertEqual(self.from_env({"timezone": "UTC", "omarchy_install": {"region": "cn"}}).region, "cn")
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.from_env({"omarchy_install": {"region": "zz"}})
|
||||
|
||||
def test_marker_is_written_only_for_regional_targets(self):
|
||||
target = self.root / "target"
|
||||
phases_impl._write_region_marker(types.SimpleNamespace(target=target, region="global"))
|
||||
self.assertFalse((target / "etc/omarchy/region").exists())
|
||||
|
||||
phases_impl._write_region_marker(types.SimpleNamespace(target=target, region="cn"))
|
||||
marker = target / "etc/omarchy/region"
|
||||
self.assertEqual(marker.read_text(), "cn\n")
|
||||
self.assertEqual(marker.stat().st_mode & 0o777, 0o644)
|
||||
|
||||
def test_region_packages_install_while_the_offline_mirror_is_mounted(self):
|
||||
for selected, expected_packages in [("global", []), ("cn", [["archlinuxcn-keyring", "extra-tool"]])]:
|
||||
with self.subTest(region=selected), ExitStack() as stack:
|
||||
events = []
|
||||
installer = mock.MagicMock()
|
||||
installer.add_additional_packages.side_effect = lambda packages: events.append(packages)
|
||||
config = types.SimpleNamespace(
|
||||
kernels=["linux-omarchy"], locale_config=None, mirror_config=None,
|
||||
swap=None, auth_config=None, 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=self.root / "target", tailscale_authkey_path=None, omarchy_install={},
|
||||
region=selected,
|
||||
)
|
||||
for name in ("_mount_offline_package_cache", "_mask_mkinitcpio_pacman_hooks",
|
||||
"_unmask_mkinitcpio_pacman_hooks", "_configure_limine_boot",
|
||||
"_write_pre_mounted_fstab", "_install_early_packages"):
|
||||
stack.enter_context(mock.patch.object(phases_impl, name))
|
||||
stack.enter_context(mock.patch.object(
|
||||
phases_impl, "_unmount_offline_package_cache", side_effect=lambda _: events.append("unmount")))
|
||||
stack.enter_context(mock.patch.object(phases_impl, "configure_keyboard", return_value=True))
|
||||
stack.enter_context(mock.patch.object(phases_impl, "_runtime_package_list", return_value=["omarchy"]))
|
||||
for name, value in (("is_pre_mount", True), ("root_user", None)):
|
||||
stack.enter_context(mock.patch.object(phases_impl.arch, name, return_value=value, create=True))
|
||||
stack.enter_context(mock.patch.object(phases_impl.arch, "sanity_check", create=True))
|
||||
opened = stack.enter_context(mock.patch.object(phases_impl.arch, "open_installer", create=True))
|
||||
opened.return_value.__enter__.return_value = installer
|
||||
|
||||
phases_impl.arch_install_system(ctx)
|
||||
|
||||
self.assertEqual(events, [["linux-omarchy-headers"], ["omarchy"], *expected_packages, "unmount"])
|
||||
self.assertEqual((ctx.target / "etc/omarchy/region").exists(), selected != "global")
|
||||
|
||||
def test_finalizer_trusts_only_the_selected_region(self):
|
||||
for selected, expected in [
|
||||
("global", []),
|
||||
("cn", [["pacman-key", "--init"], ["pacman-key", "--populate", "archlinux", "archlinuxcn"]]),
|
||||
]:
|
||||
with self.subTest(region=selected), \
|
||||
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"):
|
||||
ctx = types.SimpleNamespace(region=selected, defer_provisioning=True, target=Path("/unused"))
|
||||
phases_impl.run_system_finalizer(ctx)
|
||||
commands = [call.args[1] for call in run.call_args_list]
|
||||
self.assertEqual(commands[:-1], expected)
|
||||
self.assertEqual(commands[-1][0], "/usr/bin/omarchy-apply-system")
|
||||
|
||||
|
||||
class PrepareRegionsTest(unittest.TestCase):
|
||||
def run_prepare(self, regions: Path, root: Path) -> tuple[Path, Path, str]:
|
||||
payload = root / "payload"
|
||||
payload.mkdir()
|
||||
online = root / "online.conf"
|
||||
online.write_text("[core]\nInclude = /etc/pacman.d/mirrorlist\n")
|
||||
stubs = root / "bin"
|
||||
stubs.mkdir()
|
||||
log = root / "calls.log"
|
||||
for name in ("pacman", "pacman-key"):
|
||||
stub = stubs / name
|
||||
stub.write_text(f'#!/bin/bash\necho "{name} $*" >> "{log}"\n')
|
||||
stub.chmod(0o755)
|
||||
subprocess.run(
|
||||
["bash", str(ROOT / "builder/prepare-regions.sh"), str(regions), str(payload), str(online)],
|
||||
env={**os.environ, "PATH": f"{stubs}:{os.environ['PATH']}"},
|
||||
check=True, capture_output=True, text=True,
|
||||
)
|
||||
return payload, online, log.read_text() if log.exists() else ""
|
||||
|
||||
def test_ships_every_profile_and_bootstraps_its_keyring(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
regions = write_profiles(root)
|
||||
payload, online, calls = self.run_prepare(regions, root)
|
||||
self.assertEqual((payload / "regions/cn/timezones").read_text(), (regions / "cn/timezones").read_text())
|
||||
self.assertIn("[archlinuxcn]", online.read_text())
|
||||
self.assertTrue(online.read_text().startswith("[core]\n"))
|
||||
self.assertEqual(calls.splitlines(), [
|
||||
f"pacman --config {online} --noconfirm -Sy --needed archlinuxcn-keyring",
|
||||
"pacman-key --populate archlinuxcn",
|
||||
])
|
||||
|
||||
def settings_package(self, root: Path, with_regions: bool) -> Path:
|
||||
tree = root / "pkgroot"
|
||||
(tree / "usr/share/omarchy/default/pacman").mkdir(parents=True)
|
||||
if with_regions:
|
||||
write_profiles(root / "src")
|
||||
(root / "src/regions").rename(tree / "usr/share/omarchy/default/regions")
|
||||
package = root / "omarchy-settings-1-1-any.pkg.tar.zst"
|
||||
subprocess.run(["bsdtar", "--zstd", "-cf", str(package), "-C", str(tree), "usr"], check=True)
|
||||
return package
|
||||
|
||||
def test_published_build_reads_profiles_from_the_settings_package(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
payload, online, calls = self.run_prepare(self.settings_package(root, with_regions=True), root)
|
||||
self.assertEqual((payload / "regions/cn/packages").read_text(), "# keyring first\narchlinuxcn-keyring\nextra-tool\n")
|
||||
self.assertIn("[archlinuxcn]", online.read_text())
|
||||
self.assertIn("pacman-key --populate archlinuxcn", calls)
|
||||
|
||||
def test_settings_package_without_profiles_builds_a_global_iso(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
payload, online, calls = self.run_prepare(self.settings_package(root, with_regions=False), root)
|
||||
self.assertFalse((payload / "regions").exists())
|
||||
self.assertEqual(calls, "")
|
||||
|
||||
def test_unreadable_settings_package_fails_the_build(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
package = root / "omarchy-settings-1-1-any.pkg.tar.zst"
|
||||
package.write_text("not an archive")
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
self.run_prepare(package, root)
|
||||
|
||||
def test_runtime_without_profiles_builds_a_global_iso(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
payload, online, calls = self.run_prepare(root / "missing", root)
|
||||
self.assertFalse((payload / "regions").exists())
|
||||
self.assertNotIn("archlinuxcn", online.read_text())
|
||||
self.assertEqual(calls, "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user