Merge omacom/omarchy-iso#236 regional ISO builds, as an install-time choice
Every ISO bundles the cn profile; the first-screen pick decides. --region cn only preselects China and names the artifact. The region value is cn, the cn timezone is preselected when China is chosen, and the profile packages and ArchLinuxCN keyring reach the target only for China.
This commit is contained in:
commit
02ee068504
12 files changed
+492
-50
No files matched your search
@@ -32,6 +32,20 @@ The configurator asks for the region on its first screen. China reruns the insta
|
||||
|
||||
Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror.
|
||||
|
||||
### Regional builds
|
||||
|
||||
Every ISO bundles the China profile from its runtime, so either region can be picked on the first screen. `--region cn` only preselects China there and adds a `-cn` suffix to the artifact and offline cache; the default, `--region global`, keeps the existing 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
|
||||
```
|
||||
|
||||
A China install puts each channel server's `.omarchy.cn` twin first, keeps `.omarchy.org` behind it, appends the USTC Arch mirror, and adds ArchLinuxCN with its verified keyring. USTC is rolling, not a matching stable/RC snapshot, so that last fallback's version compatibility is not guaranteed. The region architecture also stages optional new-user defaults before user creation, including deferred setup, without touching existing homes. China preselects `Asia/Shanghai` as the timezone. 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.
|
||||
|
||||
## 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.
|
||||
@@ -52,7 +66,7 @@ These are the configurator's own output files, so the way to get a starting set
|
||||
| `authorized_keys` | No | SSH public keys in sshd's own format, one per line |
|
||||
| `tailscale_authkey` | No | Tailscale auth key; the machine joins your tailnet on first boot |
|
||||
|
||||
Both required files must be present or the installer falls back to the configurator. `user_configuration.json` may set `omarchy_install.region` to `global` (the default) or `china`, the region the configurator asks for on its first screen. Generate the password hash for `user_credentials.json` with `openssl passwd -6 "yourpassword"`.
|
||||
Both required files must be present or the installer falls back to the configurator. `user_configuration.json` may set `omarchy_install.region` to `global` (the default) or `cn`, the region the configurator asks for on its first screen. Generate the password hash for `user_credentials.json` with `openssl passwd -6 "yourpassword"`.
|
||||
|
||||
Encryption itself is configured by the `disk_encryption` block inside `user_configuration.json` — which carries the passphrase in plaintext, so treat a drive built from an encrypted install accordingly. The flag file must match it: it drives the encrypted install's SDDM autologin and the final boot validation, not the encryption.
|
||||
|
||||
|
||||
+36
-8
@@ -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 <omarchy-checkout> <pkgs-checkout>]"
|
||||
echo "Usage: $0 [--no-cache] [--keep-pkg-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--region global|cn] [--local-source <omarchy-checkout> <pkgs-checkout>]"
|
||||
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; }
|
||||
@@ -116,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}"
|
||||
@@ -125,10 +143,11 @@ 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)"
|
||||
-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"
|
||||
@@ -146,10 +165,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
|
||||
@@ -168,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.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
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# Usage: omarchy-iso-test [release/omarchy.iso] [options]
|
||||
#
|
||||
# --encrypt Drive the encrypted install flow (default: unencrypted)
|
||||
# --region REGION Pick global (default) or china on the first screen
|
||||
# --region REGION Pick global (default) or cn on the first screen
|
||||
# --provision Drive the deferred-provisioning install flow: no user during install, the
|
||||
# first boot runs omarchy-provision-owner and creates it there
|
||||
# --reuse-base Skip the install phase if a base image already exists
|
||||
@@ -87,8 +87,8 @@ if [[ ! -f ${ISO:-} ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $REGION != "global" && $REGION != "china" ]]; then
|
||||
echo "--region must be global or china, not $REGION" >&2
|
||||
if [[ $REGION != "global" && $REGION != "cn" ]]; then
|
||||
echo "--region must be global or cn, not $REGION" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -96,10 +96,10 @@ omarchy-pkg-add qemu-full edk2-ovmf socat imagemagick tesseract tesseract-data-e
|
||||
|
||||
# After the region screen a China install reads in Chinese.
|
||||
OCR_LANGS=eng
|
||||
[[ $REGION == "china" ]] && OCR_LANGS=eng+chi_sim
|
||||
[[ $REGION == "cn" ]] && OCR_LANGS=eng+chi_sim
|
||||
# region_text <english> <chinese> picks the text a screen shows in this run.
|
||||
region_text() {
|
||||
if [[ $REGION == "china" ]]; then
|
||||
if [[ $REGION == "cn" ]]; then
|
||||
printf '%s' "$2"
|
||||
else
|
||||
printf '%s' "$1"
|
||||
@@ -111,7 +111,7 @@ OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
|
||||
|
||||
BASE_NAME="$(basename "$ISO" .iso)"
|
||||
$ENCRYPT && BASE_NAME+="-encrypted"
|
||||
[[ $REGION == "china" ]] && BASE_NAME+="-china"
|
||||
[[ $REGION == "cn" ]] && BASE_NAME+="-cn"
|
||||
$PROVISION && BASE_NAME+="-provision"
|
||||
BASE_DIR="$REPO_DIR/test-runs/$BASE_NAME"
|
||||
RUN_DIR="$BASE_DIR/runs/$(date +%Y%m%d-%H%M%S)"
|
||||
@@ -262,7 +262,7 @@ ocr_screen() {
|
||||
[[ -s $shot ]] || return 0
|
||||
magick "$shot" -colorspace gray -negate -resize 150% "$prepped" 2>/dev/null || return 0
|
||||
# The logo and the black strip under foot throw tesseract off CJK, so China runs also read between them, without spaces.
|
||||
if [[ $REGION == "china" ]]; then
|
||||
if [[ $REGION == "cn" ]]; then
|
||||
magick "$shot" -gravity north -chop 0x35% -gravity south -chop 0x4% -colorspace gray -negate -resize 150% "$prepped.text.png" 2>/dev/null || return 0
|
||||
{ tesseract "$prepped" - --psm 6 -l "$OCR_LANGS"; tesseract "$prepped.text.png" - --psm 6 -l "$OCR_LANGS"; } 2>/dev/null | sed 'p; s/ //g' || true
|
||||
else
|
||||
@@ -648,7 +648,7 @@ drive_configurator() {
|
||||
# Region comes first, Global preselected and China one row below.
|
||||
wait_for_screen "Select region" 300
|
||||
capture_console "success-installer-01-region"
|
||||
[[ $REGION == "china" ]] && press down
|
||||
[[ $REGION == "cn" ]] && press down
|
||||
press ret
|
||||
|
||||
# Global asks for the keyboard; China reruns in cage + foot on a bilingual region screen with a US keyboard. Ctrl+C on either defers provisioning.
|
||||
|
||||
+26
-2
@@ -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" <<EOF
|
||||
OMARCHY_RUNTIME_PACKAGE=$OMARCHY_RUNTIME_PACKAGE
|
||||
OMARCHY_SETTINGS_PACKAGE=$OMARCHY_SETTINGS_PACKAGE
|
||||
@@ -80,6 +86,7 @@ if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then
|
||||
echo "built_at=$(date -Is)"
|
||||
echo "ref=$OMARCHY_ISO_REF"
|
||||
echo "mirror=$OMARCHY_MIRROR"
|
||||
echo "region=$OMARCHY_REGION"
|
||||
echo "runtime_package=$OMARCHY_RUNTIME_PACKAGE"
|
||||
echo "settings_package=$OMARCHY_SETTINGS_PACKAGE"
|
||||
echo "nvim_package=$OMARCHY_NVIM_PACKAGE"
|
||||
@@ -157,6 +164,7 @@ sed -i -E '/^(linux|broadcom-wl)$/d' "$build_cache_dir/packages.x86_64"
|
||||
# already in the mirror and we filter them out below. Without it, pacman -Syw
|
||||
# pulls the published omarchy* from the network mirror like any other package.
|
||||
if [[ -d /omarchy-source ]]; then
|
||||
runtime_root=/omarchy-source
|
||||
base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages)
|
||||
setup_form=/omarchy-source/install/provisioning/setup-form.sh
|
||||
else
|
||||
@@ -173,6 +181,12 @@ else
|
||||
fi
|
||||
mkdir -p /tmp/omarchy-pkglists
|
||||
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/omarchy-base.packages usr/share/omarchy/install/omarchy-other.packages
|
||||
runtime_root=/tmp/omarchy-pkglists/usr/share/omarchy
|
||||
if ! bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists \
|
||||
usr/share/omarchy/default/regions/cn usr/share/omarchy/bin/omarchy-apply-pacman; then
|
||||
echo "ERROR: runtime lacks region support; publish a matching runtime or use --local-source." >&2
|
||||
exit 1
|
||||
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
|
||||
@@ -186,6 +200,16 @@ 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"
|
||||
# Every ISO offers China on its first screen, whatever region it preselects.
|
||||
bash /builder/prepare-region.sh cn "$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
|
||||
@@ -251,7 +275,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
|
||||
}
|
||||
|
||||
@@ -269,7 +293,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
|
||||
|
||||
@@ -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
|
||||
|
||||
# The offline mirror carries the profile packages; the installer adds them to
|
||||
# the target only when the person picks this region.
|
||||
payload="$iso_root/usr/share/omarchy-iso"
|
||||
cp -a "$profile" "$payload/region"
|
||||
printf '\n' >> "$payload/omarchy-other.packages"
|
||||
cat "$profile/packages" >> "$payload/omarchy-other.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
|
||||
@@ -116,7 +116,7 @@ else
|
||||
mkdir -p -m 700 "$cjk_runtime"
|
||||
rm -f "$cjk_runtime/cjk-console-started" "$cjk_runtime/return-to-vt"
|
||||
cage_status=0
|
||||
env -u OMARCHY_CJK_RELAUNCH XDG_RUNTIME_DIR=$cjk_runtime OMARCHY_CJK_CONSOLE=1 OMARCHY_INSTALL_REGION=china \
|
||||
env -u OMARCHY_CJK_RELAUNCH XDG_RUNTIME_DIR=$cjk_runtime OMARCHY_CJK_CONSOLE=1 OMARCHY_INSTALL_REGION=cn \
|
||||
LANG=C.UTF-8 WLR_RENDERER=pixman cage -- foot -c /usr/share/omarchy-iso/foot.ini "$0" || cage_status=$?
|
||||
if [[ -f $cjk_runtime/return-to-vt ]]; then
|
||||
OMARCHY_INSTALL_REGION=global ./configurator && status=0 || status=$?
|
||||
@@ -125,7 +125,7 @@ else
|
||||
else
|
||||
echo "The Chinese console could not start (cage exit $cage_status); continuing in English."
|
||||
unset OMARCHY_CJK_RELAUNCH
|
||||
OMARCHY_INSTALL_REGION=china ./configurator && status=0 || status=$?
|
||||
OMARCHY_INSTALL_REGION=cn ./configurator && status=0 || status=$?
|
||||
fi
|
||||
done
|
||||
if [[ -n ${OMARCHY_CJK_CONSOLE:-} ]] && ((status == vt_return)); then
|
||||
@@ -145,7 +145,7 @@ if [[ -f /root/defer-provisioning ]] ||
|
||||
export OMARCHY_UI_DEFER_PROVISIONING=yes
|
||||
fi
|
||||
|
||||
if [[ -n ${OMARCHY_CJK_CONSOLE:-} && "$(jq -r '.omarchy_install.region // "global"' /root/user_configuration.json)" == "china" ]]; then
|
||||
if [[ -n ${OMARCHY_CJK_CONSOLE:-} && "$(jq -r '.omarchy_install.region // "global"' /root/user_configuration.json)" == "cn" ]]; then
|
||||
export OMARCHY_INSTALL_LANG=zh
|
||||
fi
|
||||
|
||||
|
||||
@@ -91,6 +91,15 @@ OMARCHY_ZH+=(
|
||||
["Everything will be overwritten. There is no recovery possible."]="磁盘上的一切都会被覆盖,无法恢复。"
|
||||
["Confirm overwriting %s"]="确认覆盖 %s"
|
||||
)
|
||||
# Every ISO carries the cn profile here (builder/prepare-region.sh copies it);
|
||||
# China preselects its 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 [[ $region == "cn" ]]; 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.
|
||||
@@ -273,13 +282,13 @@ notice() {
|
||||
|
||||
# .automated_script.sh moves China into the CJK console and Global back to the VT, which applies keymaps.
|
||||
apply_region_language() {
|
||||
if [[ $region == "china" && -n ${OMARCHY_CJK_RELAUNCH:-} ]]; then
|
||||
if [[ $region == "cn" && -n ${OMARCHY_CJK_RELAUNCH:-} ]]; then
|
||||
exit "$OMARCHY_CJK_RELAUNCH"
|
||||
fi
|
||||
if [[ $region == "global" && -n ${OMARCHY_VT_RETURN:-} ]]; then
|
||||
exit "$OMARCHY_VT_RETURN"
|
||||
fi
|
||||
if [[ $region == "china" && -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
||||
if [[ $region == "cn" && -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
||||
export OMARCHY_INSTALL_LANG=zh
|
||||
else
|
||||
unset OMARCHY_INSTALL_LANG
|
||||
@@ -363,7 +372,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
|
||||
@@ -384,8 +393,9 @@ user_step() {
|
||||
fi
|
||||
|
||||
# Add manual padding since gum table -p doesn't respect padding
|
||||
local skipped
|
||||
local skipped region_label=Global
|
||||
skipped=$(omarchy_text "[Skipped]")
|
||||
[[ $region == "cn" ]] && region_label=China
|
||||
echo -e "$(omarchy_text Field),$(omarchy_text Value)
|
||||
$(omarchy_text Username),$username
|
||||
$(omarchy_text Password),$(printf "%${#password}s" | tr ' ' '*')
|
||||
@@ -394,7 +404,7 @@ $(omarchy_text "Email address"),${email_address:-$skipped}
|
||||
$(omarchy_text Hostname),$hostname
|
||||
$(omarchy_text Timezone),$timezone
|
||||
$(omarchy_text Keyboard),$keyboard
|
||||
$(omarchy_text Region),$(omarchy_text "${region^}")" |
|
||||
$(omarchy_text Region),$(omarchy_text "$region_label")" |
|
||||
gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/"
|
||||
|
||||
echo
|
||||
@@ -1131,8 +1141,9 @@ select_installation() {
|
||||
|
||||
defer_provisioning=false
|
||||
install_target="full_disk"
|
||||
# A rerun in the other console arrives with the region already chosen.
|
||||
region="${OMARCHY_INSTALL_REGION:-global}"
|
||||
# A rerun in the other console arrives with the region already chosen; a
|
||||
# regional build preselects its own.
|
||||
region="${OMARCHY_INSTALL_REGION:-$(<"${ISO_REGION_FILE:-/root/omarchy_region}")}"
|
||||
|
||||
# Let the live VT reach its settled width before the first screen so the logo
|
||||
# isn't measured against the transient 80-column boot console.
|
||||
|
||||
@@ -67,8 +67,8 @@ class InstallContext:
|
||||
|
||||
# Autoinstall configs written before regions existed install Global.
|
||||
region = omarchy_install.get("region", "global")
|
||||
if region not in ("global", "china"):
|
||||
raise RuntimeError(f"omarchy_install.region must be global or china, not {region!r}")
|
||||
if region not in ("global", "cn"):
|
||||
raise RuntimeError(f"omarchy_install.region must be global or cn, not {region!r}")
|
||||
omarchy_install["region"] = region
|
||||
|
||||
if creds_path.exists():
|
||||
|
||||
@@ -39,6 +39,32 @@ from .keyboard import configure_keyboard
|
||||
from .ui import error, info
|
||||
|
||||
|
||||
ISO_REGION_PAYLOAD = Path("/usr/share/omarchy-iso/region")
|
||||
|
||||
|
||||
def _region_packages(ctx: InstallContext) -> list[str]:
|
||||
if ctx.omarchy_install["region"] != "cn":
|
||||
return []
|
||||
lines = (ISO_REGION_PAYLOAD / "packages").read_text().splitlines()
|
||||
return [s for s in (raw.strip() for raw in lines) if s and not s.startswith("#")]
|
||||
|
||||
|
||||
def _stage_region_defaults(ctx: InstallContext) -> 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 = ctx.omarchy_install["region"]
|
||||
skel = ISO_REGION_PAYLOAD / "skel"
|
||||
if region == "cn" and skel.is_dir():
|
||||
shutil.copytree(skel, ctx.target / "etc/skel", dirs_exist_ok=True)
|
||||
marker = ctx.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.
|
||||
@@ -286,6 +312,7 @@ def arch_install_system(ctx: InstallContext) -> None:
|
||||
_install_early_packages(installer)
|
||||
_configure_limine_boot(ctx, installer, config)
|
||||
|
||||
_stage_region_defaults(ctx)
|
||||
info("› creating user (with /etc/skel populated)")
|
||||
if config.auth_config and config.auth_config.users:
|
||||
installer.create_users(config.auth_config.users)
|
||||
@@ -746,6 +773,7 @@ def _runtime_package_list(ctx: InstallContext) -> list[str]:
|
||||
continue
|
||||
if s not in already_installed and s not in pkgs:
|
||||
pkgs.append(s)
|
||||
pkgs += [p for p in _region_packages(ctx) if p not in pkgs]
|
||||
# Local builds live only in this ISO's mirror; a later region switch could not fetch them.
|
||||
if PINNED_PACKAGES.exists():
|
||||
pkgs += [p for p in PINNED_PACKAGES.read_text().split() if p not in already_installed and p not in pkgs]
|
||||
@@ -1138,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 ctx.omarchy_install["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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -104,7 +104,7 @@ class RuntimePackagesTest(unittest.TestCase):
|
||||
mock.patch.object(phases_impl, "_early_packages", return_value=["git"]), \
|
||||
mock.patch.object(phases_impl, "_package_targets", return_value={
|
||||
"runtime": "omarchy-dev", "settings": "omarchy-settings-dev", "nvim": "omarchy-nvim"}):
|
||||
return phases_impl._runtime_package_list(types.SimpleNamespace())
|
||||
return phases_impl._runtime_package_list(types.SimpleNamespace(omarchy_install={"region": "global"}))
|
||||
|
||||
def test_local_builds_install_on_every_region(self):
|
||||
packages = self.runtime_packages("omarchy-dev\nomarchy-settings-dev\nrime-ice-installer\n")
|
||||
|
||||
+303
-16
@@ -1,8 +1,9 @@
|
||||
"""The configurator's region reaches the target setup as OMARCHY_REGION."""
|
||||
"""Region: the install-time choice, the bundled cn profile, and regional builds."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -42,7 +43,7 @@ class RegionTest(unittest.TestCase):
|
||||
(None, "global"),
|
||||
({"mode": "full_disk"}, "global"),
|
||||
({"mode": "full_disk", "region": "global"}, "global"),
|
||||
({"mode": "full_disk", "region": "china"}, "china"),
|
||||
({"mode": "full_disk", "region": "cn"}, "cn"),
|
||||
]:
|
||||
with self.subTest(omarchy_install=omarchy_install), tempfile.TemporaryDirectory() as tmp:
|
||||
ctx = load_context(Path(tmp), omarchy_install)
|
||||
@@ -51,11 +52,11 @@ class RegionTest(unittest.TestCase):
|
||||
def test_context_rejects_unknown_region(self):
|
||||
for region in ("China", "mars", ""):
|
||||
with self.subTest(region=region), tempfile.TemporaryDirectory() as tmp:
|
||||
with self.assertRaisesRegex(RuntimeError, "omarchy_install.region must be global or china"):
|
||||
with self.assertRaisesRegex(RuntimeError, "omarchy_install.region must be global or cn"):
|
||||
load_context(Path(tmp), {"mode": "full_disk", "region": region})
|
||||
|
||||
def test_target_setup_receives_region(self):
|
||||
for region in ("global", "china"):
|
||||
for region in ("global", "cn"):
|
||||
with self.subTest(region=region), tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
ctx = types.SimpleNamespace(
|
||||
@@ -74,11 +75,11 @@ class RegionTest(unittest.TestCase):
|
||||
chroot = next(call.args[0] for call in run.call_args_list if call.args[0][0] == "arch-chroot")
|
||||
self.assertIn(f"OMARCHY_REGION={region}", chroot)
|
||||
|
||||
def run_keyboard_form(self, script: str, **env: str) -> subprocess.CompletedProcess:
|
||||
def run_keyboard_form(self, script: str, preselect: str = "global", **env: str) -> subprocess.CompletedProcess:
|
||||
configurator = (ROOT / "configs/airootfs/root/configurator").read_text()
|
||||
language = re.search(r"^apply_region_language\(\) \{\n.*?^\}", configurator, re.M | re.S)
|
||||
form = re.search(r"^keyboard_form\(\) \{\n.*?^\}", configurator, re.M | re.S)
|
||||
init = re.search(r'^region="\$\{OMARCHY_INSTALL_REGION:-global\}"$', configurator, re.M)
|
||||
init = re.search(r'^region="\$\{OMARCHY_INSTALL_REGION:-.*\}"$', configurator, re.M)
|
||||
self.assertIsNotNone(language)
|
||||
self.assertIsNotNone(form)
|
||||
self.assertIsNotNone(init)
|
||||
@@ -98,8 +99,11 @@ class RegionTest(unittest.TestCase):
|
||||
"keyboard_form",
|
||||
'echo "region=$region defer=$defer_provisioning keyboard=${keyboard:-} lang=${OMARCHY_INSTALL_LANG:-}"',
|
||||
])
|
||||
return subprocess.run(["bash", "-c", harness], capture_output=True, text=True,
|
||||
env={"PATH": os.environ["PATH"], **env})
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
iso_region = Path(tmp) / "omarchy_region"
|
||||
iso_region.write_text(preselect + "\n")
|
||||
return subprocess.run(["bash", "-c", harness], capture_output=True, text=True,
|
||||
env={"PATH": os.environ["PATH"], "ISO_REGION_FILE": str(iso_region), **env})
|
||||
|
||||
def lines(self, result: subprocess.CompletedProcess) -> list[str]:
|
||||
return [line for line in result.stdout.splitlines() if line]
|
||||
@@ -109,34 +113,317 @@ class RegionTest(unittest.TestCase):
|
||||
self.assertIn("region=global defer=true", out.stdout)
|
||||
|
||||
def test_back_from_keyboard_keeps_the_chosen_region(self):
|
||||
out = self.run_keyboard_form("ok:region=china rc:1 ok:true ok:keyboard=us")
|
||||
out = self.run_keyboard_form("ok:region=cn rc:1 ok:true ok:keyboard=us")
|
||||
self.assertEqual(self.lines(out), [
|
||||
"region-preselect=global",
|
||||
"keyboard-prompt",
|
||||
"region-preselect=china",
|
||||
"region-preselect=cn",
|
||||
"keyboard-prompt",
|
||||
"region=china defer=false keyboard=us lang=",
|
||||
"region=cn defer=false keyboard=us lang=",
|
||||
])
|
||||
|
||||
def test_a_cn_iso_preselects_china(self):
|
||||
out = self.run_keyboard_form("ok:true", preselect="cn", OMARCHY_CJK_RELAUNCH="10")
|
||||
self.assertEqual(out.returncode, 10)
|
||||
self.assertEqual(self.lines(out), ["region-preselect=cn"])
|
||||
|
||||
def test_china_on_the_vt_hands_over_to_the_cjk_console(self):
|
||||
out = self.run_keyboard_form("ok:region=china", OMARCHY_CJK_RELAUNCH="10")
|
||||
out = self.run_keyboard_form("ok:region=cn", OMARCHY_CJK_RELAUNCH="10")
|
||||
self.assertEqual(out.returncode, 10)
|
||||
self.assertEqual(self.lines(out), ["region-preselect=global"])
|
||||
|
||||
def test_cjk_console_reads_chinese_with_a_us_keyboard(self):
|
||||
out = self.run_keyboard_form("ok:true", OMARCHY_CJK_CONSOLE="1", OMARCHY_VT_RETURN="11",
|
||||
OMARCHY_INSTALL_REGION="china")
|
||||
OMARCHY_INSTALL_REGION="cn")
|
||||
self.assertEqual(out.returncode, 0)
|
||||
self.assertEqual(self.lines(out), [
|
||||
"region-preselect=china",
|
||||
"region=china defer=false keyboard=us lang=zh",
|
||||
"region-preselect=cn",
|
||||
"region=cn defer=false keyboard=us lang=zh",
|
||||
])
|
||||
|
||||
def test_global_in_the_cjk_console_returns_to_the_vt(self):
|
||||
out = self.run_keyboard_form("ok:region=global", OMARCHY_CJK_CONSOLE="1", OMARCHY_VT_RETURN="11",
|
||||
OMARCHY_INSTALL_REGION="china")
|
||||
OMARCHY_INSTALL_REGION="cn")
|
||||
self.assertEqual(out.returncode, 11)
|
||||
self.assertNotIn("keyboard-prompt", out.stdout)
|
||||
|
||||
|
||||
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.payload = self.root / "payload"
|
||||
patch = mock.patch.object(phases_impl, "ISO_REGION_PAYLOAD", self.payload)
|
||||
patch.start()
|
||||
self.addCleanup(patch.stop)
|
||||
|
||||
def ctx(self, region, deferred=False):
|
||||
return types.SimpleNamespace(target=self.target, username="alice", defer_provisioning=deferred,
|
||||
omarchy_install={"region": region})
|
||||
|
||||
def test_global_does_not_change_user_defaults(self):
|
||||
(self.payload / "skel").mkdir(parents=True)
|
||||
(self.payload / "skel/.regional").write_text("cn only\n")
|
||||
phases_impl._stage_region_defaults(self.ctx("global"))
|
||||
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):
|
||||
profile = self.payload / "skel/.config/example/defaults"
|
||||
profile.parent.mkdir(parents=True)
|
||||
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"
|
||||
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.ctx("cn"))
|
||||
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")
|
||||
self.assertEqual((self.target / "etc/omarchy/region").read_text(), "cn\n")
|
||||
|
||||
def test_cn_without_user_defaults_only_records_the_region(self):
|
||||
phases_impl._stage_region_defaults(self.ctx("cn"))
|
||||
self.assertEqual((self.target / "etc/omarchy/region").read_text(), "cn\n")
|
||||
self.assertFalse((self.target / "etc/skel").exists())
|
||||
|
||||
def test_cn_keyring_is_initialized_before_finalization_for_both_install_modes(self):
|
||||
for deferred in (False, True):
|
||||
ctx = self.ctx("cn", 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):
|
||||
ctx = self.ctx("cn")
|
||||
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)
|
||||
|
||||
def test_global_trusts_no_regional_keyring(self):
|
||||
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(self.ctx("global"))
|
||||
self.assertNotIn("pacman-key", [call.args[1][0] for call in run.call_args_list])
|
||||
|
||||
|
||||
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\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)
|
||||
self.packages = self.payload / "omarchy-base.packages"
|
||||
self.packages.write_text("base\n")
|
||||
self.other = self.payload / "omarchy-other.packages"
|
||||
self.other.write_text("other\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_bundles_its_packages_installs_them_only_for_china_and_verifies_keyring_first(self):
|
||||
result = self.prepare("cn")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(self.packages.read_text(), "base\n")
|
||||
self.assertEqual(self.other.read_text(), "other\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, "ISO_REGION_PAYLOAD", self.payload / "region"), \
|
||||
mock.patch.object(phases_impl, "_package_targets", return_value={"runtime": "omarchy-dev", "settings": "omarchy-settings-dev", "nvim": "omarchy-nvim"}):
|
||||
for region, extra in (("global", []), ("cn", ["archlinuxcn-keyring", "example-regional-package"])):
|
||||
ctx = types.SimpleNamespace(omarchy_install={"region": region})
|
||||
self.assertEqual(phases_impl._runtime_package_list(ctx), ["omarchy-dev", "base", *extra])
|
||||
|
||||
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.other.read_text(), "other\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_profile_without_user_defaults_is_supported(self):
|
||||
result = self.prepare("cn")
|
||||
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")
|
||||
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"
|
||||
for arg in "$@"; do
|
||||
if [[ $arg == *:/out/ ]]; then touch "${arg%:/out/}/omarchy-test-x86_64.iso"; fi
|
||||
done
|
||||
''',
|
||||
}
|
||||
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_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()
|
||||
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())
|
||||
|
||||
|
||||
|
||||
class ConfiguratorRegionTimezoneTest(unittest.TestCase):
|
||||
"""The configurator hands the cn timezone to the setup form when China is chosen."""
|
||||
|
||||
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, region):
|
||||
self.file.write_text("Asia/Shanghai\n")
|
||||
return subprocess.run(
|
||||
["bash", "-c", f"set -euo pipefail\nregion={region}\n{self.helper}\nregion_timezone"],
|
||||
env={**os.environ, "REGION_TIMEZONE": str(self.file)},
|
||||
capture_output=True, text=True, check=True,
|
||||
).stdout
|
||||
|
||||
def test_global_offers_no_timezone(self):
|
||||
self.assertEqual(self.region_timezone("global"), "")
|
||||
|
||||
def test_china_offers_the_profile_timezone(self):
|
||||
self.assertEqual(self.region_timezone("cn"), "Asia/Shanghai\n")
|
||||
|
||||
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()
|
||||
Reference in new issue
Block a user