Source the setup form from the Omarchy runtime (#104)
The keyboard layout list, the account and hostname validation rules, and the gum prompts that ask for them existed here and again in the runtime's first-boot owner setup, with nothing keeping the copies honest. They had already drifted: dropping two layouts from this list moved English (US) from the top of one gum choose page to the bottom of the previous one, leaving the preselected default pinned under a screenful of layouts. The form now lives in the runtime at install/provisioning/setup-form.sh, and build-iso.sh vendors it out of the very runtime package this ISO bundles — the same two-branch machinery already used for omarchy-base.packages, so a --local-source build takes it from the mounted checkout and everything else extracts it from the downloaded package. An ISO therefore ships the form belonging to the runtime it installs. That file is extracted on its own rather than alongside the package lists: bsdtar exits non-zero for a member it cannot find, so bundling the request would abort the build under set -e with a bare "Not found in archive" instead of the actionable error about publishing a runtime that carries the form. Cancel handling is unified with first-boot setup, which is what made the prompts shareable. Every prompt reports 0 (answered), 1 (Esc), or 130 (Ctrl+C). Esc now unwinds to the start of the form instead of ending the install — it previously hit the same `|| abort` as Ctrl+C. Ctrl+C keeps both of its hidden affordances: arming deferred provisioning on the first keyboard screen, and toggling unencrypted on the disk screens. test_keyboard.py reads the layout list from a runtime checkout now, and skips its localectl coverage rather than failing when there is none to read. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b2f8a6ac7
commit
8858ac2544
5 files changed
+116
-140
No files matched your search
@@ -711,17 +711,17 @@ drive_provision_owner() {
|
||||
log "Driving the deferred-provisioning first-boot setup"
|
||||
|
||||
# Deferred provisioning defers the keyboard step to first boot; it comes before the user form.
|
||||
# Pick English (UK) (one row above the preselected English (US)) so the
|
||||
# verifier can prove the deferred step applied a non-default layout — UK
|
||||
# leaves every letter identical to US, so the typed password (sent as raw
|
||||
# keycodes) still matches the literal one the harness pipes to sudo.
|
||||
# Pick English (UK) (one row below the preselected English (US), which leads
|
||||
# the list) so the verifier can prove the deferred step applied a non-default
|
||||
# layout — UK leaves every letter identical to US, so the typed password (sent
|
||||
# as raw keycodes) still matches the literal one the harness pipes to sudo.
|
||||
# A greeter (animated logo + tagline) opens first boot; Return begins.
|
||||
wait_for_screen "Opinionated" 600
|
||||
capture_console "success-provision-00-greeter"
|
||||
press ret
|
||||
|
||||
wait_for_screen "keyboard layout" 120
|
||||
press up
|
||||
press down
|
||||
capture_console "success-provision-01-keyboard-layout-uk"
|
||||
press ret
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ sed -i -E '/^(linux|broadcom-wl)$/d' "$build_cache_dir/packages.x86_64"
|
||||
# pulls the published omarchy* from the network mirror like any other package.
|
||||
if [[ -d /omarchy-source ]]; then
|
||||
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
|
||||
# Pull the same package lists out of the freshly-downloaded Omarchy runtime
|
||||
# package so we don't need a local checkout in the non-local-source path.
|
||||
@@ -154,12 +155,32 @@ else
|
||||
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
|
||||
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
|
||||
setup_form=/tmp/omarchy-pkglists/usr/share/omarchy/install/provisioning/setup-form.sh
|
||||
fi
|
||||
|
||||
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"
|
||||
|
||||
# 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
|
||||
# the configurator with no prompts at all.
|
||||
if [[ ! -f $setup_form ]]; then
|
||||
echo "ERROR: $OMARCHY_RUNTIME_PACKAGE does not ship install/provisioning/setup-form.sh" >&2
|
||||
echo " The configurator sources its prompts from that file, so this ISO" >&2
|
||||
echo " would boot into an installer with no questions to ask." >&2
|
||||
echo " Publish a runtime carrying the shared setup form, or build with" >&2
|
||||
echo " --local-source against a checkout that has it." >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$setup_form" "$build_cache_dir/airootfs/usr/share/omarchy-iso/setup-form.sh"
|
||||
|
||||
# Collect every package we want available in the offline mirror.
|
||||
declare -a all_packages
|
||||
mapfile -t all_packages < <(
|
||||
|
||||
@@ -9,6 +9,21 @@ OMARCHY_SETTINGS_PACKAGE="${OMARCHY_SETTINGS_PACKAGE:-omarchy-settings}"
|
||||
|
||||
LOGO_PATH="$OMARCHY_PATH/logo.txt"
|
||||
|
||||
# The setup form — keyboard, account, hostname, and timezone questions, plus the
|
||||
# rules their answers are checked against — shared verbatim with the first-boot
|
||||
# owner setup that finishes a deferred install. build-iso.sh vendors it out of
|
||||
# the runtime package this ISO bundles, so the two can never offer different
|
||||
# layouts or accept different usernames. On the ISO that vendored copy is what
|
||||
# loads; a dry run outside the ISO falls back to the omarchy checkout
|
||||
# bin/omarchy-iso-configurator already points OMARCHY_PATH at.
|
||||
SETUP_FORM="${SETUP_FORM:-/usr/share/omarchy-iso/setup-form.sh}"
|
||||
[[ -r $SETUP_FORM ]] || SETUP_FORM="$OMARCHY_PATH/install/provisioning/setup-form.sh"
|
||||
if [[ ! -r $SETUP_FORM ]]; then
|
||||
echo "ERROR: shared setup form not found at $SETUP_FORM" >&2
|
||||
exit 1
|
||||
fi
|
||||
source "$SETUP_FORM"
|
||||
|
||||
LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH")
|
||||
|
||||
export GUM_CONFIRM_PROMPT_FOREGROUND="6"
|
||||
@@ -177,76 +192,32 @@ keyboard_form() {
|
||||
# screen is the hidden entry into deferred provisioning; $1 is "true" only for that first
|
||||
# invocation, so the review-loop re-edit can't flip a user install into it.
|
||||
local allow_defer_provisioning="${1:-false}"
|
||||
clear_logo
|
||||
echo
|
||||
say "Let's setup your machine..."
|
||||
if $allow_defer_provisioning; then
|
||||
say --foreground 8 "Press Ctrl+C to prepare this machine for another owner."
|
||||
fi
|
||||
echo
|
||||
local status
|
||||
|
||||
keyboards=$'Azerbaijani|azerty
|
||||
Belarusian|by
|
||||
Belgian|be-latin1
|
||||
Bulgarian|bg-cp1251
|
||||
Croatian|croat
|
||||
Czech|cz
|
||||
Danish|dk-latin1
|
||||
Dutch|nl
|
||||
English (UK)|uk
|
||||
English (US)|us
|
||||
English (US, Dvorak)|dvorak
|
||||
English (US, Colemak)|colemak
|
||||
Estonian|et
|
||||
Finnish|fi
|
||||
French|fr
|
||||
French (Canada)|cf
|
||||
French (Switzerland)|fr_CH
|
||||
Georgian|ge
|
||||
German|de
|
||||
German (Switzerland)|de_CH-latin1
|
||||
Greek|gr
|
||||
Hebrew|il
|
||||
Hungarian|hu
|
||||
Icelandic|is-latin1
|
||||
Irish|ie
|
||||
Italian|it
|
||||
Japanese|jp106
|
||||
Kazakh|kazakh
|
||||
Kyrgyz|kyrgyz
|
||||
Lao|la-latin1
|
||||
Latvian|lv
|
||||
Lithuanian|lt
|
||||
Macedonian|mk-utf
|
||||
Norwegian|no-latin1
|
||||
Polish|pl
|
||||
Portuguese|pt-latin1
|
||||
Portuguese (Brazil)|br-abnt2
|
||||
Romanian|ro
|
||||
Russian|ru
|
||||
Serbian|sr-latin
|
||||
Slovak|sk-qwertz
|
||||
Slovenian|slovene
|
||||
Spanish|es
|
||||
Spanish (Latin American)|la-latin1
|
||||
Swedish|sv-latin1
|
||||
Tajik|tj_alt-UTF8
|
||||
Turkish|trq
|
||||
Ukrainian|ua'
|
||||
choice=$(printf '%s\n' "$keyboards" | cut -d'|' -f1 | gum choose --height 10 --selected "English (US)" --header "Select keyboard layout")
|
||||
local status=$?
|
||||
|
||||
if $allow_defer_provisioning && (( status == 130 )); then
|
||||
if confirm_prepare_for_another_owner; then
|
||||
defer_provisioning=true
|
||||
return 0
|
||||
while true; do
|
||||
clear_logo
|
||||
echo
|
||||
say "Let's setup your machine..."
|
||||
if $allow_defer_provisioning; then
|
||||
say --foreground 8 "Press Ctrl+C to prepare this machine for another owner."
|
||||
fi
|
||||
keyboard_form true # declined: back to the keyboard picker
|
||||
return
|
||||
fi
|
||||
(( status == 0 )) || abort
|
||||
echo
|
||||
|
||||
keyboard=$(printf '%s\n' "$keyboards" | awk -F'|' -v c="$choice" '$1==c{print $2; exit}')
|
||||
omarchy_prompt_keyboard && status=0 || status=$?
|
||||
((status == 0)) && break
|
||||
|
||||
# Esc means "back", and nothing precedes the first screen, so re-ask.
|
||||
((status == OMARCHY_FORM_BACK)) && continue
|
||||
|
||||
if $allow_defer_provisioning && ((status == OMARCHY_FORM_SIGNAL)); then
|
||||
if confirm_prepare_for_another_owner; then
|
||||
defer_provisioning=true
|
||||
return 0
|
||||
fi
|
||||
continue # declined: back to the keyboard picker
|
||||
fi
|
||||
abort
|
||||
done
|
||||
|
||||
# Only attempt to load keyboard layout if we're on a real console
|
||||
# loadkeys only works on Linux virtual consoles (tty), not in terminal emulators
|
||||
@@ -275,66 +246,34 @@ confirm_prepare_for_another_owner() {
|
||||
user_form() {
|
||||
step "Let's setup your user account..."
|
||||
|
||||
while true; do
|
||||
username=$(gum input --placeholder "Alphanumeric without spaces (like dhh)" --prompt.foreground="#845DF9" --prompt "Username> ") || abort
|
||||
|
||||
if [[ "$username" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]]; then
|
||||
if [[ "$username" =~ ^(root|bin|daemon|mail|ftp|http|nobody|dbus|systemd-coredump|systemd-network|systemd-oom|systemd-journal-remote|systemd-resolve|systemd-timesync|tss|uuidd|alpm|git|avahi|cups|lp|_talkd|polkitd|rtkit|qemu|brltty|gluster|rpc|libvirt-qemu|pcscd|nvidia-persistenced|sddm)$ ]] ; then
|
||||
notice "Username is reserved for system" 1
|
||||
else
|
||||
break
|
||||
fi
|
||||
else
|
||||
notice "Username must be alphanumeric with no spaces" 1
|
||||
fi
|
||||
done
|
||||
|
||||
while true; do
|
||||
password=$(gum input --placeholder "Used for user + root, and disk encryption when enabled" --prompt.foreground="#845DF9" --password --prompt "Password> ") || abort
|
||||
password_confirmation=$(gum input --placeholder "Must match the password you just typed" --prompt.foreground="#845DF9" --password --prompt "Confirm> ") || abort
|
||||
|
||||
if [[ -n "$password" && "$password" == "$password_confirmation" ]]; then
|
||||
break
|
||||
elif [[ -z "$password" ]]; then
|
||||
notice "Your password can't be blank!" 1
|
||||
else
|
||||
notice "Passwords didn't match!" 1
|
||||
fi
|
||||
done
|
||||
# Each prompt reports 0 (set), OMARCHY_FORM_BACK (Esc), or OMARCHY_FORM_SIGNAL
|
||||
# (Ctrl+C). Both non-zero cases unwind to user_step, which decides what they
|
||||
# mean here; the form itself never aborts.
|
||||
omarchy_prompt_username || return $?
|
||||
omarchy_prompt_password || return $?
|
||||
|
||||
# Hash the password using yescrypt
|
||||
password_hash=$(printf '%s' "$password" | openssl passwd -6 -stdin)
|
||||
|
||||
full_name=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Full name> ")
|
||||
email_address=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Email address> ")
|
||||
|
||||
while true; do
|
||||
hostname=$(gum input --placeholder "Letters, digits, and dashes (or return for 'omarchy')" --prompt.foreground="#845DF9" --prompt "Hostname> ")
|
||||
|
||||
if [[ "$hostname" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$ ]]; then
|
||||
break
|
||||
elif [[ -z $hostname ]]; then
|
||||
hostname="omarchy"
|
||||
break
|
||||
else
|
||||
notice "Hostname must be 1-63 letters, digits, or dashes, and cannot start or end with a dash" 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Pick timezone
|
||||
geo_guessed_timezone=$(tzupdate -p 2>/dev/null)
|
||||
|
||||
if [[ -n $geo_guessed_timezone ]]; then
|
||||
timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$geo_guessed_timezone" --header "Timezone") || abort
|
||||
else
|
||||
timezone=$(timedatectl list-timezones | gum filter --height 10 --header "Timezone") || abort
|
||||
fi
|
||||
omarchy_prompt_identity || return $?
|
||||
omarchy_prompt_hostname || return $?
|
||||
omarchy_prompt_timezone || return $?
|
||||
}
|
||||
|
||||
user_step() {
|
||||
user_form
|
||||
local status
|
||||
|
||||
while true; do
|
||||
user_form && status=0 || status=$?
|
||||
if ((status != 0)); then
|
||||
# Esc unwinds to the start of the form. Ctrl+C has no side channel in the
|
||||
# user step (only the keyboard and disk screens give it a meaning), so it
|
||||
# keeps its long-standing behaviour of ending the install.
|
||||
((status == OMARCHY_FORM_BACK)) || abort
|
||||
keyboard_form
|
||||
continue
|
||||
fi
|
||||
|
||||
# Add manual padding since gum table -p doesn't respect padding
|
||||
echo -e "Field,Value
|
||||
Username,$username
|
||||
@@ -351,7 +290,6 @@ Keyboard,$keyboard" |
|
||||
break
|
||||
else
|
||||
keyboard_form
|
||||
user_form
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ from pathlib import Path
|
||||
def configure_keyboard(target: Path, language: str) -> bool:
|
||||
"""Write the console keymap into a mounted target without booting it.
|
||||
|
||||
Returns False for layouts localectl doesn't know (the configurator offers
|
||||
two, ba and khmer), matching archinstall: warn and keep the default.
|
||||
Returns False for layouts localectl doesn't know, matching archinstall:
|
||||
warn and keep the default. Every layout the configurator offers is known;
|
||||
the guard is for the kb_layout an autoinstall drive can name freely.
|
||||
"""
|
||||
if not language.strip():
|
||||
return True
|
||||
|
||||
+30
-14
@@ -14,14 +14,30 @@ KEYBOARD = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader
|
||||
SPEC.loader.exec_module(KEYBOARD)
|
||||
|
||||
CONFIGURATOR = (ROOT / "configs/airootfs/root/configurator").read_text()
|
||||
KEYBOARD_BLOCK = re.search(r"keyboards=\$'(.*?)'\n choice=", CONFIGURATOR, re.DOTALL)
|
||||
assert KEYBOARD_BLOCK
|
||||
SUPPORTED_KEYMAPS = [
|
||||
line.split("|", 1)[1]
|
||||
for line in KEYBOARD_BLOCK.group(1).splitlines()
|
||||
]
|
||||
UNSUPPORTED_KEYMAPS = {"ba", "khmer"}
|
||||
# The layout list lives in the Omarchy runtime now, shared verbatim with the
|
||||
# first-boot owner setup; build-iso.sh vendors it onto the ISO. Read it from
|
||||
# wherever this checkout can see a runtime, and skip the coverage test rather
|
||||
# than fail when none is around (a bare CI checkout of just this repo).
|
||||
SETUP_FORM_CANDIDATES = (
|
||||
Path("/omarchy-source/install/provisioning/setup-form.sh"),
|
||||
ROOT.parent / "omarchy/install/provisioning/setup-form.sh",
|
||||
Path("/usr/share/omarchy/install/provisioning/setup-form.sh"),
|
||||
)
|
||||
|
||||
|
||||
def supported_keymaps():
|
||||
for candidate in SETUP_FORM_CANDIDATES:
|
||||
if not candidate.is_file():
|
||||
continue
|
||||
block = re.search(
|
||||
r"OMARCHY_KEYBOARD_LAYOUTS=\$'(.*?)'\n", candidate.read_text(), re.DOTALL
|
||||
)
|
||||
assert block, f"no layout list in {candidate}"
|
||||
return [line.split("|", 1)[1] for line in block.group(1).splitlines()]
|
||||
return None
|
||||
|
||||
|
||||
SUPPORTED_KEYMAPS = supported_keymaps()
|
||||
|
||||
|
||||
class KeyboardConfigurationTest(unittest.TestCase):
|
||||
@@ -45,15 +61,15 @@ class KeyboardConfigurationTest(unittest.TestCase):
|
||||
self.assertEqual(lines.count("FONT=default8x16"), 1)
|
||||
|
||||
def test_all_configurator_keymaps_are_known_to_localectl(self):
|
||||
if SUPPORTED_KEYMAPS is None:
|
||||
self.skipTest("no Omarchy runtime checkout to read the layout list from")
|
||||
for keymap in SUPPORTED_KEYMAPS:
|
||||
with self.subTest(keymap=keymap), tempfile.TemporaryDirectory() as directory:
|
||||
target = self.target(directory)
|
||||
configured = KEYBOARD.configure_keyboard(target, keymap)
|
||||
self.assertEqual(configured, keymap not in UNSUPPORTED_KEYMAPS)
|
||||
if configured:
|
||||
lines = (target / "etc/vconsole.conf").read_text().splitlines()
|
||||
self.assertIn(f"KEYMAP={keymap}", lines)
|
||||
self.assertEqual(lines.count("FONT=default8x16"), 1)
|
||||
self.assertTrue(KEYBOARD.configure_keyboard(target, keymap))
|
||||
lines = (target / "etc/vconsole.conf").read_text().splitlines()
|
||||
self.assertIn(f"KEYMAP={keymap}", lines)
|
||||
self.assertEqual(lines.count("FONT=default8x16"), 1)
|
||||
|
||||
def test_unknown_and_empty_keymaps_match_archinstall_behavior(self):
|
||||
for keymap, expected in (("definitely-not-a-keymap", False), ("", True)):
|
||||
|
||||
Reference in new issue
Block a user