From 53a41664fa21df678525d5190e2469825bfb7fac Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:49:09 -0400 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20feat(build):=20isolate=20regi?= =?UTF-8?q?onal=20iso=20variants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: separate regional payloads and caches without changing global defaults - :lock: verify regional package trust before building the offline repository Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567 --- bin/omarchy-iso-make | 29 ++++++++++++++++++++++----- builder/build-iso.sh | 29 +++++++++++++++++++++++++-- builder/prepare-region.sh | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 builder/prepare-region.sh diff --git a/bin/omarchy-iso-make b/bin/omarchy-iso-make index 5dbbeff..500de4b 100755 --- a/bin/omarchy-iso-make +++ b/bin/omarchy-iso-make @@ -6,8 +6,17 @@ NO_CACHE="" KEEP_PKG_CACHE="" LOCAL_OMARCHY_PATH="" LOCAL_PKGS_PATH="" +OMARCHY_REGION="${OMARCHY_REGION:-global}" while [[ $# -gt 0 ]]; do case $1 in + --region) + OMARCHY_REGION="${2:-}" + if [[ $OMARCHY_REGION != "global" && $OMARCHY_REGION != "cn" ]]; then + echo "Error: --region requires global or cn (ISO 3166-1 alpha-2)." >&2 + exit 1 + fi + shift 2 + ;; --no-cache) NO_CACHE=1 shift @@ -57,12 +66,17 @@ while [[ $# -gt 0 ]]; do ;; *) echo "Unknown option: $1" - echo "Usage: $0 [--no-cache] [--keep-pkg-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--local-source ]" + echo "Usage: $0 [--no-cache] [--keep-pkg-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--region global|cn] [--local-source ]" exit 1 ;; esac done +if [[ $OMARCHY_REGION != "global" && $OMARCHY_REGION != "cn" ]]; then + echo "Error: unsupported region '$OMARCHY_REGION'. Expected global or cn." >&2 + exit 1 +fi + if [[ -n $LOCAL_OMARCHY_PATH ]]; then [[ -d $LOCAL_OMARCHY_PATH ]] || { echo "Error: Omarchy checkout not found at $LOCAL_OMARCHY_PATH" >&2; exit 1; } [[ -d $LOCAL_PKGS_PATH ]] || { echo "Error: pkgs checkout not found at $LOCAL_PKGS_PATH" >&2; exit 1; } @@ -125,6 +139,7 @@ DOCKER_ARGS=( --privileged -e "OMARCHY_ISO_REF=$OMARCHY_ISO_REF" -e "OMARCHY_MIRROR=$OMARCHY_MIRROR" + -e "OMARCHY_REGION=$OMARCHY_REGION" -e "OMARCHY_INSTALL_DEBUG=${OMARCHY_INSTALL_DEBUG:-}" -e "HOST_UID=$(id -u)" -e "HOST_GID=$(id -g)" @@ -146,10 +161,14 @@ if [[ -d /var/cache/pacman/pkg ]]; then DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg") fi -# Mount the build cache directory where packages are downloaded. Channels use -# different package snapshots and must never share an offline mirror cache. +# Channels and regions must not share an offline mirror cache. Keep the +# existing global cache path and artifact suffix unchanged. +region_suffix="" +if [[ $OMARCHY_REGION != "global" ]]; then + region_suffix="-$OMARCHY_REGION" +fi if [[ -z "$NO_CACHE" ]]; then - OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${OMARCHY_MIRROR}/airootfs/var/cache/omarchy" + OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${OMARCHY_MIRROR}${region_suffix}/airootfs/var/cache/omarchy" mkdir -p "$OFFLINE_REPO_BUILD_CACHE_DIR" DOCKER_ARGS+=(-v "$OFFLINE_REPO_BUILD_CACHE_DIR:/var/cache/airootfs/var/cache/omarchy") else @@ -169,7 +188,7 @@ fi "${DOCKER[@]}" run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*.iso | head -n1) -iso_ref="${latest_iso%.*}-$OMARCHY_ISO_REF.iso" +iso_ref="${latest_iso%.*}-${OMARCHY_ISO_REF}${region_suffix}.iso" mv -f "$latest_iso" "$iso_ref" if [[ -z "${NO_BOOT_OFFER:-}" ]]; then diff --git a/builder/build-iso.sh b/builder/build-iso.sh index 0c917d6..5224eef 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -4,6 +4,11 @@ set -e OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}" OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}" +OMARCHY_REGION="${OMARCHY_REGION:-global}" +case "$OMARCHY_REGION" in + global|cn) ;; + *) echo "Unsupported region: $OMARCHY_REGION" >&2; exit 1 ;; +esac # Edge, dev, and local-source ISOs install the dev packages explicitly. Those # package recipes track the quattro branch. This avoids relying on pacman's @@ -67,6 +72,7 @@ cp -r /configs/* "$build_cache_dir/" mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso" echo "$OMARCHY_MIRROR" > "$build_cache_dir/airootfs/root/omarchy_mirror" echo "$OMARCHY_ISO_REF" > "$build_cache_dir/airootfs/root/omarchy_iso_ref" +echo "$OMARCHY_REGION" > "$build_cache_dir/airootfs/root/omarchy_region" cat > "$build_cache_dir/airootfs/usr/share/omarchy-iso/package-targets" <&2 + exit 1 + fi + fi base_pkg_lists=(/tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-base.packages /tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-other.packages) # Extracted on its own, tolerating a miss: bsdtar exits non-zero for a member # it can't find, so asking for this alongside the package lists would abort the @@ -168,6 +184,15 @@ mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso" cp "${base_pkg_lists[0]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages" cp "${base_pkg_lists[1]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages" +# Work on writable copies, leaving the mounted runtime and ISO configs intact. +online_config=/tmp/omarchy-pacman-online.conf +cp "/configs/pacman-online-${OMARCHY_MIRROR}.conf" "$online_config" +bash /builder/prepare-region.sh "$OMARCHY_REGION" "$runtime_root" "$build_cache_dir/airootfs" "$online_config" +base_pkg_lists=( + "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages" + "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages" +) + # The configurator's setup form comes from the runtime this ISO bundles, so the # installer and the first-boot setup that finishes a deferred install can never # disagree. A runtime predating the split ships no such file, which would leave @@ -232,7 +257,7 @@ fi mkdir -p /tmp/offlinedb download_offline_packages() { - pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw \ + pacman --config "$online_config" --noconfirm -Syw \ "${all_packages[@]}" --cachedir "$offline_mirror_dir/" --dbpath /tmp/offlinedb --needed } @@ -250,7 +275,7 @@ fi # newest version of every cached package name) removes packages that have left # the lists or dependency closure, such as an old Electron major version. if ! resolved_package_files="$( - pacman --config "/configs/pacman-online-${OMARCHY_MIRROR}.conf" --noconfirm \ + pacman --config "$online_config" --noconfirm \ --dbpath /tmp/offlinedb -S --print --print-format '%f' "${all_packages[@]}" )"; then echo "ERROR: could not resolve the package files required by the offline mirror" >&2 diff --git a/builder/prepare-region.sh b/builder/prepare-region.sh new file mode 100644 index 0000000..2707fba --- /dev/null +++ b/builder/prepare-region.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Add the selected runtime's regional payload to the ISO. Repository defaults +# stay in the runtime package, not in a second set of ISO-specific templates. +set -euo pipefail + +region="$1" +runtime="$2" +iso_root="$3" +online_config="$4" + +case "$region" in + global) exit 0 ;; + cn) ;; + *) echo "Unsupported region: $region" >&2; exit 1 ;; +esac + +profile="$runtime/default/regions/$region" +if [[ ! -f $runtime/bin/omarchy-apply-pacman || ! -f $profile/packages || + ! -f $profile/pacman/pacman.conf.append || ! -f $profile/pacman/mirrorlist.append ]]; then + echo "Runtime does not support region '$region'; publish a matching runtime or use --local-source." >&2 + exit 1 +fi + +# This is the target package list used both for download resolution and by the +# installer. Merely putting extra packages in the cache would not install them. +payload="$iso_root/usr/share/omarchy-iso" +cp -a "$profile" "$payload/region" +printf '\n' >> "$payload/omarchy-base.packages" +cat "$profile/packages" >> "$payload/omarchy-base.packages" + +# Only the community repository is needed for regional package downloads. +# Keep the channel's build mirrors intact; target mirrors are restored later. +printf '\n' >> "$online_config" +cat "$profile/pacman/pacman.conf.append" >> "$online_config" + +# The keyring package must be signed by a key already trusted by Arch. Do not +# bootstrap trust from an unchecked keyserver response or disable signatures. +# https://github.com/archlinuxcn/repo#usage +pacman --config "$online_config" --noconfirm -Sy --needed archlinuxcn-keyring +pacman-key --populate archlinuxcn From 27b33e7f1c877edf05f6aa220ccef1da19f54ae4 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:49:09 -0400 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=A8=20feat(install):=20seed=20regio?= =?UTF-8?q?nal=20user=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: support offline input for immediate and deferred owners - :lock: initialize target trust before enabling regional repositories Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567 --- .../omarchy-iso/orchestrator/phases_impl.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py index 1ebd4ce..efe5575 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -39,6 +39,36 @@ from .keyboard import configure_keyboard from .ui import error, info +ISO_REGION_FILE = Path("/root/omarchy_region") +ISO_REGION_PAYLOAD = Path("/usr/share/omarchy-iso/region") + + +def _iso_region() -> str: + region = ISO_REGION_FILE.read_text().strip() if ISO_REGION_FILE.exists() else "global" + if region not in {"global", "cn"}: + raise RuntimeError(f"Unsupported ISO region: {region}") + return region + + +def _stage_region_defaults(target: Path) -> None: + """Seed the target before useradd, including deferred first-boot users. + + Region is a bootstrap default, not a locale or a user-level policy. + Existing home directories are never rewritten by this step. + """ + region = _iso_region() + if region != "global": + if not ISO_REGION_PAYLOAD.is_dir(): + raise RuntimeError(f"Missing ISO payload for region: {region}") + skel = ISO_REGION_PAYLOAD / "skel" + if skel.is_dir(): + shutil.copytree(skel, target / "etc/skel", dirs_exist_ok=True) + marker = target / "etc/omarchy/region" + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text(region + "\n") + marker.chmod(0o644) + + # Package targets are written by builder/build-iso.sh. Stable ISOs use the # stable package names, while dev/local-source ISOs install the dev package # names explicitly instead of relying on provides=omarchy resolution. @@ -287,6 +317,7 @@ def arch_install_system(ctx: InstallContext) -> None: _install_early_packages(installer) _configure_limine_boot(ctx, installer, config) + _stage_region_defaults(ctx.target) info("› creating user (with /etc/skel populated)") if config.auth_config and config.auth_config.users: installer.create_users(config.auth_config.users) @@ -1135,6 +1166,13 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str def run_system_finalizer(ctx: InstallContext) -> None: + # The runtime package transaction has installed the regional keyring from + # the ISO. Initialize the target's trust database before enabling the online + # community repository; the build container's GPG state is not inherited. + if _iso_region() == "cn": + _run_target_setup_command(ctx, ["pacman-key", "--init"]) + _run_target_setup_command(ctx, ["pacman-key", "--populate", "archlinux", "archlinuxcn"]) + if ctx.defer_provisioning: cmd = ["/usr/bin/omarchy-apply-system", "--defer-provisioning", "--first-install"] else: From 39d65593dfd115fe29c8a0c35b65a85000c9bdd5 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:49:09 -0400 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=85=20test(region):=20cover=20regio?= =?UTF-8?q?nal=20iso=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :white_check_mark: guard cache isolation, trust failures, and installation ordering - :shield: keep global builds and existing user settings unchanged Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567 --- test/unit/test_kernel_selection.py | 6 +- test/unit/test_region.py | 234 +++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 test/unit/test_region.py diff --git a/test/unit/test_kernel_selection.py b/test/unit/test_kernel_selection.py index 5e01f73..ceabe50 100644 --- a/test/unit/test_kernel_selection.py +++ b/test/unit/test_kernel_selection.py @@ -93,6 +93,7 @@ class KernelSelectionTest(unittest.TestCase): events = [] installer = mock.MagicMock() installer.minimal_installation.side_effect = lambda **kwargs: events.append("base") + installer.create_users.side_effect = lambda users: events.append("user") def install_packages(packages): events.append(packages) @@ -102,7 +103,7 @@ class KernelSelectionTest(unittest.TestCase): installer.add_additional_packages.side_effect = install_packages config = types.SimpleNamespace( kernels=kernels, locale_config=None, mirror_config=None, - swap=None, auth_config=None, app_config=None, timezone=None, + swap=None, auth_config=types.SimpleNamespace(users=["alice"]), app_config=None, timezone=None, ntp=False, hostname="test", pacman_config="/etc/pacman.conf", ) ctx = types.SimpleNamespace( @@ -116,6 +117,7 @@ class KernelSelectionTest(unittest.TestCase): unmount = stack.enter_context(mock.patch.object(phases_impl, "_unmount_offline_package_cache")) stack.enter_context(mock.patch.object(phases_impl, "configure_keyboard", return_value=True)) stack.enter_context(mock.patch.object(phases_impl, "_install_early_packages", side_effect=lambda inst: events.append("early"))) + stack.enter_context(mock.patch.object(phases_impl, "_stage_region_defaults", side_effect=lambda target: events.append("region"))) stack.enter_context(mock.patch.object(phases_impl, "_runtime_package_list", return_value=["omarchy"])) stack.enter_context(mock.patch.object(phases_impl.arch, "is_pre_mount", return_value=True, create=True)) stack.enter_context(mock.patch.object(phases_impl.arch, "root_user", return_value=None, create=True)) @@ -128,7 +130,7 @@ class KernelSelectionTest(unittest.TestCase): phases_impl.arch_install_system(ctx) expected = ["base", [f"{kernel}-headers" for kernel in kernels]] if not fail_headers: - expected += ["early", ["omarchy"]] + expected += ["early", "region", "user", ["omarchy"]] self.assertEqual(events, expected) unmask.assert_called_once_with(ctx) unmount.assert_called_once_with(ctx) diff --git a/test/unit/test_region.py b/test/unit/test_region.py new file mode 100644 index 0000000..5fb514e --- /dev/null +++ b/test/unit/test_region.py @@ -0,0 +1,234 @@ +"""Region bootstrap contracts without Docker, pacman, or a running guest.""" + +import os +import shutil +import subprocess +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest import mock + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "configs/airootfs/usr/share/omarchy-iso")) +sys.modules.setdefault("orchestrator.archinstall_adapter", types.ModuleType("orchestrator.archinstall_adapter")) +from orchestrator import phases_impl + + +class RegionTargetTest(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.root = Path(tmp.name) + self.target = self.root / "target" + self.marker = self.root / "omarchy_region" + self.payload = self.root / "payload" + for name, value in (("ISO_REGION_FILE", self.marker), ("ISO_REGION_PAYLOAD", self.payload)): + patch = mock.patch.object(phases_impl, name, value) + patch.start() + self.addCleanup(patch.stop) + + def test_global_does_not_need_a_profile_or_change_user_defaults(self): + phases_impl._stage_region_defaults(self.target) + self.assertEqual((self.target / "etc/omarchy/region").read_text(), "global\n") + self.assertFalse((self.target / "etc/skel").exists()) + + def test_cn_seeds_skel_without_touching_existing_users_or_locale(self): + self.marker.write_text("cn\n") + profile = self.payload / "skel/.config/fcitx5/profile" + profile.parent.mkdir(parents=True) + profile.write_text("regional input defaults\n") + existing = self.target / "home/alice/.config/fcitx5/profile" + existing.parent.mkdir(parents=True) + existing.write_text("personal settings\n") + skel = self.target / "etc/skel" + skel.mkdir(parents=True) + (skel / ".bashrc").write_text("original\n") + (self.target / "etc/locale.conf").write_text("LANG=en_US.UTF-8\n") + + phases_impl._stage_region_defaults(self.target) + self.assertEqual((skel / ".config/fcitx5/profile").read_text(), "regional input defaults\n") + self.assertEqual((skel / ".bashrc").read_text(), "original\n") + self.assertEqual(existing.read_text(), "personal settings\n") + self.assertEqual((self.target / "etc/locale.conf").read_text(), "LANG=en_US.UTF-8\n") + self.assertEqual((self.target / "etc/omarchy/region").read_text(), "cn\n") + + def test_bad_region_and_missing_payload_fail_before_target_changes(self): + for region in ("CN", "chn", "zz", "../cn", "", "cn"): + with self.subTest(region=region): + self.marker.write_text(region) + with self.assertRaises(RuntimeError): + phases_impl._stage_region_defaults(self.target) + self.assertFalse(self.target.exists()) + + def test_cn_keyring_is_initialized_before_finalization_for_both_install_modes(self): + self.marker.write_text("cn\n") + for deferred in (False, True): + ctx = types.SimpleNamespace(target=self.target, username="alice", defer_provisioning=deferred) + with mock.patch.object(phases_impl, "_run_target_setup_command") as run, \ + mock.patch.object(phases_impl, "_mask_mkinitcpio_pacman_hooks"), \ + mock.patch.object(phases_impl, "_unmask_mkinitcpio_pacman_hooks"): + phases_impl.run_system_finalizer(ctx) + commands = [call.args[1] for call in run.call_args_list] + self.assertEqual(commands[:2], [["pacman-key", "--init"], ["pacman-key", "--populate", "archlinux", "archlinuxcn"]]) + expected = ["--defer-provisioning"] if deferred else ["--install-user", "alice"] + self.assertEqual(commands[2], ["/usr/bin/omarchy-apply-system", *expected, "--first-install"]) + + def test_keyring_failure_prevents_enabling_online_repositories(self): + self.marker.write_text("cn\n") + ctx = types.SimpleNamespace(target=self.target, username="alice", defer_provisioning=False) + with mock.patch.object(phases_impl, "_run_target_setup_command", side_effect=RuntimeError("keyring failed")) as run: + with self.assertRaisesRegex(RuntimeError, "keyring failed"): + phases_impl.run_system_finalizer(ctx) + self.assertEqual(run.call_count, 1) + + +class RegionBuildTest(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.root = Path(tmp.name) + self.runtime = self.root / "runtime" + self.profile = self.runtime / "default/regions/cn" + (self.profile / "pacman").mkdir(parents=True) + (self.runtime / "bin").mkdir() + (self.runtime / "bin/omarchy-apply-pacman").touch() + (self.profile / "packages").write_text("archlinuxcn-keyring\nfcitx5-rime\n") + (self.profile / "pacman/pacman.conf.append").write_text("[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n") + (self.profile / "pacman/mirrorlist.append").write_text("Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch\n") + self.iso = self.root / "iso" + self.payload = self.iso / "usr/share/omarchy-iso" + self.payload.mkdir(parents=True) + self.packages = self.payload / "omarchy-base.packages" + self.packages.write_text("base\n") + self.config = self.root / "pacman.conf" + shutil.copyfile(ROOT / "configs/pacman-online-stable.conf", self.config) + self.original_config = self.config.read_bytes() + self.bin = self.root / "bin" + self.bin.mkdir() + self.log = self.root / "commands" + for command in ("pacman", "pacman-key"): + stub = self.bin / command + stub.write_text('#!/bin/bash\nprintf "%s %s\\n" "${0##*/}" "$*" >> "$TEST_LOG"\nif [[ ${0##*/} == pacman ]]; then exit "${TEST_PACMAN_STATUS:-0}"; fi\n') + stub.chmod(0o755) + + def prepare(self, region, **env): + return subprocess.run( + ["bash", str(ROOT / "builder/prepare-region.sh"), region, str(self.runtime), str(self.iso), str(self.config)], + env={**os.environ, "PATH": f"{self.bin}:{os.environ['PATH']}", "TEST_LOG": str(self.log), **env}, + capture_output=True, text=True, + ) + + def test_cn_adds_real_install_targets_and_verifies_keyring_first(self): + result = self.prepare("cn") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(self.packages.read_text(), "base\n\narchlinuxcn-keyring\nfcitx5-rime\n") + self.assertEqual(self.config.read_bytes(), self.original_config + b"\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n") + self.assertEqual((self.payload / "region/packages").read_text(), "archlinuxcn-keyring\nfcitx5-rime\n") + self.assertEqual(self.log.read_text().splitlines(), [f"pacman --config {self.config} --noconfirm -Sy --needed archlinuxcn-keyring", "pacman-key --populate archlinuxcn"]) + with mock.patch.object(phases_impl, "Path", return_value=self.packages), \ + mock.patch.object(phases_impl, "_package_targets", return_value={"runtime": "omarchy-dev", "settings": "omarchy-settings-dev", "nvim": "omarchy-nvim"}): + self.assertEqual(phases_impl._runtime_package_list(None), ["omarchy-dev", "base", "archlinuxcn-keyring", "fcitx5-rime"]) + + def test_global_is_unchanged_without_region_support_in_runtime(self): + shutil.rmtree(self.runtime) + result = self.prepare("global") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(self.packages.read_text(), "base\n") + self.assertEqual(self.config.read_bytes(), self.original_config) + self.assertFalse(self.log.exists()) + self.assertFalse((self.payload / "region").exists()) + + def test_missing_runtime_support_fails_before_package_operations(self): + (self.runtime / "bin/omarchy-apply-pacman").unlink() + result = self.prepare("cn") + self.assertNotEqual(result.returncode, 0) + self.assertIn("--local-source", result.stderr) + self.assertFalse(self.log.exists()) + self.assertEqual(self.config.read_bytes(), self.original_config) + + def test_untrusted_keyring_aborts_without_importing_keys(self): + result = self.prepare("cn", TEST_PACMAN_STATUS="1") + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("pacman-key", self.log.read_text()) + + +class RegionMakeTest(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.root = Path(tmp.name) + (self.root / "bin").mkdir() + shutil.copyfile(ROOT / "bin/omarchy-iso-make", self.root / "bin/omarchy-iso-make") + for directory in ("configs/airootfs/root", "configs/airootfs/usr/local/bin", "stubs"): + (self.root / directory).mkdir(parents=True) + (self.root / "configs/profiledef.sh").touch() + self.log = self.root / "docker-args" + stubs = { + "git": '#!/bin/bash\nexit 0\n', + "sudo": '#!/bin/bash\necho "unexpected sudo" >&2\nexit 99\n', + "docker": '''#!/bin/bash +if [[ $1 == version ]]; then exit 0; fi +printf '%s\\n' "$@" > "$TEST_LOG" +touch "$TEST_ROOT/release/omarchy-test-x86_64.iso" +''', + } + for name, content in stubs.items(): + stub = self.root / "stubs" / name + stub.write_text(content) + stub.chmod(0o755) + + def make(self, *args): + env = {key: value for key, value in os.environ.items() if not key.startswith("OMARCHY_")} + return subprocess.run( + ["bash", "bin/omarchy-iso-make", "--keep-pkg-cache", "--no-boot-offer", *args], + cwd=self.root, env={**env, "HOME": str(self.root), "PATH": f"{self.root / 'stubs'}:{env['PATH']}", "TEST_LOG": str(self.log), "TEST_ROOT": str(self.root)}, + capture_output=True, text=True, + ) + + def test_region_is_independent_of_channel_and_names_cache_and_artifact(self): + for flags, channel, ref in (([], "stable", "quattro"), (["--rc"], "rc", "rc"), (["--edge"], "edge", "edge"), (["--dev"], "edge", "dev")): + with self.subTest(channel=ref): + release = self.root / "release" + if release.exists(): + shutil.rmtree(release) + result = self.make(*flags, "--region", "cn") + self.assertEqual(result.returncode, 0, result.stderr) + args = self.log.read_text().splitlines() + self.assertIn("OMARCHY_REGION=cn", args) + self.assertIn(f"OMARCHY_MIRROR={channel}", args) + self.assertIn(f"OMARCHY_ISO_REF={ref}", args) + self.assertIn(f"{self.root}/.cache/omarchy/iso_{channel}-cn/airootfs/var/cache/omarchy:/var/cache/airootfs/var/cache/omarchy", args) + self.assertTrue((release / f"omarchy-test-x86_64-{ref}-cn.iso").exists()) + + def test_default_preserves_existing_global_cache_and_name(self): + result = self.make() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("OMARCHY_REGION=global", self.log.read_text().splitlines()) + self.assertIn("/iso_stable/", self.log.read_text()) + self.assertTrue((self.root / "release/omarchy-test-x86_64-quattro.iso").exists()) + + def test_local_source_keeps_region_and_mounts_both_checkouts(self): + for name in ("runtime", "packages"): + (self.root / name).mkdir() + result = self.make("--region", "cn", "--local-source", str(self.root / "runtime"), str(self.root / "packages")) + self.assertEqual(result.returncode, 0, result.stderr) + args = self.log.read_text().splitlines() + self.assertIn("OMARCHY_REGION=cn", args) + self.assertIn("OMARCHY_ISO_REF=local", args) + self.assertIn(f"{self.root}/runtime:/omarchy-source:ro", args) + self.assertIn(f"{self.root}/packages:/omarchy-pkgs:ro", args) + self.assertTrue((self.root / "release/omarchy-test-x86_64-local-cn.iso").exists()) + + def test_invalid_regions_fail_without_starting_build(self): + for flags in (("--region",), ("--region", "chn"), ("--region", "CN"), ("--region", "zz"), ("--region", "../cn")): + with self.subTest(flags=flags): + result = self.make(*flags) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(self.log.exists()) + self.assertFalse((self.root / "release").exists()) + + +if __name__ == "__main__": + unittest.main() From 422d8980336a4638739a7286e15c059243c77cc2 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 17:49:09 -0400 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=93=9D=20docs(iso):=20explain=20reg?= =?UTF-8?q?ional=20build=20requirements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: explain matching runtime sources and optional regional bootstrap - :warning: disclose rolling-mirror compatibility and acceptance requirements Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567 --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 1ec23ea..ecc47b5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,20 @@ Despite the local folder name, the first argument is the Omarchy source checkout Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror. +### Regional builds + +`--region cn` adds China bootstrap defaults independently of the channel. The default, `--region global`, preserves the existing package set, mirrors, cache paths, and artifact names. Supported country profiles use lowercase ISO 3166-1 alpha-2 codes; currently only `cn` is implemented. + +For an unpublished regional runtime, use matching local source and package-recipe checkouts: + +```bash +./bin/omarchy-iso-make --region cn --no-boot-offer --local-source /path/to/omarchy /path/to/omarchy-pkgs +``` + +China builds keep the channel mirror first, append the USTC Arch mirror, and add ArchLinuxCN with its verified keyring. USTC is rolling, not a matching stable/RC snapshot; fallback version compatibility is not guaranteed. The runtime profile also bundles offline Simplified Chinese Pinyin using the existing Fcitx5 service, with `Alt+Space` to switch input. New-user defaults are staged before user creation, including deferred setup, without changing existing homes, language, timezone, or keyboard settings. Users can change these defaults after installation; see `docs/regions.md` in the companion runtime checkout. + +Non-global cache and ISO names gain a `-cn` suffix. A runtime without the selected profile fails the build. These builds require an x86_64 Arch container environment; packaging and input checks on another architecture do not substitute for a fresh build, offline installation, and graphical acceptance run. + ## Autoinstall The shipped ISO installs itself with no keyboard when it finds its configuration on a second drive. Attach a drive labeled `cidata` alongside the ISO and the installer copies the config off it and skips the configurator; with no such drive, nothing changes and the wizard runs as usual. No rebuild, no extra boot entry. From 55d5f831d406eca5019f23ea3e4463438d1fb6fd Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 18:23:50 -0400 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=90=9B=20fix(build):=20require=20re?= =?UTF-8?q?gional=20user=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :shield: fail a regional build whose profile lacks `skel`, instead of shipping an ISO that silently skips the advertised input defaults Co-Authored-By: Claude Opus 5.5 --- builder/prepare-region.sh | 3 ++- test/unit/test_region.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/builder/prepare-region.sh b/builder/prepare-region.sh index 2707fba..9fec359 100644 --- a/builder/prepare-region.sh +++ b/builder/prepare-region.sh @@ -17,7 +17,8 @@ esac profile="$runtime/default/regions/$region" if [[ ! -f $runtime/bin/omarchy-apply-pacman || ! -f $profile/packages || - ! -f $profile/pacman/pacman.conf.append || ! -f $profile/pacman/mirrorlist.append ]]; then + ! -f $profile/pacman/pacman.conf.append || ! -f $profile/pacman/mirrorlist.append || + ! -d $profile/skel ]]; then echo "Runtime does not support region '$region'; publish a matching runtime or use --local-source." >&2 exit 1 fi diff --git a/test/unit/test_region.py b/test/unit/test_region.py index 5fb514e..3961b56 100644 --- a/test/unit/test_region.py +++ b/test/unit/test_region.py @@ -93,6 +93,7 @@ class RegionBuildTest(unittest.TestCase): self.profile = self.runtime / "default/regions/cn" (self.profile / "pacman").mkdir(parents=True) (self.runtime / "bin").mkdir() + (self.profile / "skel").mkdir() (self.runtime / "bin/omarchy-apply-pacman").touch() (self.profile / "packages").write_text("archlinuxcn-keyring\nfcitx5-rime\n") (self.profile / "pacman/pacman.conf.append").write_text("[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n") @@ -148,6 +149,13 @@ class RegionBuildTest(unittest.TestCase): self.assertFalse(self.log.exists()) self.assertEqual(self.config.read_bytes(), self.original_config) + def test_profile_without_user_defaults_fails_before_package_operations(self): + (self.profile / "skel").rmdir() + result = self.prepare("cn") + self.assertNotEqual(result.returncode, 0) + self.assertFalse(self.log.exists()) + self.assertEqual(self.packages.read_text(), "base\n") + def test_untrusted_keyring_aborts_without_importing_keys(self): result = self.prepare("cn", TEST_PACMAN_STATUS="1") self.assertNotEqual(result.returncode, 0) From 24d657344768cf0ba857b010ab96b22b59237296 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 18:23:50 -0400 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=90=9B=20fix(build):=20name=20only?= =?UTF-8?q?=20the=20iso=20this=20build=20produced?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :bug: concurrent global and regional builds shared `release/`, so picking the newest ISO could apply the wrong region suffix - :white_check_mark: cover the per-build output directory and its cleanup Co-Authored-By: Claude Opus 5.5 --- bin/omarchy-iso-make | 17 +++++++++++++---- test/unit/test_region.py | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/bin/omarchy-iso-make b/bin/omarchy-iso-make index 500de4b..b38001c 100755 --- a/bin/omarchy-iso-make +++ b/bin/omarchy-iso-make @@ -130,6 +130,10 @@ BUILD_SCRIPT="builder/build-iso.sh" BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") BUILD_RELEASE_PATH="$BUILD_ROOT/release" mkdir -p "$BUILD_RELEASE_PATH" +# Each build writes into its own directory, so concurrent builds (such as a +# global and a regional one) never pick up and rename each other's ISO. +BUILD_OUT=$(mktemp -d "$BUILD_RELEASE_PATH/.build-XXXXXX") +trap 'rm -rf "$BUILD_OUT"' EXIT OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}" OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}" @@ -143,7 +147,7 @@ DOCKER_ARGS=( -e "OMARCHY_INSTALL_DEBUG=${OMARCHY_INSTALL_DEBUG:-}" -e "HOST_UID=$(id -u)" -e "HOST_GID=$(id -g)" - -v "$BUILD_RELEASE_PATH/:/out/" + -v "$BUILD_OUT/:/out/" -v "$BUILD_ROOT/$BUILD_SCRIPT:/$BUILD_SCRIPT:ro" -v "$BUILD_ROOT/archiso:/archiso:ro" -v "$BUILD_ROOT/builder:/builder:ro" @@ -187,9 +191,14 @@ fi "${DOCKER[@]}" run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT -latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*.iso | head -n1) -iso_ref="${latest_iso%.*}-${OMARCHY_ISO_REF}${region_suffix}.iso" -mv -f "$latest_iso" "$iso_ref" +built_isos=("$BUILD_OUT"/*.iso) +if (( ${#built_isos[@]} != 1 )) || [[ ! -f ${built_isos[0]} ]]; then + echo "Error: expected exactly one ISO in $BUILD_OUT" >&2 + exit 1 +fi +built_iso_name=$(basename "${built_isos[0]}" .iso) +iso_ref="$BUILD_RELEASE_PATH/$built_iso_name-${OMARCHY_ISO_REF}${region_suffix}.iso" +mv -f "${built_isos[0]}" "$iso_ref" if [[ -z "${NO_BOOT_OFFER:-}" ]]; then echo diff --git a/test/unit/test_region.py b/test/unit/test_region.py index 3961b56..75d16b4 100644 --- a/test/unit/test_region.py +++ b/test/unit/test_region.py @@ -179,7 +179,9 @@ class RegionMakeTest(unittest.TestCase): "docker": '''#!/bin/bash if [[ $1 == version ]]; then exit 0; fi printf '%s\\n' "$@" > "$TEST_LOG" -touch "$TEST_ROOT/release/omarchy-test-x86_64.iso" +for arg in "$@"; do + if [[ $arg == *:/out/ ]]; then touch "${arg%:/out/}/omarchy-test-x86_64.iso"; fi +done ''', } for name, content in stubs.items(): @@ -217,6 +219,17 @@ touch "$TEST_ROOT/release/omarchy-test-x86_64.iso" self.assertIn("/iso_stable/", self.log.read_text()) self.assertTrue((self.root / "release/omarchy-test-x86_64-quattro.iso").exists()) + def test_build_renames_only_its_own_iso(self): + release = self.root / "release" + release.mkdir() + other = release / "omarchy-other-x86_64.iso" + other.touch() + result = self.make("--region", "cn") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(other.exists()) + self.assertTrue((release / "omarchy-test-x86_64-quattro-cn.iso").exists()) + self.assertEqual(list(release.glob(".build-*")), []) + def test_local_source_keeps_region_and_mounts_both_checkouts(self): for name in ("runtime", "packages"): (self.root / name).mkdir() From 043e819d019a40720a904ceb467f15ef7615f3e0 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 22:56:12 -0400 Subject: [PATCH 07/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(build):=20m?= =?UTF-8?q?ake=20regional=20user=20defaults=20optional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :recycle: a repository-only profile is valid, so the build no longer requires skel while the installer still stages it when present - :white_check_mark: use neutral fixtures instead of an input-method profile Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr --- builder/prepare-region.sh | 3 +-- test/unit/test_region.py | 25 +++++++++++-------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/builder/prepare-region.sh b/builder/prepare-region.sh index 9fec359..2707fba 100644 --- a/builder/prepare-region.sh +++ b/builder/prepare-region.sh @@ -17,8 +17,7 @@ esac profile="$runtime/default/regions/$region" if [[ ! -f $runtime/bin/omarchy-apply-pacman || ! -f $profile/packages || - ! -f $profile/pacman/pacman.conf.append || ! -f $profile/pacman/mirrorlist.append || - ! -d $profile/skel ]]; then + ! -f $profile/pacman/pacman.conf.append || ! -f $profile/pacman/mirrorlist.append ]]; then echo "Runtime does not support region '$region'; publish a matching runtime or use --local-source." >&2 exit 1 fi diff --git a/test/unit/test_region.py b/test/unit/test_region.py index 75d16b4..f070fff 100644 --- a/test/unit/test_region.py +++ b/test/unit/test_region.py @@ -36,10 +36,10 @@ class RegionTargetTest(unittest.TestCase): def test_cn_seeds_skel_without_touching_existing_users_or_locale(self): self.marker.write_text("cn\n") - profile = self.payload / "skel/.config/fcitx5/profile" + profile = self.payload / "skel/.config/example/defaults" profile.parent.mkdir(parents=True) - profile.write_text("regional input defaults\n") - existing = self.target / "home/alice/.config/fcitx5/profile" + profile.write_text("regional defaults\n") + existing = self.target / "home/alice/.config/example/defaults" existing.parent.mkdir(parents=True) existing.write_text("personal settings\n") skel = self.target / "etc/skel" @@ -48,7 +48,7 @@ class RegionTargetTest(unittest.TestCase): (self.target / "etc/locale.conf").write_text("LANG=en_US.UTF-8\n") phases_impl._stage_region_defaults(self.target) - self.assertEqual((skel / ".config/fcitx5/profile").read_text(), "regional input defaults\n") + self.assertEqual((skel / ".config/example/defaults").read_text(), "regional defaults\n") self.assertEqual((skel / ".bashrc").read_text(), "original\n") self.assertEqual(existing.read_text(), "personal settings\n") self.assertEqual((self.target / "etc/locale.conf").read_text(), "LANG=en_US.UTF-8\n") @@ -93,9 +93,8 @@ class RegionBuildTest(unittest.TestCase): self.profile = self.runtime / "default/regions/cn" (self.profile / "pacman").mkdir(parents=True) (self.runtime / "bin").mkdir() - (self.profile / "skel").mkdir() (self.runtime / "bin/omarchy-apply-pacman").touch() - (self.profile / "packages").write_text("archlinuxcn-keyring\nfcitx5-rime\n") + (self.profile / "packages").write_text("archlinuxcn-keyring\nexample-regional-package\n") (self.profile / "pacman/pacman.conf.append").write_text("[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n") (self.profile / "pacman/mirrorlist.append").write_text("Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch\n") self.iso = self.root / "iso" @@ -124,13 +123,13 @@ class RegionBuildTest(unittest.TestCase): def test_cn_adds_real_install_targets_and_verifies_keyring_first(self): result = self.prepare("cn") self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(self.packages.read_text(), "base\n\narchlinuxcn-keyring\nfcitx5-rime\n") + self.assertEqual(self.packages.read_text(), "base\n\narchlinuxcn-keyring\nexample-regional-package\n") self.assertEqual(self.config.read_bytes(), self.original_config + b"\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n") - self.assertEqual((self.payload / "region/packages").read_text(), "archlinuxcn-keyring\nfcitx5-rime\n") + self.assertEqual((self.payload / "region/packages").read_text(), "archlinuxcn-keyring\nexample-regional-package\n") self.assertEqual(self.log.read_text().splitlines(), [f"pacman --config {self.config} --noconfirm -Sy --needed archlinuxcn-keyring", "pacman-key --populate archlinuxcn"]) with mock.patch.object(phases_impl, "Path", return_value=self.packages), \ mock.patch.object(phases_impl, "_package_targets", return_value={"runtime": "omarchy-dev", "settings": "omarchy-settings-dev", "nvim": "omarchy-nvim"}): - self.assertEqual(phases_impl._runtime_package_list(None), ["omarchy-dev", "base", "archlinuxcn-keyring", "fcitx5-rime"]) + self.assertEqual(phases_impl._runtime_package_list(None), ["omarchy-dev", "base", "archlinuxcn-keyring", "example-regional-package"]) def test_global_is_unchanged_without_region_support_in_runtime(self): shutil.rmtree(self.runtime) @@ -149,12 +148,10 @@ class RegionBuildTest(unittest.TestCase): self.assertFalse(self.log.exists()) self.assertEqual(self.config.read_bytes(), self.original_config) - def test_profile_without_user_defaults_fails_before_package_operations(self): - (self.profile / "skel").rmdir() + def test_profile_without_user_defaults_is_supported(self): result = self.prepare("cn") - self.assertNotEqual(result.returncode, 0) - self.assertFalse(self.log.exists()) - self.assertEqual(self.packages.read_text(), "base\n") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse((self.payload / "region/skel").exists()) def test_untrusted_keyring_aborts_without_importing_keys(self): result = self.prepare("cn", TEST_PACMAN_STATUS="1") From 81106d04e89df575c545c5b1fe5c4e1affa31514 Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Sat, 26 Sep 2026 22:56:12 -0400 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=93=9D=20docs(iso):=20scope=20regio?= =?UTF-8?q?nal=20builds=20to=20the=20architecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :memo: describe the cn build as repository defaults plus optional new-user staging, with locale and input to follow Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecc47b5..ee3275c 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ For an unpublished regional runtime, use matching local source and package-recip ./bin/omarchy-iso-make --region cn --no-boot-offer --local-source /path/to/omarchy /path/to/omarchy-pkgs ``` -China builds keep the channel mirror first, append the USTC Arch mirror, and add ArchLinuxCN with its verified keyring. USTC is rolling, not a matching stable/RC snapshot; fallback version compatibility is not guaranteed. The runtime profile also bundles offline Simplified Chinese Pinyin using the existing Fcitx5 service, with `Alt+Space` to switch input. New-user defaults are staged before user creation, including deferred setup, without changing existing homes, language, timezone, or keyboard settings. Users can change these defaults after installation; see `docs/regions.md` in the companion runtime checkout. +China builds keep the channel mirror first, append the USTC Arch mirror, and add ArchLinuxCN with its verified keyring. These are a practical bridge until Omarchy runs its own mirror in China; USTC is rolling, not a matching stable/RC snapshot, so fallback version compatibility is not guaranteed. The region architecture also stages optional new-user defaults before user creation, including deferred setup, without touching existing homes. The China profile currently ships repository defaults only; language, timezone, and input-method defaults come as separate changes. See `docs/regions.md` in the companion runtime checkout. -Non-global cache and ISO names gain a `-cn` suffix. A runtime without the selected profile fails the build. These builds require an x86_64 Arch container environment; packaging and input checks on another architecture do not substitute for a fresh build, offline installation, and graphical acceptance run. +Non-global cache and ISO names gain a `-cn` suffix. A runtime without the selected profile fails the build. These builds require an x86_64 Arch container environment; packaging checks on another architecture do not substitute for a fresh build, offline installation, and graphical acceptance run. ## Autoinstall From d2e37df6a4bd7d945ffff568285e0bd142e7b0bc Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Tue, 6 Oct 2026 16:33:24 -0400 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=A8=20feat(region):=20preselect=20t?= =?UTF-8?q?he=20profile=20timezone=20in=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :sparkles: regional ISOs pass their profile's timezone to the shared setup form, so an offline China install opens on `Asia/Shanghai` - :white_check_mark: cover the payload copy and the configurator hand-off Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8 --- README.md | 2 +- configs/airootfs/root/configurator | 12 ++++++++++- test/unit/test_region.py | 34 ++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee3275c..532bfce 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ For an unpublished regional runtime, use matching local source and package-recip ./bin/omarchy-iso-make --region cn --no-boot-offer --local-source /path/to/omarchy /path/to/omarchy-pkgs ``` -China builds keep the channel mirror first, append the USTC Arch mirror, and add ArchLinuxCN with its verified keyring. These are a practical bridge until Omarchy runs its own mirror in China; USTC is rolling, not a matching stable/RC snapshot, so fallback version compatibility is not guaranteed. The region architecture also stages optional new-user defaults before user creation, including deferred setup, without touching existing homes. The China profile currently ships repository defaults only; language, timezone, and input-method defaults come as separate changes. See `docs/regions.md` in the companion runtime checkout. +China builds keep the channel mirror first, append the USTC Arch mirror, and add ArchLinuxCN with its verified keyring. These are a practical bridge until Omarchy runs its own mirror in China; USTC is rolling, not a matching stable/RC snapshot, so fallback version compatibility is not guaranteed. The region architecture also stages optional new-user defaults before user creation, including deferred setup, without touching existing homes. The China profile ships repository defaults and preselects `Asia/Shanghai` as the timezone; language and input-method defaults come as separate changes. See `docs/regions.md` in the companion runtime checkout. Non-global cache and ISO names gain a `-cn` suffix. A runtime without the selected profile fails the build. These builds require an x86_64 Arch container environment; packaging checks on another architecture do not substitute for a fresh build, offline installation, and graphical acceptance run. diff --git a/configs/airootfs/root/configurator b/configs/airootfs/root/configurator index 2960c06..34351cc 100644 --- a/configs/airootfs/root/configurator +++ b/configs/airootfs/root/configurator @@ -24,6 +24,16 @@ if [[ ! -r $SETUP_FORM ]]; then fi source "$SETUP_FORM" +# A regional ISO carries its profile here (builder/prepare-region.sh copies it); +# the form preselects the profile's timezone when there is no geo guess, which +# is every offline install. +REGION_TIMEZONE="${REGION_TIMEZONE:-/usr/share/omarchy-iso/region/timezone}" +region_timezone() { + if [[ -r $REGION_TIMEZONE ]]; then + head -n 1 "$REGION_TIMEZONE" + fi +} + # Partition creation helpers, shared with test/partition-numbering-test.sh so # the numbering logic can be exercised without a real disk. DISK_PARTITIONING="${DISK_PARTITIONING:-/usr/share/omarchy-iso/disk-partitioning.sh}" @@ -270,7 +280,7 @@ user_form() { omarchy_prompt_identity || return $? omarchy_prompt_hostname || return $? - omarchy_prompt_timezone || return $? + omarchy_prompt_timezone "$(region_timezone)" || return $? } # Returns 0 on confirmed account details, or as soon as the keyboard screen diff --git a/test/unit/test_region.py b/test/unit/test_region.py index f070fff..3beae82 100644 --- a/test/unit/test_region.py +++ b/test/unit/test_region.py @@ -1,6 +1,7 @@ """Region bootstrap contracts without Docker, pacman, or a running guest.""" import os +import re import shutil import subprocess import sys @@ -97,6 +98,7 @@ class RegionBuildTest(unittest.TestCase): (self.profile / "packages").write_text("archlinuxcn-keyring\nexample-regional-package\n") (self.profile / "pacman/pacman.conf.append").write_text("[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n") (self.profile / "pacman/mirrorlist.append").write_text("Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch\n") + (self.profile / "timezone").write_text("Asia/Shanghai\n") self.iso = self.root / "iso" self.payload = self.iso / "usr/share/omarchy-iso" self.payload.mkdir(parents=True) @@ -126,6 +128,7 @@ class RegionBuildTest(unittest.TestCase): self.assertEqual(self.packages.read_text(), "base\n\narchlinuxcn-keyring\nexample-regional-package\n") self.assertEqual(self.config.read_bytes(), self.original_config + b"\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n") self.assertEqual((self.payload / "region/packages").read_text(), "archlinuxcn-keyring\nexample-regional-package\n") + self.assertEqual((self.payload / "region/timezone").read_text(), "Asia/Shanghai\n") self.assertEqual(self.log.read_text().splitlines(), [f"pacman --config {self.config} --noconfirm -Sy --needed archlinuxcn-keyring", "pacman-key --populate archlinuxcn"]) with mock.patch.object(phases_impl, "Path", return_value=self.packages), \ mock.patch.object(phases_impl, "_package_targets", return_value={"runtime": "omarchy-dev", "settings": "omarchy-settings-dev", "nvim": "omarchy-nvim"}): @@ -250,3 +253,34 @@ done if __name__ == "__main__": unittest.main() + + +class ConfiguratorRegionTimezoneTest(unittest.TestCase): + """The configurator hands the ISO's regional timezone to the setup form.""" + + def setUp(self): + configurator = (ROOT / "configs/airootfs/root/configurator").read_text() + helper = re.search(r"^region_timezone\(\) \{\n.*?^\}", configurator, re.M | re.S) + self.assertIsNotNone(helper) + self.helper = helper.group(0) + self.configurator = configurator + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.file = Path(tmp.name) / "timezone" + + def region_timezone(self): + return subprocess.run( + ["bash", "-c", f"set -euo pipefail\n{self.helper}\nregion_timezone"], + env={**os.environ, "REGION_TIMEZONE": str(self.file)}, + capture_output=True, text=True, check=True, + ).stdout + + def test_global_iso_offers_no_timezone(self): + self.assertEqual(self.region_timezone(), "") + + def test_regional_iso_offers_its_profile_timezone(self): + self.file.write_text("Asia/Shanghai\n") + self.assertEqual(self.region_timezone(), "Asia/Shanghai\n") + + def test_user_form_passes_it_to_the_timezone_prompt(self): + self.assertIn('omarchy_prompt_timezone "$(region_timezone)"', self.configurator) From e3de148897a58c9ddf63c94475d6541fdb2863bf Mon Sep 17 00:00:00 2001 From: Kros Dai Date: Tue, 6 Oct 2026 21:10:59 -0400 Subject: [PATCH 10/10] =?UTF-8?q?=E2=9C=85=20test(region):=20run=20timezon?= =?UTF-8?q?e=20tests=20when=20invoked=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :white_check_mark: the new class sat below `unittest.main()`, so running the file directly exited before defining it; suite discovery still found it Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8 --- test/unit/test_region.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/unit/test_region.py b/test/unit/test_region.py index 3beae82..c6f100e 100644 --- a/test/unit/test_region.py +++ b/test/unit/test_region.py @@ -251,9 +251,6 @@ done self.assertFalse((self.root / "release").exists()) -if __name__ == "__main__": - unittest.main() - class ConfiguratorRegionTimezoneTest(unittest.TestCase): """The configurator hands the ISO's regional timezone to the setup form.""" @@ -284,3 +281,7 @@ class ConfiguratorRegionTimezoneTest(unittest.TestCase): def test_user_form_passes_it_to_the_timezone_prompt(self): self.assertIn('omarchy_prompt_timezone "$(region_timezone)"', self.configurator) + + +if __name__ == "__main__": + unittest.main()