Merge chinese-installer (with omarchy-iso #241)

# Conflicts:
#	README.md
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 01:21:51 -04:00
commit c407101cef
15 files changed
+943 -136

No files matched your search

+7 -1
View File
@@ -273,6 +273,12 @@ mkdir -p "$omarchy_share"
printf 'logo bottom row%*s\n' 66 ''
} >"$omarchy_share/logo.txt"
# The ISO vendors the runtime's setup form; the dashboard only needs its text lookup.
cat >"$work/setup-form.sh" <<'FORM'
declare -gA OMARCHY_ZH=()
omarchy_text() { local text=$1; shift; printf -- "$text" "$@"; }
FORM
run_dashboard() {
local install_log="$1"
@@ -284,7 +290,7 @@ run_dashboard() {
"error": "Pacstrap failed. See /var/log/archinstall.log"}]}
STATE
: >"$screen"
script -qefc "stty rows 40 cols 120; PATH='$stubs:$PATH' OMARCHY_PATH='$omarchy_share' OMARCHY_UI_INTERACTIVE=no OMARCHY_UI_FAILURE_ACTION=exit OMARCHY_FAILURE_TAIL_LOG='$install_log' '$DASHBOARD' '$install_log' '$state_file' -- bash -c 'exit 1'" \
script -qefc "stty rows 40 cols 120; PATH='$stubs:$PATH' OMARCHY_PATH='$omarchy_share' SETUP_FORM='$work/setup-form.sh' OMARCHY_UI_INTERACTIVE=no OMARCHY_UI_FAILURE_ACTION=exit OMARCHY_FAILURE_TAIL_LOG='$install_log' '$DASHBOARD' '$install_log' '$state_file' -- bash -c 'exit 1'" \
"$screen" >/dev/null 2>&1
}
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
#
# Every installer text needs an OMARCHY_ZH entry, or a Chinese install shows it in English.
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
SETUP_FORM="${OMARCHY_SOURCE:-$ROOT/../omarchy}/install/provisioning/setup-form.sh"
fail() {
printf 'not ok - %s\n' "$1"
[[ -z ${2:-} ]] || printf '%s\n' "$2" | sed 's/^/# /'
exit 1
}
if [[ ! -r $SETUP_FORM ]]; then
printf 'ok - # skip the setup form is not checked out next to this repo (set OMARCHY_SOURCE)\n'
exit 0
fi
source "$SETUP_FORM"
for file in configs/airootfs/root/configurator configs/airootfs/usr/local/bin/omarchy-install-dashboard; do
eval "$(sed -n '/^OMARCHY_ZH+=(/,/^)/p' "$ROOT/$file")"
done
mapfile -t texts < <(
grep -ho 'omarchy_text "[^"$]*"' "$ROOT/configs/airootfs/root/configurator" "$ROOT/configs/airootfs/usr/local/bin/omarchy-install-dashboard" |
sed 's/^omarchy_text "//; s/"$//' | sort -u
)
((${#texts[@]} > 50)) || fail "the installer routes its text through omarchy_text" "found ${#texts[@]}"
for text in "${texts[@]}"; do
[[ -n ${OMARCHY_ZH[$text]:-} ]] || fail "every installer text has a Chinese entry" "missing: $text"
done
printf 'ok - every installer and dashboard text has a Chinese entry\n'
for action in "View full log" "Drop to shell" "Reboot" "Power off" "Upload log for support"; do
[[ -n ${OMARCHY_ZH[$action]:-} ]] || fail "labels passed through variables have Chinese entries" "missing: $action"
done
printf 'ok - labels passed through variables have Chinese entries\n'
mapfile -t diagnosis < <(grep -x ' *echo "[^"$]*"' "$ROOT/configs/airootfs/usr/local/bin/omarchy-install-diagnose-media" | sed 's/^ *echo "//; s/"$//')
((${#diagnosis[@]} >= 10)) || fail "the media diagnosis sentences are found" "found ${#diagnosis[@]}"
for line in "${diagnosis[@]}"; do
[[ -n ${OMARCHY_ZH[$line]:-} ]] || fail "every media diagnosis sentence has a Chinese entry" "missing: $line"
done
printf 'ok - every media diagnosis sentence has a Chinese entry\n'
[[ $(OMARCHY_INSTALL_LANG=zh omarchy_text "Confirm overwriting %s" /dev/vda) == "确认覆盖 /dev/vda" ]] ||
fail "Chinese text keeps its printf arguments"
[[ $(omarchy_text "Confirm overwriting %s" /dev/vda) == "Confirm overwriting /dev/vda" ]] ||
fail "English stays the default"
printf 'ok - omarchy_text formats Chinese and English alike\n'
+1
View File
@@ -108,6 +108,7 @@ class KernelSelectionTest(unittest.TestCase):
ctx = types.SimpleNamespace(
state={"arch_config_handler": types.SimpleNamespace(config=config), "mirror_handler": None},
target=Path("/unused"), tailscale_authkey_path=None, omarchy_install={},
region="global",
)
for name in ("_mount_offline_package_cache", "_mask_mkinitcpio_pacman_hooks",
"_configure_limine_boot", "_write_pre_mounted_fstab"):
+236
View File
@@ -0,0 +1,236 @@
"""The target's region comes from its timezone, using the runtime's profiles."""
import json
import os
import subprocess
import sys
import tempfile
import types
import unittest
from contextlib import ExitStack
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "configs/airootfs/usr/share/omarchy-iso"))
sys.modules.setdefault(
"orchestrator.archinstall_adapter",
types.ModuleType("orchestrator.archinstall_adapter"),
)
from orchestrator import context, phases_impl, region # noqa: E402
def write_profiles(root: Path) -> Path:
regions = root / "regions"
cn = regions / "cn"
(cn / "pacman").mkdir(parents=True)
(cn / "timezones").write_text("# China\nAsia/Shanghai\n\n Asia/Urumqi \n")
(cn / "packages").write_text("# keyring first\narchlinuxcn-keyring\nextra-tool\n")
(cn / "pacman/pacman.conf.append").write_text("[archlinuxcn]\nServer = https://example.invalid/$arch\n")
for channel in ("stable", "rc", "edge"):
(cn / f"pacman/mirrorlist-{channel}.prepend").write_text(f"Server = https://{channel}.example.invalid/$repo/os/$arch\n")
return regions
class RegionSelectionTest(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.root = Path(tmp.name)
self.regions = write_profiles(self.root)
patcher = mock.patch.object(region, "REGIONS_DIR", self.regions)
patcher.start()
self.addCleanup(patcher.stop)
def test_timezone_selects_region(self):
for timezone, expected in [
("Asia/Shanghai", "cn"),
("Asia/Urumqi", "cn"),
("Asia/Hong_Kong", "global"),
("America/New_York", "global"),
("UTC", "global"),
("# China", "global"),
("", "global"),
(None, "global"),
]:
with self.subTest(timezone=timezone):
self.assertEqual(region.region_for_timezone(timezone), expected)
def test_iso_without_profiles_is_global(self):
self.assertEqual(region.region_for_timezone("Asia/Shanghai", self.root / "missing"), "global")
self.assertEqual(region.resolve_region({}, "Asia/Shanghai", self.root / "missing"), "global")
def test_autoinstall_region_overrides_timezone(self):
self.assertEqual(region.resolve_region({"region": "cn"}, "UTC"), "cn")
self.assertEqual(region.resolve_region({"region": "global"}, "Asia/Shanghai"), "global")
self.assertEqual(region.resolve_region({}, "Asia/Shanghai"), "cn")
for bad in ("us", "CN", "..", "cn/../cn", True, 1, ["cn"]):
with self.subTest(region=bad), self.assertRaises(RuntimeError):
region.resolve_region({"region": bad}, "UTC")
def test_profile_packages_and_keyrings(self):
self.assertEqual(region.region_packages("cn"), ["archlinuxcn-keyring", "extra-tool"])
self.assertEqual(region.region_keyrings("cn"), ["archlinuxcn"])
self.assertEqual(region.region_packages("global"), [])
self.assertEqual(region.region_keyrings("global"), [])
def from_env(self, config: dict) -> context.InstallContext:
config_path = self.root / "config.json"
creds_path = self.root / "creds.json"
config_path.write_text(json.dumps(config))
creds_path.write_text(json.dumps({"users": [{"username": "test"}]}))
with mock.patch.dict(os.environ, {
"OMARCHY_INSTALL_CONFIG": str(config_path),
"OMARCHY_INSTALL_CREDS": str(creds_path),
"OMARCHY_INSTALL_STATE_DIR": str(self.root / "state"),
}, clear=True), mock.patch.object(context, "_default_kernel", return_value="linux-omarchy"):
return context.InstallContext.from_env()
def test_context_resolves_region_before_install(self):
self.assertEqual(self.from_env({"timezone": "Asia/Shanghai"}).region, "cn")
self.assertEqual(self.from_env({"timezone": "Europe/Berlin"}).region, "global")
# Deferred installs carry UTC until first boot, so they are global
# unless the autoinstall config names a region.
self.assertEqual(self.from_env({"timezone": "UTC"}).region, "global")
self.assertEqual(self.from_env({"timezone": "UTC", "omarchy_install": {"region": "cn"}}).region, "cn")
with self.assertRaises(RuntimeError):
self.from_env({"omarchy_install": {"region": "zz"}})
def test_marker_is_written_only_for_regional_targets(self):
target = self.root / "target"
phases_impl._write_region_marker(types.SimpleNamespace(target=target, region="global"))
self.assertFalse((target / "etc/omarchy/region").exists())
phases_impl._write_region_marker(types.SimpleNamespace(target=target, region="cn"))
marker = target / "etc/omarchy/region"
self.assertEqual(marker.read_text(), "cn\n")
self.assertEqual(marker.stat().st_mode & 0o777, 0o644)
def test_region_packages_install_while_the_offline_mirror_is_mounted(self):
for selected, expected_packages in [("global", []), ("cn", [["archlinuxcn-keyring", "extra-tool"]])]:
with self.subTest(region=selected), ExitStack() as stack:
events = []
installer = mock.MagicMock()
installer.add_additional_packages.side_effect = lambda packages: events.append(packages)
config = types.SimpleNamespace(
kernels=["linux-omarchy"], locale_config=None, mirror_config=None,
swap=None, auth_config=None, app_config=None, timezone=None,
ntp=False, hostname="test", pacman_config="/etc/pacman.conf",
)
ctx = types.SimpleNamespace(
state={"arch_config_handler": types.SimpleNamespace(config=config), "mirror_handler": None},
target=self.root / "target", tailscale_authkey_path=None, omarchy_install={},
region=selected,
)
for name in ("_mount_offline_package_cache", "_mask_mkinitcpio_pacman_hooks",
"_unmask_mkinitcpio_pacman_hooks", "_configure_limine_boot",
"_write_pre_mounted_fstab", "_install_early_packages"):
stack.enter_context(mock.patch.object(phases_impl, name))
stack.enter_context(mock.patch.object(
phases_impl, "_unmount_offline_package_cache", side_effect=lambda _: events.append("unmount")))
stack.enter_context(mock.patch.object(phases_impl, "configure_keyboard", return_value=True))
stack.enter_context(mock.patch.object(phases_impl, "_runtime_package_list", return_value=["omarchy"]))
for name, value in (("is_pre_mount", True), ("root_user", None)):
stack.enter_context(mock.patch.object(phases_impl.arch, name, return_value=value, create=True))
stack.enter_context(mock.patch.object(phases_impl.arch, "sanity_check", create=True))
opened = stack.enter_context(mock.patch.object(phases_impl.arch, "open_installer", create=True))
opened.return_value.__enter__.return_value = installer
phases_impl.arch_install_system(ctx)
self.assertEqual(events, [["linux-omarchy-headers"], ["omarchy"], *expected_packages, "unmount"])
self.assertEqual((ctx.target / "etc/omarchy/region").exists(), selected != "global")
def test_finalizer_trusts_only_the_selected_region(self):
for selected, expected in [
("global", []),
("cn", [["pacman-key", "--init"], ["pacman-key", "--populate", "archlinux", "archlinuxcn"]]),
]:
with self.subTest(region=selected), \
mock.patch.object(phases_impl, "_run_target_setup_command") as run, \
mock.patch.object(phases_impl, "_mask_mkinitcpio_pacman_hooks"), \
mock.patch.object(phases_impl, "_unmask_mkinitcpio_pacman_hooks"):
ctx = types.SimpleNamespace(region=selected, defer_provisioning=True, target=Path("/unused"))
phases_impl.run_system_finalizer(ctx)
commands = [call.args[1] for call in run.call_args_list]
self.assertEqual(commands[:-1], expected)
self.assertEqual(commands[-1][0], "/usr/bin/omarchy-apply-system")
class PrepareRegionsTest(unittest.TestCase):
def run_prepare(self, regions: Path, root: Path) -> tuple[Path, Path, str]:
payload = root / "payload"
payload.mkdir()
online = root / "online.conf"
online.write_text("[core]\nInclude = /etc/pacman.d/mirrorlist\n")
stubs = root / "bin"
stubs.mkdir()
log = root / "calls.log"
for name in ("pacman", "pacman-key"):
stub = stubs / name
stub.write_text(f'#!/bin/bash\necho "{name} $*" >> "{log}"\n')
stub.chmod(0o755)
subprocess.run(
["bash", str(ROOT / "builder/prepare-regions.sh"), str(regions), str(payload), str(online)],
env={**os.environ, "PATH": f"{stubs}:{os.environ['PATH']}"},
check=True, capture_output=True, text=True,
)
return payload, online, log.read_text() if log.exists() else ""
def test_ships_every_profile_and_bootstraps_its_keyring(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
regions = write_profiles(root)
payload, online, calls = self.run_prepare(regions, root)
self.assertEqual((payload / "regions/cn/timezones").read_text(), (regions / "cn/timezones").read_text())
self.assertIn("[archlinuxcn]", online.read_text())
self.assertTrue(online.read_text().startswith("[core]\n"))
self.assertEqual(calls.splitlines(), [
f"pacman --config {online} --noconfirm -Sy --needed archlinuxcn-keyring",
"pacman-key --populate archlinuxcn",
])
def settings_package(self, root: Path, with_regions: bool) -> Path:
tree = root / "pkgroot"
(tree / "usr/share/omarchy/default/pacman").mkdir(parents=True)
if with_regions:
write_profiles(root / "src")
(root / "src/regions").rename(tree / "usr/share/omarchy/default/regions")
package = root / "omarchy-settings-1-1-any.pkg.tar.zst"
subprocess.run(["bsdtar", "--zstd", "-cf", str(package), "-C", str(tree), "usr"], check=True)
return package
def test_published_build_reads_profiles_from_the_settings_package(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
payload, online, calls = self.run_prepare(self.settings_package(root, with_regions=True), root)
self.assertEqual((payload / "regions/cn/packages").read_text(), "# keyring first\narchlinuxcn-keyring\nextra-tool\n")
self.assertIn("[archlinuxcn]", online.read_text())
self.assertIn("pacman-key --populate archlinuxcn", calls)
def test_settings_package_without_profiles_builds_a_global_iso(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
payload, online, calls = self.run_prepare(self.settings_package(root, with_regions=False), root)
self.assertFalse((payload / "regions").exists())
self.assertEqual(calls, "")
def test_unreadable_settings_package_fails_the_build(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
package = root / "omarchy-settings-1-1-any.pkg.tar.zst"
package.write_text("not an archive")
with self.assertRaises(subprocess.CalledProcessError):
self.run_prepare(package, root)
def test_runtime_without_profiles_builds_a_global_iso(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
payload, online, calls = self.run_prepare(root / "missing", root)
self.assertFalse((payload / "regions").exists())
self.assertNotIn("archlinuxcn", online.read_text())
self.assertEqual(calls, "")
if __name__ == "__main__":
unittest.main()