Merge omacom/omarchy-iso#241: region from the timezone in one ISO
Drop our --region build, prepare-region.sh, region skel, the preselected China timezone, the explicit region in the configurator's JSON, Rime bundling and its pin.
This commit is contained in:
commit
75d00a2036
14 files changed
+409
-618
No files matched your search
@@ -26,26 +26,12 @@ For local development, build the ISO from sibling checkouts:
|
||||
|
||||
Despite the local folder name, the first argument is the Omarchy source checkout (runtime commands, configs, setup scripts, themes, shell, migrations). The installer itself lives in this ISO repo.
|
||||
|
||||
A local-source install pins `omarchy-dev`, `omarchy-settings-dev` and `rime-ice-installer`, since the published builds share their version number but lack the local changes. `IgnorePkg` in `/etc/pacman.conf` makes updates skip them, and a `pre-refresh-pacman` hook (in the user's `~/.config/omarchy/hooks` and in `/etc/skel`) puts that line back after `omarchy refresh pacman`. `/etc/pacman.d/hooks/00-omarchy-pin-local-builds.hook` aborts any transaction that would still replace them. Remove all three to take the published packages.
|
||||
A local-source install pins `omarchy-dev` and `omarchy-settings-dev`, since the published builds share their version number but lack the local changes. `IgnorePkg` in `/etc/pacman.conf` makes updates skip them, and a `pre-refresh-pacman` hook (in the user's `~/.config/omarchy/hooks` and in `/etc/skel`) puts that line back after `omarchy refresh pacman`. `/etc/pacman.d/hooks/00-omarchy-pin-local-builds.hook` aborts any transaction that would still replace them. Remove all three to take the published packages.
|
||||
|
||||
The configurator asks for the region on its first screen. China reruns the installer in cage + foot, a console that draws Chinese: it opens on a bilingual region screen, keeps a US keyboard, and every later screen and the install progress read in Chinese. Choosing Global there returns to the VT, which applies the chosen keymap; without a working display China continues in English on the VT. The Chinese text lives in `OMARCHY_ZH` tables beside the English it replaces.
|
||||
|
||||
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.
|
||||
@@ -66,7 +52,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 `cn`, 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. 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.
|
||||
|
||||
@@ -80,6 +66,8 @@ When `authorized_keys` is present, autoinstall installs it as the user's `~/.ssh
|
||||
|
||||
When `tailscale_authkey` is present (one key, blank lines and `#` comments ignored), the install adds the `tailscale` package from the ISO's bundled mirror — nothing is fetched from the network at install or boot — and stages the join for first boot: the key lands at `/etc/tailscale/authkey` (root-only), `tailscaled` is enabled, ufw allows traffic in on `tailscale0`, and a background unit runs `tailscale up` once the network is actually up, retrying until it succeeds without holding up the boot. After a successful join the key is deleted and the unit disables itself; until then both survive reboots, so a machine installed offline joins whenever it first gets connectivity. The node appears on the tailnet under the configured hostname. Use a reusable, pre-authorized (tagged) key so one drive image serves many machines — or an ephemeral key for disposable VMs.
|
||||
|
||||
The region follows `timezone`, exactly as in an interactive install: a timezone listed in one of the runtime's region profiles (`default/regions/*/timezones`, such as `Asia/Shanghai` for `cn`) selects that profile's mirrors and repositories, and anything else is global. Set `omarchy_install.region` in `user_configuration.json` to pick one explicitly, for example `"cn"` on a deferred-provisioning install, which carries `UTC` until first boot. Every ISO bundles every profile's packages, so a regional install still needs no network.
|
||||
|
||||
### Building the drive
|
||||
|
||||
```bash
|
||||
|
||||
+8
-36
@@ -6,17 +6,8 @@ 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
|
||||
@@ -66,17 +57,12 @@ while [[ $# -gt 0 ]]; do
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: $0 [--no-cache] [--keep-pkg-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--region global|cn] [--local-source <omarchy-checkout> <pkgs-checkout>]"
|
||||
echo "Usage: $0 [--no-cache] [--keep-pkg-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--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; }
|
||||
@@ -130,10 +116,6 @@ 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,11 +125,10 @@ 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_OUT/:/out/"
|
||||
-v "$BUILD_RELEASE_PATH/:/out/"
|
||||
-v "$BUILD_ROOT/$BUILD_SCRIPT:/$BUILD_SCRIPT:ro"
|
||||
-v "$BUILD_ROOT/archiso:/archiso:ro"
|
||||
-v "$BUILD_ROOT/builder:/builder:ro"
|
||||
@@ -165,14 +146,10 @@ if [[ -d /var/cache/pacman/pkg ]]; then
|
||||
DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg")
|
||||
fi
|
||||
|
||||
# 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
|
||||
# Mount the build cache directory where packages are downloaded. Channels use
|
||||
# different package snapshots and must never share an offline mirror cache.
|
||||
if [[ -z "$NO_CACHE" ]]; then
|
||||
OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${OMARCHY_MIRROR}${region_suffix}/airootfs/var/cache/omarchy"
|
||||
OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${OMARCHY_MIRROR}/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
|
||||
@@ -191,14 +168,9 @@ fi
|
||||
|
||||
"${DOCKER[@]}" run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
|
||||
|
||||
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"
|
||||
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*.iso | head -n1)
|
||||
iso_ref="${latest_iso%.*}-$OMARCHY_ISO_REF.iso"
|
||||
mv -f "$latest_iso" "$iso_ref"
|
||||
|
||||
if [[ -z "${NO_BOOT_OFFER:-}" ]]; then
|
||||
echo
|
||||
|
||||
+37
-43
@@ -4,11 +4,6 @@ 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
|
||||
@@ -35,7 +30,7 @@ pacman --noconfirm -Sy archlinux-keyring
|
||||
# so this container can be months behind the mirror it installs from. A plain
|
||||
# -Sy install is then a partial upgrade — new packages linked against a glibc
|
||||
# the container doesn't have yet.
|
||||
pacman --noconfirm -Syu archiso git sudo base-devel jq grub imagemagick neovim nodejs npm tree-sitter-cli go
|
||||
pacman --noconfirm -Syu archiso git sudo base-devel jq grub imagemagick neovim nodejs npm tree-sitter-cli
|
||||
|
||||
# Pre-import the omarchy signing key (so pacman trusts our [omarchy] repo
|
||||
# during the build without keyserver lookups).
|
||||
@@ -75,7 +70,6 @@ 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
|
||||
@@ -89,7 +83,6 @@ 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"
|
||||
@@ -113,7 +106,7 @@ if [[ -d /omarchy-source && -d /omarchy-pkgs ]]; then
|
||||
bash /builder/build-omarchy-packages.sh "$offline_mirror_dir"
|
||||
LOCAL_OMARCHY_BUILD=1
|
||||
# Published builds of these lack the local changes, so the target pins them.
|
||||
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" rime-ice-installer \
|
||||
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" \
|
||||
> "$build_cache_dir/airootfs/usr/share/omarchy-iso/pinned-packages"
|
||||
fi
|
||||
|
||||
@@ -127,20 +120,6 @@ echo "$NODE_SHA /tmp/$NODE_FILENAME" | sha256sum -c -
|
||||
mkdir -p "$build_cache_dir/airootfs/opt/packages/"
|
||||
cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/"
|
||||
|
||||
# Rime Ice and Wanxiang for offline China installs.
|
||||
rime_dir="$build_cache_dir/airootfs/opt/packages/rime-ice"
|
||||
mkdir -p "$rime_dir"
|
||||
bundle_release_asset() {
|
||||
local url digest
|
||||
read -r url digest < <(curl -fsSL "https://api.github.com/repos/$1/releases/tags/$2" |
|
||||
jq -r --arg name "$3" '.assets[] | select(.name == $name) | "\(.browser_download_url) \(.digest)"') ||
|
||||
{ echo "ERROR: $3 is missing from the $1 $2 release" >&2; exit 1; }
|
||||
curl -fsSL "$url" -o "$rime_dir/$3"
|
||||
echo "${digest#sha256:} $rime_dir/$3" | sha256sum -c -
|
||||
}
|
||||
bundle_release_asset iDvel/rime-ice nightly full.zip
|
||||
bundle_release_asset amzxyz/RIME-LMDG LTS wanxiang-lts-zh-hans.gram
|
||||
|
||||
# Packages installed into the live ISO environment itself (NOT the target system).
|
||||
# The selected omarchy-settings package is needed here so its post_install hook
|
||||
# drops Omarchy's plymouthd.conf into /etc/plymouth before mkarchiso builds the
|
||||
@@ -167,7 +146,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
|
||||
regions_source=/omarchy-source/default/regions
|
||||
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
|
||||
@@ -184,18 +163,23 @@ 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
|
||||
# build here (set -e) with a bare "Not found in archive" instead of the
|
||||
# actionable error below.
|
||||
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/provisioning/setup-form.sh 2>/dev/null || true
|
||||
# Region profiles live under default/, which the settings package ships,
|
||||
# not the runtime. Its own cache dir keeps the runtime lookup above from
|
||||
# ever matching it.
|
||||
settings_cache_dir="$bootstrap_cache_dir/settings"
|
||||
mkdir -p "$settings_cache_dir"
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw "$OMARCHY_SETTINGS_PACKAGE" --cachedir "$settings_cache_dir" --dbpath /tmp/offlinedb-bootstrap >/dev/null
|
||||
regions_source=$(find "$settings_cache_dir" -maxdepth 1 -type f -name "$OMARCHY_SETTINGS_PACKAGE-*.pkg.tar.zst" | sort | head -1)
|
||||
if [[ -z $regions_source ]]; then
|
||||
echo "ERROR: downloaded package for $OMARCHY_SETTINGS_PACKAGE not found in $settings_cache_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
setup_form=/tmp/omarchy-pkglists/usr/share/omarchy/install/provisioning/setup-form.sh
|
||||
fi
|
||||
|
||||
@@ -203,16 +187,6 @@ 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
|
||||
@@ -232,6 +206,24 @@ if [[ ! -f $setup_form ]]; then
|
||||
fi
|
||||
cp "$setup_form" "$build_cache_dir/airootfs/usr/share/omarchy-iso/setup-form.sh"
|
||||
|
||||
# Every region's packages ride in the offline mirror, and its community
|
||||
# repositories join a writable copy of the online config to download them.
|
||||
online_config=/tmp/omarchy-pacman-online.conf
|
||||
cp "/configs/pacman-online-${OMARCHY_MIRROR}.conf" "$online_config"
|
||||
bash /builder/prepare-regions.sh "$regions_source" \
|
||||
"$build_cache_dir/airootfs/usr/share/omarchy-iso" "$online_config"
|
||||
|
||||
# Profiles ship in the settings package, but the finalizer that applies them
|
||||
# ships in the runtime. A runtime without it would ignore the region marker,
|
||||
# leaving regional targets with the keyring but none of the repositories.
|
||||
if [[ -n ${omarchy_pkg:-} && -d $build_cache_dir/airootfs/usr/share/omarchy-iso/regions ]]; then
|
||||
runtime_listing=$(bsdtar -tf "$omarchy_pkg")
|
||||
if ! grep -qx 'usr/bin/omarchy-apply-pacman' <<<"$runtime_listing"; then
|
||||
echo "ERROR: $OMARCHY_SETTINGS_PACKAGE ships region profiles, but $OMARCHY_RUNTIME_PACKAGE lacks omarchy-apply-pacman to apply them." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Collect every package we want available in the offline mirror.
|
||||
declare -a all_packages
|
||||
mapfile -t all_packages < <(
|
||||
@@ -239,6 +231,9 @@ mapfile -t all_packages < <(
|
||||
cat "$build_cache_dir/packages.x86_64"
|
||||
grep -hv '^#\|^$' "${base_pkg_lists[@]}"
|
||||
grep -hv '^#\|^$' /builder/archinstall.packages
|
||||
# Downloaded but not installed: the installer adds a region's packages
|
||||
# only to targets in that region.
|
||||
grep -hsv '^#\|^$' "$build_cache_dir"/airootfs/usr/share/omarchy-iso/regions/*/packages || true
|
||||
# Always include the selected Omarchy packages so the target install can
|
||||
# find the runtime and companion packages in the offline mirror.
|
||||
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" "$OMARCHY_NVIM_PACKAGE"
|
||||
@@ -271,8 +266,7 @@ if [[ -n ${LOCAL_OMARCHY_BUILD:-} ]]; then
|
||||
grep -Fxv \
|
||||
-e "$OMARCHY_RUNTIME_PACKAGE" \
|
||||
-e "$OMARCHY_SETTINGS_PACKAGE" \
|
||||
-e "$OMARCHY_NVIM_PACKAGE" \
|
||||
-e rime-ice-installer || true
|
||||
-e "$OMARCHY_NVIM_PACKAGE" || true
|
||||
)
|
||||
fi
|
||||
|
||||
@@ -309,7 +303,7 @@ mapfile -t required_package_files <<< "$resolved_package_files"
|
||||
# that the local build left exactly one file for each selected package name.
|
||||
if [[ -n ${LOCAL_OMARCHY_BUILD:-} ]]; then
|
||||
for local_package_name in \
|
||||
"$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" "$OMARCHY_NVIM_PACKAGE" rime-ice-installer; do
|
||||
"$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" "$OMARCHY_NVIM_PACKAGE"; do
|
||||
local_package_file=""
|
||||
for candidate in "$offline_mirror_dir/$local_package_name-"*.pkg.tar.*; do
|
||||
[[ -f $candidate && $candidate != *.sig ]] || continue
|
||||
|
||||
@@ -40,7 +40,6 @@ packages=(
|
||||
"$OMARCHY_SETTINGS_PACKAGE"
|
||||
"$OMARCHY_RUNTIME_PACKAGE"
|
||||
"$OMARCHY_NVIM_PACKAGE"
|
||||
rime-ice-installer
|
||||
)
|
||||
|
||||
# Local-source packages must replace every cached build of the same package,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/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
|
||||
|
||||
# Offline mirror only; the installer adds them when China is picked.
|
||||
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
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ship every region profile from the bundled runtime, so one ISO serves every
|
||||
# region. The installer picks a profile from the target's timezone; this only
|
||||
# stages the profiles and makes their packages downloadable. Repository
|
||||
# defaults stay in the runtime, not in a second set of ISO-specific templates.
|
||||
set -euo pipefail
|
||||
|
||||
regions_source="$1"
|
||||
iso_payload="$2"
|
||||
online_config="$3"
|
||||
|
||||
# The source is a checkout's default/regions, or the published settings
|
||||
# package that ships default/. One predating region profiles ships none; the
|
||||
# ISO then installs global targets only, exactly as before. Anything else that
|
||||
# fails here, such as an unreadable package, aborts the build.
|
||||
regions="$regions_source"
|
||||
if [[ -f $regions_source ]]; then
|
||||
member=usr/share/omarchy/default/regions
|
||||
listing=$(bsdtar -tf "$regions_source")
|
||||
if grep -q "^$member/" <<<"$listing"; then
|
||||
extracted=$(mktemp -d)
|
||||
trap 'rm -rf "$extracted"' EXIT
|
||||
bsdtar -xf "$regions_source" -C "$extracted" "$member"
|
||||
regions="$extracted/$member"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -d $regions ]]; then
|
||||
echo "Runtime ships no region profiles; every install will be global."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cp -a "$regions" "$iso_payload/regions"
|
||||
|
||||
for profile in "$regions"/*/; do
|
||||
# Only the community repositories are needed to download regional packages.
|
||||
# Keep the channel's build mirrors intact; target mirrors are set later by
|
||||
# the runtime's own pacman finalizer.
|
||||
printf '\n' >> "$online_config"
|
||||
cat "$profile/pacman/pacman.conf.append" >> "$online_config"
|
||||
|
||||
# Each keyring package must be signed by a key Arch already trusts. Do not
|
||||
# bootstrap trust from an unchecked keyserver response or disable signatures.
|
||||
# https://github.com/archlinuxcn/repo#usage
|
||||
mapfile -t keyring_packages < <(grep -hv '^#\|^$' "$profile/packages" | grep -- '-keyring$' || true)
|
||||
if (( ${#keyring_packages[@]} > 0 )); then
|
||||
pacman --config "$online_config" --noconfirm -Sy --needed "${keyring_packages[@]}"
|
||||
pacman-key --populate "${keyring_packages[@]%-keyring}"
|
||||
fi
|
||||
done
|
||||
@@ -145,7 +145,8 @@ 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)" == "cn" ]]; then
|
||||
# Only Chinese reaches the install from the CJK console; English returns to the VT.
|
||||
if [[ -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
||||
export OMARCHY_INSTALL_LANG=zh
|
||||
fi
|
||||
|
||||
|
||||
@@ -91,13 +91,6 @@ OMARCHY_ZH+=(
|
||||
["Everything will be overwritten. There is no recovery possible."]="磁盘上的一切都会被覆盖,无法恢复。"
|
||||
["Confirm overwriting %s"]="确认覆盖 %s"
|
||||
)
|
||||
# China preselects the bundled cn profile's timezone when geo guessing fails.
|
||||
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.
|
||||
@@ -370,7 +363,7 @@ user_form() {
|
||||
|
||||
omarchy_prompt_identity || return $?
|
||||
omarchy_prompt_hostname || return $?
|
||||
omarchy_prompt_timezone "$(region_timezone)" || return $?
|
||||
omarchy_prompt_timezone || return $?
|
||||
}
|
||||
|
||||
# Returns 0 on confirmed account details, or as soon as the keyboard screen
|
||||
@@ -390,19 +383,25 @@ user_step() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# The timezone selects the region (mirrors and repositories), using the
|
||||
# same lookup as the orchestrator. Global installs get no extra row.
|
||||
region=$(cd /usr/share/omarchy-iso 2>/dev/null && python -m orchestrator.region "$timezone") || region="global"
|
||||
region_row=""
|
||||
if [[ $region != "global" ]]; then
|
||||
region_row="\n$(omarchy_text Region),$region"
|
||||
fi
|
||||
|
||||
# Add manual padding since gum table -p doesn't respect padding
|
||||
local skipped region_label=Global
|
||||
local skipped
|
||||
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 ' ' '*')
|
||||
$(omarchy_text "Full name"),${full_name:-$skipped}
|
||||
$(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_label")" |
|
||||
$(omarchy_text Timezone),$timezone$region_row
|
||||
$(omarchy_text Keyboard),$keyboard" |
|
||||
gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/"
|
||||
|
||||
echo
|
||||
@@ -907,7 +906,6 @@ run_partition_execute() {
|
||||
"input_method": "${keyboard_input_method:-none}",
|
||||
"input_xkb_layout": "${keyboard_xkb_layout:-}",
|
||||
"mode": "protected",
|
||||
"region": "$region",
|
||||
"target_mount": "/mnt",
|
||||
"boot": {
|
||||
"esp_mount": "$esp_mount_in_target",
|
||||
@@ -1141,8 +1139,8 @@ select_installation() {
|
||||
|
||||
defer_provisioning=false
|
||||
install_target="full_disk"
|
||||
# A rerun arrives with the region chosen; otherwise the ISO's default.
|
||||
region="${OMARCHY_INSTALL_REGION:-$(<"${ISO_REGION_FILE:-/root/omarchy_region}")}"
|
||||
# A rerun arrives with the console language chosen; otherwise Global.
|
||||
region="${OMARCHY_INSTALL_REGION:-global}"
|
||||
|
||||
# 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.
|
||||
@@ -1256,7 +1254,6 @@ cat <<-_EOF_ >user_configuration.json
|
||||
"input_method": "${keyboard_input_method:-none}",
|
||||
"input_xkb_layout": "${keyboard_xkb_layout:-}",
|
||||
"mode": "full_disk",
|
||||
"region": "$region",
|
||||
"defer_provisioning": $defer_provisioning,
|
||||
"target_mount": "/mnt",
|
||||
"boot": {
|
||||
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .keyboard import validate_input_selection
|
||||
from .region import GLOBAL, resolve_region
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -29,6 +30,7 @@ class InstallContext:
|
||||
arch_config_path: Path
|
||||
omarchy_install: dict[str, Any]
|
||||
defer_provisioning: bool = False
|
||||
region: str = GLOBAL
|
||||
|
||||
target: Path = Path("/mnt")
|
||||
omarchy_path: Path = Path("/usr/share/omarchy")
|
||||
@@ -61,6 +63,11 @@ class InstallContext:
|
||||
|
||||
validate_input_selection(omarchy_install.get("input_method"), omarchy_install.get("input_xkb_layout", ""))
|
||||
|
||||
# Resolved before any disk is touched, so an autoinstall naming a
|
||||
# region this ISO lacks fails without wiping anything.
|
||||
region = resolve_region(omarchy_install, user_configuration.get("timezone"))
|
||||
omarchy_install["region"] = region
|
||||
|
||||
# Deferred provisioning: the whole system installs but user creation is deferred to
|
||||
# first boot. Selected by the configurator (omarchy_install.defer_provisioning) or by
|
||||
# an `defer-provisioning` marker file on an autoinstall drive, which also replaces the
|
||||
@@ -69,12 +76,6 @@ class InstallContext:
|
||||
defer_provisioning = bool(omarchy_install.get("defer_provisioning")) or defer_provisioning_marker is not None
|
||||
omarchy_install["defer_provisioning"] = defer_provisioning
|
||||
|
||||
# Autoinstall configs written before regions existed install Global.
|
||||
region = omarchy_install.get("region", "global")
|
||||
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():
|
||||
user_credentials = json.loads(creds_path.read_text())
|
||||
elif defer_provisioning:
|
||||
@@ -131,6 +132,7 @@ class InstallContext:
|
||||
arch_config_path=arch_config_path,
|
||||
omarchy_install=omarchy_install,
|
||||
defer_provisioning=defer_provisioning,
|
||||
region=region,
|
||||
state_dir=state_dir,
|
||||
)
|
||||
disk_config = user_configuration.get("disk_config", {})
|
||||
|
||||
@@ -36,35 +36,10 @@ from . import archinstall_adapter as arch
|
||||
from .command import capture, capture_identifier, require_text
|
||||
from .context import InstallContext
|
||||
from .keyboard import configure_keyboard
|
||||
from .region import GLOBAL, region_keyrings, region_packages
|
||||
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.
|
||||
@@ -313,7 +288,6 @@ 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)
|
||||
@@ -325,6 +299,11 @@ def arch_install_system(ctx: InstallContext) -> None:
|
||||
info("› installing Omarchy runtime + omarchy-base.packages")
|
||||
installer.add_additional_packages(_runtime_package_list(ctx))
|
||||
|
||||
if region_pkgs := region_packages(ctx.region):
|
||||
info(f"› installing {ctx.region} region packages: {', '.join(region_pkgs)}")
|
||||
installer.add_additional_packages(region_pkgs)
|
||||
_write_region_marker(ctx)
|
||||
|
||||
# Tailscale is bundled in the offline mirror but only installed
|
||||
# when an autoinstall drive staged an auth key; must happen here,
|
||||
# while the mirror is still bind-mounted, not in the phase that
|
||||
@@ -755,6 +734,17 @@ def _unmask_mkinitcpio_pacman_hooks(
|
||||
info(f"warning: failed to restore pacman hook mask for {name}: {exc}")
|
||||
|
||||
|
||||
def _write_region_marker(ctx: InstallContext) -> None:
|
||||
"""Tell the runtime's pacman finalizer which profile to apply. Global
|
||||
installs write nothing, so they stay identical to a region-less install."""
|
||||
if ctx.region == GLOBAL:
|
||||
return
|
||||
marker = ctx.target / "etc/omarchy/region"
|
||||
marker.parent.mkdir(parents=True, exist_ok=True)
|
||||
marker.write_text(ctx.region + "\n")
|
||||
marker.chmod(0o644)
|
||||
|
||||
|
||||
def _runtime_package_list(ctx: InstallContext) -> list[str]:
|
||||
"""Selected Omarchy runtime package + every package in the ISO-bundled
|
||||
base package list that isn't already installed early."""
|
||||
@@ -774,10 +764,6 @@ 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]
|
||||
return pkgs
|
||||
|
||||
|
||||
@@ -1129,7 +1115,6 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
|
||||
f"OMARCHY_USER_NAME={ctx.full_name}",
|
||||
f"OMARCHY_USER_EMAIL={ctx.email}",
|
||||
f"OMARCHY_MIRROR={mirror_channel}",
|
||||
f"OMARCHY_REGION={ctx.omarchy_install['region']}",
|
||||
f"OMARCHY_ISO_REF={_iso_ref()}",
|
||||
f"OMARCHY_RUNTIME_PACKAGE={_omarchy_runtime_package()}",
|
||||
f"OMARCHY_SETTINGS_PACKAGE={_omarchy_settings_package()}",
|
||||
@@ -1167,12 +1152,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":
|
||||
# The region's keyring packages are installed by now, but the build
|
||||
# container's GPG state is not inherited. Initialize the target's trust
|
||||
# database with only this region's keys before the finalizer enables its
|
||||
# online repositories; global installs never trust them.
|
||||
if keyrings := region_keyrings(ctx.region):
|
||||
_run_target_setup_command(ctx, ["pacman-key", "--init"])
|
||||
_run_target_setup_command(ctx, ["pacman-key", "--populate", "archlinux", "archlinuxcn"])
|
||||
_run_target_setup_command(ctx, ["pacman-key", "--populate", "archlinux", *keyrings])
|
||||
|
||||
if ctx.defer_provisioning:
|
||||
cmd = ["/usr/bin/omarchy-apply-system", "--defer-provisioning", "--first-install"]
|
||||
@@ -1414,7 +1400,6 @@ def run_chroot_finalizer(ctx: InstallContext) -> None:
|
||||
)
|
||||
|
||||
|
||||
|
||||
PINNED_PACKAGES = Path("/usr/share/omarchy-iso/pinned-packages")
|
||||
PIN_HOOK_SCRIPT = """#!/bin/bash
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Region selection: which of the runtime's region profiles a target gets.
|
||||
|
||||
The builder copies every profile from the bundled runtime's default/regions/
|
||||
into REGIONS_DIR, and their packages into the offline mirror. Nobody picks a
|
||||
region by hand: the timezone the user already chose selects it, and an
|
||||
autoinstall config may name one explicitly in omarchy_install.region. Run as
|
||||
`python -m orchestrator.region <timezone>` to print the region the installer
|
||||
would use, which is how the configurator fills its summary.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REGIONS_DIR = Path("/usr/share/omarchy-iso/regions")
|
||||
GLOBAL = "global"
|
||||
|
||||
|
||||
def _entries(path: Path) -> list[str]:
|
||||
if not path.is_file():
|
||||
return []
|
||||
lines = (line.strip() for line in path.read_text().splitlines())
|
||||
return [line for line in lines if line and not line.startswith("#")]
|
||||
|
||||
|
||||
def region_for_timezone(timezone: str | None, regions_dir: Path | None = None) -> str:
|
||||
regions_dir = regions_dir or REGIONS_DIR
|
||||
if timezone and regions_dir.is_dir():
|
||||
for profile in sorted(regions_dir.iterdir()):
|
||||
if timezone in _entries(profile / "timezones"):
|
||||
return profile.name
|
||||
return GLOBAL
|
||||
|
||||
|
||||
def resolve_region(omarchy_install: dict, timezone: str | None, regions_dir: Path | None = None) -> str:
|
||||
regions_dir = regions_dir or REGIONS_DIR
|
||||
region = omarchy_install.get("region")
|
||||
if region is None or region == "":
|
||||
return region_for_timezone(timezone, regions_dir)
|
||||
# Profiles are named by ISO 3166-1 alpha-2 code; checking the shape first
|
||||
# keeps a path like ".." from passing the directory test.
|
||||
if not isinstance(region, str) or (
|
||||
region != GLOBAL and not (re.fullmatch(r"[a-z]{2}", region) and (regions_dir / region).is_dir())
|
||||
):
|
||||
raise RuntimeError(f"Unsupported region: {region!r}")
|
||||
return region
|
||||
|
||||
|
||||
def region_packages(region: str, regions_dir: Path | None = None) -> list[str]:
|
||||
if region == GLOBAL:
|
||||
return []
|
||||
return _entries((regions_dir or REGIONS_DIR) / region / "packages")
|
||||
|
||||
|
||||
def region_keyrings(region: str, regions_dir: Path | None = None) -> list[str]:
|
||||
"""Pacman keyrings the profile ships, named the Arch way: <name>-keyring
|
||||
installs /usr/share/pacman/keyrings/<name>.gpg."""
|
||||
return [
|
||||
package.removesuffix("-keyring")
|
||||
for package in region_packages(region, regions_dir)
|
||||
if package.endswith("-keyring")
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(region_for_timezone(sys.argv[1] if len(sys.argv) > 1 else None))
|
||||
@@ -93,7 +93,6 @@ 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)
|
||||
@@ -103,12 +102,13 @@ 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=types.SimpleNamespace(users=["alice"]), app_config=None, timezone=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=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"):
|
||||
@@ -117,7 +117,6 @@ 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))
|
||||
@@ -131,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", "region", "user", ["omarchy"]]
|
||||
expected += ["early", ["omarchy"]]
|
||||
self.assertEqual(events, expected)
|
||||
unmask.assert_called_once_with(ctx)
|
||||
unmount.assert_called_once_with(ctx)
|
||||
|
||||
@@ -16,7 +16,7 @@ sys.modules.setdefault(
|
||||
)
|
||||
from orchestrator import phases_impl # noqa: E402
|
||||
|
||||
LINE = "IgnorePkg = omarchy-dev omarchy-settings-dev rime-ice-installer"
|
||||
LINE = "IgnorePkg = omarchy-dev omarchy-settings-dev"
|
||||
CONF = "[options]\nHoldPkg = pacman glibc\n\n[core]\nInclude = /etc/pacman.d/mirrorlist\n"
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class PinLocalPackagesTest(unittest.TestCase):
|
||||
(root / "etc/pacman.conf").write_text(CONF)
|
||||
pinned_file = root / "pinned-packages"
|
||||
if pinned:
|
||||
pinned_file.write_text("omarchy-dev\nomarchy-settings-dev\nrime-ice-installer\n")
|
||||
pinned_file.write_text("omarchy-dev\nomarchy-settings-dev\n")
|
||||
ctx = types.SimpleNamespace(target=root, username="test", defer_provisioning=defer)
|
||||
with mock.patch.object(phases_impl, "PINNED_PACKAGES", pinned_file), \
|
||||
mock.patch.object(phases_impl.subprocess, "run") as run:
|
||||
@@ -43,7 +43,7 @@ class PinLocalPackagesTest(unittest.TestCase):
|
||||
self.assertIn(f'line="{LINE}"', hook.read_text())
|
||||
self.assertTrue(hook.stat().st_mode & 0o111)
|
||||
guard = (root / "etc/pacman.d/hooks/00-omarchy-pin-local-builds.hook").read_text()
|
||||
for package in ("omarchy-dev", "omarchy-settings-dev", "rime-ice-installer"):
|
||||
for package in ("omarchy-dev", "omarchy-settings-dev"):
|
||||
self.assertIn(f"Target = {package}\n", guard)
|
||||
self.assertIn("AbortOnFail", guard)
|
||||
skel = root / "etc/skel/.config/omarchy/hooks/pre-refresh-pacman.d/pin-local-packages"
|
||||
@@ -83,36 +83,5 @@ class PinLocalPackagesTest(unittest.TestCase):
|
||||
self.assertFalse((root / "etc/pacman.d").exists())
|
||||
|
||||
|
||||
class RuntimePackagesTest(unittest.TestCase):
|
||||
def runtime_packages(self, pinned: str | None) -> list[str]:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
pinned_file = root / "pinned-packages"
|
||||
if pinned is not None:
|
||||
pinned_file.write_text(pinned)
|
||||
base = root / "omarchy-base.packages"
|
||||
base.write_text("# base\nfcitx5\ngit\n")
|
||||
real_path = phases_impl.Path
|
||||
|
||||
def path(arg, *rest):
|
||||
if arg == "/usr/share/omarchy-iso/omarchy-base.packages":
|
||||
return base
|
||||
return real_path(arg, *rest)
|
||||
|
||||
with mock.patch.object(phases_impl, "PINNED_PACKAGES", pinned_file), \
|
||||
mock.patch.object(phases_impl, "Path", side_effect=path), \
|
||||
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(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")
|
||||
self.assertEqual(packages, ["omarchy-dev", "fcitx5", "rime-ice-installer"])
|
||||
|
||||
def test_published_builds_add_nothing(self):
|
||||
self.assertEqual(self.runtime_packages(None), ["omarchy-dev", "fcitx5"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+189
-382
@@ -1,14 +1,13 @@
|
||||
"""Region: the install-time choice, the bundled cn profile, and regional builds."""
|
||||
"""The target's region comes from its timezone, using the runtime's profiles."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from contextlib import ExitStack
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
@@ -18,411 +17,219 @@ sys.modules.setdefault(
|
||||
"orchestrator.archinstall_adapter",
|
||||
types.ModuleType("orchestrator.archinstall_adapter"),
|
||||
)
|
||||
from orchestrator import context, phases_impl # noqa: E402
|
||||
from orchestrator import context, phases_impl, region # noqa: E402
|
||||
|
||||
|
||||
def load_context(root: Path, omarchy_install: dict | None) -> context.InstallContext:
|
||||
config = {"kernels": ["linux-omarchy"]}
|
||||
if omarchy_install is not None:
|
||||
config["omarchy_install"] = omarchy_install
|
||||
config_path = root / "config.json"
|
||||
creds_path = 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(root / "state"),
|
||||
}, clear=True):
|
||||
return context.InstallContext.from_env()
|
||||
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 RegionTest(unittest.TestCase):
|
||||
def test_context_reads_region(self):
|
||||
for omarchy_install, expected in [
|
||||
(None, "global"),
|
||||
({"mode": "full_disk"}, "global"),
|
||||
({"mode": "full_disk", "region": "global"}, "global"),
|
||||
({"mode": "full_disk", "region": "cn"}, "cn"),
|
||||
]:
|
||||
with self.subTest(omarchy_install=omarchy_install), tempfile.TemporaryDirectory() as tmp:
|
||||
ctx = load_context(Path(tmp), omarchy_install)
|
||||
self.assertEqual(ctx.omarchy_install["region"], expected)
|
||||
|
||||
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 cn"):
|
||||
load_context(Path(tmp), {"mode": "full_disk", "region": region})
|
||||
|
||||
def test_target_setup_receives_region(self):
|
||||
for region in ("global", "cn"):
|
||||
with self.subTest(region=region), tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
ctx = types.SimpleNamespace(
|
||||
target=root / "mnt",
|
||||
log_path=root / "install.log",
|
||||
omarchy_install={"region": region},
|
||||
username="test",
|
||||
full_name="",
|
||||
email="",
|
||||
state={"omarchy_start_time": "now", "omarchy_start_epoch": 0,
|
||||
"omarchy_finalizer_header_written": True},
|
||||
)
|
||||
with mock.patch.object(phases_impl, "_prepare_target_setup"), \
|
||||
mock.patch.object(phases_impl.subprocess, "run") as run:
|
||||
phases_impl._run_target_setup_command(ctx, ["/usr/bin/omarchy-apply-system"])
|
||||
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, 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:-.*\}"$', configurator, re.M)
|
||||
self.assertIsNotNone(language)
|
||||
self.assertIsNotNone(form)
|
||||
self.assertIsNotNone(init)
|
||||
harness = "\n".join([
|
||||
"set -euo pipefail",
|
||||
"OMARCHY_FORM_BACK=1; OMARCHY_FORM_SIGNAL=130; defer_provisioning=false",
|
||||
"clear_logo() { :; }; say() { :; }; abort() { exit 99; }; tty() { echo /dev/pts/0; }",
|
||||
"omarchy_text() { printf '%s' \"$1\"; }",
|
||||
"answers=(" + script + "); step=0",
|
||||
"answer() { local a=${answers[step]}; step=$((step + 1)); [[ $a == ok:* ]] && eval \"${a#ok:}\" && return 0; return ${a#rc:}; }",
|
||||
"omarchy_prompt_region() { echo \"region-preselect=$region\"; answer; }",
|
||||
"omarchy_prompt_keyboard() { echo keyboard-prompt; answer; }",
|
||||
"confirm_prepare_for_another_owner() { answer; }",
|
||||
init.group(),
|
||||
language.group(),
|
||||
form.group(),
|
||||
"keyboard_form",
|
||||
'echo "region=$region defer=$defer_provisioning keyboard=${keyboard:-} lang=${OMARCHY_INSTALL_LANG:-}"',
|
||||
])
|
||||
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]
|
||||
|
||||
def test_ctrl_c_before_choosing_defers_as_global(self):
|
||||
out = self.run_keyboard_form("rc:130 ok:true")
|
||||
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=cn rc:1 ok:true ok:keyboard=us")
|
||||
self.assertEqual(self.lines(out), [
|
||||
"region-preselect=global",
|
||||
"keyboard-prompt",
|
||||
"region-preselect=cn",
|
||||
"keyboard-prompt",
|
||||
"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=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="cn")
|
||||
self.assertEqual(out.returncode, 0)
|
||||
self.assertEqual(self.lines(out), [
|
||||
"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="cn")
|
||||
self.assertEqual(out.returncode, 11)
|
||||
self.assertNotIn("keyboard-prompt", out.stdout)
|
||||
|
||||
|
||||
class RegionTargetTest(unittest.TestCase):
|
||||
class RegionSelectionTest(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)
|
||||
self.regions = write_profiles(self.root)
|
||||
patcher = mock.patch.object(region, "REGIONS_DIR", self.regions)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def ctx(self, region, deferred=False):
|
||||
return types.SimpleNamespace(target=self.target, username="alice", defer_provisioning=deferred,
|
||||
omarchy_install={"region": region})
|
||||
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_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_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_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")
|
||||
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")
|
||||
|
||||
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_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 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 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_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"):
|
||||
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[: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])
|
||||
self.assertEqual(commands[:-1], expected)
|
||||
self.assertEqual(commands[-1][0], "/usr/bin/omarchy-apply-system")
|
||||
|
||||
|
||||
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')
|
||||
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)
|
||||
|
||||
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,
|
||||
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_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_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 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 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_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_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_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_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_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())
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
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__":
|
||||
|
||||
Reference in new issue
Block a user