From 7727d40c92ba3d805afc8db33522e3e37c08e002 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:48:09 -0400 Subject: [PATCH 01/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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/27] =?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 0de49b231aa9bee8d007a0cee8ef43a10acfbe9f Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:03:41 -0400 Subject: [PATCH 16/27] =?UTF-8?q?=E2=9C=A8=20feat(region):=20map=20timezon?= =?UTF-8?q?es=20to=20regions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: one iso serves every region, so the installer derives it from the timezone the user already picks - :white_check_mark: reject unknown zones and zones claimed by two regions Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- default/regions/cn/timezones | 9 +++++++++ test/shell.d/region-pacman-test.sh | 28 ++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 default/regions/cn/timezones diff --git a/default/regions/cn/timezones b/default/regions/cn/timezones new file mode 100644 index 00000000..2520c323 --- /dev/null +++ b/default/regions/cn/timezones @@ -0,0 +1,9 @@ +# Installs in these timezones get this region. The installer's timezone list +# includes tzdata's backward-compatible names, so they are listed too. +Asia/Shanghai +Asia/Urumqi +Asia/Chongqing +Asia/Chungking +Asia/Harbin +Asia/Kashgar +PRC diff --git a/test/shell.d/region-pacman-test.sh b/test/shell.d/region-pacman-test.sh index d183e674..92b394ef 100644 --- a/test/shell.d/region-pacman-test.sh +++ b/test/shell.d/region-pacman-test.sh @@ -91,8 +91,8 @@ 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" -# The shipped China profile is repository scaffolding only. Locale, timezone, -# and input defaults are separate changes built on the same mechanism. +# The shipped China profile is repository defaults only. Language, locale, and +# input methods follow the installer's language choice, not the region. python3 - <<'PY' import os from pathlib import Path @@ -103,3 +103,27 @@ assert packages == {"archlinuxcn-keyring"}, packages assert not (region / "skel").exists() PY pass "China profile adds only its repository keyring" + +# The installer derives the region from the chosen timezone, so every profile +# needs a mapping, and no timezone may select two regions. +python3 - <<'PY' +import os +from pathlib import Path + +zoneinfo = Path("/usr/share/zoneinfo") +owners = {} +for profile in sorted((Path(os.environ["ROOT"]) / "default/regions").iterdir()): + lines = (profile / "timezones").read_text().splitlines() + zones = [line.strip() for line in lines if line.strip() and not line.startswith("#")] + assert zones, f"{profile.name} maps no timezones" + for zone in zones: + assert zone not in owners, f"{zone} maps to {owners[zone]} and {profile.name}" + owners[zone] = profile.name + if zoneinfo.is_dir(): + assert (zoneinfo / zone).is_file(), f"{profile.name} lists unknown timezone {zone}" + +china = {zone for zone, region in owners.items() if region == "cn"} +assert {"Asia/Shanghai", "Asia/Urumqi"} <= china, china +assert not china & {"Asia/Hong_Kong", "Asia/Macau", "Asia/Taipei"}, china +PY +pass "region profiles map each timezone to at most one region" From b89def6c7f4ba8316ed879c363d0e9a0097d6dee Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:03:41 -0400 Subject: [PATCH 17/27] =?UTF-8?q?=F0=9F=93=9D=20docs(region):=20describe?= =?UTF-8?q?=20timezone-derived=20regions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: the --region build flag is gone, so the installer contract and validation plan change with it Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- docs/regions.md | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/regions.md b/docs/regions.md index 2bbf3d59..514fa957 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -1,10 +1,10 @@ -# Regional ISO defaults +# Regional 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`. +Regions customize bootstrap defaults independently of update channels and of the installer's language. 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. +The ISO installer writes the region it derived 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. @@ -12,24 +12,30 @@ Both installation finalization and `omarchy-refresh-pacman` use this command. Ex 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 +## Profile layout -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. +Each profile lives in `default/regions//`: -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. +- `timezones`: the timezones that select this region, one per line, with blank lines and `#` comments allowed. List tzdata's backward-compatible names too, since the installer's timezone list includes them. A timezone may belong to at most one profile. +- `packages`: additional target packages, in the same format. Every ISO carries every profile's packages in its offline repository; the installer installs them only on targets in that region. A package named `-keyring` is the profile's trust anchor: the builder installs it through Arch's existing trust chain, and a regional target populates the `` keyring. +- `pacman/`: `pacman.conf.append` and `mirrorlist.append`, the repository fragments `omarchy-apply-pacman` adds to the channel templates. -- `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. +## ISO installer contract -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. +There is one ISO for every region. The companion `omarchy-iso` repository reads the profiles from the runtime it bundles, so a region and its repository defaults can never disagree. + +- **Build:** every profile's `packages` go into the offline repository, but not into the base package list. The builder adds each profile's community repository to its writable online config to download them; it does not change its Arch channel mirrors. +- **Install:** the installer maps the timezone the user picked to a region through the `timezones` files; a timezone no profile lists means `global`. It shows the result on the summary screen. Unattended installs can set the region explicitly instead. For a regional install it installs the profile's packages from the offline repository and writes `/etc/omarchy/region`; the runtime finalizer then applies the fragments. +- **Deferred installs** have no timezone until first boot, so they are `global` unless the unattended configuration sets a region. + +For China, the builder installs `archlinuxcn-keyring` using the existing Arch trust chain before downloading ArchLinuxCN packages. Signature or keyring failures abort the build; no unverified keyserver trust or online signature bypass is added. On a China target the installer installs the keyring from the offline repository, then initializes and populates it before the online repositories are enabled. Global targets never populate ArchLinuxCN's keys. 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 only. Language is a separate installer choice, not part of a region: a Chinese speaker outside China keeps global mirrors, and a China install in English still gets the regional ones. ## Remaining release validation -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`. +Unit tests cover region/channel independence, the timezone mapping, 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, including deferred first-boot user creation, and keep the regional mirrors, repository, marker, and keyring trust through `stable`, `rc`, and `edge` refreshes. +The acceptance run must install without network access in both directions: a non-China timezone must produce pacman files byte for byte identical to the channel templates, and `Asia/Shanghai` must produce the China profile, boot to a new user, and keep the regional mirrors, repository, marker, and keyring trust through `stable`, `rc`, and `edge` refreshes. From 3c68a1bb04156e21c3cb49cf37b4f782449e699e Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:21:51 -0400 Subject: [PATCH 18/27] =?UTF-8?q?=E2=9C=A8=20feat(region):=20put=20the=20c?= =?UTF-8?q?hina=20channel=20mirrors=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: omarchy now runs per-channel china replicas, so they should lead instead of trailing as a fallback - :recycle: prepend per-channel fragments so each channel's own mirror stays the fallback - :fire: drop the ustc arch mirror, which was never a stable or rc snapshot replica Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- bin/omarchy-apply-pacman | 9 ++++++--- bin/omarchy-channel-set | 6 +++--- default/regions/cn/pacman/mirrorlist-edge.prepend | 1 + default/regions/cn/pacman/mirrorlist-rc.prepend | 1 + .../regions/cn/pacman/mirrorlist-stable.prepend | 1 + default/regions/cn/pacman/mirrorlist.append | 1 - test/shell.d/channel-test.sh | 2 +- test/shell.d/region-pacman-test.sh | 14 +++++++------- test/shell.d/region-refresh-boundary-test.sh | 4 ++-- 9 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 default/regions/cn/pacman/mirrorlist-edge.prepend create mode 100644 default/regions/cn/pacman/mirrorlist-rc.prepend create mode 100644 default/regions/cn/pacman/mirrorlist-stable.prepend delete mode 100644 default/regions/cn/pacman/mirrorlist.append diff --git a/bin/omarchy-apply-pacman b/bin/omarchy-apply-pacman index e004637d..59f41506 100755 --- a/bin/omarchy-apply-pacman +++ b/bin/omarchy-apply-pacman @@ -53,12 +53,15 @@ else 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 +if [[ $region == "global" ]]; then + cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist" +else 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" + # pacman tries servers in order, so the regional mirror leads and the + # channel's own mirror stays as the fallback. + cat "$profile/mirrorlist-$channel.prepend" "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" > "$staging/mirrorlist" fi [[ -z $render_dir ]] || exit 0 diff --git a/bin/omarchy-channel-set b/bin/omarchy-channel-set index 6a5c018f..60200f97 100755 --- a/bin/omarchy-channel-set +++ b/bin/omarchy-channel-set @@ -55,10 +55,10 @@ validate_dev_region() { [[ -f $region_file ]] && region=$(<"$region_file") [[ $region == "global" ]] && return 0 - # The refresh after linking needs a runnable helper and both fragments, not - # just a directory with the region's name. + # The refresh after linking needs a runnable helper and the edge fragments + # dev uses, 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 + if [[ ! -x $checkout/bin/omarchy-apply-pacman || ! -r $profile/pacman.conf.append || ! -r $profile/mirrorlist-edge.prepend ]]; 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/default/regions/cn/pacman/mirrorlist-edge.prepend b/default/regions/cn/pacman/mirrorlist-edge.prepend new file mode 100644 index 00000000..50ca5dd2 --- /dev/null +++ b/default/regions/cn/pacman/mirrorlist-edge.prepend @@ -0,0 +1 @@ +Server = https://mirror.omarchy.cn/$repo/os/$arch diff --git a/default/regions/cn/pacman/mirrorlist-rc.prepend b/default/regions/cn/pacman/mirrorlist-rc.prepend new file mode 100644 index 00000000..0ffc1ba1 --- /dev/null +++ b/default/regions/cn/pacman/mirrorlist-rc.prepend @@ -0,0 +1 @@ +Server = https://rc-mirror.omarchy.cn/$repo/os/$arch diff --git a/default/regions/cn/pacman/mirrorlist-stable.prepend b/default/regions/cn/pacman/mirrorlist-stable.prepend new file mode 100644 index 00000000..90a2678c --- /dev/null +++ b/default/regions/cn/pacman/mirrorlist-stable.prepend @@ -0,0 +1 @@ +Server = https://stable-mirror.omarchy.cn/$repo/os/$arch diff --git a/default/regions/cn/pacman/mirrorlist.append b/default/regions/cn/pacman/mirrorlist.append deleted file mode 100644 index d0fe979a..00000000 --- a/default/regions/cn/pacman/mirrorlist.append +++ /dev/null @@ -1 +0,0 @@ -Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch diff --git a/test/shell.d/channel-test.sh b/test/shell.d/channel-test.sh index 72418727..fb670e3e 100644 --- a/test/shell.d/channel-test.sh +++ b/test/shell.d/channel-test.sh @@ -213,7 +213,7 @@ 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" +touch "$checkout/default/regions/cn/pacman/pacman.conf.append" "$checkout/default/regions/cn/pacman/mirrorlist-edge.prepend" 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" diff --git a/test/shell.d/region-pacman-test.sh b/test/shell.d/region-pacman-test.sh index 92b394ef..d02ea60d 100644 --- a/test/shell.d/region-pacman-test.sh +++ b/test/shell.d/region-pacman-test.sh @@ -27,12 +27,12 @@ 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 - stable) host=stable-mirror.omarchy.org ;; - rc) host=rc-mirror.omarchy.org ;; - edge) host=mirror.omarchy.org ;; + stable) host=stable-mirror.omarchy ;; + rc) host=rc-mirror.omarchy ;; + edge) host=mirror.omarchy ;; esac apply "$channel" - expected=$(printf 'Server = https://%s/$repo/os/$arch\nServer = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch' "$host") + expected=$(printf 'Server = https://%s.cn/$repo/os/$arch\nServer = https://%s.org/$repo/os/$arch' "$host" "$host") [[ $(<"$TARGET_ROOT/etc/pacman.d/mirrorlist") == "$expected" ]] || fail "$channel mirror order and variables" { cat "$ROOT/default/pacman/pacman-$channel.conf" @@ -43,7 +43,7 @@ for channel in stable rc edge; do 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" +pass "China puts its channel mirror first, keeps the global one as fallback, and is idempotent" cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf" cp "$TARGET_ROOT/etc/pacman.d/mirrorlist" "$scratch/before.mirrors" @@ -75,8 +75,8 @@ 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" +[[ $(<"$scratch/rendered/mirrorlist") == $'Server = https://rc-mirror.omarchy.cn/$repo/os/$arch\nServer = https://rc-mirror.omarchy.org/$repo/os/$arch' ]] || + fail "render puts the regional channel mirror first" cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf" pass "render builds the regional channel files without touching the target" diff --git a/test/shell.d/region-refresh-boundary-test.sh b/test/shell.d/region-refresh-boundary-test.sh index 6bca2559..916e35e9 100755 --- a/test/shell.d/region-refresh-boundary-test.sh +++ b/test/shell.d/region-refresh-boundary-test.sh @@ -7,7 +7,7 @@ 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 + default/regions/cn/pacman/pacman.conf.append default/regions/cn/pacman/mirrorlist-rc.prepend; do copy_boundary_file "$file" done @@ -63,7 +63,7 @@ PY 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" +cat "$ROOT/default/regions/cn/pacman/mirrorlist-rc.prepend" "$ROOT/default/pacman/mirrorlist-rc" >"$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" || From fce85abf481cf1dd40f6c70673a8b0e57fe9b275 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:21:51 -0400 Subject: [PATCH 19/27] =?UTF-8?q?=F0=9F=93=9D=20docs(region):=20explain=20?= =?UTF-8?q?mirror=20order=20and=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: pacman's three-error skip and first-server databases decide what the china replicas must serve Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- docs/regions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/regions.md b/docs/regions.md index 514fa957..cc49bb77 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -6,11 +6,11 @@ Regions customize bootstrap defaults independently of update channels and of the The ISO installer writes the region it derived 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. +`omarchy-apply-pacman [target-root]` restores the selected channel's two pacman templates, appends the region's `pacman.conf.append` to `pacman.conf`, and puts the region's `mirrorlist-.prepend` ahead of the channel's mirrorlist. 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 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'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. +The China profile puts Omarchy's China replica of the channel's Arch mirror first (`stable-mirror.omarchy.cn`, `rc-mirror.omarchy.cn`, or `mirror.omarchy.cn` for edge and dev) and keeps the channel's `.org` mirror as the fallback. pacman tries servers in order and moves to the next one when a download fails, including a 404. After three failed downloads from one server, it skips that server for the rest of the transaction. It also takes the package databases from the first server that answers, without comparing them, so the China replicas must carry complete, current metadata. The profile also adds the ArchLinuxCN community repository from USTC's mirror, run by the University of Science and Technology of China. It does not change the `[omarchy]` package repository. ## Profile layout @@ -18,7 +18,7 @@ Each profile lives in `default/regions//`: - `timezones`: the timezones that select this region, one per line, with blank lines and `#` comments allowed. List tzdata's backward-compatible names too, since the installer's timezone list includes them. A timezone may belong to at most one profile. - `packages`: additional target packages, in the same format. Every ISO carries every profile's packages in its offline repository; the installer installs them only on targets in that region. A package named `-keyring` is the profile's trust anchor: the builder installs it through Arch's existing trust chain, and a regional target populates the `` keyring. -- `pacman/`: `pacman.conf.append` and `mirrorlist.append`, the repository fragments `omarchy-apply-pacman` adds to the channel templates. +- `pacman/`: `pacman.conf.append`, added after the channel's `pacman.conf`, and one `mirrorlist-.prepend` per channel (`stable`, `rc`, `edge`), placed ahead of the channel's mirrorlist so the regional mirror is tried first. ## ISO installer contract From 9d74c813f162470bb5c1cad908e531d7e9ba2a26 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:31:04 -0400 Subject: [PATCH 20/27] =?UTF-8?q?=F0=9F=90=9B=20fix(channel):=20require=20?= =?UTF-8?q?every=20region=20fragment=20for=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :bug: leaving dev refreshes stable or rc with the checkout's tools, so a profile missing those fragments would drop regional repositories - :white_check_mark: refuse a profile missing any single fragment Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- bin/omarchy-channel-set | 13 +++++++++---- test/shell.d/channel-test.sh | 20 ++++++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/bin/omarchy-channel-set b/bin/omarchy-channel-set index 60200f97..93f05bc9 100755 --- a/bin/omarchy-channel-set +++ b/bin/omarchy-channel-set @@ -55,10 +55,15 @@ validate_dev_region() { [[ -f $region_file ]] && region=$(<"$region_file") [[ $region == "global" ]] && return 0 - # The refresh after linking needs a runnable helper and the edge fragments - # dev uses, 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-edge.prepend ]]; then + # The refresh after linking needs a runnable helper and every fragment, not + # just a directory with the region's name: dev refreshes edge, and leaving + # dev later refreshes stable or rc with this checkout's tools. + local profile="$checkout/default/regions/$region/pacman" fragment supported=true + [[ -x $checkout/bin/omarchy-apply-pacman ]] || supported=false + for fragment in pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend; do + [[ -r $profile/$fragment ]] || supported=false + done + if ! $supported; 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 fb670e3e..791ff2e9 100644 --- a/test/shell.d/channel-test.sh +++ b/test/shell.d/channel-test.sh @@ -213,7 +213,23 @@ 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-edge.prepend" +cn_fragments=(pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend) +for missing in "${cn_fragments[@]}"; do + for fragment in "${cn_fragments[@]}"; do + if [[ $fragment == "$missing" ]]; then + rm -f "$checkout/default/regions/cn/pacman/$fragment" + else + touch "$checkout/default/regions/cn/pacman/$fragment" + fi + done + 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 region profile without $missing" + fi + if grep -Eq $'^(link|refresh)\t' "$log_file"; then + fail "dev refuses a profile without $missing before linking or refreshing pacman" "$(cat "$log_file")" + fi +done +touch "${cn_fragments[@]/#/$checkout/default/regions/cn/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 a checkout whose pacman helper cannot run" @@ -221,7 +237,7 @@ 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" +pass "dev refuses a region profile missing any fragment or a helper that cannot run" chmod +x "$checkout/bin/omarchy-apply-pacman" OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev From b381093e32e17df813fb29ba5d74926da1670d47 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:31:04 -0400 Subject: [PATCH 21/27] =?UTF-8?q?=E2=9C=85=20test(region):=20derive=20chin?= =?UTF-8?q?a=20zones=20from=20tzdata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :white_check_mark: dropping a backward-compatible zone name from the profile now fails the test Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- test/shell.d/region-pacman-test.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/shell.d/region-pacman-test.sh b/test/shell.d/region-pacman-test.sh index d02ea60d..c2289b21 100644 --- a/test/shell.d/region-pacman-test.sh +++ b/test/shell.d/region-pacman-test.sh @@ -122,8 +122,16 @@ for profile in sorted((Path(os.environ["ROOT"]) / "default/regions").iterdir()): if zoneinfo.is_dir(): assert (zoneinfo / zone).is_file(), f"{profile.name} lists unknown timezone {zone}" +# The installer offers tzdata's backward-compatible names too, so China +# covers every name that links to its two zones and nothing else. china = {zone for zone, region in owners.items() if region == "cn"} -assert {"Asia/Shanghai", "Asia/Urumqi"} <= china, china -assert not china & {"Asia/Hong_Kong", "Asia/Macau", "Asia/Taipei"}, china +expected = {"Asia/Shanghai", "Asia/Urumqi"} +tzdata = zoneinfo / "tzdata.zi" +if tzdata.is_file(): + for line in tzdata.read_text().splitlines(): + fields = line.split() + if len(fields) == 3 and fields[0] == "L" and fields[1] in {"Asia/Shanghai", "Asia/Urumqi"}: + expected.add(fields[2]) +assert china == expected, (china, expected) PY pass "region profiles map each timezone to at most one region" From 545190596da340e76a97c3a71a9c675156fbcb8d Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:31:04 -0400 Subject: [PATCH 22/27] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(pacman):=20?= =?UTF-8?q?branch=20on=20render=20with=20if/else?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :recycle: follow the repo style instead of exiting early to skip the install block Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- bin/omarchy-apply-pacman | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bin/omarchy-apply-pacman b/bin/omarchy-apply-pacman index 59f41506..5893c80d 100755 --- a/bin/omarchy-apply-pacman +++ b/bin/omarchy-apply-pacman @@ -63,14 +63,15 @@ else # channel's own mirror stays as the fallback. cat "$profile/mirrorlist-$channel.prepend" "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" > "$staging/mirrorlist" fi -[[ -z $render_dir ]] || exit 0 -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 -# 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" +if [[ -z $render_dir ]]; then + 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 + # 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" +fi From a6c3480ff8891e71b877d88ae07e6f4aecdcdf58 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:31:04 -0400 Subject: [PATCH 23/27] =?UTF-8?q?=F0=9F=93=9D=20docs(region):=20state=20ma?= =?UTF-8?q?rker=20mode=20and=20archlinuxcn=20risk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: refresh reads the marker unprivileged, and rolling archlinuxcn packages can outpace stable snapshots Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- docs/regions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/regions.md b/docs/regions.md index cc49bb77..1a4930c8 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -4,13 +4,13 @@ Regions customize bootstrap defaults independently of update channels and of the ## Runtime configuration -The ISO installer writes the region it derived 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. +The ISO installer writes the region it derived to `/etc/omarchy/region` before target finalization. It is plain text, not executable configuration, owned by root and world-readable (`0644`), because refresh renders as the calling user. A missing file means `global`; an unsupported value is an error. `omarchy-apply-pacman [target-root]` restores the selected channel's two pacman templates, appends the region's `pacman.conf.append` to `pacman.conf`, and puts the region's `mirrorlist-.prepend` ahead of the channel's mirrorlist. 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 anything in existing home directories. -The China profile puts Omarchy's China replica of the channel's Arch mirror first (`stable-mirror.omarchy.cn`, `rc-mirror.omarchy.cn`, or `mirror.omarchy.cn` for edge and dev) and keeps the channel's `.org` mirror as the fallback. pacman tries servers in order and moves to the next one when a download fails, including a 404. After three failed downloads from one server, it skips that server for the rest of the transaction. It also takes the package databases from the first server that answers, without comparing them, so the China replicas must carry complete, current metadata. The profile also adds the ArchLinuxCN community repository from USTC's mirror, run by the University of Science and Technology of China. It does not change the `[omarchy]` package repository. +The China profile puts Omarchy's China replica of the channel's Arch mirror first (`stable-mirror.omarchy.cn`, `rc-mirror.omarchy.cn`, or `mirror.omarchy.cn` for edge and dev) and keeps the channel's `.org` mirror as the fallback. pacman tries servers in order and moves to the next one when a download fails, including a 404. After three failed downloads from one server, it skips that server for the rest of the transaction. It also takes the package databases from the first server that answers, without comparing them, so the China replicas must carry complete, current metadata. The profile also adds the ArchLinuxCN community repository from USTC's mirror, run by the University of Science and Technology of China. It does not change the `[omarchy]` package repository. ArchLinuxCN is rolling and built against current Arch, while stable and RC are snapshots, so its packages can need newer libraries than a stable machine has. ## Profile layout From d9a781e3c935cf77fc1ff2afb2f81c0eb722e84d Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 16:08:36 -0400 Subject: [PATCH 24/27] =?UTF-8?q?=F0=9F=93=9D=20docs(region):=20scope=20th?= =?UTF-8?q?e=20refresh=20render=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: state that same-user code is outside the refresh boundary, so reviewers see the trade-off was deliberate Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- docs/regions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/regions.md b/docs/regions.md index 1a4930c8..23858f37 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -10,6 +10,8 @@ The ISO installer writes the region it derived to `/etc/omarchy/region` before t 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. +Refresh renders both files as the calling user and gives root only the finished copies, so the sudo boundary stays command-scoped. That boundary does not defend against other code already running as the same user: such code could change the rendered files while refresh waits for the password, as it could alter that user's own environment in other ways. + The China profile puts Omarchy's China replica of the channel's Arch mirror first (`stable-mirror.omarchy.cn`, `rc-mirror.omarchy.cn`, or `mirror.omarchy.cn` for edge and dev) and keeps the channel's `.org` mirror as the fallback. pacman tries servers in order and moves to the next one when a download fails, including a 404. After three failed downloads from one server, it skips that server for the rest of the transaction. It also takes the package databases from the first server that answers, without comparing them, so the China replicas must carry complete, current metadata. The profile also adds the ArchLinuxCN community repository from USTC's mirror, run by the University of Science and Technology of China. It does not change the `[omarchy]` package repository. ArchLinuxCN is rolling and built against current Arch, while stable and RC are snapshots, so its packages can need newer libraries than a stable machine has. ## Profile layout From ec4579a461c7bc1e28185f8955cb4b8a37563bd4 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 16:44:15 -0400 Subject: [PATCH 25/27] =?UTF-8?q?=F0=9F=90=9B=20fix(test):=20skip=20exact?= =?UTF-8?q?=20zone=20match=20without=20tzdata.zi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :bug: minimal containers lack the link table, so the exact china zone check failed on a correct profile Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g --- test/shell.d/region-pacman-test.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/shell.d/region-pacman-test.sh b/test/shell.d/region-pacman-test.sh index c2289b21..938739b6 100644 --- a/test/shell.d/region-pacman-test.sh +++ b/test/shell.d/region-pacman-test.sh @@ -124,6 +124,8 @@ for profile in sorted((Path(os.environ["ROOT"]) / "default/regions").iterdir()): # The installer offers tzdata's backward-compatible names too, so China # covers every name that links to its two zones and nothing else. +# Without tzdata.zi (minimal containers) only the canonical zones can be +# checked, so completeness is enforced where the host has the link table. china = {zone for zone, region in owners.items() if region == "cn"} expected = {"Asia/Shanghai", "Asia/Urumqi"} tzdata = zoneinfo / "tzdata.zi" @@ -132,6 +134,8 @@ if tzdata.is_file(): fields = line.split() if len(fields) == 3 and fields[0] == "L" and fields[1] in {"Asia/Shanghai", "Asia/Urumqi"}: expected.add(fields[2]) -assert china == expected, (china, expected) + assert china == expected, (china, expected) +else: + assert expected <= china, (china, expected) PY pass "region profiles map each timezone to at most one region" From 466c2ca14aa3572972fccbf4cc9efabe74b2012b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 12 Oct 2026 00:27:23 -0400 Subject: [PATCH 26/27] Point package managers at China mirrors on cn installs npm, PyPI and uv, Go, rustup and crates.io, RubyGems and Bundler, and Maven reach their registries slowly or not at all from China. On a cn install, link the profile's environment, cargo and gem drop-ins into place. A regular file the admin wrote is never replaced; the install fails instead. --- AGENTS.md | 5 +++ default/regions/cn/cargo.toml | 5 +++ default/regions/cn/environment.conf | 8 +++++ default/regions/cn/gemrc | 3 ++ default/regions/cn/maven-settings.xml | 9 +++++ docs/regions.md | 6 +++- install/config/all.sh | 1 + install/config/region.sh | 33 +++++++++++++++++++ test/shell.d/region-test.sh | 47 +++++++++++++++++++++++++++ 9 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 default/regions/cn/cargo.toml create mode 100644 default/regions/cn/environment.conf create mode 100644 default/regions/cn/gemrc create mode 100644 default/regions/cn/maven-settings.xml create mode 100644 install/config/region.sh create mode 100644 test/shell.d/region-test.sh diff --git a/AGENTS.md b/AGENTS.md index 6de19cb9..12dea04b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,6 +96,11 @@ Exceptions are allowed for migration and package-helper scripts where the helper - Do not add `aliases` to new menu entries. Aliases are reserved for established alternate names users already type, kept for compatibility. +# Region + +- `/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`. +- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create. + # Config Structure - `config/` - default configs copied to `~/.config/` diff --git a/default/regions/cn/cargo.toml b/default/regions/cn/cargo.toml new file mode 100644 index 00000000..5b56b114 --- /dev/null +++ b/default/regions/cn/cargo.toml @@ -0,0 +1,5 @@ +[source.crates-io] +replace-with = "aliyun" + +[source.aliyun] +registry = "sparse+https://mirrors.aliyun.com/crates.io-index/" diff --git a/default/regions/cn/environment.conf b/default/regions/cn/environment.conf new file mode 100644 index 00000000..60905de2 --- /dev/null +++ b/default/regions/cn/environment.conf @@ -0,0 +1,8 @@ +NPM_CONFIG_REGISTRY=https://registry.npmmirror.com +PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ +GOPROXY=https://mirrors.aliyun.com/goproxy/,direct +RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup +RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup +BUNDLE_MIRROR__ALL=https://mirrors.aliyun.com/rubygems/ +MAVEN_ARGS=--global-settings /usr/share/omarchy/default/regions/cn/maven-settings.xml diff --git a/default/regions/cn/gemrc b/default/regions/cn/gemrc new file mode 100644 index 00000000..145984f2 --- /dev/null +++ b/default/regions/cn/gemrc @@ -0,0 +1,3 @@ +--- +:sources: +- https://mirrors.aliyun.com/rubygems/ diff --git a/default/regions/cn/maven-settings.xml b/default/regions/cn/maven-settings.xml new file mode 100644 index 00000000..5066b6bd --- /dev/null +++ b/default/regions/cn/maven-settings.xml @@ -0,0 +1,9 @@ + + + + aliyun + central + https://maven.aliyun.com/repository/public + + + diff --git a/docs/regions.md b/docs/regions.md index 23858f37..e64d89c0 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -34,7 +34,11 @@ For China, the builder installs `archlinuxcn-keyring` using the existing Arch tr ## Scope of the China profile -The China profile provides repository defaults only. Language is a separate installer choice, not part of a region: a Chinese speaker outside China keeps global mirrors, and a China install in English still gets the regional ones. +The China profile provides repository defaults and developer mirrors. Language is a separate installer choice, not part of a region: a Chinese speaker outside China keeps global mirrors, and a China install in English still gets the regional ones. + +## Developer mirrors + +The `cn` profile also points the common package managers at mirrors inside China: `environment.conf` (npm, PyPI and uv, Go, rustup, Bundler, and Maven through `MAVEN_ARGS`), `cargo.toml` for crates.io, `gemrc` for RubyGems, and `maven-settings.xml`, which Maven reads in place. During installation `install/config/region.sh` links the first three to `/etc/environment.d/90-omarchy-china.conf`, `/.cargo/config.toml` and `/etc/gemrc` when the marker says `cn`. It refuses to replace a regular file it did not create. The environment reaches sessions started after login. ## Remaining release validation diff --git a/install/config/all.sh b/install/config/all.sh index a221a398..3b5655a9 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -1,3 +1,4 @@ +run_logged "$OMARCHY_INSTALL/config/region.sh" run_logged "$OMARCHY_INSTALL/config/theme-system.sh" run_logged "$OMARCHY_INSTALL/config/browser-policy.sh" run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh" diff --git a/install/config/region.sh b/install/config/region.sh new file mode 100644 index 00000000..a009c123 --- /dev/null +++ b/install/config/region.sh @@ -0,0 +1,33 @@ +cn_links=( + "environment.conf /etc/environment.d/90-omarchy-china.conf" + "cargo.toml /.cargo/config.toml" + "gemrc /etc/gemrc" +) + +# The installer writes the marker from the timezone. +region=global +if [[ -e /etc/omarchy/region ]]; then + region=$(&2 + exit 1 + fi + done + for link in "${cn_links[@]}"; do + read -r source target <<<"$link" + mkdir -p "${target%/*}" + ln -sfn "$OMARCHY_PATH/default/regions/cn/$source" "$target" + done + ;; +*) + echo "Unknown region: $region" >&2 + exit 1 + ;; +esac diff --git a/test/shell.d/region-test.sh b/test/shell.d/region-test.sh new file mode 100644 index 00000000..ef638034 --- /dev/null +++ b/test/shell.d/region-test.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +region_setup="$test_tmp/region.sh" +root="$test_tmp/root" +sed -e "s|/etc/|$root/etc/|g" -e "s| /\.cargo/| $root/.cargo/|g" "$ROOT/install/config/region.sh" >"$region_setup" +export OMARCHY_PATH="$ROOT" +mkdir -p "$root/etc/omarchy" + +bash -e "$region_setup" +[[ -z $(find "$root" -type l) ]] || fail "no marker links nothing" +pass "no marker installs Global" + +echo cn >"$root/etc/omarchy/region" +bash -e "$region_setup" +for link in "environment.conf etc/environment.d/90-omarchy-china.conf" "cargo.toml .cargo/config.toml" "gemrc etc/gemrc"; do + read -r source target <<<"$link" + [[ $(readlink "$root/$target") == "$ROOT/default/regions/cn/$source" ]] || fail "China links /$target to the shipped $source" +done +bash -e "$region_setup" +pass "the cn marker links the developer mirror drop-ins, again without error" + +rm "$root/etc/gemrc" +echo ":sources: []" >"$root/etc/gemrc" +if output=$(bash -e "$region_setup" 2>&1); then + fail "China refuses to replace an admin's /etc/gemrc" "$output" +fi +[[ $output == "Refusing to replace $root/etc/gemrc, which Omarchy did not create" && $(<"$root/etc/gemrc") == ":sources: []" ]] || + fail "China names the file it refuses to replace and leaves it" "$output" +rm "$root/etc/gemrc" +pass "region setup never replaces mirror config Omarchy did not create" + +echo mars >"$root/etc/omarchy/region" +if output=$(bash -e "$region_setup" 2>&1); then + fail "an unknown region fails" "$output" +fi +[[ $output == "Unknown region: mars" ]] || fail "region setup names the unknown region" "$output" +pass "region setup rejects an unknown region" + +grep -qx 'run_logged "$OMARCHY_INSTALL/config/region.sh"' "$ROOT/install/config/all.sh" || fail "the install runs region setup" +pass "the install runs region setup" From 16faf06fcff6adcdbfd973536acb003a4d136e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 12 Oct 2026 00:29:40 -0400 Subject: [PATCH 27/27] Switch the region under Setup > Region, and offer China Apps omarchy region cn|global switches an installed machine: cn upgrades, trusts ArchLinuxCN through the rendered China config, records the marker and links the developer mirrors, then refreshes pacman; global undoes only what Omarchy made. The developer mirrors reach new sessions only, so a terminal switch offers to log out. Install > China Apps, shown only in China, installs WeChat, QQ, Feishu, DingTalk, Tencent Meeting and WPS Office from the AUR and sets up Tencent Docs, Yuque and Shimo as web apps. manual/52-china.md describes the China region. --- AGENTS.md | 4 +- README.md | 1 + bin/omarchy | 1 + bin/omarchy-region | 57 ++++++++++++++++++++++ default/omarchy/omarchy-menu.jsonc | 13 +++++ docs/regions.md | 2 + install/config/region.sh | 15 +++++- manual/02-getting-started.md | 4 ++ manual/52-china.md | 28 +++++++++++ shell/plugins/menu/MenuModel.js | 3 +- test/shell.d/china-apps-test.sh | 75 +++++++++++++++++++++++++++++ test/shell.d/menu-test.sh | 11 ++++- test/shell.d/region-switch-test.sh | 76 ++++++++++++++++++++++++++++++ test/shell.d/region-test.sh | 28 +++++++++++ 14 files changed, 313 insertions(+), 5 deletions(-) create mode 100755 bin/omarchy-region create mode 100644 manual/52-china.md create mode 100644 test/shell.d/china-apps-test.sh create mode 100644 test/shell.d/region-switch-test.sh diff --git a/AGENTS.md b/AGENTS.md index 12dea04b..3f86b2fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,7 +99,9 @@ Exceptions are allowed for migration and package-helper scripts where the helper # Region - `/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`. -- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create. +- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create. It serves both the install (the marker) and `omarchy region cn|global` (`OMARCHY_REGION`), which also records or removes the marker; Global removes only the links it made. +- `omarchy-region` prints the region (`Global` or `China`) and switches it. Nothing in `omarchy update` or migrations may change the region. +- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard. User-facing docs: `manual/52-china.md`. # Config Structure diff --git a/README.md b/README.md index 5834780f..b49e441b 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ The manual lives in [`manual/`](manual/), which is its authoritative source. - [Omarchy on...](manual/49-omarchy-on.md) - [Dual Boot Install](manual/50-dual-boot-install.md) - [Unattended Installs](manual/51-unattended-installs.md) +- [China](manual/52-china.md) ## License diff --git a/bin/omarchy b/bin/omarchy index 35b8094a..0649f0a5 100755 --- a/bin/omarchy +++ b/bin/omarchy @@ -73,6 +73,7 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management" GROUP_DESCRIPTIONS[power]="Power supply detection" GROUP_DESCRIPTIONS[powerprofiles]="Power profile management" GROUP_DESCRIPTIONS[refresh]="Reset config to defaults" +GROUP_DESCRIPTIONS[region]="Region-specific package sources and setup" GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows" GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders" GROUP_DESCRIPTIONS[remove]="Removal workflows" diff --git a/bin/omarchy-region b/bin/omarchy-region new file mode 100755 index 00000000..cc557a47 --- /dev/null +++ b/bin/omarchy-region @@ -0,0 +1,57 @@ +#!/bin/bash + +# omarchy:summary=Show or switch the region this machine is set up for +# omarchy:args=[global|cn] +# omarchy:examples=omarchy region | omarchy region cn +# omarchy:requires-sudo=true + +set -euo pipefail + +region_file=/etc/omarchy/region + +if (($# == 0)); then + if [[ ! -e $region_file ]]; then + echo "Global" + elif [[ $(<"$region_file") == "cn" ]]; then + echo "China" + else + echo "Unknown region in $region_file: $(<"$region_file")" >&2 + exit 1 + fi + exit 0 +fi + +if [[ $1 != "global" && $1 != "cn" ]]; then + echo "Usage: omarchy-region [global|cn]" >&2 + exit 1 +fi + +channel=$(omarchy-version-channel) +if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then + echo "Cannot tell this machine's package channel ($channel); run omarchy channel set first" >&2 + exit 1 +fi + +if [[ $1 == "cn" ]]; then + # Upgrade first: the keyring install below syncs new databases. + omarchy-update-pacman -Syu --noconfirm + sudo env OMARCHY_REGION=cn OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh" + # Trust ArchLinuxCN before the refresh upgrades through it. + rendered=$(mktemp -d) + trap 'rm -rf "$rendered"' EXIT + omarchy-apply-pacman --render "$channel" "$rendered" + mapfile -t profile_packages < <(grep -Ev '^[[:space:]]*(#|$)' "$OMARCHY_PATH/default/regions/cn/packages") + sudo pacman --config "$rendered/pacman.conf" -Sy --noconfirm --needed "${profile_packages[@]}" + sudo pacman-key --populate archlinuxcn + omarchy-refresh-pacman "$channel" +else + sudo env OMARCHY_REGION=global OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh" + omarchy-refresh-pacman "$channel" +fi + +# The developer mirrors reach only a new session, so offer one. +if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "Log out now" --negative "Later" "Log out now to switch the developer mirrors?"; then + omarchy-system-logout +else + echo "Log back in to switch the developer mirrors." +fi diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 736cba1a..4b0c188c 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -173,6 +173,9 @@ "setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"}, "setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"}, "setup.default.editor.emacs": {"icon":" ","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"}, + "setup.region": {"icon":"󰇧","label":"Region"}, + "setup.region.global": {"icon":"󰖟","label":"Global","checked":"[[ \"$(omarchy-region)\" == \"Global\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region global'"}, + "setup.region.china": {"icon":"󰈻","label":"China","checked":"[[ \"$(omarchy-region)\" == \"China\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region cn'"}, "setup.plugin": {"icon":"󰐱","label":"Plugins","aliases":["plugin","plugins"]}, "setup.plugin.enable": {"icon":"󰄬","label":"Enable Plugin","action":"omarchy-menu-plugin enable"}, "setup.plugin.disable": {"icon":"󰅖","label":"Disable Plugin","action":"omarchy-menu-plugin disable"}, @@ -216,6 +219,16 @@ "install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"}, "install.windows": {"icon":"󰍲","label":"Windows","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"}, "install.aur": {"icon":"󰣇","label":"AUR","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-aur-install"}, + "install.china": {"icon":"󰓜","label":"China Apps","when":"[[ \"$(omarchy-region)\" == \"China\" ]]"}, + "install.china.wechat": {"icon":"󰘑","label":"WeChat","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wechat-universal-bwrap","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wechat-universal-bwrap'"}, + "install.china.qq": {"icon":"󰘅","label":"QQ","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present linuxqq","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add linuxqq'"}, + "install.china.feishu": {"icon":"󱗆","label":"Feishu","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present feishu-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add feishu-bin'"}, + "install.china.dingtalk": {"icon":"󰭹","label":"DingTalk","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present dingtalk-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add dingtalk-bin'"}, + "install.china.wemeet": {"icon":"󰕧","label":"Tencent Meeting","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wemeet-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wemeet-bin'"}, + "install.china.wps": {"icon":"󰈬","label":"WPS Office","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wps-office-cn","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wps-office-cn'"}, + "install.china.tencent-docs": {"icon":"󰈙","label":"Tencent Docs","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/腾讯文档.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 腾讯文档 https://docs.qq.com \"\"'"}, + "install.china.yuque": {"icon":"󰠮","label":"Yuque","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/语雀.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 语雀 https://www.yuque.com/dashboard \"\"'"}, + "install.china.shimo": {"icon":"󰈙","label":"Shimo","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/石墨文档.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 石墨文档 https://shimo.im/desktop \"\"'"}, "install.preinstalls": {"icon":"󰏓","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"}, "install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"}, "install.browser.edge": {"icon":"󰇩","label":"Edge","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"}, diff --git a/docs/regions.md b/docs/regions.md index e64d89c0..06bdccd3 100644 --- a/docs/regions.md +++ b/docs/regions.md @@ -40,6 +40,8 @@ The China profile provides repository defaults and developer mirrors. Language i The `cn` profile also points the common package managers at mirrors inside China: `environment.conf` (npm, PyPI and uv, Go, rustup, Bundler, and Maven through `MAVEN_ARGS`), `cargo.toml` for crates.io, `gemrc` for RubyGems, and `maven-settings.xml`, which Maven reads in place. During installation `install/config/region.sh` links the first three to `/etc/environment.d/90-omarchy-china.conf`, `/.cargo/config.toml` and `/etc/gemrc` when the marker says `cn`. It refuses to replace a regular file it did not create. The environment reaches sessions started after login. +`omarchy region cn|global` (_Setup > Region_) switches an installed machine. `cn` upgrades, installs and trusts the profile's keyring through a rendered China config, records the marker and links the developer mirrors through the same script, then refreshes pacman. `global` removes the marker and only the links that point into the profile, then refreshes. The region also shows _Install > China Apps_. + ## Remaining release validation Unit tests cover region/channel independence, the timezone mapping, 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`. diff --git a/install/config/region.sh b/install/config/region.sh index a009c123..d40a0082 100644 --- a/install/config/region.sh +++ b/install/config/region.sh @@ -4,14 +4,23 @@ cn_links=( "gemrc /etc/gemrc" ) -# The installer writes the marker from the timezone. +# The installer writes the marker from the timezone; omarchy-region passes the region it switches to. region=global if [[ -e /etc/omarchy/region ]]; then region=$(/etc/omarchy/region for link in "${cn_links[@]}"; do read -r source target <<<"$link" mkdir -p "${target%/*}" diff --git a/manual/02-getting-started.md b/manual/02-getting-started.md index 86ca7aa9..7a7bd7f4 100644 --- a/manual/02-getting-started.md +++ b/manual/02-getting-started.md @@ -20,6 +20,10 @@ Now you're ready to Omarchy! The full-disk encryption won't allow you to enter the password from a Bluetooth keyboard at startup. Just like you can't use a Bluetooth keyboard to enter the BIOS on a PC. You'll need a keyboard that either uses a 2.4ghz dongle or a cable (which is much nicer for latency anyway!). I personally love the [Lofree Flow84](https://www.lofree.co/products/lofree-flow-the-smoothest-mechanical-keyboard)! +### Installing in China + +The timezone you pick in the installer decides the region: `Asia/Shanghai` brings Chinese package and developer mirrors and the China apps. See [China](52-china.md). + ### Installing for another owner If you're setting up a machine for someone else — a family member, a new employee, a buyer — you shouldn't be answering the personal questions on their behalf. Hit `Ctrl + C` on the very first screen of the installer (the keyboard selection), and Omarchy will offer to prepare the machine for another owner instead. The system installs right away, but all the personal setup — keyboard layout, username, password — is deferred until the machine boots for the first time. The drive is still encrypted by default, and the password the new owner picks on that first boot becomes the encryption password too. (A machine you've already been using can be handed over without a reinstall too — see [resetting the computer](48-security.md).) diff --git a/manual/52-china.md b/manual/52-china.md new file mode 100644 index 00000000..3e00af21 --- /dev/null +++ b/manual/52-china.md @@ -0,0 +1,28 @@ +# China + +Omarchy works in mainland China without a VPN. The timezone you pick in the installer decides the region: `Asia/Shanghai`, `Asia/Urumqi` and their older names select China; any other timezone is Global. The summary screen shows _Region: cn_ before anything touches the disk. + +## What the China region sets up + +| Area | China | Global | +|------|-------|--------| +| Arch packages | The channel's `.omarchy.cn` mirror first, its `.omarchy.org` mirror behind it, and the ArchLinuxCN repository with its keyring | `.omarchy.org` mirrors | +| Omarchy packages | `pkgs.omarchy.org` | `pkgs.omarchy.org` | +| Developer mirrors | npm, PyPI and uv, Go, rustup and crates.io, RubyGems and Bundler, Maven | Upstream registries | +| Software | _Install > China Apps_ | Hidden | + +pacman tries the `.cn` mirror first. If it is unreachable or lacks a package, pacman falls through to `.org`, which works for anyone with their own VPN. The installer carries the China packages on the ISO, so a China install needs no network. + +## Developer mirrors + +China machines point the common package managers at mirrors inside China. Environment variables in `/etc/environment.d/90-omarchy-china.conf` cover npm, PyPI and uv, Go, rustup, Bundler and Maven; they reach everything started from the desktop after your next login (not SSH or TTY shells), and they take precedence over npm, pip, uv and Go config files, so unset the variable in a shell to opt out. `/.cargo/config.toml` points crates.io at the mirror unless a project's `.cargo/config.toml` says otherwise, and `/etc/gemrc` sets the RubyGems source unless your `~/.gemrc` sets another. Omarchy never replaces one of those two files if it is a regular file you created. Toolchain downloads by mise, such as Node.js or Go themselves, still come from upstream. + +## China Apps + +_Install > China Apps_ installs WeChat, QQ, Feishu, DingTalk, Tencent Meeting and WPS Office from the AUR, and sets up Tencent Docs, Yuque and Shimo as web apps. + +## Switching later + +_Setup > Region_, or `omarchy region cn` and `omarchy region global`, switches the region. China runs a full system update, trusts the ArchLinuxCN keyring, records the region, moves pacman to the `.cn` mirrors and links the developer mirrors. Global moves pacman back to `.org`, removes the developer mirrors and hides _Install > China Apps_; apps you installed from it stay. The developer mirrors change at the next login, so the switch offers to log out. `omarchy update` never changes the region. + +A machine prepared for another owner installs Global, since its timezone is only set on first boot. Switch it under _Setup > Region_ once it is online. diff --git a/shell/plugins/menu/MenuModel.js b/shell/plugins/menu/MenuModel.js index 53c49768..b5b22ac4 100644 --- a/shell/plugins/menu/MenuModel.js +++ b/shell/plugins/menu/MenuModel.js @@ -398,7 +398,8 @@ var GUARD_READERS = [ "omarchy-default-browser", "omarchy-default-editor", "omarchy-default-terminal", - "omarchy-dns" + "omarchy-dns", + "omarchy-region" ] // Package and command presence account for most of what the guards ask, and diff --git a/test/shell.d/china-apps-test.sh b/test/shell.d/china-apps-test.sh new file mode 100644 index 00000000..734a1a3d --- /dev/null +++ b/test/shell.d/china-apps-test.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +require_command node + +node - "$ROOT" <<'JS' +const fs = require('fs') +const root = process.argv[2] +const model = require(`${root}/shell/plugins/menu/MenuModel.js`) +const items = model.parseMenuJsonc(fs.readFileSync(`${root}/default/omarchy/omarchy-menu.jsonc`, 'utf8')) +const china = items.filter(item => item.id === 'install.china' || item.id.startsWith('install.china.')) +const guard = '[[ "$(omarchy-region)" == "China" ]]' +const fail = message => { console.error(`not ok - ${message}`); process.exit(1) } + +const aur = { + wechat: 'wechat-universal-bwrap', qq: 'linuxqq', feishu: 'feishu-bin', + dingtalk: 'dingtalk-bin', wemeet: 'wemeet-bin', wps: 'wps-office-cn', +} +const web = { + 'tencent-docs': ['腾讯文档', 'https://docs.qq.com'], + yuque: ['语雀', 'https://www.yuque.com/dashboard'], + shimo: ['石墨文档', 'https://shimo.im/desktop'], +} + +if (china.length !== 1 + Object.keys(aur).length + Object.keys(web).length) fail(`China Apps has exactly the expected entries: ${china.map(i => i.id)}`) +if (!china.every(item => item.when === guard)) fail('every China Apps entry, including search hits, shows only in China') +for (const [id, pkg] of Object.entries(aur)) { + const item = china.find(i => i.id === `install.china.${id}`) + if (!item || item.action !== `omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add ${pkg}'` || item.disabled !== `omarchy-pkg-present ${pkg}`) + fail(`${id} installs and checks the AUR package ${pkg}`) +} +for (const [id, [name, url]] of Object.entries(web)) { + const item = china.find(i => i.id === `install.china.${id}`) + if (!item || item.action !== `omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install ${name} ${url} ""'` || item.disabled !== `[[ -f "$HOME/.local/share/applications/${name}.desktop" ]]`) + fail(`${id} installs and checks the ${name} web app`) +} +console.log('ok - China Apps lists the AUR packages and web apps, shown only in China') +JS + +aur_names="wechat-universal-bwrap linuxqq feishu-bin dingtalk-bin wemeet-bin wps-office-cn" +if [[ ${OMARCHY_TEST_NETWORK:-} == "1" ]]; then + query=$(printf '&arg[]=%s' $aur_names) + found=$(curl -fsSL "https://aur.archlinux.org/rpc/v5/info?${query#&}" | jq -r '.results[].Name' | sort | tr '\n' ' ') + [[ $found == "$(printf '%s\n' $aur_names | sort | tr '\n' ' ')" ]] || fail "every China Apps package exists in the AUR" "$found" + pass "every China Apps package exists in the AUR" +else + skip "AUR existence check needs OMARCHY_TEST_NETWORK=1" +fi + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT +mkdir -p "$test_tmp/bin" "$test_tmp/home" +cat >"$test_tmp/bin/curl" <<'SH' +#!/bin/bash +while (($#)); do [[ $1 == "-o" ]] && out=$2; shift; done +[[ -n ${out:-} ]] && printf '\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0\x01\0\0\0\x01\x08\x06\0\0\0\x1f\x15\xc4\x89' >"$out" +exit 0 +SH +printf '#!/bin/bash\necho "the web app wizard opened" >&2\nexit 1\n' >"$test_tmp/bin/gum" +chmod +x "$test_tmp/bin/curl" "$test_tmp/bin/gum" + +grep -o "omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install [^']*'" "$ROOT/default/omarchy/omarchy-menu.jsonc" | + sed -e "s/^[^']*'//" -e "s/'$//" -e 's/\\"/"/g' >"$test_tmp/actions" +(($(wc -l <"$test_tmp/actions") == 3)) || fail "the menu has three China web app actions" +while read -r action; do + HOME="$test_tmp/home" PATH="$test_tmp/bin:$ROOT/bin:$PATH" bash -c "$action" >/dev/null || + fail "the menu action installs its web app: $action" +done <"$test_tmp/actions" +for app in 腾讯文档 语雀 石墨文档; do + [[ -f $test_tmp/home/.local/share/applications/$app.desktop ]] || fail "$app gets a launcher" +done +pass "each China web app action creates its launcher without the wizard" diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index f752ffc8..0be189ff 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -284,13 +284,22 @@ assert( 'menu always exposes every supported browser, terminal, and editor under Defaults' ) assert(!defaultById['install.ai.crush'], 'menu removes Crush from Install > AI') +const regionEntries = defaultItems.filter(item => item.parent === 'setup.region') +assert( + regionEntries.map(item => item.label).join('\0') === 'Global\0China' + && regionEntries.every(item => item.checked.includes(`== \"${item.label}\"`) && !item.when) + && defaultById['setup.region.global'].action.includes('omarchy-region global') + && defaultById['setup.region.china'].action.includes('omarchy-region cn'), + 'menu offers Global and China under Setup > Region' +) // Software you already have keeps its place in Install, dimmed rather than // dropped, so the list reads as a catalog of what Omarchy can install. // Chromium Account is the sole Install row with anything left to hide for, so // any other `when:` here is a row that went back to vanishing once installed. assertDeepEqual( defaultItems - .filter(item => item.id.startsWith('install.') && item.action && item.when) + // China Apps rows hide by region, not because the software is there. + .filter(item => item.id.startsWith('install.') && item.action && item.when && item.when !== '[[ "$(omarchy-region)" == "China" ]]') .map(item => item.id), ['install.service.chromium-account'], 'menu never hides an Install row because the software is already there' diff --git a/test/shell.d/region-switch-test.sh b/test/shell.d/region-switch-test.sh new file mode 100644 index 00000000..64fd9786 --- /dev/null +++ b/test/shell.d/region-switch-test.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +root="$test_tmp/root" sandbox="$test_tmp/omarchy" calls="$test_tmp/calls" +mkdir -p "$root/etc/pacman.d" "$sandbox/bin" "$sandbox/install/config" "$sandbox/default/regions" "$test_tmp/bin" "$test_tmp/home" +cp -r "$ROOT/default/regions/cn" "$sandbox/default/regions/" +rewrite() { sed -e "s|/etc/|$root/etc/|g" -e "s| /\\.cargo/| $root/.cargo/|g" "$1" >"$2"; chmod +x "$2"; } +rewrite "$ROOT/install/config/region.sh" "$sandbox/install/config/region.sh" +rewrite "$ROOT/bin/omarchy-region" "$sandbox/bin/omarchy-region" + +for command in omarchy-refresh-pacman omarchy-system-logout pacman-key pacman omarchy-apply-pacman; do + printf '#!/bin/bash\necho "%s $*" >>"%s"\n' "$command" "$calls" >"$test_tmp/bin/$command" +done +printf '#!/bin/bash\necho "omarchy-update-pacman $*" >>"%s"\n[[ -z ${STUB_UPDATE_FAIL:-} ]]\n' "$calls" >"$test_tmp/bin/omarchy-update-pacman" +printf '#!/bin/bash\necho "${STUB_CHANNEL:-stable}"\n' >"$test_tmp/bin/omarchy-version-channel" +printf '#!/bin/bash\n[[ $1 == env ]] && shift\nexec env "$@"\n' >"$test_tmp/bin/sudo" +printf '#!/bin/bash\necho "gum $*" >>"%s"\nexit "${GUM_ANSWER:-1}"\n' "$calls" >"$test_tmp/bin/gum" +chmod +x "$test_tmp/bin"/* +mkdir -p "$test_tmp/tmp" +export PATH="$test_tmp/bin:$sandbox/bin:$PATH" OMARCHY_PATH="$sandbox" HOME="$test_tmp/home" TMPDIR="$test_tmp/tmp" +switch_calls() { sed "s|$test_tmp/tmp/[^/ ]*|RENDERED|g" "$calls"; } + +omarchy-region cn >/dev/null +[[ $(omarchy-region) == "China" && $(<"$root/etc/omarchy/region") == "cn" ]] || + fail "switching to China records cn" +[[ $(switch_calls) == $'omarchy-update-pacman -Syu --noconfirm\nomarchy-apply-pacman --render stable RENDERED\npacman --config RENDERED/pacman.conf -Sy --noconfirm --needed archlinuxcn-keyring\npacman-key --populate archlinuxcn\nomarchy-refresh-pacman stable' ]] || + fail "China upgrades, trusts ArchLinuxCN through the rendered cn config, then refreshes" "$(switch_calls)" +[[ -z $(ls -A "$test_tmp/tmp") ]] || fail "the rendered config is removed" +pass "switching to China trusts ArchLinuxCN before the refresh upgrades through it" + +: >"$calls" +STUB_CHANNEL=edge omarchy-region cn >/dev/null +grep -qx 'omarchy-refresh-pacman edge' "$calls" || fail "the refresh keeps the machine's channel" "$(<"$calls")" +pass "switching to China again keeps the channel" + +: >"$calls" +omarchy-region global >/dev/null +[[ $(omarchy-region) == "Global" && ! -e $root/etc/omarchy/region ]] || + fail "switching to Global removes the region" +[[ $(<"$calls") == 'omarchy-refresh-pacman stable' ]] || + fail "Global refreshes pacman back to the channel defaults" "$(<"$calls")" +pass "switching to Global restores the channel's pacman defaults" + +if output=$(omarchy-region mars 2>&1); then + fail "an unknown region is refused" "$output" +fi +if output=$(STUB_CHANNEL="stable / edge" omarchy-region cn 2>&1); then + fail "a mixed channel is refused" "$output" +fi +[[ $(omarchy-region) == "Global" ]] || fail "a refused switch changes nothing" +pass "an unknown region or an unclear channel is refused" + +: >"$calls" +if output=$(STUB_UPDATE_FAIL=1 omarchy-region cn 2>&1); then + fail "switching to China fails when the upgrade fails" "$output" +fi +[[ $(omarchy-region) == "Global" ]] && ! grep -q '^omarchy-refresh-pacman' "$calls" || + fail "a failed switch to China leaves the region Global and pacman untouched" "$(<"$calls")" +pass "a failed switch to China leaves the machine Global" + +grep -q '^gum' "$calls" && fail "a switch without a terminal asks nothing" "$(<"$calls")" +: >"$calls" +output=$(GUM_ANSWER=1 script -qec "omarchy-region cn" /dev/null) +[[ $output == *"Log back in to switch the developer mirrors."* ]] && ! grep -q '^omarchy-system-logout' "$calls" || + fail "declining the logout leaves a reminder and the session" "$output" +: >"$calls" +GUM_ANSWER=0 script -qec "omarchy-region global" /dev/null >/dev/null +grep -qxF 'gum confirm --affirmative Log out now --negative Later Log out now to switch the developer mirrors?' "$calls" && + grep -qx 'omarchy-system-logout ' "$calls" || fail "a terminal switch offers to log out and logs out on yes" "$(<"$calls")" +pass "a switch in a terminal offers to log out" diff --git a/test/shell.d/region-test.sh b/test/shell.d/region-test.sh index ef638034..9639b774 100644 --- a/test/shell.d/region-test.sh +++ b/test/shell.d/region-test.sh @@ -43,5 +43,33 @@ fi [[ $output == "Unknown region: mars" ]] || fail "region setup names the unknown region" "$output" pass "region setup rejects an unknown region" +echo cn >"$root/etc/omarchy/region" +bash -e "$region_setup" +OMARCHY_REGION=global bash -e "$region_setup" +[[ ! -e $root/etc/omarchy/region && -z $(find "$root" -type l) ]] || fail "Global removes the marker and the developer mirror links" +ln -s /usr/share/somewhere/gemrc "$root/etc/gemrc" +OMARCHY_REGION=global bash -e "$region_setup" +[[ -L $root/etc/gemrc ]] || fail "Global keeps a gemrc link Omarchy did not create" +rm "$root/etc/gemrc" +pass "switching to Global removes only what Omarchy linked" + +OMARCHY_REGION=cn bash -e "$region_setup" +[[ $(<"$root/etc/omarchy/region") == "cn" && $(readlink "$root/etc/gemrc") == "$ROOT/default/regions/cn/gemrc" ]] || + fail "switching to China records cn and links the drop-ins" +pass "switching to China records cn and links the drop-ins" + +region_bin="$test_tmp/omarchy-region" +sed "s|/etc/omarchy/region|$root/etc/omarchy/region|" "$ROOT/bin/omarchy-region" >"$region_bin" +chmod +x "$region_bin" +[[ $("$region_bin") == "China" ]] || fail "the cn marker reads as China" +rm "$root/etc/omarchy/region" +[[ $("$region_bin") == "Global" ]] || fail "no marker reads as Global" +echo mars >"$root/etc/omarchy/region" +if output=$("$region_bin" 2>&1); then + fail "an unknown marker fails" "$output" +fi +[[ $output == "Unknown region in $root/etc/omarchy/region: mars" ]] || fail "the reader names the file and value" "$output" +pass "omarchy region reads Global, China, and fails on anything else" + grep -qx 'run_logged "$OMARCHY_INSTALL/config/region.sh"' "$ROOT/install/config/all.sh" || fail "the install runs region setup" pass "the install runs region setup"