From 7727d40c92ba3d805afc8db33522e3e37c08e002 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:48:09 -0400 Subject: [PATCH 01/17] =?UTF-8?q?=E2=9C=A8=20feat(region):=20seed=20offlin?= =?UTF-8?q?e=20chinese=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: make first-login pinyin available without downloads - :keyboard: preserve compose and terminal prefix shortcuts Amp-Thread-ID: https://ampcode.com/threads/T-01a0df38-0ea7-759b-bcff-29da223cc567 Co-authored-by: Amp --- default/regions/cn/packages | 5 +++++ default/regions/cn/pacman/mirrorlist.append | 1 + default/regions/cn/pacman/pacman.conf.append | 2 ++ default/regions/cn/skel/.config/fcitx5/config | 14 ++++++++++++++ default/regions/cn/skel/.config/fcitx5/profile | 12 ++++++++++++ 5 files changed, 34 insertions(+) create mode 100644 default/regions/cn/packages create mode 100644 default/regions/cn/pacman/mirrorlist.append create mode 100644 default/regions/cn/pacman/pacman.conf.append create mode 100644 default/regions/cn/skel/.config/fcitx5/config create mode 100644 default/regions/cn/skel/.config/fcitx5/profile diff --git a/default/regions/cn/packages b/default/regions/cn/packages new file mode 100644 index 00000000..99eeb5be --- /dev/null +++ b/default/regions/cn/packages @@ -0,0 +1,5 @@ +# Installed from the ISO's offline repository before enabling ArchLinuxCN. +archlinuxcn-keyring +# Reuse the base Fcitx5 service and toolkit integration. libime supplies the +# offline Pinyin dictionary/model through this package's dependencies. +fcitx5-chinese-addons diff --git a/default/regions/cn/pacman/mirrorlist.append b/default/regions/cn/pacman/mirrorlist.append new file mode 100644 index 00000000..d0fe979a --- /dev/null +++ b/default/regions/cn/pacman/mirrorlist.append @@ -0,0 +1 @@ +Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch diff --git a/default/regions/cn/pacman/pacman.conf.append b/default/regions/cn/pacman/pacman.conf.append new file mode 100644 index 00000000..1a0ad22c --- /dev/null +++ b/default/regions/cn/pacman/pacman.conf.append @@ -0,0 +1,2 @@ +[archlinuxcn] +Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch diff --git a/default/regions/cn/skel/.config/fcitx5/config b/default/regions/cn/skel/.config/fcitx5/config new file mode 100644 index 00000000..d817f382 --- /dev/null +++ b/default/regions/cn/skel/.config/fcitx5/config @@ -0,0 +1,14 @@ +[Hotkey] +EnumerateWithTriggerKeys=False +# Keep lone Shift and Ctrl+Shift available to applications and CapsLock. +AltTriggerKeys= +EnumerateForwardKeys= +EnumerateBackwardKeys= + +[Hotkey/TriggerKeys] +# Ctrl+Space belongs to tmux/Herdr; CapsLock remains the Compose key. +0=Alt+space + +[Behavior] +# Start in direct input; Alt+Space activates offline Simplified Chinese Pinyin. +ActiveByDefault=False diff --git a/default/regions/cn/skel/.config/fcitx5/profile b/default/regions/cn/skel/.config/fcitx5/profile new file mode 100644 index 00000000..56ba909a --- /dev/null +++ b/default/regions/cn/skel/.config/fcitx5/profile @@ -0,0 +1,12 @@ +[Groups/0] +Name=Default +# Fcitx requires a nonempty group layout. Leave item layouts unset so it uses +# the compositor's keymap; the base xcb.conf also disables XKB overrides. +Default Layout=us +DefaultIM=pinyin + +[Groups/0/Items/0] +Name=keyboard-us + +[Groups/0/Items/1] +Name=pinyin From 48f7c411c06653fce8220f3a0fdd825cb6672a78 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:48:54 -0400 Subject: [PATCH 02/17] =?UTF-8?q?=E2=9C=A8=20feat(pacman):=20preserve=20re?= =?UTF-8?q?gional=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: share regional repository generation across install and refresh - :shield: reject invalid profiles before replacing configuration Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567 --- bin/omarchy-apply-pacman | 53 ++++++++++++++++++++++++++++++++++ bin/omarchy-refresh-pacman | 7 ++--- install/post-install/pacman.sh | 3 +- 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100755 bin/omarchy-apply-pacman diff --git a/bin/omarchy-apply-pacman b/bin/omarchy-apply-pacman new file mode 100755 index 00000000..d5a92234 --- /dev/null +++ b/bin/omarchy-apply-pacman @@ -0,0 +1,53 @@ +#!/bin/bash + +# omarchy:summary=Restore channel and regional pacman defaults in an installed target +# omarchy:args= [target-root] +# omarchy:requires-sudo=true +# omarchy:hidden=true + +set -euo pipefail + +if (( $# < 1 || $# > 2 )); then + echo "Usage: omarchy-apply-pacman [target-root]" >&2 + exit 1 +fi + +channel="$1" +case "$channel" in + stable|rc|edge) ;; + *) echo "Invalid pacman channel: $channel" >&2; exit 1 ;; +esac + +# The ISO writes this plain-text marker before target finalization. Missing +# markers keep existing installations global; never source it as shell code. +etc="${2:-/}/etc" +region=global +if [[ -e $etc/omarchy/region ]]; then + region=$(<"$etc/omarchy/region") +fi +case "$region" in + global|cn) ;; + *) echo "Unsupported region: $region (expected global or cn)" >&2; exit 1 ;; +esac + +# Build both files before replacing either. Explicit refreshes retain backups; +# ordinary updates do not call this command or enforce regional defaults. +staging=$(mktemp -d) +trap 'rm -rf "$staging"' EXIT +cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf" +cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist" +if [[ $region != "global" ]]; then + profile="$OMARCHY_PATH/default/regions/$region/pacman" + printf '\n' >> "$staging/pacman.conf" + cat "$profile/pacman.conf.append" >> "$staging/pacman.conf" + cat "$profile/mirrorlist.append" >> "$staging/mirrorlist" +fi + +mkdir -p "$etc/pacman.d" +for file in pacman.conf pacman.d/mirrorlist; do + if [[ -e $etc/$file ]]; then + cp -f "$etc/$file" "$etc/$file.bak" + fi +done +install -m 644 "$staging/pacman.conf" "$etc/pacman.conf" +install -m 644 "$staging/mirrorlist" "$etc/pacman.d/mirrorlist" diff --git a/bin/omarchy-refresh-pacman b/bin/omarchy-refresh-pacman index 7406e6ad..be2a256d 100755 --- a/bin/omarchy-refresh-pacman +++ b/bin/omarchy-refresh-pacman @@ -30,11 +30,10 @@ if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then exit 2 fi -sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak -sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak echo "Setting channel to $channel" -sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf -sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist +# Restores the channel templates plus this machine's regional additions, and +# backs up both files first. OMARCHY_PATH was verified as this command's root. +sudo env "OMARCHY_PATH=$OMARCHY_PATH" "$OMARCHY_PATH/bin/omarchy-apply-pacman" "$channel" # Allow user customization of /etc/pacman.conf before the upgrade runs, so # custom repositories and IgnorePkg entries shape the downgrade-capable diff --git a/install/post-install/pacman.sh b/install/post-install/pacman.sh index da84975b..bb673353 100644 --- a/install/post-install/pacman.sh +++ b/install/post-install/pacman.sh @@ -1,7 +1,6 @@ # Configure pacman after package installation completes. Offline target package # installs use the live ISO's offline pacman.conf until this final restore. -cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf -cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist +"$OMARCHY_PATH/bin/omarchy-apply-pacman" "${OMARCHY_MIRROR:-stable}" # Wait for CUPS to own the file, the way omarchy-settings does, so pacman does # not turn the override into a .pacnew during ISO package installation. From e989960895160db7da3e5c78d2a73821d45c1643 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:48:54 -0400 Subject: [PATCH 03/17] =?UTF-8?q?=E2=9C=85=20test(region):=20guard=20runti?= =?UTF-8?q?me=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :white_check_mark: protect global behavior and regional mirror ordering - :keyboard: guard personal settings and reserved input shortcuts Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567 --- test/shell.d/region-pacman-test.sh | 138 +++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 test/shell.d/region-pacman-test.sh diff --git a/test/shell.d/region-pacman-test.sh b/test/shell.d/region-pacman-test.sh new file mode 100644 index 00000000..92682e5e --- /dev/null +++ b/test/shell.d/region-pacman-test.sh @@ -0,0 +1,138 @@ +#!/bin/bash + +set -euo pipefail +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +export OMARCHY_PATH="$ROOT" +export TARGET_ROOT="$scratch/target root" +mkdir -p "$TARGET_ROOT/etc/omarchy" + +apply() { "$ROOT/bin/omarchy-apply-pacman" "$1" "$TARGET_ROOT"; } + +for channel in stable rc edge; do + apply "$channel" + cmp "$ROOT/default/pacman/pacman-$channel.conf" "$TARGET_ROOT/etc/pacman.conf" + cmp "$ROOT/default/pacman/mirrorlist-$channel" "$TARGET_ROOT/etc/pacman.d/mirrorlist" +done +pass "missing region preserves all global channel templates byte for byte" + +printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region" +for channel in stable rc edge; do + case "$channel" in + stable) host=stable-mirror.omarchy.org ;; + rc) host=rc-mirror.omarchy.org ;; + edge) host=mirror.omarchy.org ;; + esac + apply "$channel" + expected=$(printf 'Server = https://%s/$repo/os/$arch\nServer = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch' "$host") + [[ $(<"$TARGET_ROOT/etc/pacman.d/mirrorlist") == "$expected" ]] || fail "$channel mirror order and variables" + { + cat "$ROOT/default/pacman/pacman-$channel.conf" + printf '\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n' + } > "$scratch/expected.conf" + cmp "$scratch/expected.conf" "$TARGET_ROOT/etc/pacman.conf" + apply "$channel" + cmp "$TARGET_ROOT/etc/pacman.conf.bak" "$TARGET_ROOT/etc/pacman.conf" + cmp "$TARGET_ROOT/etc/pacman.d/mirrorlist.bak" "$TARGET_ROOT/etc/pacman.d/mirrorlist" +done +pass "China keeps each channel first, appends only CN defaults, and is idempotent" + +cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf" +cp "$TARGET_ROOT/etc/pacman.d/mirrorlist" "$scratch/before.mirrors" +for region in '' CN chn zz '../cn' '$(touch NEVER_EXECUTE)'; do + printf '%s\n' "$region" > "$TARGET_ROOT/etc/omarchy/region" + if apply stable > "$scratch/error" 2>&1; then + fail "invalid region accepted: $region" + fi + cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf" + cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist" +done +pass "unsupported and malformed regions fail before replacing either config" + +printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region" +mkdir -p "$scratch/incomplete/default/regions/cn/pacman" +cp -a "$ROOT/default/pacman" "$scratch/incomplete/default/" +cp "$ROOT/default/regions/cn/pacman/pacman.conf.append" "$scratch/incomplete/default/regions/cn/pacman/" +if OMARCHY_PATH="$scratch/incomplete" apply stable > "$scratch/error" 2>&1; then + fail "incomplete region profile accepted" +fi +cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf" +cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist" +pass "missing fragment cannot leave a half-generated config" + +# Execute the real refresh and apply commands against the same disposable root. +mkdir -p "$scratch/bin" +cat > "$scratch/bin/sudo" <<'SH' +#!/bin/bash +exec "$@" "$TARGET_ROOT" +SH +cat > "$scratch/bin/omarchy-hook" <<'SH' +#!/bin/bash +[[ $1 == "pre-refresh-pacman" ]] || exit 1 +printf '# user customization\n' >> "$TARGET_ROOT/etc/pacman.conf" +SH +cat > "$scratch/bin/omarchy-update-pacman" <<'SH' +#!/bin/bash +[[ $* == "-Syyuu --noconfirm" ]] || exit 1 +tail -n 1 "$TARGET_ROOT/etc/pacman.conf" > "$TARGET_ROOT/update-observed" +SH +chmod +x "$scratch/bin/"* +PATH="$scratch/bin:$PATH" "$ROOT/bin/omarchy-refresh-pacman" rc +[[ $(<"$TARGET_ROOT/update-observed") == '# user customization' ]] || fail "hook runs before update" +rg -qxF 'Server = https://pkgs.omarchy.org/rc/$arch' "$TARGET_ROOT/etc/pacman.conf" +rg -qxF '[archlinuxcn]' "$TARGET_ROOT/etc/pacman.conf" +pass "channel refresh retains region and applies user customization before updating" + +printf 'not-updated\n' > "$TARGET_ROOT/update-observed" +if PATH="$scratch/bin:$PATH" "$ROOT/bin/omarchy-refresh-pacman" dev > "$scratch/error" 2>&1; then + fail "refresh accepts a nonexistent dev pacman template" +fi +[[ $(<"$TARGET_ROOT/update-observed") == 'not-updated' ]] || fail "failed generation still updates packages" +pass "failed refresh does not proceed to a package update" + +printf 'global\n' > "$TARGET_ROOT/etc/omarchy/region" +apply stable +cmp "$ROOT/default/pacman/pacman-stable.conf" "$TARGET_ROOT/etc/pacman.conf" +cmp "$ROOT/default/pacman/mirrorlist-stable" "$TARGET_ROOT/etc/pacman.d/mirrorlist" +pass "users can opt out of regional repository defaults" + +# Check the shipped input defaults, not a synthetic profile. ISO tests cover +# staging these files before user creation; refresh must never touch them. +python3 - <<'PY' +import configparser +import os +from pathlib import Path + +root = Path(os.environ["ROOT"]) +region = root / "default/regions/cn" +packages = {line.strip() for line in (region / "packages").read_text().splitlines() if line.strip() and not line.startswith("#")} +assert packages == {"archlinuxcn-keyring", "fcitx5-chinese-addons"} +base = (root / "install/omarchy-base.packages").read_text().splitlines() +assert {"fcitx5", "fcitx5-gtk", "fcitx5-qt"} <= set(base) + +def read_config(name): + config = configparser.ConfigParser() + config.read(region / "skel/.config/fcitx5" / name) + return config + +profile = read_config("profile") +assert profile["Groups/0"]["DefaultIM"] == "pinyin" +assert profile["Groups/0"]["Default Layout"] # Fcitx rejects empty layouts. +assert profile["Groups/0/Items/0"]["Name"] == "keyboard-us" +assert profile["Groups/0/Items/1"]["Name"] == "pinyin" +assert all("Layout" not in profile[section] for section in ("Groups/0/Items/0", "Groups/0/Items/1")) +assert (root / "config/fcitx5/conf/xcb.conf").read_text().strip() == "Allow Overriding System XKB Settings=False" +config = read_config("config") +assert dict(config["Hotkey/TriggerKeys"]) == {"0": "Alt+space"} +assert not config["Hotkey"].getboolean("EnumerateWithTriggerKeys") +for key in ("AltTriggerKeys", "EnumerateForwardKeys", "EnumerateBackwardKeys"): + assert config["Hotkey"][key] == "" +assert not config["Behavior"].getboolean("ActiveByDefault") +# No second startup path or regional locale/keyboard defaults. +assert {str(path.relative_to(region / "skel")) for path in (region / "skel").rglob("*") if path.is_file()} == { + ".config/fcitx5/config", ".config/fcitx5/profile", +} +PY +pass "China ships offline Pinyin using the base Fcitx service without stealing compose or terminal shortcuts" From 645cfc2a3634400a6fcc12e47a92f3008de1fc58 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:48:54 -0400 Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=93=9D=20docs(region):=20explain=20?= =?UTF-8?q?china=20bootstrap=20choices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: clarify optional defaults, trust requirements, and opt-out behavior - :warning: document snapshot compatibility and release validation limits Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567 --- docs/regions.md | 41 ++++++++++++++++++++++++++++++ plans/china-iso.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 docs/regions.md create mode 100644 plans/china-iso.md diff --git a/docs/regions.md b/docs/regions.md new file mode 100644 index 00000000..d99732b7 --- /dev/null +++ b/docs/regions.md @@ -0,0 +1,41 @@ +# Regional ISO defaults + +Regions customize bootstrap defaults independently of update channels. Country identifiers are lowercase, officially assigned ISO 3166-1 alpha-2 codes. `global` is a reserved value for no country-specific additions. Only implemented profiles are accepted; currently `global` and `cn`. + +## Runtime configuration + +The ISO installer writes the selected region to `/etc/omarchy/region` before target finalization. It is plain text, not executable configuration. A missing file means `global`; an unsupported value is an error. + +`omarchy-apply-pacman [target-root]` restores the selected channel's two pacman templates and appends `default/regions//pacman/{pacman.conf.append,mirrorlist.append}`. Both files are generated before either live file is replaced. Existing files are backed up with `.bak`. Dev uses the edge pacman channel, as before. + +Both installation finalization and `omarchy-refresh-pacman` use this command. Explicit refreshes and channel changes restore regional defaults; ordinary updates do not enforce them. The existing `pre-refresh-pacman` user hook runs after regeneration and before the update, so users can maintain overrides there. Users can edit the files directly or set the region to `global` before a refresh to stop applying regional repository defaults. This does not uninstall packages or rewrite user input-method settings. + +The China profile keeps the channel's Omarchy Arch mirror first, appends USTC's regular Arch mirror, and adds the USTC ArchLinuxCN community repository. It does not change the `[omarchy]` package repository. USTC is not a stable/RC snapshot replica: missing old packages and newer fallback databases remain compatibility risks. + +## ISO builder contract + +The companion `omarchy-iso` repository accepts `--region global|cn`, passes `OMARCHY_REGION` into the build container, and records it in `/root/omarchy_region` on the ISO. Non-global artifact and offline-cache names gain a region suffix. Existing global names remain unchanged. + +Regional content comes from the same runtime used by the ISO: either the `--local-source` checkout or the downloaded runtime package. A China build against a runtime without regional support fails rather than silently producing a global installation. + +- `packages`: additional target packages, one name per line, with blank lines and `#` comments allowed. The builder merges these into the ISO's copy of `omarchy-base.packages`, so they are both included in the offline repository and installed on the target. The source manifest is not modified. +- `pacman/`: repository fragments used by the runtime finalizer. The builder also adds the community repository to its writable online config to obtain regional packages; it does not change its Arch channel mirrors. +- `skel/` (optional): new-user defaults. The ISO installer overlays these onto target `/etc/skel` after the base settings packages and before user creation, including installations that defer user creation until first boot. It never overlays existing home directories. Region selection does not itself set locale, timezone, or keyboard layout. + +For China, the builder installs `archlinuxcn-keyring` using the existing Arch trust chain before downloading other regional packages. Signature or keyring failures abort the build; no unverified keyserver trust or online signature bypass is added. The keyring is also installed on the target from the offline package set, then initialized and populated before online repositories are enabled. The ISO's existing offline repository verification policy is unchanged. + +## Chinese input defaults + +The China profile adds `fcitx5-chinese-addons`. Its `libime` dependency supplies the offline Pinyin dictionary and language model; no cloud service or extra dictionary download is required. Upstream Pinyin defaults to Simplified Chinese with Cloud Pinyin disabled. The base system already includes Fcitx5, GTK/Qt integration, and `omarchy-fcitx5.service`; the region neither starts another daemon nor changes the session environment. + +New users start in direct input. `Alt+Space` toggles Pinyin. `Ctrl+Space` remains available for the shipped tmux and Herdr prefixes; lone Shift and Ctrl+Shift do not switch input methods, and CapsLock remains Compose. Applications may assign their own `Alt+Space` shortcut; users can change the trigger in `~/.config/fcitx5/config`. Selecting `global` later does not remove or reset this personal configuration. + +Fcitx requires a nonempty group layout, so the profile pairs `Default Layout=us` with `keyboard-us`. This is an Fcitx group identifier, not a system keyboard selection: no per-item layout override is set, and the base `fcitx5/conf/xcb.conf` disables overriding system XKB settings. The compositor continues to use the keyboard selected during installation. Non-US layouts still need coverage in the installed Hyprland session. + +Upstream references: [Pinyin registration](https://github.com/fcitx/fcitx5-chinese-addons/blob/master/im/pinyin/pinyin.conf.in), [local dictionary loading](https://github.com/fcitx/fcitx5-chinese-addons/blob/master/im/pinyin/pinyin.cpp), [global hotkey configuration](https://github.com/fcitx/fcitx5/blob/master/src/lib/fcitx/globalconfig.cpp), and [Arch package dependencies](https://archlinux.org/packages/extra/x86_64/fcitx5-chinese-addons/). + +## Remaining release validation + +Unit tests cover region/channel independence, package-list augmentation, keyring failure handling, pre-user defaults, generated pacman contents, repeat application, refresh hook ordering, and the shipped Pinyin configuration. They do not establish live mirror availability, real package trust, or successful offline installation. Before distribution or a Show & Tell announcement, build from matching local checkouts on an x86_64 builder and run the disposable-VM acceptance workflow in `agents/skills/acceptance-tests.md`. + +The China acceptance run must install without network access, boot to a new user, and type Chinese using physical/VM key events in a terminal, Chromium/Electron, fullscreen windows, and the Omarchy menu. Check candidate-window placement, `Alt+Space` in both directions, tmux/Herdr `Ctrl+Space`, lone Shift, CapsLock Compose, and a non-US keyboard. Also check deferred first-boot user creation and preservation of customized input settings through a channel refresh. An ARM64 Ubuntu/X11 Fcitx smoke test cannot replace these installed-system checks. diff --git a/plans/china-iso.md b/plans/china-iso.md new file mode 100644 index 00000000..33adcb0a --- /dev/null +++ b/plans/china-iso.md @@ -0,0 +1,63 @@ +# Proposal: an optional China ISO for easier Omarchy bootstrap + +## Goal + +Offer an optional China ISO with domestic package mirrors and Chinese input ready to use after installation. + +The Great Firewall (GFW) can make access to official mirrors unreliable in China. Domestic mirrors would help users bootstrap Omarchy more reliably. + +**This is a convenient starting point, not a restricted edition.** Anyone can use it, and users remain free to change or remove all regional settings after installation. It introduces no geographic restrictions, service restrictions, or lock-in. + +## What it would include + +### 1. USTC package mirrors + +Keep the selected channel’s Omarchy mirror first and add USTC as a fallback. For stable: + +```ini +# /etc/pacman.d/mirrorlist +Server = https://stable-mirror.omarchy.org/$repo/os/$arch +Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch +``` + +Pacman tries servers in order; see [pacman.conf(5)](https://man.archlinux.org/man/pacman.conf.5.en#REPOSITORY_SECTIONS). USTC is a rolling mirror, not a matching stable/RC snapshot, so fallback compatibility needs validation. + +Also append the ArchLinuxCN community repository: + +```ini +# /etc/pacman.conf +[archlinuxcn] +Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch +``` + +Bundle and initialize its keyring through a verified trust-bootstrap process, keeping package signature verification enabled. The existing `[omarchy]` repository stays unchanged. + +### 2. Preconfigured Chinese input + +Bundle a Chinese input engine, its dependencies, and default configuration in the ISO so the installed system supports Chinese input on first login, without additional downloads or manual setup. + +The engine and switching shortcut are open for discussion. Testing should cover actual typing, physical-key behavior, shortcut conflicts, and candidate windows in terminals and browsers. + +## Build approach + +- Add `--region cn`, independently of the existing stable, RC, edge, and dev options. +- Use lowercase **ISO 3166-1 alpha-2** country codes; reserve `global` for no country-specific customization. +- Keep regional additions separate from channel defaults: repository fragments, extra packages, and new-user configuration. +- Preserve offline installation and retain the chosen region across channel changes. +- Keep regional build caches separate so ordinary ISO builds remain unaffected. + +Region selection would **not force a system language, timezone, or keyboard layout**. Users could change mirrors, remove ArchLinuxCN, replace the input method, or switch channels. Updates should respect user customization rather than repeatedly reapplying regional defaults. + +## Related work + +- [#7858 — A version customized for China](https://github.com/omacom/omarchy/discussions/7858) +- [#8697 — Simplified Chinese system language and stock Pinyin](https://github.com/omacom/omarchy/discussions/8697) +- [#11456 — Out-of-the-box Chinese input](https://github.com/omacom/omarchy/issues/11456) +- [#9695 — Setup > Region toggle](https://github.com/omacom/omarchy/pull/9695) +- [#10052 — Internationalization overview](https://github.com/omacom/omarchy/discussions/10052) + +This proposal focuses on **installation and bootstrap**, not full UI translation. We should reuse applicable input-method work rather than create a competing setup path. + +## Feedback welcome + +Would this approach fit Omarchy? Feedback on the mirror policy, ArchLinuxCN compatibility with stable/RC, and input-method defaults would help shape the implementation. From 123535ea144b50b2f77f78dd0c87b5630faad90e Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 18:25:00 -0400 Subject: [PATCH 05/17] =?UTF-8?q?=F0=9F=90=9B=20fix(channel):=20keep=20reg?= =?UTF-8?q?ional=20repos=20when=20switching=20to=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :shield: refuse a dev checkout that lacks this machine's region profile before linking it, since its own refresh would silently replace the regional pacman files - :white_check_mark: isolate channel tests from the host region marker Co-Authored-By: Claude Opus 5.5 --- bin/omarchy-channel-set | 14 ++++++++++++++ test/shell.d/channel-test.sh | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/bin/omarchy-channel-set b/bin/omarchy-channel-set index 039d3292..f347317b 100755 --- a/bin/omarchy-channel-set +++ b/bin/omarchy-channel-set @@ -47,9 +47,23 @@ validate_dev_checkout() { fi } +# The dev channel refreshes pacman with the checkout's own tools. A checkout +# without this machine's region profile would silently drop its repositories. +validate_dev_region() { + local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global + + [[ -f $region_file ]] && region=$(<"$region_file") + [[ $region == "global" ]] && return 0 + + if [[ ! -f $checkout/bin/omarchy-apply-pacman || ! -d $checkout/default/regions/$region ]]; then + fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults." + fi +} + link_dev_checkout() { local checkout="$1" required [[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout" + validate_dev_region "$checkout" # Check the destination before changing /etc/omarchy.conf or sudo's path. # An existing checkout is not pulled automatically and may predate this policy. diff --git a/test/shell.d/channel-test.sh b/test/shell.d/channel-test.sh index cbbfc6e8..7872c6f7 100644 --- a/test/shell.d/channel-test.sh +++ b/test/shell.d/channel-test.sh @@ -113,6 +113,7 @@ run_channel() { : >"$log_file" OMARCHY_CHANNEL_TEST_LOG="$log_file" \ OMARCHY_PATH="${OMARCHY_TEST_PATH:-$package_root}" \ + OMARCHY_REGION_FILE="${OMARCHY_TEST_REGION_FILE:-$test_tmp/no-region}" \ HOME="$test_tmp/home" \ PATH="$stub_bin:$ROOT/bin:$PATH" \ "${OMARCHY_TEST_PATH:-$package_root}/bin/omarchy-channel-set" "$@" @@ -190,6 +191,21 @@ fi assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "switching back to dev links ~/omarchy" pass "switching back to dev reuses the existing ~/omarchy checkout" +printf 'cn\n' >"$test_tmp/region" +if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then + fail "dev refuses a checkout without this machine's region" +fi +grep -q "does not support the 'cn' region" "$test_tmp/region.err" || fail "dev explains the missing region" "$(cat "$test_tmp/region.err")" +if grep -Eq $'^(link|refresh)\t' "$log_file"; then + fail "dev refuses before linking or refreshing pacman" "$(cat "$log_file")" +fi +pass "dev refuses a checkout without the region before touching pacman" + +mkdir -p "$checkout/default/regions/cn" +touch "$checkout/bin/omarchy-apply-pacman" +OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev +assert_log_line $'refresh\tedge' "dev proceeds with a checkout that supports the region" + current_channel() { OMARCHY_TEST_VERSION_CHANNEL="$1" \ OMARCHY_TEST_PACKAGES="$2" \ From 647979bde7eb0c3ef13c91565bd90d2f8402b93c Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 18:25:01 -0400 Subject: [PATCH 06/17] =?UTF-8?q?=F0=9F=90=9B=20fix(pacman):=20keep=20exis?= =?UTF-8?q?ting=20config=20file=20modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :bug: refresh used to copy over the existing files, so a customized mode must survive the shared regional applicator too Co-Authored-By: Claude Opus 5.5 --- bin/omarchy-apply-pacman | 5 +++-- test/shell.d/region-pacman-test.sh | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bin/omarchy-apply-pacman b/bin/omarchy-apply-pacman index d5a92234..0af0fb0c 100755 --- a/bin/omarchy-apply-pacman +++ b/bin/omarchy-apply-pacman @@ -49,5 +49,6 @@ for file in pacman.conf pacman.d/mirrorlist; do cp -f "$etc/$file" "$etc/$file.bak" fi done -install -m 644 "$staging/pacman.conf" "$etc/pacman.conf" -install -m 644 "$staging/mirrorlist" "$etc/pacman.d/mirrorlist" +# Copying over an existing file keeps its owner and mode, as refresh always has. +cp -f "$staging/pacman.conf" "$etc/pacman.conf" +cp -f "$staging/mirrorlist" "$etc/pacman.d/mirrorlist" diff --git a/test/shell.d/region-pacman-test.sh b/test/shell.d/region-pacman-test.sh index 92682e5e..2d0eb25a 100644 --- a/test/shell.d/region-pacman-test.sh +++ b/test/shell.d/region-pacman-test.sh @@ -18,6 +18,12 @@ for channel in stable rc edge; do done pass "missing region preserves all global channel templates byte for byte" +chmod 600 "$TARGET_ROOT/etc/pacman.conf" +apply stable +[[ $(stat -c %a "$TARGET_ROOT/etc/pacman.conf") == 600 ]] || fail "refresh keeps a customized pacman.conf mode" +chmod 644 "$TARGET_ROOT/etc/pacman.conf" +pass "refresh keeps existing config file modes" + printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region" for channel in stable rc edge; do case "$channel" in From 87a29312097e8bae99f5b150685e786f9d281f7c Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 18:49:30 -0400 Subject: [PATCH 07/17] =?UTF-8?q?=F0=9F=93=9D=20docs(region):=20drop=20the?= =?UTF-8?q?=20unreachable=20menu=20input=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: the menu bypasses the input method, so requiring Pinyin there made the release procedure impossible until upstream fixes it - :memo: record the required `skel` profile and the dev-switch region check Co-Authored-By: Claude Opus 5.5 --- docs/regions.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/regions.md b/docs/regions.md index d99732b7..725d6163 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -8,7 +8,7 @@ The ISO installer writes the selected region to `/etc/omarchy/region` before tar `omarchy-apply-pacman [target-root]` restores the selected channel's two pacman templates and appends `default/regions//pacman/{pacman.conf.append,mirrorlist.append}`. Both files are generated before either live file is replaced. Existing files are backed up with `.bak`. Dev uses the edge pacman channel, as before. -Both installation finalization and `omarchy-refresh-pacman` use this command. Explicit refreshes and channel changes restore regional defaults; ordinary updates do not enforce them. The existing `pre-refresh-pacman` user hook runs after regeneration and before the update, so users can maintain overrides there. Users can edit the files directly or set the region to `global` before a refresh to stop applying regional repository defaults. This does not uninstall packages or rewrite user input-method settings. +Both installation finalization and `omarchy-refresh-pacman` use this command. Explicit refreshes and channel changes restore regional defaults; ordinary updates do not enforce them. The existing `pre-refresh-pacman` user hook runs after regeneration and before the update, so users can maintain overrides there. Switching to the dev channel refuses a checkout that lacks this machine's region profile, because that checkout's own refresh would otherwise drop the regional repositories. Users can edit the files directly or set the region to `global` before a refresh to stop applying regional repository defaults. This does not uninstall packages or rewrite user input-method settings. The China profile keeps the channel's Omarchy Arch mirror first, appends USTC's regular Arch mirror, and adds the USTC ArchLinuxCN community repository. It does not change the `[omarchy]` package repository. USTC is not a stable/RC snapshot replica: missing old packages and newer fallback databases remain compatibility risks. @@ -20,7 +20,7 @@ Regional content comes from the same runtime used by the ISO: either the `--loca - `packages`: additional target packages, one name per line, with blank lines and `#` comments allowed. The builder merges these into the ISO's copy of `omarchy-base.packages`, so they are both included in the offline repository and installed on the target. The source manifest is not modified. - `pacman/`: repository fragments used by the runtime finalizer. The builder also adds the community repository to its writable online config to obtain regional packages; it does not change its Arch channel mirrors. -- `skel/` (optional): new-user defaults. The ISO installer overlays these onto target `/etc/skel` after the base settings packages and before user creation, including installations that defer user creation until first boot. It never overlays existing home directories. Region selection does not itself set locale, timezone, or keyboard layout. +- `skel/`: new-user defaults. Required: a regional build fails without it rather than shipping an ISO that lacks the advertised defaults. The ISO installer overlays these onto target `/etc/skel` after the base settings packages and before user creation, including installations that defer user creation until first boot. It never overlays existing home directories. Region selection does not itself set locale, timezone, or keyboard layout. For China, the builder installs `archlinuxcn-keyring` using the existing Arch trust chain before downloading other regional packages. Signature or keyring failures abort the build; no unverified keyserver trust or online signature bypass is added. The keyring is also installed on the target from the offline package set, then initialized and populated before online repositories are enabled. The ISO's existing offline repository verification policy is unchanged. @@ -38,4 +38,6 @@ Upstream references: [Pinyin registration](https://github.com/fcitx/fcitx5-chine Unit tests cover region/channel independence, package-list augmentation, keyring failure handling, pre-user defaults, generated pacman contents, repeat application, refresh hook ordering, and the shipped Pinyin configuration. They do not establish live mirror availability, real package trust, or successful offline installation. Before distribution or a Show & Tell announcement, build from matching local checkouts on an x86_64 builder and run the disposable-VM acceptance workflow in `agents/skills/acceptance-tests.md`. -The China acceptance run must install without network access, boot to a new user, and type Chinese using physical/VM key events in a terminal, Chromium/Electron, fullscreen windows, and the Omarchy menu. Check candidate-window placement, `Alt+Space` in both directions, tmux/Herdr `Ctrl+Space`, lone Shift, CapsLock Compose, and a non-US keyboard. Also check deferred first-boot user creation and preservation of customized input settings through a channel refresh. An ARM64 Ubuntu/X11 Fcitx smoke test cannot replace these installed-system checks. +The China acceptance run must install without network access, boot to a new user, and type Chinese using physical/VM key events in a terminal, Chromium/Electron, and fullscreen windows. Check candidate-window placement, `Alt+Space` in both directions, tmux/Herdr `Ctrl+Space`, lone Shift, CapsLock Compose, and a non-US keyboard. Also check deferred first-boot user creation and preservation of customized input settings through a channel refresh. An ARM64 Ubuntu/X11 Fcitx smoke test cannot replace these installed-system checks. + +The Omarchy menu is not part of this gate. It handles key presses itself instead of using an input-method-aware text field, so it cannot accept Pinyin composition until upstream [omacom/omarchy#11682](https://github.com/omacom/omarchy/issues/11682) is fixed. From d7f17a0424002411bf3b1f974dfd6e44cef74cbb Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 19:39:28 -0400 Subject: [PATCH 08/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(pacman):=20?= =?UTF-8?q?render=20regional=20config=20without=20root?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :lock: upstream refresh now gives root only command-scoped copies, so regional files are rendered as the caller and installed with the same four `sudo cp` calls - :white_check_mark: exercise refresh only inside the sudo-boundary sandbox; the old host-level test reached real sudo once refresh pinned its PATH Co-Authored-By: Claude Opus 5.5 --- bin/omarchy-apply-pacman | 31 ++++++++++++--- bin/omarchy-refresh-pacman | 13 ++++-- test/shell.d/channel-sudo-boundary-test.sh | 1 + test/shell.d/region-pacman-test.sh | 41 +++++++------------ test/shell.d/region-refresh-boundary-test.sh | 42 ++++++++++++++++++++ test/shell.d/update-hook-security-test.sh | 1 + 6 files changed, 93 insertions(+), 36 deletions(-) create mode 100755 test/shell.d/region-refresh-boundary-test.sh diff --git a/bin/omarchy-apply-pacman b/bin/omarchy-apply-pacman index 0af0fb0c..e004637d 100755 --- a/bin/omarchy-apply-pacman +++ b/bin/omarchy-apply-pacman @@ -1,18 +1,33 @@ #!/bin/bash # omarchy:summary=Restore channel and regional pacman defaults in an installed target -# omarchy:args= [target-root] +# omarchy:args= [target-root] | --render [target-root] # omarchy:requires-sudo=true # omarchy:hidden=true set -euo pipefail -if (( $# < 1 || $# > 2 )); then +usage() { echo "Usage: omarchy-apply-pacman [target-root]" >&2 + echo " omarchy-apply-pacman --render [target-root]" >&2 exit 1 +} + +# --render only builds the two files, so an unprivileged caller can hand root +# nothing but plain copies of the finished result. +render_dir="" +if [[ ${1:-} == "--render" ]]; then + (( $# == 3 || $# == 4 )) || usage + channel="$2" + render_dir="$3" + [[ -d $render_dir ]] || { echo "Render directory not found: $render_dir" >&2; exit 1; } + etc="${4:-/}/etc" +else + (( $# >= 1 && $# <= 2 )) || usage + channel="$1" + etc="${2:-/}/etc" fi -channel="$1" case "$channel" in stable|rc|edge) ;; *) echo "Invalid pacman channel: $channel" >&2; exit 1 ;; @@ -20,7 +35,6 @@ esac # The ISO writes this plain-text marker before target finalization. Missing # markers keep existing installations global; never source it as shell code. -etc="${2:-/}/etc" region=global if [[ -e $etc/omarchy/region ]]; then region=$(<"$etc/omarchy/region") @@ -32,8 +46,12 @@ esac # Build both files before replacing either. Explicit refreshes retain backups; # ordinary updates do not call this command or enforce regional defaults. -staging=$(mktemp -d) -trap 'rm -rf "$staging"' EXIT +if [[ -n $render_dir ]]; then + staging="$render_dir" +else + staging=$(mktemp -d) + trap 'rm -rf "$staging"' EXIT +fi cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf" cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist" if [[ $region != "global" ]]; then @@ -42,6 +60,7 @@ if [[ $region != "global" ]]; then cat "$profile/pacman.conf.append" >> "$staging/pacman.conf" cat "$profile/mirrorlist.append" >> "$staging/mirrorlist" fi +[[ -z $render_dir ]] || exit 0 mkdir -p "$etc/pacman.d" for file in pacman.conf pacman.d/mirrorlist; do diff --git a/bin/omarchy-refresh-pacman b/bin/omarchy-refresh-pacman index be2a256d..80f3c356 100755 --- a/bin/omarchy-refresh-pacman +++ b/bin/omarchy-refresh-pacman @@ -30,10 +30,17 @@ if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then exit 2 fi +# Render the channel templates plus this machine's regional additions as the +# caller, so root only copies finished files. +rendered=$(mktemp -d) +"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered" + +sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak +sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak echo "Setting channel to $channel" -# Restores the channel templates plus this machine's regional additions, and -# backs up both files first. OMARCHY_PATH was verified as this command's root. -sudo env "OMARCHY_PATH=$OMARCHY_PATH" "$OMARCHY_PATH/bin/omarchy-apply-pacman" "$channel" +sudo cp -f "$rendered/pacman.conf" /etc/pacman.conf +sudo cp -f "$rendered/mirrorlist" /etc/pacman.d/mirrorlist +rm -rf "$rendered" # Allow user customization of /etc/pacman.conf before the upgrade runs, so # custom repositories and IgnorePkg entries shape the downgrade-capable diff --git a/test/shell.d/channel-sudo-boundary-test.sh b/test/shell.d/channel-sudo-boundary-test.sh index f98934c2..7fe15ded 100755 --- a/test/shell.d/channel-sudo-boundary-test.sh +++ b/test/shell.d/channel-sudo-boundary-test.sh @@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh" copy_boundary_file bin/omarchy-channel-set copy_boundary_file bin/omarchy-refresh-pacman +copy_boundary_file bin/omarchy-apply-pacman copy_boundary_file bin/omarchy-update export OMARCHY_UPDATE_LOGGED=1 diff --git a/test/shell.d/region-pacman-test.sh b/test/shell.d/region-pacman-test.sh index 2d0eb25a..c15d6d63 100644 --- a/test/shell.d/region-pacman-test.sh +++ b/test/shell.d/region-pacman-test.sh @@ -68,35 +68,22 @@ cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf" cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist" pass "missing fragment cannot leave a half-generated config" -# Execute the real refresh and apply commands against the same disposable root. -mkdir -p "$scratch/bin" -cat > "$scratch/bin/sudo" <<'SH' -#!/bin/bash -exec "$@" "$TARGET_ROOT" -SH -cat > "$scratch/bin/omarchy-hook" <<'SH' -#!/bin/bash -[[ $1 == "pre-refresh-pacman" ]] || exit 1 -printf '# user customization\n' >> "$TARGET_ROOT/etc/pacman.conf" -SH -cat > "$scratch/bin/omarchy-update-pacman" <<'SH' -#!/bin/bash -[[ $* == "-Syyuu --noconfirm" ]] || exit 1 -tail -n 1 "$TARGET_ROOT/etc/pacman.conf" > "$TARGET_ROOT/update-observed" -SH -chmod +x "$scratch/bin/"* -PATH="$scratch/bin:$PATH" "$ROOT/bin/omarchy-refresh-pacman" rc -[[ $(<"$TARGET_ROOT/update-observed") == '# user customization' ]] || fail "hook runs before update" -rg -qxF 'Server = https://pkgs.omarchy.org/rc/$arch' "$TARGET_ROOT/etc/pacman.conf" -rg -qxF '[archlinuxcn]' "$TARGET_ROOT/etc/pacman.conf" -pass "channel refresh retains region and applies user customization before updating" +# Refresh renders these files unprivileged and hands root plain copies; its +# privileged path is covered in the sudo-boundary sandbox, never on the host. +cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf" +mkdir -p "$scratch/rendered" +"$ROOT/bin/omarchy-apply-pacman" --render rc "$scratch/rendered" "$TARGET_ROOT" +rg -qxF 'Server = https://pkgs.omarchy.org/rc/$arch' "$scratch/rendered/pacman.conf" +rg -qxF '[archlinuxcn]' "$scratch/rendered/pacman.conf" +[[ $(<"$scratch/rendered/mirrorlist") == $'Server = https://rc-mirror.omarchy.org/$repo/os/$arch\nServer = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch' ]] || + fail "render keeps the channel mirror first" +cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf" +pass "render builds the regional channel files without touching the target" -printf 'not-updated\n' > "$TARGET_ROOT/update-observed" -if PATH="$scratch/bin:$PATH" "$ROOT/bin/omarchy-refresh-pacman" dev > "$scratch/error" 2>&1; then - fail "refresh accepts a nonexistent dev pacman template" +if "$ROOT/bin/omarchy-apply-pacman" --render dev "$scratch/rendered" "$TARGET_ROOT" > "$scratch/error" 2>&1; then + fail "render accepts a nonexistent dev pacman template" fi -[[ $(<"$TARGET_ROOT/update-observed") == 'not-updated' ]] || fail "failed generation still updates packages" -pass "failed refresh does not proceed to a package update" +pass "render rejects channels without pacman templates" printf 'global\n' > "$TARGET_ROOT/etc/omarchy/region" apply stable diff --git a/test/shell.d/region-refresh-boundary-test.sh b/test/shell.d/region-refresh-boundary-test.sh new file mode 100755 index 00000000..b3b2f346 --- /dev/null +++ b/test/shell.d/region-refresh-boundary-test.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" +source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh" +copy_boundary_file bin/omarchy-refresh-pacman +copy_boundary_file bin/omarchy-apply-pacman + +# Regional rendering must stay unprivileged: root only copies finished files, +# exactly as the channel refresh did before regions existed. +reset_boundary +"$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" rc >"$boundary_tmp/output" 2>&1 || fail "refresh failed" "$(<"$boundary_tmp/output")" +assert_boundary_cold "regional refresh" +python3 - "$SUDO_TEST_LOG" "$SUDO_TEST_ROOT" <<'PY' +import re +import sys +events = open(sys.argv[1]).read().splitlines() +root = sys.argv[2] +render = [i for i, e in enumerate(events) if e.startswith('step:cp ' + root + '/default/pacman/pacman-rc.conf ')] +assert len(render) == 1, events +rendered = events[render[0]].split()[-1].rsplit('/', 1)[0] +copies = [i for i, e in enumerate(events) if e.startswith('sudo -N cp ')] +assert [events[i] for i in copies[2:]] == [ + f'sudo -N cp -f {rendered}/pacman.conf /etc/pacman.conf', + f'sudo -N cp -f {rendered}/mirrorlist /etc/pacman.d/mirrorlist', +], events +assert not any(e.startswith('sudo ') and 'omarchy-apply-pacman' in e for e in events), events +assert render[0] < copies[0], events +transaction = next(i for i, e in enumerate(events) if e.startswith('step:pacman ')) +assert max(copies) < transaction, events +PY +pass "refresh renders regional config unprivileged and gives root only plain copies" + +reset_boundary +if "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" dev >"$boundary_tmp/output" 2>&1; then + fail "refresh accepts an invalid channel" +fi +if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then + fail "invalid channel reached privileged work" "$(<"$SUDO_TEST_LOG")" +fi +pass "failed refresh does not proceed to privileged copies or a package update" diff --git a/test/shell.d/update-hook-security-test.sh b/test/shell.d/update-hook-security-test.sh index af7ec70a..243254cf 100755 --- a/test/shell.d/update-hook-security-test.sh +++ b/test/shell.d/update-hook-security-test.sh @@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh" copy_boundary_file bin/omarchy-update copy_boundary_file bin/omarchy-refresh-pacman +copy_boundary_file bin/omarchy-apply-pacman # Replace the step symlink, preserving the real fixture dispatcher. rm "$SUDO_TEST_ROOT/bin/omarchy-update-aur-pkgs" copy_boundary_file bin/omarchy-update-aur-pkgs From d024c82a9ddbaa39f02d92c618aadf1e08feb954 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 21:52:57 -0400 Subject: [PATCH 09/17] =?UTF-8?q?=F0=9F=93=9D=20docs(plans):=20move=20the?= =?UTF-8?q?=20china=20proposal=20to=20a=20discussion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: upstream plans are implementation plans; the open questions belong in the discussion that links these changes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr --- plans/china-iso.md | 63 ---------------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 plans/china-iso.md diff --git a/plans/china-iso.md b/plans/china-iso.md deleted file mode 100644 index 33adcb0a..00000000 --- a/plans/china-iso.md +++ /dev/null @@ -1,63 +0,0 @@ -# Proposal: an optional China ISO for easier Omarchy bootstrap - -## Goal - -Offer an optional China ISO with domestic package mirrors and Chinese input ready to use after installation. - -The Great Firewall (GFW) can make access to official mirrors unreliable in China. Domestic mirrors would help users bootstrap Omarchy more reliably. - -**This is a convenient starting point, not a restricted edition.** Anyone can use it, and users remain free to change or remove all regional settings after installation. It introduces no geographic restrictions, service restrictions, or lock-in. - -## What it would include - -### 1. USTC package mirrors - -Keep the selected channel’s Omarchy mirror first and add USTC as a fallback. For stable: - -```ini -# /etc/pacman.d/mirrorlist -Server = https://stable-mirror.omarchy.org/$repo/os/$arch -Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch -``` - -Pacman tries servers in order; see [pacman.conf(5)](https://man.archlinux.org/man/pacman.conf.5.en#REPOSITORY_SECTIONS). USTC is a rolling mirror, not a matching stable/RC snapshot, so fallback compatibility needs validation. - -Also append the ArchLinuxCN community repository: - -```ini -# /etc/pacman.conf -[archlinuxcn] -Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch -``` - -Bundle and initialize its keyring through a verified trust-bootstrap process, keeping package signature verification enabled. The existing `[omarchy]` repository stays unchanged. - -### 2. Preconfigured Chinese input - -Bundle a Chinese input engine, its dependencies, and default configuration in the ISO so the installed system supports Chinese input on first login, without additional downloads or manual setup. - -The engine and switching shortcut are open for discussion. Testing should cover actual typing, physical-key behavior, shortcut conflicts, and candidate windows in terminals and browsers. - -## Build approach - -- Add `--region cn`, independently of the existing stable, RC, edge, and dev options. -- Use lowercase **ISO 3166-1 alpha-2** country codes; reserve `global` for no country-specific customization. -- Keep regional additions separate from channel defaults: repository fragments, extra packages, and new-user configuration. -- Preserve offline installation and retain the chosen region across channel changes. -- Keep regional build caches separate so ordinary ISO builds remain unaffected. - -Region selection would **not force a system language, timezone, or keyboard layout**. Users could change mirrors, remove ArchLinuxCN, replace the input method, or switch channels. Updates should respect user customization rather than repeatedly reapplying regional defaults. - -## Related work - -- [#7858 — A version customized for China](https://github.com/omacom/omarchy/discussions/7858) -- [#8697 — Simplified Chinese system language and stock Pinyin](https://github.com/omacom/omarchy/discussions/8697) -- [#11456 — Out-of-the-box Chinese input](https://github.com/omacom/omarchy/issues/11456) -- [#9695 — Setup > Region toggle](https://github.com/omacom/omarchy/pull/9695) -- [#10052 — Internationalization overview](https://github.com/omacom/omarchy/discussions/10052) - -This proposal focuses on **installation and bootstrap**, not full UI translation. We should reuse applicable input-method work rather than create a competing setup path. - -## Feedback welcome - -Would this approach fit Omarchy? Feedback on the mirror policy, ArchLinuxCN compatibility with stable/RC, and input-method defaults would help shape the implementation. From d5ef1c1d15131cd3626fdf4f4c7bf6d4d824f7a7 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 22:56:12 -0400 Subject: [PATCH 10/17] =?UTF-8?q?=F0=9F=94=A5=20refactor(region):=20drop?= =?UTF-8?q?=20input=20defaults=20from=20the=20cn=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :fire: keep this change to the regional build architecture; Chinese input gets its own proposal instead of fixing one engine here - :white_check_mark: assert the shipped profile adds only its repository keyring Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr --- default/regions/cn/packages | 3 -- default/regions/cn/skel/.config/fcitx5/config | 14 ------- .../regions/cn/skel/.config/fcitx5/profile | 12 ------ test/shell.d/region-pacman-test.sh | 38 +++---------------- 4 files changed, 6 insertions(+), 61 deletions(-) delete mode 100644 default/regions/cn/skel/.config/fcitx5/config delete mode 100644 default/regions/cn/skel/.config/fcitx5/profile diff --git a/default/regions/cn/packages b/default/regions/cn/packages index 99eeb5be..098d137e 100644 --- a/default/regions/cn/packages +++ b/default/regions/cn/packages @@ -1,5 +1,2 @@ # Installed from the ISO's offline repository before enabling ArchLinuxCN. archlinuxcn-keyring -# Reuse the base Fcitx5 service and toolkit integration. libime supplies the -# offline Pinyin dictionary/model through this package's dependencies. -fcitx5-chinese-addons diff --git a/default/regions/cn/skel/.config/fcitx5/config b/default/regions/cn/skel/.config/fcitx5/config deleted file mode 100644 index d817f382..00000000 --- a/default/regions/cn/skel/.config/fcitx5/config +++ /dev/null @@ -1,14 +0,0 @@ -[Hotkey] -EnumerateWithTriggerKeys=False -# Keep lone Shift and Ctrl+Shift available to applications and CapsLock. -AltTriggerKeys= -EnumerateForwardKeys= -EnumerateBackwardKeys= - -[Hotkey/TriggerKeys] -# Ctrl+Space belongs to tmux/Herdr; CapsLock remains the Compose key. -0=Alt+space - -[Behavior] -# Start in direct input; Alt+Space activates offline Simplified Chinese Pinyin. -ActiveByDefault=False diff --git a/default/regions/cn/skel/.config/fcitx5/profile b/default/regions/cn/skel/.config/fcitx5/profile deleted file mode 100644 index 56ba909a..00000000 --- a/default/regions/cn/skel/.config/fcitx5/profile +++ /dev/null @@ -1,12 +0,0 @@ -[Groups/0] -Name=Default -# Fcitx requires a nonempty group layout. Leave item layouts unset so it uses -# the compositor's keymap; the base xcb.conf also disables XKB overrides. -Default Layout=us -DefaultIM=pinyin - -[Groups/0/Items/0] -Name=keyboard-us - -[Groups/0/Items/1] -Name=pinyin diff --git a/test/shell.d/region-pacman-test.sh b/test/shell.d/region-pacman-test.sh index c15d6d63..d183e674 100644 --- a/test/shell.d/region-pacman-test.sh +++ b/test/shell.d/region-pacman-test.sh @@ -91,41 +91,15 @@ cmp "$ROOT/default/pacman/pacman-stable.conf" "$TARGET_ROOT/etc/pacman.conf" cmp "$ROOT/default/pacman/mirrorlist-stable" "$TARGET_ROOT/etc/pacman.d/mirrorlist" pass "users can opt out of regional repository defaults" -# Check the shipped input defaults, not a synthetic profile. ISO tests cover -# staging these files before user creation; refresh must never touch them. +# The shipped China profile is repository scaffolding only. Locale, timezone, +# and input defaults are separate changes built on the same mechanism. python3 - <<'PY' -import configparser import os from pathlib import Path -root = Path(os.environ["ROOT"]) -region = root / "default/regions/cn" +region = Path(os.environ["ROOT"]) / "default/regions/cn" packages = {line.strip() for line in (region / "packages").read_text().splitlines() if line.strip() and not line.startswith("#")} -assert packages == {"archlinuxcn-keyring", "fcitx5-chinese-addons"} -base = (root / "install/omarchy-base.packages").read_text().splitlines() -assert {"fcitx5", "fcitx5-gtk", "fcitx5-qt"} <= set(base) - -def read_config(name): - config = configparser.ConfigParser() - config.read(region / "skel/.config/fcitx5" / name) - return config - -profile = read_config("profile") -assert profile["Groups/0"]["DefaultIM"] == "pinyin" -assert profile["Groups/0"]["Default Layout"] # Fcitx rejects empty layouts. -assert profile["Groups/0/Items/0"]["Name"] == "keyboard-us" -assert profile["Groups/0/Items/1"]["Name"] == "pinyin" -assert all("Layout" not in profile[section] for section in ("Groups/0/Items/0", "Groups/0/Items/1")) -assert (root / "config/fcitx5/conf/xcb.conf").read_text().strip() == "Allow Overriding System XKB Settings=False" -config = read_config("config") -assert dict(config["Hotkey/TriggerKeys"]) == {"0": "Alt+space"} -assert not config["Hotkey"].getboolean("EnumerateWithTriggerKeys") -for key in ("AltTriggerKeys", "EnumerateForwardKeys", "EnumerateBackwardKeys"): - assert config["Hotkey"][key] == "" -assert not config["Behavior"].getboolean("ActiveByDefault") -# No second startup path or regional locale/keyboard defaults. -assert {str(path.relative_to(region / "skel")) for path in (region / "skel").rglob("*") if path.is_file()} == { - ".config/fcitx5/config", ".config/fcitx5/profile", -} +assert packages == {"archlinuxcn-keyring"}, packages +assert not (region / "skel").exists() PY -pass "China ships offline Pinyin using the base Fcitx service without stealing compose or terminal shortcuts" +pass "China profile adds only its repository keyring" From 50a26be6a789ff2524eaec314680078bae609f3d Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 22:56:12 -0400 Subject: [PATCH 11/17] =?UTF-8?q?=F0=9F=93=9D=20docs(region):=20scope=20th?= =?UTF-8?q?e=20cn=20profile=20to=20repositories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: frame USTC and ArchLinuxCN as a bridge until an official China mirror exists - :memo: record that language, timezone, and input defaults follow as separate changes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr --- docs/regions.md | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/docs/regions.md b/docs/regions.md index 725d6163..2bbf3d59 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -8,9 +8,9 @@ The ISO installer writes the selected region to `/etc/omarchy/region` before tar `omarchy-apply-pacman [target-root]` restores the selected channel's two pacman templates and appends `default/regions//pacman/{pacman.conf.append,mirrorlist.append}`. Both files are generated before either live file is replaced. Existing files are backed up with `.bak`. Dev uses the edge pacman channel, as before. -Both installation finalization and `omarchy-refresh-pacman` use this command. Explicit refreshes and channel changes restore regional defaults; ordinary updates do not enforce them. The existing `pre-refresh-pacman` user hook runs after regeneration and before the update, so users can maintain overrides there. Switching to the dev channel refuses a checkout that lacks this machine's region profile, because that checkout's own refresh would otherwise drop the regional repositories. Users can edit the files directly or set the region to `global` before a refresh to stop applying regional repository defaults. This does not uninstall packages or rewrite user input-method settings. +Both installation finalization and `omarchy-refresh-pacman` use this command. Explicit refreshes and channel changes restore regional defaults; ordinary updates do not enforce them. The existing `pre-refresh-pacman` user hook runs after regeneration and before the update, so users can maintain overrides there. Switching to the dev channel refuses a checkout that lacks this machine's region profile, because that checkout's own refresh would otherwise drop the regional repositories. Users can edit the files directly or set the region to `global` before a refresh to stop applying regional repository defaults. This does not uninstall packages or rewrite anything in existing home directories. -The China profile keeps the channel's Omarchy Arch mirror first, appends USTC's regular Arch mirror, and adds the USTC ArchLinuxCN community repository. It does not change the `[omarchy]` package repository. USTC is not a stable/RC snapshot replica: missing old packages and newer fallback databases remain compatibility risks. +The China profile keeps the channel's Omarchy Arch mirror first, appends USTC's regular Arch mirror, and adds the USTC ArchLinuxCN community repository. It does not change the `[omarchy]` package repository. USTC's mirror, run by the University of Science and Technology of China, and ArchLinuxCN, a long-standing community repository, are a practical bridge until Omarchy runs its own mirror in China. USTC is not a stable/RC snapshot replica: missing old packages and newer fallback databases remain compatibility risks. ## ISO builder contract @@ -20,24 +20,16 @@ Regional content comes from the same runtime used by the ISO: either the `--loca - `packages`: additional target packages, one name per line, with blank lines and `#` comments allowed. The builder merges these into the ISO's copy of `omarchy-base.packages`, so they are both included in the offline repository and installed on the target. The source manifest is not modified. - `pacman/`: repository fragments used by the runtime finalizer. The builder also adds the community repository to its writable online config to obtain regional packages; it does not change its Arch channel mirrors. -- `skel/`: new-user defaults. Required: a regional build fails without it rather than shipping an ISO that lacks the advertised defaults. The ISO installer overlays these onto target `/etc/skel` after the base settings packages and before user creation, including installations that defer user creation until first boot. It never overlays existing home directories. Region selection does not itself set locale, timezone, or keyboard layout. +- `skel/` (optional): new-user defaults. The ISO installer overlays these onto target `/etc/skel` after the base settings packages and before user creation, including installations that defer user creation until first boot. It never overlays existing home directories. For China, the builder installs `archlinuxcn-keyring` using the existing Arch trust chain before downloading other regional packages. Signature or keyring failures abort the build; no unverified keyserver trust or online signature bypass is added. The keyring is also installed on the target from the offline package set, then initialized and populated before online repositories are enabled. The ISO's existing offline repository verification policy is unchanged. -## Chinese input defaults +## Scope of the China profile -The China profile adds `fcitx5-chinese-addons`. Its `libime` dependency supplies the offline Pinyin dictionary and language model; no cloud service or extra dictionary download is required. Upstream Pinyin defaults to Simplified Chinese with Cloud Pinyin disabled. The base system already includes Fcitx5, GTK/Qt integration, and `omarchy-fcitx5.service`; the region neither starts another daemon nor changes the session environment. - -New users start in direct input. `Alt+Space` toggles Pinyin. `Ctrl+Space` remains available for the shipped tmux and Herdr prefixes; lone Shift and Ctrl+Shift do not switch input methods, and CapsLock remains Compose. Applications may assign their own `Alt+Space` shortcut; users can change the trigger in `~/.config/fcitx5/config`. Selecting `global` later does not remove or reset this personal configuration. - -Fcitx requires a nonempty group layout, so the profile pairs `Default Layout=us` with `keyboard-us`. This is an Fcitx group identifier, not a system keyboard selection: no per-item layout override is set, and the base `fcitx5/conf/xcb.conf` disables overriding system XKB settings. The compositor continues to use the keyboard selected during installation. Non-US layouts still need coverage in the installed Hyprland session. - -Upstream references: [Pinyin registration](https://github.com/fcitx/fcitx5-chinese-addons/blob/master/im/pinyin/pinyin.conf.in), [local dictionary loading](https://github.com/fcitx/fcitx5-chinese-addons/blob/master/im/pinyin/pinyin.cpp), [global hotkey configuration](https://github.com/fcitx/fcitx5/blob/master/src/lib/fcitx/globalconfig.cpp), and [Arch package dependencies](https://archlinux.org/packages/extra/x86_64/fcitx5-chinese-addons/). +The China profile currently provides repository defaults only. System language (Simplified Chinese), timezone (Asia/Shanghai), and input-method defaults are planned as separate changes on top of this mechanism: `packages` and `skel/` carry them into the ISO, and the installer stages `skel/` before any user exists. ## Remaining release validation -Unit tests cover region/channel independence, package-list augmentation, keyring failure handling, pre-user defaults, generated pacman contents, repeat application, refresh hook ordering, and the shipped Pinyin configuration. They do not establish live mirror availability, real package trust, or successful offline installation. Before distribution or a Show & Tell announcement, build from matching local checkouts on an x86_64 builder and run the disposable-VM acceptance workflow in `agents/skills/acceptance-tests.md`. +Unit tests cover region/channel independence, package-list augmentation, keyring failure handling, pre-user defaults, generated pacman contents, repeat application, and refresh ordering; refresh itself is exercised only inside the sudo-boundary sandbox. They do not establish live mirror availability, real package trust, or successful offline installation. Before distribution, build from matching local checkouts on an x86_64 builder and run the disposable-VM acceptance workflow in `agents/skills/acceptance-tests.md`. -The China acceptance run must install without network access, boot to a new user, and type Chinese using physical/VM key events in a terminal, Chromium/Electron, and fullscreen windows. Check candidate-window placement, `Alt+Space` in both directions, tmux/Herdr `Ctrl+Space`, lone Shift, CapsLock Compose, and a non-US keyboard. Also check deferred first-boot user creation and preservation of customized input settings through a channel refresh. An ARM64 Ubuntu/X11 Fcitx smoke test cannot replace these installed-system checks. - -The Omarchy menu is not part of this gate. It handles key presses itself instead of using an input-method-aware text field, so it cannot accept Pinyin composition until upstream [omacom/omarchy#11682](https://github.com/omacom/omarchy/issues/11682) is fixed. +The China acceptance run must install without network access, boot to a new user, including deferred first-boot user creation, and keep the regional mirrors, repository, marker, and keyring trust through `stable`, `rc`, and `edge` refreshes. From 6be88d14a503ea54f681f3f42234efef0a495cd3 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Tue, 6 Oct 2026 21:58:48 -0400 Subject: [PATCH 12/17] =?UTF-8?q?=F0=9F=90=9B=20fix(pacman):=20remove=20th?= =?UTF-8?q?e=20render=20dir=20on=20every=20refresh=20exit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :bug: a failed render, a refused sudo prompt, or a signal left the scratch directory behind; repeated attempts piled them up - :lock: the cleanup runs before the sudo revocation, which stays the last step of the exit path Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8 --- bin/omarchy-refresh-pacman | 10 +++++++++- test/shell.d/region-refresh-boundary-test.sh | 21 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/bin/omarchy-refresh-pacman b/bin/omarchy-refresh-pacman index 80f3c356..b86fb209 100755 --- a/bin/omarchy-refresh-pacman +++ b/bin/omarchy-refresh-pacman @@ -31,8 +31,16 @@ if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then fi # Render the channel templates plus this machine's regional additions as the -# caller, so root only copies finished files. +# caller, so root only copies finished files. The scratch directory goes on +# every exit, including a failed render, a refused sudo prompt, or a signal; +# revoking the sudo timestamp stays the last thing the exit path does. rendered=$(mktemp -d) +cleanup_refresh() { + local status=$? + rm -rf "$rendered" + omarchy_security_exit_with_revoked_sudo "$status" +} +trap cleanup_refresh EXIT "$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered" sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak diff --git a/test/shell.d/region-refresh-boundary-test.sh b/test/shell.d/region-refresh-boundary-test.sh index b3b2f346..6ee3db8f 100755 --- a/test/shell.d/region-refresh-boundary-test.sh +++ b/test/shell.d/region-refresh-boundary-test.sh @@ -40,3 +40,24 @@ if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then fail "invalid channel reached privileged work" "$(<"$SUDO_TEST_LOG")" fi pass "failed refresh does not proceed to privileged copies or a package update" + +# A failed render still removes its scratch directory and exits cold. +reset_boundary +mv "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" "$boundary_tmp/omarchy-apply-pacman" +cat >"$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" <<'STUB' +#!/bin/bash +: >"$3/pacman.conf" +exit 1 +STUB +chmod +x "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" +mkdir -p "$boundary_tmp/scratch" +if TMPDIR="$boundary_tmp/scratch" "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" rc >"$boundary_tmp/output" 2>&1; then + fail "refresh succeeds after a failed render" +fi +mv "$boundary_tmp/omarchy-apply-pacman" "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" +[[ -z $(ls -A "$boundary_tmp/scratch") ]] || fail "failed render leaves its scratch directory" "$(ls -AR "$boundary_tmp/scratch")" +if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then + fail "failed render reached privileged work" "$(<"$SUDO_TEST_LOG")" +fi +assert_boundary_cold "failed render" +pass "failed render removes its scratch directory and exits cold" From 7a60c8dea4373d5e53ae805efc625fe6e0e65c60 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Tue, 6 Oct 2026 22:00:59 -0400 Subject: [PATCH 13/17] =?UTF-8?q?=F0=9F=90=9B=20fix(channel):=20require=20?= =?UTF-8?q?a=20usable=20region=20profile=20for=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :bug: an empty profile directory or a helper that cannot run passed the guard, linked dev, and then broke the refresh - :white_check_mark: the success case now uses a complete profile Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8 --- bin/omarchy-channel-set | 5 ++++- test/shell.d/channel-test.sh | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/bin/omarchy-channel-set b/bin/omarchy-channel-set index f347317b..6a5c018f 100755 --- a/bin/omarchy-channel-set +++ b/bin/omarchy-channel-set @@ -55,7 +55,10 @@ validate_dev_region() { [[ -f $region_file ]] && region=$(<"$region_file") [[ $region == "global" ]] && return 0 - if [[ ! -f $checkout/bin/omarchy-apply-pacman || ! -d $checkout/default/regions/$region ]]; then + # The refresh after linking needs a runnable helper and both fragments, not + # just a directory with the region's name. + local profile="$checkout/default/regions/$region/pacman" + if [[ ! -x $checkout/bin/omarchy-apply-pacman || ! -r $profile/pacman.conf.append || ! -r $profile/mirrorlist.append ]]; then fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults." fi } diff --git a/test/shell.d/channel-test.sh b/test/shell.d/channel-test.sh index 7872c6f7..72418727 100644 --- a/test/shell.d/channel-test.sh +++ b/test/shell.d/channel-test.sh @@ -201,8 +201,29 @@ if grep -Eq $'^(link|refresh)\t' "$log_file"; then fi pass "dev refuses a checkout without the region before touching pacman" +# A directory with the region's name, or a helper that cannot run, is not +# support: the refresh after linking would fail with dev already linked. mkdir -p "$checkout/default/regions/cn" touch "$checkout/bin/omarchy-apply-pacman" +chmod +x "$checkout/bin/omarchy-apply-pacman" +if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then + fail "dev refuses an empty region profile" +fi +if grep -Eq $'^(link|refresh)\t' "$log_file"; then + fail "dev refuses an empty region profile before linking or refreshing pacman" "$(cat "$log_file")" +fi +mkdir -p "$checkout/default/regions/cn/pacman" +touch "$checkout/default/regions/cn/pacman/pacman.conf.append" "$checkout/default/regions/cn/pacman/mirrorlist.append" +chmod -x "$checkout/bin/omarchy-apply-pacman" +if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then + fail "dev refuses a checkout whose pacman helper cannot run" +fi +if grep -Eq $'^(link|refresh)\t' "$log_file"; then + fail "dev refuses incomplete region support before linking or refreshing pacman" "$(cat "$log_file")" +fi +pass "dev refuses an empty region profile or a helper that cannot run" + +chmod +x "$checkout/bin/omarchy-apply-pacman" OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev assert_log_line $'refresh\tedge' "dev proceeds with a checkout that supports the region" From a1bdc46119f56daf5f5d29b09200ec697f2084ae Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Tue, 6 Oct 2026 22:00:59 -0400 Subject: [PATCH 14/17] =?UTF-8?q?=E2=9C=85=20test(pacman):=20isolate=20the?= =?UTF-8?q?=20regional=20refresh=20boundary=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :white_check_mark: the render read the host's region marker, so a global machine never rendered China and a China machine failed on missing fragments - :white_check_mark: check the finished China files root installs, not only the copy order Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8 --- test/shell.d/region-refresh-boundary-test.sh | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/shell.d/region-refresh-boundary-test.sh b/test/shell.d/region-refresh-boundary-test.sh index 6ee3db8f..c7c181d2 100755 --- a/test/shell.d/region-refresh-boundary-test.sh +++ b/test/shell.d/region-refresh-boundary-test.sh @@ -6,6 +6,43 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh" copy_boundary_file bin/omarchy-refresh-pacman copy_boundary_file bin/omarchy-apply-pacman +for file in default/pacman/pacman-rc.conf default/pacman/mirrorlist-rc \ + default/regions/cn/pacman/pacman.conf.append default/regions/cn/pacman/mirrorlist.append; do + copy_boundary_file "$file" +done + +# A sandboxed China machine: the render reads this region marker, never the +# host's /etc/omarchy/region. +export SUDO_TEST_TARGET="$boundary_tmp/target" +mkdir -p "$SUDO_TEST_TARGET/etc/omarchy" +printf 'cn\n' >"$SUDO_TEST_TARGET/etc/omarchy/region" +python3 - "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" <<'PY' +import sys +path = sys.argv[1] +source = open(path).read() +render_etc = 'etc="${4:-/}/etc"' +assert source.count(render_etc) == 1, "render target lookup moved" +open(path, 'w').write(source.replace(render_etc, 'etc="${4:-$SUDO_TEST_TARGET}/etc"')) +PY + +# cp still logs its step, but now really copies, so the rendered files exist. +# Root's copies into /etc land in a capture directory instead of the host. +export SUDO_TEST_CAPTURE="$boundary_tmp/capture" +rm "$SUDO_TEST_ROOT/bin/cp" +cat >"$SUDO_TEST_ROOT/bin/cp" <<'STUB' +#!/bin/bash +set -euo pipefail +printf 'step:cp %s\n' "$*" >>"$SUDO_TEST_LOG" +destination=${!#} +if [[ $destination == /etc/* ]]; then + source=${*: -2:1} + [[ $source == /etc/* ]] && exit 0 + /usr/bin/mkdir -p "$SUDO_TEST_CAPTURE${destination%/*}" + exec /usr/bin/cp "$source" "$SUDO_TEST_CAPTURE$destination" +fi +exec /usr/bin/cp "$@" +STUB +chmod +x "$SUDO_TEST_ROOT/bin/cp" # Regional rendering must stay unprivileged: root only copies finished files, # exactly as the channel refresh did before regions existed. @@ -30,6 +67,16 @@ assert render[0] < copies[0], events transaction = next(i for i, e in enumerate(events) if e.startswith('step:pacman ')) assert max(copies) < transaction, events PY +{ + cat "$ROOT/default/pacman/pacman-rc.conf" + printf '\n' + cat "$ROOT/default/regions/cn/pacman/pacman.conf.append" +} >"$boundary_tmp/expected-pacman.conf" +cat "$ROOT/default/pacman/mirrorlist-rc" "$ROOT/default/regions/cn/pacman/mirrorlist.append" >"$boundary_tmp/expected-mirrorlist" +cmp -s "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" || + fail "root installs the rendered China pacman.conf" "$(diff "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" 2>&1)" +cmp -s "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" || + fail "root installs the rendered China mirrorlist" "$(diff "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" 2>&1)" pass "refresh renders regional config unprivileged and gives root only plain copies" reset_boundary From 3e00a48c007eea911fb13bcb4acc559871bb8f6d Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Tue, 6 Oct 2026 22:14:56 -0400 Subject: [PATCH 15/17] =?UTF-8?q?=E2=9C=85=20test(pacman):=20sandbox=20the?= =?UTF-8?q?=20region=20for=20every=20boundary=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :white_check_mark: The channel and update boundary tests copied the real render and dev-region guard, so on a China install they read the host's `/etc/omarchy/region` and failed before reaching their checks - :recycle: The shared fixture now points both lookups at a sandbox target that starts global, and the regional test only writes `cn` there Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8 --- test/shell.d/fixtures/sudo-boundary-test.sh | 10 +++++++++- test/shell.d/region-refresh-boundary-test.sh | 9 --------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/test/shell.d/fixtures/sudo-boundary-test.sh b/test/shell.d/fixtures/sudo-boundary-test.sh index 1fb123d0..6332ed5a 100644 --- a/test/shell.d/fixtures/sudo-boundary-test.sh +++ b/test/shell.d/fixtures/sudo-boundary-test.sh @@ -9,7 +9,11 @@ export SUDO_TEST_LOG="$boundary_tmp/events" export SUDO_TEST_CACHE="$boundary_tmp/cache" export OMARCHY_PATH="$SUDO_TEST_ROOT" export SUDO_TEST_HOME="$boundary_tmp/home" -mkdir -p "$SUDO_TEST_HOME" +# The machine the render reads its region marker from. It starts global; a test +# writes etc/omarchy/region here to model a regional install, never the host's. +export SUDO_TEST_TARGET="$boundary_tmp/target" +export OMARCHY_REGION_FILE="$SUDO_TEST_TARGET/etc/omarchy/region" +mkdir -p "$SUDO_TEST_HOME" "$SUDO_TEST_TARGET/etc" mkdir -p "$SUDO_TEST_ROOT/bin" "$SUDO_TEST_ROOT/mock" "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update" : >"$SUDO_TEST_LOG" @@ -24,6 +28,10 @@ s=(source/name).read_text().replace('$HOME', '$SUDO_TEST_HOME') for command in ['sudo','pkexec','pacman','omarchy-pkg-missing','systemd-inhibit','setpriv','snapper']: s=s.replace('/usr/bin/'+command, str(target/'mock'/command)) s=s.replace('PATH=/usr/bin:/usr/sbin:/bin:/sbin', 'PATH="'+str(target/'bin')+':/usr/bin:/usr/sbin:/bin:/sbin"') +if name.name == 'omarchy-apply-pacman': + render_etc='etc="${4:-/}/etc"' + assert s.count(render_etc)==1, 'render target lookup moved' + s=s.replace(render_etc, 'etc="${4:-$SUDO_TEST_TARGET}/etc"') p.write_text(s) p.chmod((source/name).stat().st_mode & 0o777) PY diff --git a/test/shell.d/region-refresh-boundary-test.sh b/test/shell.d/region-refresh-boundary-test.sh index c7c181d2..6bca2559 100755 --- a/test/shell.d/region-refresh-boundary-test.sh +++ b/test/shell.d/region-refresh-boundary-test.sh @@ -13,17 +13,8 @@ done # A sandboxed China machine: the render reads this region marker, never the # host's /etc/omarchy/region. -export SUDO_TEST_TARGET="$boundary_tmp/target" mkdir -p "$SUDO_TEST_TARGET/etc/omarchy" printf 'cn\n' >"$SUDO_TEST_TARGET/etc/omarchy/region" -python3 - "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" <<'PY' -import sys -path = sys.argv[1] -source = open(path).read() -render_etc = 'etc="${4:-/}/etc"' -assert source.count(render_etc) == 1, "render target lookup moved" -open(path, 'w').write(source.replace(render_etc, 'etc="${4:-$SUDO_TEST_TARGET}/etc"')) -PY # cp still logs its step, but now really copies, so the rendered files exist. # Root's copies into /etc land in a capture directory instead of the host. From d53b24cfc75e6208fb60334af5cf5a1559e184df Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Tue, 6 Oct 2026 16:33:24 -0400 Subject: [PATCH 16/17] =?UTF-8?q?=E2=9C=A8=20feat(form):=20let=20callers?= =?UTF-8?q?=20preselect=20a=20fallback=20timezone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: an offline install has no geo guess, so the timezone list opened on Africa/Abidjan; a caller-supplied zone now stands in for the missing guess - :white_check_mark: a successful geo guess still outranks the fallback Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8 --- install/provisioning/setup-form.sh | 5 ++++- test/shell.d/setup-form-test.sh | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/install/provisioning/setup-form.sh b/install/provisioning/setup-form.sh index f4265d1d..c39330cd 100644 --- a/install/provisioning/setup-form.sh +++ b/install/provisioning/setup-form.sh @@ -167,10 +167,13 @@ omarchy_prompt_hostname() { } # A fresh machine often hasn't joined a network yet, so the geo guess fails -# often; guard it or a `set -e` caller dies before the filter fallback. +# often; guard it or a `set -e` caller dies before the filter fallback. Callers +# may pass the zone to preselect when there is no guess (a region's default), +# so an offline install does not open on the top of an alphabetical list. omarchy_prompt_timezone() { local guess status guess=$(tzupdate -p 2>/dev/null) || guess="" + [[ -n $guess ]] || guess="${1:-}" if [[ -n $guess ]]; then timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$guess" --header "Timezone") && status=0 || status=$? diff --git a/test/shell.d/setup-form-test.sh b/test/shell.d/setup-form-test.sh index 2b9878eb..0fc23dc6 100755 --- a/test/shell.d/setup-form-test.sh +++ b/test/shell.d/setup-form-test.sh @@ -59,7 +59,7 @@ fi trap 'if [[ ${FUNCNAME[0]:-} == "$PROMPT_FN" ]]; then printf "returned\n" >>"$MARKER"; fi' RETURN -"$PROMPT_FN" +"$PROMPT_FN" ${PROMPT_ARG:+"$PROMPT_ARG"} printf 'keyboard=%s\n' "${keyboard:-}" printf 'keyboard_label=%s\n' "${keyboard_label:-}" @@ -225,6 +225,19 @@ assert_status 0 "timezone prompt survives a failed geo guess" [[ $(head -n 1 "$GUM_ARGS") == filter* ]] || fail "timezone prompt filters when there is no geo guess" pass "timezone prompt falls back to filtering when the geo guess fails" +# A region's default stands in for the missing guess, so an offline China +# install opens on Asia/Shanghai instead of the top of the list +PROMPT_ARG=Asia/Shanghai run_prompt omarchy_prompt_timezone "0:Asia/Shanghai" +assert_status 0 "timezone prompt accepts the caller's fallback" +[[ $(field timezone) == "Asia/Shanghai" ]] || fail "timezone prompt keeps the preselected fallback" +grep -qF -- '--selected Asia/Shanghai' "$GUM_ARGS" || fail "timezone prompt preselects the caller's fallback" +[[ $(head -n 1 "$GUM_ARGS") == choose* ]] || fail "timezone prompt chooses from the list when it has a fallback" +pass "timezone prompt preselects the caller's fallback when the geo guess fails" + +TZ_GUESS=Europe/Copenhagen PROMPT_ARG=Asia/Shanghai run_prompt omarchy_prompt_timezone "0:Europe/Copenhagen" +grep -qF -- '--selected Europe/Copenhagen' "$GUM_ARGS" || fail "a geo guess outranks the caller's fallback" +pass "timezone prompt prefers the geo guess over the caller's fallback" + run_prompt omarchy_prompt_timezone "0:" assert_status 0 "timezone prompt accepts an empty selection" [[ $(field timezone) == "UTC" ]] || fail "timezone prompt falls back to UTC" From 1454e04b7f6f663e1e2bc9d438de918dbf668e18 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Tue, 6 Oct 2026 16:33:24 -0400 Subject: [PATCH 17/17] =?UTF-8?q?=E2=9C=A8=20feat(region):=20default=20chi?= =?UTF-8?q?na=20installs=20to=20`Asia/Shanghai`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: offline China first boots preselect `Asia/Shanghai` from the region profile instead of the top of the list - :memo: profiles may name a `timezone`; first-boot setup reads it for the region in `/etc/omarchy/region` Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8 --- bin/omarchy-provision-owner | 17 ++++++++++++++- default/regions/cn/timezone | 1 + docs/regions.md | 3 ++- test/shell.d/region-timezone-test.sh | 32 ++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 default/regions/cn/timezone create mode 100755 test/shell.d/region-timezone-test.sh diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index a1a22559..5b8f0d36 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -16,6 +16,7 @@ set -euo pipefail PROVISIONING_DIR=/var/lib/omarchy/provisioning OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}" LOG_FILE=/var/log/omarchy-provision-owner.log +REGION_MARKER=/etc/omarchy/region if (( EUID != 0 )); then echo "Error: omarchy-provision-owner must run as root" >&2 @@ -650,7 +651,21 @@ user_form() { omarchy_prompt_hostname || return $? step "Let's set your timezone..." - omarchy_prompt_timezone || return $? + omarchy_prompt_timezone "$(region_timezone)" || return $? +} + +# The zone a region profile preselects when there is no geo guess, which is +# every offline first boot. Reads the marker omarchy-apply-pacman reads; global, +# missing, or malformed regions offer nothing. +region_timezone() { + local region=global profile + if [[ -r $REGION_MARKER ]]; then + region=$(<"$REGION_MARKER") + fi + profile="$OMARCHY_PATH/default/regions/$region/timezone" + if [[ $region =~ ^[a-z]{2}$ && -r $profile ]]; then + head -n 1 "$profile" + fi } confirm_form() { diff --git a/default/regions/cn/timezone b/default/regions/cn/timezone new file mode 100644 index 00000000..421b7a4c --- /dev/null +++ b/default/regions/cn/timezone @@ -0,0 +1 @@ +Asia/Shanghai diff --git a/docs/regions.md b/docs/regions.md index 2bbf3d59..b7f330ff 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -20,13 +20,14 @@ Regional content comes from the same runtime used by the ISO: either the `--loca - `packages`: additional target packages, one name per line, with blank lines and `#` comments allowed. The builder merges these into the ISO's copy of `omarchy-base.packages`, so they are both included in the offline repository and installed on the target. The source manifest is not modified. - `pacman/`: repository fragments used by the runtime finalizer. The builder also adds the community repository to its writable online config to obtain regional packages; it does not change its Arch channel mirrors. +- `timezone` (optional): one zone name, such as `Asia/Shanghai`. The setup form preselects it in the timezone list when the network geo guess is unavailable, which is always the case offline. Both callers pass it: the ISO configurator from its copy of the profile, and first-boot setup from `default/regions//timezone` for the region in `/etc/omarchy/region`. A successful geo guess still wins, and the person can choose any other zone. - `skel/` (optional): new-user defaults. The ISO installer overlays these onto target `/etc/skel` after the base settings packages and before user creation, including installations that defer user creation until first boot. It never overlays existing home directories. For China, the builder installs `archlinuxcn-keyring` using the existing Arch trust chain before downloading other regional packages. Signature or keyring failures abort the build; no unverified keyserver trust or online signature bypass is added. The keyring is also installed on the target from the offline package set, then initialized and populated before online repositories are enabled. The ISO's existing offline repository verification policy is unchanged. ## Scope of the China profile -The China profile currently provides repository defaults only. System language (Simplified Chinese), timezone (Asia/Shanghai), and input-method defaults are planned as separate changes on top of this mechanism: `packages` and `skel/` carry them into the ISO, and the installer stages `skel/` before any user exists. +The China profile provides repository defaults and preselects `Asia/Shanghai` as the timezone. System language (Simplified Chinese) and input-method defaults are planned as separate changes on top of this mechanism: `packages` and `skel/` carry them into the ISO, and the installer stages `skel/` before any user exists. ## Remaining release validation diff --git a/test/shell.d/region-timezone-test.sh b/test/shell.d/region-timezone-test.sh new file mode 100755 index 00000000..8dea6e0b --- /dev/null +++ b/test/shell.d/region-timezone-test.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -euo pipefail +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT + +zone=$(<"$ROOT/default/regions/cn/timezone") +[[ $zone == "Asia/Shanghai" ]] || fail "China profile names Asia/Shanghai" +[[ -f /usr/share/zoneinfo/$zone ]] || fail "China profile timezone exists in tzdata" +pass "China profile defaults to an existing Asia/Shanghai zone" + +# Load the real region_timezone() from the first-boot command. +OMARCHY_PATH="$ROOT" +REGION_MARKER="$scratch/region" +eval "$(sed -n '/^region_timezone() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner")" + +[[ -z $(region_timezone) ]] || fail "missing region marker offers no timezone" +for region in global CN ../cn zz ""; do + printf '%s\n' "$region" >"$REGION_MARKER" + [[ -z $(region_timezone) ]] || fail "region '$region' offers no timezone" +done +pass "global, unknown, and malformed regions offer no timezone" + +printf 'cn\n' >"$REGION_MARKER" +[[ $(region_timezone) == "Asia/Shanghai" ]] || fail "cn region offers Asia/Shanghai" +pass "cn region offers its profile timezone" + +grep -qF 'omarchy_prompt_timezone "$(region_timezone)"' "$ROOT/bin/omarchy-provision-owner" || + fail "first-boot setup passes the region timezone to the prompt" +pass "first-boot setup preselects the region timezone"