From 0de49b231aa9bee8d007a0cee8ef43a10acfbe9f Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Thu, 8 Oct 2026 15:03:41 -0400 Subject: [PATCH 01/10] =?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 02/10] =?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 03/10] =?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 04/10] =?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 05/10] =?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 06/10] =?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 07/10] =?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 08/10] =?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 09/10] =?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 10/10] =?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"