Merge branch 'pr-14399' into omarchycn
# Conflicts: # bin/omarchy-refresh-pacman # install/post-install/pacman.sh
This commit is contained in:
commit
d2e4a95aed
19 files changed
+469
-17
No files matched your search
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Restore channel and regional pacman defaults in an installed target
|
||||
# omarchy:args=<stable|rc|edge> [target-root] | --render <stable|rc|edge> <output-dir> [target-root]
|
||||
# omarchy:requires-sudo=true
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-apply-pacman <stable|rc|edge> [target-root]" >&2
|
||||
echo " omarchy-apply-pacman --render <stable|rc|edge> <output-dir> [target-root]" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --render only builds the two files, so an unprivileged caller can hand root
|
||||
# nothing but plain copies of the finished result.
|
||||
render_dir=""
|
||||
if [[ ${1:-} == "--render" ]]; then
|
||||
(( $# == 3 || $# == 4 )) || usage
|
||||
channel="$2"
|
||||
render_dir="$3"
|
||||
[[ -d $render_dir ]] || { echo "Render directory not found: $render_dir" >&2; exit 1; }
|
||||
etc="${4:-/}/etc"
|
||||
else
|
||||
(( $# >= 1 && $# <= 2 )) || usage
|
||||
channel="$1"
|
||||
etc="${2:-/}/etc"
|
||||
fi
|
||||
|
||||
case "$channel" in
|
||||
stable|rc|edge) ;;
|
||||
*) echo "Invalid pacman channel: $channel" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# The ISO writes this plain-text marker before target finalization. Missing
|
||||
# markers keep existing installations global; never source it as shell code.
|
||||
region=global
|
||||
if [[ -e $etc/omarchy/region ]]; then
|
||||
region=$(<"$etc/omarchy/region")
|
||||
fi
|
||||
case "$region" in
|
||||
global|cn) ;;
|
||||
*) echo "Unsupported region: $region (expected global or cn)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Build both files before replacing either. Explicit refreshes retain backups;
|
||||
# ordinary updates do not call this command or enforce regional defaults.
|
||||
if [[ -n $render_dir ]]; then
|
||||
staging="$render_dir"
|
||||
else
|
||||
staging=$(mktemp -d)
|
||||
trap 'rm -rf "$staging"' EXIT
|
||||
fi
|
||||
cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf"
|
||||
cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist"
|
||||
if [[ $region != "global" ]]; then
|
||||
profile="$OMARCHY_PATH/default/regions/$region/pacman"
|
||||
printf '\n' >> "$staging/pacman.conf"
|
||||
cat "$profile/pacman.conf.append" >> "$staging/pacman.conf"
|
||||
cat "$profile/mirrorlist.append" >> "$staging/mirrorlist"
|
||||
fi
|
||||
[[ -z $render_dir ]] || exit 0
|
||||
|
||||
mkdir -p "$etc/pacman.d"
|
||||
for file in pacman.conf pacman.d/mirrorlist; do
|
||||
if [[ -e $etc/$file ]]; then
|
||||
cp -f "$etc/$file" "$etc/$file.bak"
|
||||
fi
|
||||
done
|
||||
# Copying over an existing file keeps its owner and mode, as refresh always has.
|
||||
cp -f "$staging/pacman.conf" "$etc/pacman.conf"
|
||||
cp -f "$staging/mirrorlist" "$etc/pacman.d/mirrorlist"
|
||||
@@ -47,9 +47,26 @@ validate_dev_checkout() {
|
||||
fi
|
||||
}
|
||||
|
||||
# The dev channel refreshes pacman with the checkout's own tools. A checkout
|
||||
# without this machine's region profile would silently drop its repositories.
|
||||
validate_dev_region() {
|
||||
local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global
|
||||
|
||||
[[ -f $region_file ]] && region=$(<"$region_file")
|
||||
[[ $region == "global" ]] && return 0
|
||||
|
||||
# The refresh after linking needs a runnable helper and both fragments, not
|
||||
# just a directory with the region's name.
|
||||
local profile="$checkout/default/regions/$region/pacman"
|
||||
if [[ ! -x $checkout/bin/omarchy-apply-pacman || ! -r $profile/pacman.conf.append || ! -r $profile/mirrorlist.append ]]; then
|
||||
fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults."
|
||||
fi
|
||||
}
|
||||
|
||||
link_dev_checkout() {
|
||||
local checkout="$1" required
|
||||
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
|
||||
validate_dev_region "$checkout"
|
||||
|
||||
# Check the destination before changing /etc/omarchy.conf or sudo's path.
|
||||
# An existing checkout is not pulled automatically and may predate this policy.
|
||||
|
||||
@@ -16,6 +16,7 @@ set -euo pipefail
|
||||
PROVISIONING_DIR=/var/lib/omarchy/provisioning
|
||||
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||
LOG_FILE=/var/log/omarchy-provision-owner.log
|
||||
REGION_MARKER=/etc/omarchy/region
|
||||
|
||||
if (( EUID != 0 )); then
|
||||
echo "Error: omarchy-provision-owner must run as root" >&2
|
||||
@@ -650,7 +651,21 @@ user_form() {
|
||||
omarchy_prompt_hostname || return $?
|
||||
|
||||
step "Let's set your timezone..."
|
||||
omarchy_prompt_timezone || return $?
|
||||
omarchy_prompt_timezone "$(region_timezone)" || return $?
|
||||
}
|
||||
|
||||
# The zone a region profile preselects when there is no geo guess, which is
|
||||
# every offline first boot. Reads the marker omarchy-apply-pacman reads; global,
|
||||
# missing, or malformed regions offer nothing.
|
||||
region_timezone() {
|
||||
local region=global profile
|
||||
if [[ -r $REGION_MARKER ]]; then
|
||||
region=$(<"$REGION_MARKER")
|
||||
fi
|
||||
profile="$OMARCHY_PATH/default/regions/$region/timezone"
|
||||
if [[ $region =~ ^[a-z]{2}$ && -r $profile ]]; then
|
||||
head -n 1 "$profile"
|
||||
fi
|
||||
}
|
||||
|
||||
confirm_form() {
|
||||
|
||||
@@ -30,16 +30,25 @@ if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Render the channel templates plus this machine's regional additions as the
|
||||
# caller, so root only copies finished files. The scratch directory goes on
|
||||
# every exit, including a failed render, a refused sudo prompt, or a signal;
|
||||
# revoking the sudo timestamp stays the last thing the exit path does.
|
||||
rendered=$(mktemp -d)
|
||||
cleanup_refresh() {
|
||||
local status=$?
|
||||
rm -rf "$rendered"
|
||||
omarchy_security_exit_with_revoked_sudo "$status"
|
||||
}
|
||||
trap cleanup_refresh EXIT
|
||||
"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered"
|
||||
|
||||
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
|
||||
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
|
||||
echo "Setting channel to $channel"
|
||||
sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf
|
||||
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist
|
||||
region=$(omarchy-region)
|
||||
if [[ $region == "China" ]]; then
|
||||
echo "Adding China mirrors"
|
||||
sudo sed -i -f "$OMARCHY_PATH/default/region/china/pacman.sed" /etc/pacman.conf /etc/pacman.d/mirrorlist
|
||||
fi
|
||||
sudo cp -f "$rendered/pacman.conf" /etc/pacman.conf
|
||||
sudo cp -f "$rendered/mirrorlist" /etc/pacman.d/mirrorlist
|
||||
rm -rf "$rendered"
|
||||
|
||||
# Allow user customization of /etc/pacman.conf before the upgrade runs, so
|
||||
# custom repositories and IgnorePkg entries shape the downgrade-capable
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Installed from the ISO's offline repository before enabling ArchLinuxCN.
|
||||
archlinuxcn-keyring
|
||||
@@ -0,0 +1 @@
|
||||
Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
|
||||
@@ -0,0 +1,2 @@
|
||||
[archlinuxcn]
|
||||
Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch
|
||||
@@ -0,0 +1 @@
|
||||
Asia/Shanghai
|
||||
@@ -0,0 +1,36 @@
|
||||
# Regional ISO defaults
|
||||
|
||||
Regions customize bootstrap defaults independently of update channels. Country identifiers are lowercase, officially assigned ISO 3166-1 alpha-2 codes. `global` is a reserved value for no country-specific additions. Only implemented profiles are accepted; currently `global` and `cn`.
|
||||
|
||||
## Runtime configuration
|
||||
|
||||
The ISO installer writes the selected region to `/etc/omarchy/region` before target finalization. It is plain text, not executable configuration. A missing file means `global`; an unsupported value is an error.
|
||||
|
||||
`omarchy-apply-pacman <stable|rc|edge> [target-root]` restores the selected channel's two pacman templates and appends `default/regions/<region>/pacman/{pacman.conf.append,mirrorlist.append}`. Both files are generated before either live file is replaced. Existing files are backed up with `.bak`. Dev uses the edge pacman channel, as before.
|
||||
|
||||
Both installation finalization and `omarchy-refresh-pacman` use this command. Explicit refreshes and channel changes restore regional defaults; ordinary updates do not enforce them. The existing `pre-refresh-pacman` user hook runs after regeneration and before the update, so users can maintain overrides there. Switching to the dev channel refuses a checkout that lacks this machine's region profile, because that checkout's own refresh would otherwise drop the regional repositories. Users can edit the files directly or set the region to `global` before a refresh to stop applying regional repository defaults. This does not uninstall packages or rewrite anything in existing home directories.
|
||||
|
||||
The China profile keeps the channel's Omarchy Arch mirror first, appends USTC's regular Arch mirror, and adds the USTC ArchLinuxCN community repository. It does not change the `[omarchy]` package repository. USTC's mirror, run by the University of Science and Technology of China, and ArchLinuxCN, a long-standing community repository, are a practical bridge until Omarchy runs its own mirror in China. USTC is not a stable/RC snapshot replica: missing old packages and newer fallback databases remain compatibility risks.
|
||||
|
||||
## ISO builder contract
|
||||
|
||||
The companion `omarchy-iso` repository accepts `--region global|cn`, passes `OMARCHY_REGION` into the build container, and records it in `/root/omarchy_region` on the ISO. Non-global artifact and offline-cache names gain a region suffix. Existing global names remain unchanged.
|
||||
|
||||
Regional content comes from the same runtime used by the ISO: either the `--local-source` checkout or the downloaded runtime package. A China build against a runtime without regional support fails rather than silently producing a global installation.
|
||||
|
||||
- `packages`: additional target packages, one name per line, with blank lines and `#` comments allowed. The builder merges these into the ISO's copy of `omarchy-base.packages`, so they are both included in the offline repository and installed on the target. The source manifest is not modified.
|
||||
- `pacman/`: repository fragments used by the runtime finalizer. The builder also adds the community repository to its writable online config to obtain regional packages; it does not change its Arch channel mirrors.
|
||||
- `timezone` (optional): one zone name, such as `Asia/Shanghai`. The setup form preselects it in the timezone list when the network geo guess is unavailable, which is always the case offline. Both callers pass it: the ISO configurator from its copy of the profile, and first-boot setup from `default/regions/<region>/timezone` for the region in `/etc/omarchy/region`. A successful geo guess still wins, and the person can choose any other zone.
|
||||
- `skel/` (optional): new-user defaults. The ISO installer overlays these onto target `/etc/skel` after the base settings packages and before user creation, including installations that defer user creation until first boot. It never overlays existing home directories.
|
||||
|
||||
For China, the builder installs `archlinuxcn-keyring` using the existing Arch trust chain before downloading other regional packages. Signature or keyring failures abort the build; no unverified keyserver trust or online signature bypass is added. The keyring is also installed on the target from the offline package set, then initialized and populated before online repositories are enabled. The ISO's existing offline repository verification policy is unchanged.
|
||||
|
||||
## Scope of the China profile
|
||||
|
||||
The China profile provides repository defaults and preselects `Asia/Shanghai` as the timezone. System language (Simplified Chinese) and input-method defaults are planned as separate changes on top of this mechanism: `packages` and `skel/` carry them into the ISO, and the installer stages `skel/` before any user exists.
|
||||
|
||||
## Remaining release validation
|
||||
|
||||
Unit tests cover region/channel independence, package-list augmentation, keyring failure handling, pre-user defaults, generated pacman contents, repeat application, and refresh ordering; refresh itself is exercised only inside the sudo-boundary sandbox. They do not establish live mirror availability, real package trust, or successful offline installation. Before distribution, build from matching local checkouts on an x86_64 builder and run the disposable-VM acceptance workflow in `agents/skills/acceptance-tests.md`.
|
||||
|
||||
The China acceptance run must install without network access, boot to a new user, including deferred first-boot user creation, and keep the regional mirrors, repository, marker, and keyring trust through `stable`, `rc`, and `edge` refreshes.
|
||||
@@ -1,11 +1,6 @@
|
||||
# Configure pacman after package installation completes. Offline target package
|
||||
# installs use the live ISO's offline pacman.conf until this final restore.
|
||||
cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf
|
||||
cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist
|
||||
region=$(omarchy-region)
|
||||
if [[ $region == "China" ]]; then
|
||||
sed -i -f "$OMARCHY_PATH/default/region/china/pacman.sed" /etc/pacman.conf /etc/pacman.d/mirrorlist
|
||||
fi
|
||||
"$OMARCHY_PATH/bin/omarchy-apply-pacman" "${OMARCHY_MIRROR:-stable}"
|
||||
|
||||
# Wait for CUPS to own the file, the way omarchy-settings does, so pacman does
|
||||
# not turn the override into a .pacnew during ISO package installation.
|
||||
|
||||
@@ -213,10 +213,13 @@ omarchy_prompt_hostname() {
|
||||
}
|
||||
|
||||
# A fresh machine often hasn't joined a network yet, so the geo guess fails
|
||||
# often; guard it or a `set -e` caller dies before the filter fallback.
|
||||
# often; guard it or a `set -e` caller dies before the filter fallback. Callers
|
||||
# may pass the zone to preselect when there is no guess (a region's default),
|
||||
# so an offline install does not open on the top of an alphabetical list.
|
||||
omarchy_prompt_timezone() {
|
||||
local guess status
|
||||
guess=$(tzupdate -p 2>/dev/null) || guess=""
|
||||
[[ -n $guess ]] || guess="${1:-}"
|
||||
|
||||
if [[ -n $guess ]]; then
|
||||
timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$guess" --header "$(omarchy_text "Timezone")") && status=0 || status=$?
|
||||
|
||||
@@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||
copy_boundary_file bin/omarchy-channel-set
|
||||
copy_boundary_file bin/omarchy-refresh-pacman
|
||||
copy_boundary_file bin/omarchy-apply-pacman
|
||||
copy_boundary_file bin/omarchy-update
|
||||
export OMARCHY_UPDATE_LOGGED=1
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ run_channel() {
|
||||
: >"$log_file"
|
||||
OMARCHY_CHANNEL_TEST_LOG="$log_file" \
|
||||
OMARCHY_PATH="${OMARCHY_TEST_PATH:-$package_root}" \
|
||||
OMARCHY_REGION_FILE="${OMARCHY_TEST_REGION_FILE:-$test_tmp/no-region}" \
|
||||
HOME="$test_tmp/home" \
|
||||
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
||||
"${OMARCHY_TEST_PATH:-$package_root}/bin/omarchy-channel-set" "$@"
|
||||
@@ -190,6 +191,42 @@ fi
|
||||
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "switching back to dev links ~/omarchy"
|
||||
pass "switching back to dev reuses the existing ~/omarchy checkout"
|
||||
|
||||
printf 'cn\n' >"$test_tmp/region"
|
||||
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
|
||||
fail "dev refuses a checkout without this machine's region"
|
||||
fi
|
||||
grep -q "does not support the 'cn' region" "$test_tmp/region.err" || fail "dev explains the missing region" "$(cat "$test_tmp/region.err")"
|
||||
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
|
||||
fail "dev refuses before linking or refreshing pacman" "$(cat "$log_file")"
|
||||
fi
|
||||
pass "dev refuses a checkout without the region before touching pacman"
|
||||
|
||||
# A directory with the region's name, or a helper that cannot run, is not
|
||||
# support: the refresh after linking would fail with dev already linked.
|
||||
mkdir -p "$checkout/default/regions/cn"
|
||||
touch "$checkout/bin/omarchy-apply-pacman"
|
||||
chmod +x "$checkout/bin/omarchy-apply-pacman"
|
||||
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
|
||||
fail "dev refuses an empty region profile"
|
||||
fi
|
||||
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
|
||||
fail "dev refuses an empty region profile before linking or refreshing pacman" "$(cat "$log_file")"
|
||||
fi
|
||||
mkdir -p "$checkout/default/regions/cn/pacman"
|
||||
touch "$checkout/default/regions/cn/pacman/pacman.conf.append" "$checkout/default/regions/cn/pacman/mirrorlist.append"
|
||||
chmod -x "$checkout/bin/omarchy-apply-pacman"
|
||||
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
|
||||
fail "dev refuses a checkout whose pacman helper cannot run"
|
||||
fi
|
||||
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
|
||||
fail "dev refuses incomplete region support before linking or refreshing pacman" "$(cat "$log_file")"
|
||||
fi
|
||||
pass "dev refuses an empty region profile or a helper that cannot run"
|
||||
|
||||
chmod +x "$checkout/bin/omarchy-apply-pacman"
|
||||
OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev
|
||||
assert_log_line $'refresh\tedge' "dev proceeds with a checkout that supports the region"
|
||||
|
||||
current_channel() {
|
||||
OMARCHY_TEST_VERSION_CHANNEL="$1" \
|
||||
OMARCHY_TEST_PACKAGES="$2" \
|
||||
|
||||
@@ -9,7 +9,11 @@ export SUDO_TEST_LOG="$boundary_tmp/events"
|
||||
export SUDO_TEST_CACHE="$boundary_tmp/cache"
|
||||
export OMARCHY_PATH="$SUDO_TEST_ROOT"
|
||||
export SUDO_TEST_HOME="$boundary_tmp/home"
|
||||
mkdir -p "$SUDO_TEST_HOME"
|
||||
# The machine the render reads its region marker from. It starts global; a test
|
||||
# writes etc/omarchy/region here to model a regional install, never the host's.
|
||||
export SUDO_TEST_TARGET="$boundary_tmp/target"
|
||||
export OMARCHY_REGION_FILE="$SUDO_TEST_TARGET/etc/omarchy/region"
|
||||
mkdir -p "$SUDO_TEST_HOME" "$SUDO_TEST_TARGET/etc"
|
||||
mkdir -p "$SUDO_TEST_ROOT/bin" "$SUDO_TEST_ROOT/mock" "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update"
|
||||
: >"$SUDO_TEST_LOG"
|
||||
|
||||
@@ -24,6 +28,10 @@ s=(source/name).read_text().replace('$HOME', '$SUDO_TEST_HOME')
|
||||
for command in ['sudo','pkexec','pacman','omarchy-pkg-missing','systemd-inhibit','setpriv','snapper']:
|
||||
s=s.replace('/usr/bin/'+command, str(target/'mock'/command))
|
||||
s=s.replace('PATH=/usr/bin:/usr/sbin:/bin:/sbin', 'PATH="'+str(target/'bin')+':/usr/bin:/usr/sbin:/bin:/sbin"')
|
||||
if name.name == 'omarchy-apply-pacman':
|
||||
render_etc='etc="${4:-/}/etc"'
|
||||
assert s.count(render_etc)==1, 'render target lookup moved'
|
||||
s=s.replace(render_etc, 'etc="${4:-$SUDO_TEST_TARGET}/etc"')
|
||||
p.write_text(s)
|
||||
p.chmod((source/name).stat().st_mode & 0o777)
|
||||
PY
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
scratch=$(mktemp -d)
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
export OMARCHY_PATH="$ROOT"
|
||||
export TARGET_ROOT="$scratch/target root"
|
||||
mkdir -p "$TARGET_ROOT/etc/omarchy"
|
||||
|
||||
apply() { "$ROOT/bin/omarchy-apply-pacman" "$1" "$TARGET_ROOT"; }
|
||||
|
||||
for channel in stable rc edge; do
|
||||
apply "$channel"
|
||||
cmp "$ROOT/default/pacman/pacman-$channel.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
cmp "$ROOT/default/pacman/mirrorlist-$channel" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
|
||||
done
|
||||
pass "missing region preserves all global channel templates byte for byte"
|
||||
|
||||
chmod 600 "$TARGET_ROOT/etc/pacman.conf"
|
||||
apply stable
|
||||
[[ $(stat -c %a "$TARGET_ROOT/etc/pacman.conf") == 600 ]] || fail "refresh keeps a customized pacman.conf mode"
|
||||
chmod 644 "$TARGET_ROOT/etc/pacman.conf"
|
||||
pass "refresh keeps existing config file modes"
|
||||
|
||||
printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region"
|
||||
for channel in stable rc edge; do
|
||||
case "$channel" in
|
||||
stable) host=stable-mirror.omarchy.org ;;
|
||||
rc) host=rc-mirror.omarchy.org ;;
|
||||
edge) host=mirror.omarchy.org ;;
|
||||
esac
|
||||
apply "$channel"
|
||||
expected=$(printf 'Server = https://%s/$repo/os/$arch\nServer = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch' "$host")
|
||||
[[ $(<"$TARGET_ROOT/etc/pacman.d/mirrorlist") == "$expected" ]] || fail "$channel mirror order and variables"
|
||||
{
|
||||
cat "$ROOT/default/pacman/pacman-$channel.conf"
|
||||
printf '\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n'
|
||||
} > "$scratch/expected.conf"
|
||||
cmp "$scratch/expected.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
apply "$channel"
|
||||
cmp "$TARGET_ROOT/etc/pacman.conf.bak" "$TARGET_ROOT/etc/pacman.conf"
|
||||
cmp "$TARGET_ROOT/etc/pacman.d/mirrorlist.bak" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
|
||||
done
|
||||
pass "China keeps each channel first, appends only CN defaults, and is idempotent"
|
||||
|
||||
cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf"
|
||||
cp "$TARGET_ROOT/etc/pacman.d/mirrorlist" "$scratch/before.mirrors"
|
||||
for region in '' CN chn zz '../cn' '$(touch NEVER_EXECUTE)'; do
|
||||
printf '%s\n' "$region" > "$TARGET_ROOT/etc/omarchy/region"
|
||||
if apply stable > "$scratch/error" 2>&1; then
|
||||
fail "invalid region accepted: $region"
|
||||
fi
|
||||
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
|
||||
done
|
||||
pass "unsupported and malformed regions fail before replacing either config"
|
||||
|
||||
printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region"
|
||||
mkdir -p "$scratch/incomplete/default/regions/cn/pacman"
|
||||
cp -a "$ROOT/default/pacman" "$scratch/incomplete/default/"
|
||||
cp "$ROOT/default/regions/cn/pacman/pacman.conf.append" "$scratch/incomplete/default/regions/cn/pacman/"
|
||||
if OMARCHY_PATH="$scratch/incomplete" apply stable > "$scratch/error" 2>&1; then
|
||||
fail "incomplete region profile accepted"
|
||||
fi
|
||||
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
|
||||
pass "missing fragment cannot leave a half-generated config"
|
||||
|
||||
# Refresh renders these files unprivileged and hands root plain copies; its
|
||||
# privileged path is covered in the sudo-boundary sandbox, never on the host.
|
||||
cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf"
|
||||
mkdir -p "$scratch/rendered"
|
||||
"$ROOT/bin/omarchy-apply-pacman" --render rc "$scratch/rendered" "$TARGET_ROOT"
|
||||
rg -qxF 'Server = https://pkgs.omarchy.org/rc/$arch' "$scratch/rendered/pacman.conf"
|
||||
rg -qxF '[archlinuxcn]' "$scratch/rendered/pacman.conf"
|
||||
[[ $(<"$scratch/rendered/mirrorlist") == $'Server = https://rc-mirror.omarchy.org/$repo/os/$arch\nServer = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch' ]] ||
|
||||
fail "render keeps the channel mirror first"
|
||||
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
pass "render builds the regional channel files without touching the target"
|
||||
|
||||
if "$ROOT/bin/omarchy-apply-pacman" --render dev "$scratch/rendered" "$TARGET_ROOT" > "$scratch/error" 2>&1; then
|
||||
fail "render accepts a nonexistent dev pacman template"
|
||||
fi
|
||||
pass "render rejects channels without pacman templates"
|
||||
|
||||
printf 'global\n' > "$TARGET_ROOT/etc/omarchy/region"
|
||||
apply stable
|
||||
cmp "$ROOT/default/pacman/pacman-stable.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
cmp "$ROOT/default/pacman/mirrorlist-stable" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
|
||||
pass "users can opt out of regional repository defaults"
|
||||
|
||||
# The shipped China profile is repository scaffolding only. Locale, timezone,
|
||||
# and input defaults are separate changes built on the same mechanism.
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
region = Path(os.environ["ROOT"]) / "default/regions/cn"
|
||||
packages = {line.strip() for line in (region / "packages").read_text().splitlines() if line.strip() and not line.startswith("#")}
|
||||
assert packages == {"archlinuxcn-keyring"}, packages
|
||||
assert not (region / "skel").exists()
|
||||
PY
|
||||
pass "China profile adds only its repository keyring"
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||
copy_boundary_file bin/omarchy-refresh-pacman
|
||||
copy_boundary_file bin/omarchy-apply-pacman
|
||||
for file in default/pacman/pacman-rc.conf default/pacman/mirrorlist-rc \
|
||||
default/regions/cn/pacman/pacman.conf.append default/regions/cn/pacman/mirrorlist.append; do
|
||||
copy_boundary_file "$file"
|
||||
done
|
||||
|
||||
# A sandboxed China machine: the render reads this region marker, never the
|
||||
# host's /etc/omarchy/region.
|
||||
mkdir -p "$SUDO_TEST_TARGET/etc/omarchy"
|
||||
printf 'cn\n' >"$SUDO_TEST_TARGET/etc/omarchy/region"
|
||||
|
||||
# cp still logs its step, but now really copies, so the rendered files exist.
|
||||
# Root's copies into /etc land in a capture directory instead of the host.
|
||||
export SUDO_TEST_CAPTURE="$boundary_tmp/capture"
|
||||
rm "$SUDO_TEST_ROOT/bin/cp"
|
||||
cat >"$SUDO_TEST_ROOT/bin/cp" <<'STUB'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
printf 'step:cp %s\n' "$*" >>"$SUDO_TEST_LOG"
|
||||
destination=${!#}
|
||||
if [[ $destination == /etc/* ]]; then
|
||||
source=${*: -2:1}
|
||||
[[ $source == /etc/* ]] && exit 0
|
||||
/usr/bin/mkdir -p "$SUDO_TEST_CAPTURE${destination%/*}"
|
||||
exec /usr/bin/cp "$source" "$SUDO_TEST_CAPTURE$destination"
|
||||
fi
|
||||
exec /usr/bin/cp "$@"
|
||||
STUB
|
||||
chmod +x "$SUDO_TEST_ROOT/bin/cp"
|
||||
|
||||
# Regional rendering must stay unprivileged: root only copies finished files,
|
||||
# exactly as the channel refresh did before regions existed.
|
||||
reset_boundary
|
||||
"$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" rc >"$boundary_tmp/output" 2>&1 || fail "refresh failed" "$(<"$boundary_tmp/output")"
|
||||
assert_boundary_cold "regional refresh"
|
||||
python3 - "$SUDO_TEST_LOG" "$SUDO_TEST_ROOT" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
events = open(sys.argv[1]).read().splitlines()
|
||||
root = sys.argv[2]
|
||||
render = [i for i, e in enumerate(events) if e.startswith('step:cp ' + root + '/default/pacman/pacman-rc.conf ')]
|
||||
assert len(render) == 1, events
|
||||
rendered = events[render[0]].split()[-1].rsplit('/', 1)[0]
|
||||
copies = [i for i, e in enumerate(events) if e.startswith('sudo -N cp ')]
|
||||
assert [events[i] for i in copies[2:]] == [
|
||||
f'sudo -N cp -f {rendered}/pacman.conf /etc/pacman.conf',
|
||||
f'sudo -N cp -f {rendered}/mirrorlist /etc/pacman.d/mirrorlist',
|
||||
], events
|
||||
assert not any(e.startswith('sudo ') and 'omarchy-apply-pacman' in e for e in events), events
|
||||
assert render[0] < copies[0], events
|
||||
transaction = next(i for i, e in enumerate(events) if e.startswith('step:pacman '))
|
||||
assert max(copies) < transaction, events
|
||||
PY
|
||||
{
|
||||
cat "$ROOT/default/pacman/pacman-rc.conf"
|
||||
printf '\n'
|
||||
cat "$ROOT/default/regions/cn/pacman/pacman.conf.append"
|
||||
} >"$boundary_tmp/expected-pacman.conf"
|
||||
cat "$ROOT/default/pacman/mirrorlist-rc" "$ROOT/default/regions/cn/pacman/mirrorlist.append" >"$boundary_tmp/expected-mirrorlist"
|
||||
cmp -s "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" ||
|
||||
fail "root installs the rendered China pacman.conf" "$(diff "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" 2>&1)"
|
||||
cmp -s "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" ||
|
||||
fail "root installs the rendered China mirrorlist" "$(diff "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" 2>&1)"
|
||||
pass "refresh renders regional config unprivileged and gives root only plain copies"
|
||||
|
||||
reset_boundary
|
||||
if "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" dev >"$boundary_tmp/output" 2>&1; then
|
||||
fail "refresh accepts an invalid channel"
|
||||
fi
|
||||
if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then
|
||||
fail "invalid channel reached privileged work" "$(<"$SUDO_TEST_LOG")"
|
||||
fi
|
||||
pass "failed refresh does not proceed to privileged copies or a package update"
|
||||
|
||||
# A failed render still removes its scratch directory and exits cold.
|
||||
reset_boundary
|
||||
mv "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" "$boundary_tmp/omarchy-apply-pacman"
|
||||
cat >"$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" <<'STUB'
|
||||
#!/bin/bash
|
||||
: >"$3/pacman.conf"
|
||||
exit 1
|
||||
STUB
|
||||
chmod +x "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman"
|
||||
mkdir -p "$boundary_tmp/scratch"
|
||||
if TMPDIR="$boundary_tmp/scratch" "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" rc >"$boundary_tmp/output" 2>&1; then
|
||||
fail "refresh succeeds after a failed render"
|
||||
fi
|
||||
mv "$boundary_tmp/omarchy-apply-pacman" "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman"
|
||||
[[ -z $(ls -A "$boundary_tmp/scratch") ]] || fail "failed render leaves its scratch directory" "$(ls -AR "$boundary_tmp/scratch")"
|
||||
if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then
|
||||
fail "failed render reached privileged work" "$(<"$SUDO_TEST_LOG")"
|
||||
fi
|
||||
assert_boundary_cold "failed render"
|
||||
pass "failed render removes its scratch directory and exits cold"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
scratch=$(mktemp -d)
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
|
||||
zone=$(<"$ROOT/default/regions/cn/timezone")
|
||||
[[ $zone == "Asia/Shanghai" ]] || fail "China profile names Asia/Shanghai"
|
||||
[[ -f /usr/share/zoneinfo/$zone ]] || fail "China profile timezone exists in tzdata"
|
||||
pass "China profile defaults to an existing Asia/Shanghai zone"
|
||||
|
||||
# Load the real region_timezone() from the first-boot command.
|
||||
OMARCHY_PATH="$ROOT"
|
||||
REGION_MARKER="$scratch/region"
|
||||
eval "$(sed -n '/^region_timezone() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner")"
|
||||
|
||||
[[ -z $(region_timezone) ]] || fail "missing region marker offers no timezone"
|
||||
for region in global CN ../cn zz ""; do
|
||||
printf '%s\n' "$region" >"$REGION_MARKER"
|
||||
[[ -z $(region_timezone) ]] || fail "region '$region' offers no timezone"
|
||||
done
|
||||
pass "global, unknown, and malformed regions offer no timezone"
|
||||
|
||||
printf 'cn\n' >"$REGION_MARKER"
|
||||
[[ $(region_timezone) == "Asia/Shanghai" ]] || fail "cn region offers Asia/Shanghai"
|
||||
pass "cn region offers its profile timezone"
|
||||
|
||||
grep -qF 'omarchy_prompt_timezone "$(region_timezone)"' "$ROOT/bin/omarchy-provision-owner" ||
|
||||
fail "first-boot setup passes the region timezone to the prompt"
|
||||
pass "first-boot setup preselects the region timezone"
|
||||
@@ -61,7 +61,7 @@ fi
|
||||
|
||||
trap 'if [[ ${FUNCNAME[0]:-} == "$PROMPT_FN" ]]; then printf "returned\n" >>"$MARKER"; fi' RETURN
|
||||
|
||||
"$PROMPT_FN"
|
||||
"$PROMPT_FN" ${PROMPT_ARG:+"$PROMPT_ARG"}
|
||||
|
||||
printf 'region=%s\n' "${region:-}"
|
||||
printf 'keyboard=%s\n' "${keyboard:-}"
|
||||
@@ -274,6 +274,19 @@ assert_status 0 "timezone prompt survives a failed geo guess"
|
||||
[[ $(head -n 1 "$GUM_ARGS") == filter* ]] || fail "timezone prompt filters when there is no geo guess"
|
||||
pass "timezone prompt falls back to filtering when the geo guess fails"
|
||||
|
||||
# A region's default stands in for the missing guess, so an offline China
|
||||
# install opens on Asia/Shanghai instead of the top of the list
|
||||
PROMPT_ARG=Asia/Shanghai run_prompt omarchy_prompt_timezone "0:Asia/Shanghai"
|
||||
assert_status 0 "timezone prompt accepts the caller's fallback"
|
||||
[[ $(field timezone) == "Asia/Shanghai" ]] || fail "timezone prompt keeps the preselected fallback"
|
||||
grep -qF -- '--selected Asia/Shanghai' "$GUM_ARGS" || fail "timezone prompt preselects the caller's fallback"
|
||||
[[ $(head -n 1 "$GUM_ARGS") == choose* ]] || fail "timezone prompt chooses from the list when it has a fallback"
|
||||
pass "timezone prompt preselects the caller's fallback when the geo guess fails"
|
||||
|
||||
TZ_GUESS=Europe/Copenhagen PROMPT_ARG=Asia/Shanghai run_prompt omarchy_prompt_timezone "0:Europe/Copenhagen"
|
||||
grep -qF -- '--selected Europe/Copenhagen' "$GUM_ARGS" || fail "a geo guess outranks the caller's fallback"
|
||||
pass "timezone prompt prefers the geo guess over the caller's fallback"
|
||||
|
||||
run_prompt omarchy_prompt_timezone "0:"
|
||||
assert_status 0 "timezone prompt accepts an empty selection"
|
||||
[[ $(field timezone) == "UTC" ]] || fail "timezone prompt falls back to UTC"
|
||||
|
||||
@@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||
copy_boundary_file bin/omarchy-update
|
||||
copy_boundary_file bin/omarchy-refresh-pacman
|
||||
copy_boundary_file bin/omarchy-apply-pacman
|
||||
# Replace the step symlink, preserving the real fixture dispatcher.
|
||||
rm "$SUDO_TEST_ROOT/bin/omarchy-update-aur-pkgs"
|
||||
copy_boundary_file bin/omarchy-update-aur-pkgs
|
||||
|
||||
Reference in new issue
Block a user