Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c69fa5768 | ||
|
|
4f60a18c87 |
No files matched your search
@@ -2,49 +2,89 @@
|
|||||||
# Preview the shipped Omarchy installer dashboard without building an ISO.
|
# Preview the shipped Omarchy installer dashboard without building an ISO.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# The dashboard drives the package phase from the target's pacman database, so
|
||||||
|
# fake one here; without it the preview falls back to its time curve and the bar
|
||||||
|
# barely moves, which reads as a bug.
|
||||||
|
PREVIEW_PACKAGES="${PREVIEW_PACKAGES:-925}"
|
||||||
|
|
||||||
write_simulation_state() {
|
write_simulation_state() {
|
||||||
local state_file="$1" started_at="$2" phase_started_at="$3" current_phase="$4"
|
local state_file="$1" started_at="$2" phase_started_at="$3" current_phase="$4"
|
||||||
local finished_at="${5:-null}" temporary="${state_file}.tmp"
|
local target="$5" index="$6" total="$7"
|
||||||
|
local finished_at="${8:-null}" temporary="${state_file}.tmp"
|
||||||
|
|
||||||
jq -n \
|
jq -n \
|
||||||
--argjson started_at "$started_at" \
|
--argjson started_at "$started_at" \
|
||||||
--argjson phase_started_at "$phase_started_at" \
|
--argjson phase_started_at "$phase_started_at" \
|
||||||
--arg current_phase "$current_phase" \
|
--arg current_phase "$current_phase" \
|
||||||
|
--arg target "$target" \
|
||||||
|
--argjson current_index "$index" \
|
||||||
|
--argjson total_phases "$total" \
|
||||||
--argjson finished_at "$finished_at" \
|
--argjson finished_at "$finished_at" \
|
||||||
'{
|
'{
|
||||||
started_at: $started_at,
|
started_at: $started_at,
|
||||||
phase_started_at: $phase_started_at,
|
phase_started_at: $phase_started_at,
|
||||||
current_phase: $current_phase
|
current_phase: $current_phase,
|
||||||
|
target: $target,
|
||||||
|
current_index: $current_index,
|
||||||
|
total_phases: $total_phases
|
||||||
} + if $finished_at == null then {} else {finished_at: $finished_at} end' \
|
} + if $finished_at == null then {} else {finished_at: $finished_at} end' \
|
||||||
>"$temporary"
|
>"$temporary"
|
||||||
mv -f -- "$temporary" "$state_file"
|
mv -f -- "$temporary" "$state_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Fill the fake database over the phase's duration, so the bar is driven by the
|
||||||
|
# same count it uses on a real install.
|
||||||
|
simulate_packages() {
|
||||||
|
local db="$1" duration="$2" total="$3" steps=25 i n
|
||||||
|
for ((i = 1; i <= steps; i++)); do
|
||||||
|
n=$(( total * i / steps ))
|
||||||
|
while (( $(ls -1 "$db" 2>/dev/null | wc -l) < n )); do
|
||||||
|
mkdir -p "$db/preview-$(( $(ls -1 "$db" 2>/dev/null | wc -l) ))-1.0-1"
|
||||||
|
done
|
||||||
|
sleep "$(awk -v d="$duration" -v s="$steps" 'BEGIN { printf "%.3f", d / s }')"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
simulate_install() {
|
simulate_install() {
|
||||||
local state_file="$1"
|
local state_file="$1"
|
||||||
local started_at="$EPOCHREALTIME"
|
local started_at="$EPOCHREALTIME"
|
||||||
local phase phase_started_at finished_at duration i
|
local phase phase_started_at finished_at duration i index total
|
||||||
|
local target db
|
||||||
|
target="$(dirname -- "$state_file")/target"
|
||||||
|
db="$target/var/lib/pacman/local"
|
||||||
|
mkdir -p "$db"
|
||||||
local -a phases=(
|
local -a phases=(
|
||||||
"Starting installation" 0.5
|
"Starting installation" 0.5
|
||||||
"Preparing live environment" 0.8
|
"Preparing live environment" 0.8
|
||||||
|
"Preparing install target" 0.3
|
||||||
"Installing Arch + Omarchy" 5.0
|
"Installing Arch + Omarchy" 5.0
|
||||||
|
"Configuring hibernation" 0.4
|
||||||
"Configuring system" 1.2
|
"Configuring system" 1.2
|
||||||
"Finalizing Limine boot" 1.0
|
"Finalizing Limine boot" 1.0
|
||||||
"Finalizing user" 1.0
|
"Finalizing user" 1.0
|
||||||
"Configuring login" 0.6
|
"Configuring login" 0.6
|
||||||
|
"Configuring DNS resolver" 0.3
|
||||||
"Validating boot setup" 0.8
|
"Validating boot setup" 0.8
|
||||||
)
|
)
|
||||||
|
total=$(( ${#phases[@]} / 2 ))
|
||||||
|
|
||||||
for ((i = 0; i < ${#phases[@]}; i += 2)); do
|
for ((i = 0; i < ${#phases[@]}; i += 2)); do
|
||||||
phase="${phases[i]}"
|
phase="${phases[i]}"
|
||||||
duration="${phases[i + 1]}"
|
duration="${phases[i + 1]}"
|
||||||
|
index=$(( i / 2 ))
|
||||||
phase_started_at="$EPOCHREALTIME"
|
phase_started_at="$EPOCHREALTIME"
|
||||||
write_simulation_state "$state_file" "$started_at" "$phase_started_at" "$phase"
|
write_simulation_state "$state_file" "$started_at" "$phase_started_at" \
|
||||||
sleep "$duration"
|
"$phase" "$target" "$index" "$total"
|
||||||
|
if [[ $phase == "Installing Arch + Omarchy" ]]; then
|
||||||
|
simulate_packages "$db" "$duration" "$PREVIEW_PACKAGES"
|
||||||
|
else
|
||||||
|
sleep "$duration"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
finished_at="$EPOCHREALTIME"
|
finished_at="$EPOCHREALTIME"
|
||||||
write_simulation_state "$state_file" "$started_at" "$finished_at" "Installation complete" "$finished_at"
|
write_simulation_state "$state_file" "$started_at" "$finished_at" \
|
||||||
|
"Installation complete" "$target" "$(( total - 1 ))" "$total" "$finished_at"
|
||||||
sleep 0.3
|
sleep 0.3
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +131,12 @@ log_file="$preview_dir/install.log"
|
|||||||
state_file="$preview_dir/state.json"
|
state_file="$preview_dir/state.json"
|
||||||
touch "$log_file"
|
touch "$log_file"
|
||||||
|
|
||||||
|
# Match what the simulated database will reach, so the preview shows the
|
||||||
|
# package-driven bar rather than the missing-signal fallback.
|
||||||
|
printf '%s\n' "$PREVIEW_PACKAGES" >"$preview_dir/expected-packages"
|
||||||
|
|
||||||
OMARCHY_UI_AUTO_REBOOT=no \
|
OMARCHY_UI_AUTO_REBOOT=no \
|
||||||
|
OMARCHY_EXPECTED_PACKAGES_FILE="$preview_dir/expected-packages" \
|
||||||
|
PREVIEW_PACKAGES="$PREVIEW_PACKAGES" \
|
||||||
"$dashboard" "$log_file" "$state_file" -- \
|
"$dashboard" "$log_file" "$state_file" -- \
|
||||||
"$0" --simulate-install "$state_file"
|
"$0" --simulate-install "$state_file"
|
||||||
@@ -234,6 +234,70 @@ repo-add "$offline_mirror_dir/offline.db.tar.gz" "$offline_mirror_dir/"*.pkg.tar
|
|||||||
mkdir -p /var/cache/omarchy/mirror
|
mkdir -p /var/cache/omarchy/mirror
|
||||||
ln -sf "$offline_mirror_dir" /var/cache/omarchy/mirror/offline
|
ln -sf "$offline_mirror_dir" /var/cache/omarchy/mirror/offline
|
||||||
|
|
||||||
|
# Denominator for the install dashboard's progress bar. Resolving the mirror's
|
||||||
|
# own package lists against the mirror we just indexed, with an empty local db,
|
||||||
|
# is the question pacstrap asks at install time — same resolver, same repo, same
|
||||||
|
# lists — so no hand-kept constant can drift.
|
||||||
|
#
|
||||||
|
# It over-counts by ~1 in 925: archinstall.packages lists both amd-ucode and
|
||||||
|
# intel-ucode because the mirror must contain either. phases.py records expected
|
||||||
|
# and actual in the timing JSON, so growing drift shows up in acceptance runs.
|
||||||
|
# The early-bootstrap set is already inside this closure, so restating it would
|
||||||
|
# only add a second list to drift.
|
||||||
|
resolve_expected_packages() {
|
||||||
|
local resolve_root=/tmp/omarchy-expected-packages
|
||||||
|
local resolved
|
||||||
|
local -a targets
|
||||||
|
|
||||||
|
rm -rf "$resolve_root"
|
||||||
|
mkdir -p "$resolve_root/var/lib/pacman"
|
||||||
|
|
||||||
|
mapfile -t targets < <(
|
||||||
|
{
|
||||||
|
grep -hv '^#\|^$' /builder/archinstall.packages
|
||||||
|
# Read the shipped copy, which is what _runtime_package_list reads at
|
||||||
|
# install time, not the build-time source it came from.
|
||||||
|
grep -hv '^#\|^$' \
|
||||||
|
"$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
|
||||||
|
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" \
|
||||||
|
"$OMARCHY_NVIM_PACKAGE"
|
||||||
|
} | sort -u
|
||||||
|
)
|
||||||
|
|
||||||
|
pacman --config "$build_cache_dir/pacman-offline.conf" \
|
||||||
|
--root "$resolve_root" --dbpath "$resolve_root/var/lib/pacman" \
|
||||||
|
--noconfirm -Sy >/dev/null || return 1
|
||||||
|
|
||||||
|
# Capture before counting: no pipefail here, so a pacman failure inside a
|
||||||
|
# pipeline would become a plausible partial count, which never trips the
|
||||||
|
# dashboard's fallback.
|
||||||
|
resolved="$(pacman --config "$build_cache_dir/pacman-offline.conf" \
|
||||||
|
--root "$resolve_root" --dbpath "$resolve_root/var/lib/pacman" \
|
||||||
|
--noconfirm -S --print --print-format '%n' "${targets[@]}")" || return 1
|
||||||
|
|
||||||
|
printf '%s\n' "$resolved" | sort -u | grep -c .
|
||||||
|
}
|
||||||
|
|
||||||
|
# Worth failing the build over: -S --print only aborts when a target is missing
|
||||||
|
# from the offline repo, which would fail pacstrap the same way. A count that
|
||||||
|
# merely looks wrong is not — the dashboard falls back without the file.
|
||||||
|
if ! expected_packages="$(resolve_expected_packages)"; then
|
||||||
|
echo "ERROR: could not resolve the target package count from the offline mirror." >&2
|
||||||
|
echo " pacman -S --print aborts the whole transaction if any single target" >&2
|
||||||
|
echo " is missing, so this almost certainly means pacstrap would fail the" >&2
|
||||||
|
echo " same way at install time." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if (( expected_packages < 600 || expected_packages > 2000 )); then
|
||||||
|
echo "WARNING: resolved target package count $expected_packages is outside the" >&2
|
||||||
|
echo " expected 600-2000 range; shipping no denominator so the install" >&2
|
||||||
|
echo " dashboard falls back to its time-based curve." >&2
|
||||||
|
else
|
||||||
|
printf '%s\n' "$expected_packages" \
|
||||||
|
>"$build_cache_dir/airootfs/usr/share/omarchy-iso/expected-packages"
|
||||||
|
echo "Target install resolves to $expected_packages packages."
|
||||||
|
fi
|
||||||
|
|
||||||
# Live ISO uses the same offline pacman.conf.
|
# Live ISO uses the same offline pacman.conf.
|
||||||
cp "$build_cache_dir/pacman-offline.conf" "$build_cache_dir/airootfs/etc/pacman.conf"
|
cp "$build_cache_dir/pacman-offline.conf" "$build_cache_dir/airootfs/etc/pacman.conf"
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,63 @@ LOGO_HEIGHT="$(logo_height)"
|
|||||||
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
|
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
|
||||||
CONTENT_WIDTH="$LOGO_WIDTH"
|
CONTENT_WIDTH="$LOGO_WIDTH"
|
||||||
DYNAMIC_ROW=$((LOGO_HEIGHT + 3))
|
DYNAMIC_ROW=$((LOGO_HEIGHT + 3))
|
||||||
LAST_PCT=1
|
LAST_PM=10
|
||||||
|
|
||||||
|
# ── Install progress ─────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Position = max(floor, work), both non-decreasing, in per-mille of the bar.
|
||||||
|
#
|
||||||
|
# floor lo + (span-1) * t / (t+tau), on time since this process saw the
|
||||||
|
# phase. Asymptotic, so it approaches the band end without reaching it.
|
||||||
|
# tau is a shape constant, not a duration prediction.
|
||||||
|
# work packages installed / PKG_TOTAL, for the package phase only (~85% of
|
||||||
|
# the install). libalpm makes one directory per package under
|
||||||
|
# <target>/var/lib/pacman/local; the denominator comes from
|
||||||
|
# build-iso.sh, so there is no second package list to drift.
|
||||||
|
#
|
||||||
|
# Per-mille because a whole-percent asymptote over a 2-point band has two
|
||||||
|
# reachable values and stops moving. No give-up predicate: two discontinuous
|
||||||
|
# curves behind a monotonic clamp freeze. With no signal the floor alone still
|
||||||
|
# finishes the band.
|
||||||
|
EXPECTED_PACKAGES_FILE="${OMARCHY_EXPECTED_PACKAGES_FILE:-/usr/share/omarchy-iso/expected-packages}"
|
||||||
|
PKG_TOTAL=0
|
||||||
|
if [[ -r $EXPECTED_PACKAGES_FILE ]]; then
|
||||||
|
read -r PKG_TOTAL _ <"$EXPECTED_PACKAGES_FILE" 2>/dev/null || PKG_TOTAL=0
|
||||||
|
fi
|
||||||
|
[[ $PKG_TOTAL =~ ^[0-9]+$ ]] || PKG_TOTAL=0
|
||||||
|
# A wrong denominator is worse than none: it never triggers the fallback.
|
||||||
|
(( PKG_TOTAL < 300 || PKG_TOTAL > 5000 )) && PKG_TOTAL=0
|
||||||
|
|
||||||
|
PKG_BONUS_MAX_PM=100 # cap on how far the bar may run ahead of the count
|
||||||
|
PKG_TAIL_PM=40 # band held back so a stalled bar has somewhere to go
|
||||||
|
PKG_FLOOR_TAU=900 # with a work signal, time is only a backstop until the
|
||||||
|
# first package, so slow it until it cannot outrun data
|
||||||
|
|
||||||
|
POS=10 # position, per-mille, monotonic
|
||||||
|
PROGRESS_PM=10
|
||||||
|
NOW=0 # monotonic seconds, refreshed once per frame
|
||||||
|
DASH_T0=0
|
||||||
|
PHASE_NAME="" # last phase this process observed
|
||||||
|
PHASE_T0=0 # NOW when it was first observed
|
||||||
|
PHASE_LO=10 # POS on entry, for an unknown phase with no index
|
||||||
|
PKG_DB_DIR=""
|
||||||
|
PKG_COUNT=0
|
||||||
|
PKG_BASE=-1 # package count when the phase started
|
||||||
|
PKG_SEEN=0 # running max of packages since PKG_BASE
|
||||||
|
PKG_LAST_CHANGE=0
|
||||||
|
PKG_BONUS=0
|
||||||
|
|
||||||
|
# Monotonic clock: tzupdate and systemd-timesyncd can step the wall clock
|
||||||
|
# mid-install, which would jump every time-driven band to its ceiling.
|
||||||
|
set_now() {
|
||||||
|
local up rest
|
||||||
|
if read -r up rest </proc/uptime 2>/dev/null && [[ $up == [0-9]* ]]; then
|
||||||
|
NOW=${up%%.*}
|
||||||
|
else
|
||||||
|
NOW=$EPOCHSECONDS
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
left_padding() {
|
left_padding() {
|
||||||
local width="${1:-$CONTENT_WIDTH}" cols pad
|
local width="${1:-$CONTENT_WIDTH}" cols pad
|
||||||
@@ -183,72 +239,146 @@ render_logo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress_bar() {
|
progress_bar() {
|
||||||
local pct="$1" width="${2:-34}" filled empty
|
local pm="$1" width="${2:-34}" filled empty
|
||||||
(( pct < 0 )) && pct=0
|
(( pm < 0 )) && pm=0
|
||||||
(( pct > 100 )) && pct=100
|
(( pm > 1000 )) && pm=1000
|
||||||
filled=$(( pct * width / 100 ))
|
filled=$(( pm * width / 1000 ))
|
||||||
empty=$(( width - filled ))
|
empty=$(( width - filled ))
|
||||||
printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET"
|
printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Packages libalpm has finished installing. local/ holds one directory per
|
||||||
|
# package and nothing else in it is a directory, so */ is exact. One glob, no
|
||||||
|
# forks: this runs twice a second while the disk is saturated.
|
||||||
|
count_packages() {
|
||||||
|
local -a entries
|
||||||
|
PKG_COUNT=0
|
||||||
|
[[ -n $PKG_DB_DIR ]] || return 0
|
||||||
|
entries=("$PKG_DB_DIR"/*/)
|
||||||
|
# Unmatched globs expand to the pattern; nullglob stays off script-wide.
|
||||||
|
if (( ${#entries[@]} == 1 )) && [[ ! -d ${entries[0]} ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
PKG_COUNT=${#entries[@]}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sets PROGRESS_PM. Never call in a command substitution: POS, the phase timer
|
||||||
|
# and the package counters must survive between frames. Print nothing — output
|
||||||
|
# here would land on the TTY between the cursor escape and the bar.
|
||||||
install_progress() {
|
install_progress() {
|
||||||
local phase finished phase_started elapsed span active pct
|
local phase finished index total target
|
||||||
local start=1 end=99 expected=90
|
local lo hi tau span t pos work gap n pkg_signal=0
|
||||||
local -a state
|
local -a state
|
||||||
|
|
||||||
[[ -f $STATE_FILE ]] || { printf '1'; return; }
|
PROGRESS_PM=$POS
|
||||||
|
[[ -f $STATE_FILE ]] || return 0
|
||||||
|
|
||||||
mapfile -t state < <(jq -r '
|
mapfile -t state < <(jq -r '
|
||||||
(.current_phase // "Starting installation"),
|
(.current_phase // "Starting installation"),
|
||||||
((.finished_at // 0) | tonumber? // 0 | floor),
|
((.finished_at // 0) | tonumber? // 0 | floor),
|
||||||
((.phase_started_at // .started_at // 0) | tonumber? // 0 | floor)
|
((.current_index // -1) | tonumber? // -1 | floor),
|
||||||
|
((.total_phases // 0) | tonumber? // 0 | floor),
|
||||||
|
(.target // "/mnt")
|
||||||
' "$STATE_FILE" 2>/dev/null)
|
' "$STATE_FILE" 2>/dev/null)
|
||||||
(( ${#state[@]} == 3 )) || { printf '1'; return; }
|
(( ${#state[@]} == 5 )) || return 0
|
||||||
phase="${state[0]}" finished="${state[1]}" phase_started="${state[2]}"
|
phase="${state[0]}" finished="${state[1]}"
|
||||||
|
index="${state[2]}" total="${state[3]}" target="${state[4]}"
|
||||||
|
[[ -n $target ]] || target="/mnt"
|
||||||
|
|
||||||
if (( finished > 0 )) || [[ $phase == "Installation complete" ]]; then
|
if (( finished > 0 )) || [[ $phase == "Installation complete" ]]; then
|
||||||
printf '100'
|
POS=1000
|
||||||
return
|
PROGRESS_PM=1000
|
||||||
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Phase weights: start%, end%, expected seconds in phase.
|
# Transitions are observed here rather than read from the state file, which
|
||||||
|
# keeps wall clock out of the dashboard entirely.
|
||||||
|
if [[ $phase != "$PHASE_NAME" ]]; then
|
||||||
|
PHASE_NAME="$phase"
|
||||||
|
PHASE_T0=$NOW
|
||||||
|
PHASE_LO=$POS
|
||||||
|
PKG_BASE=-1
|
||||||
|
PKG_SEEN=0
|
||||||
|
PKG_BONUS=0
|
||||||
|
PKG_LAST_CHANGE=$NOW
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bands in per-mille, plus tau. The tail bands are wide relative to what they
|
||||||
|
# cost on NVMe: run_system_finalizer can DKMS-build a driver and
|
||||||
|
# finalize_limine_boot rebuilds every UKI, so they vary most on real hardware.
|
||||||
|
# Over-allotting costs a step at a boundary; under-allotting costs a stall.
|
||||||
case "$phase" in
|
case "$phase" in
|
||||||
"Starting installation") start=1 end=2 expected=2 ;;
|
"Starting installation") lo=10 hi=25 tau=5 ;;
|
||||||
"Preparing live environment") start=2 end=6 expected=5 ;;
|
"Preparing live environment") lo=25 hi=50 tau=25 ;;
|
||||||
"Verifying protected mounts") start=6 end=8 expected=3 ;;
|
"Preparing install target") lo=50 hi=65 tau=20 ;;
|
||||||
"Installing Arch + Omarchy") start=8 end=86 expected=95 ;;
|
"Installing Arch + Omarchy") lo=65 hi=700 tau=150 pkg_signal=1 ;;
|
||||||
"Installing base system") start=8 end=84 expected=85 ;;
|
"Configuring hibernation") lo=700 hi=720 tau=10 ;;
|
||||||
"Configuring protected boot") start=84 end=86 expected=8 ;;
|
"Configuring system") lo=720 hi=850 tau=120 ;;
|
||||||
"Configuring hibernation") start=86 end=88 expected=5 ;;
|
"Finalizing Limine boot") lo=850 hi=950 tau=90 ;;
|
||||||
"Configuring system") start=88 end=93 expected=10 ;;
|
"Finalizing user") lo=950 hi=978 tau=30 ;;
|
||||||
"Finalizing Limine boot") start=93 end=96 expected=8 ;;
|
"Configuring login") lo=978 hi=988 tau=5 ;;
|
||||||
"Finalizing user") start=96 end=98 expected=10 ;;
|
"Configuring DNS resolver") lo=988 hi=994 tau=5 ;;
|
||||||
"Finalizing in chroot") start=88 end=98 expected=10 ;;
|
"Validating boot setup") lo=994 hi=1000 tau=5 ;;
|
||||||
"Configuring login") start=98 end=99 expected=3 ;;
|
*)
|
||||||
"Configuring DNS resolver") start=99 end=100 expected=2 ;;
|
# Unknown names used to inherit a 1->99 sweep. Derive an even band from
|
||||||
"Validating protected boot setup") start=99 end=100 expected=5 ;;
|
# the published index instead; with no index, a bounded slice, so a
|
||||||
"Validating boot setup") start=99 end=100 expected=5 ;;
|
# renamed phase degrades to one cell rather than a runaway.
|
||||||
|
if (( total >= 2 && index >= 0 && index < total )); then
|
||||||
|
lo=$(( 10 + 990 * index / total ))
|
||||||
|
hi=$(( 10 + 990 * (index + 1) / total ))
|
||||||
|
else
|
||||||
|
lo=$PHASE_LO
|
||||||
|
hi=$(( PHASE_LO + 30 ))
|
||||||
|
fi
|
||||||
|
tau=60
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
(( phase_started > 0 )) || phase_started=$EPOCHSECONDS
|
(( pkg_signal && PKG_TOTAL > 0 )) || pkg_signal=0
|
||||||
elapsed=$(( EPOCHSECONDS - phase_started ))
|
(( pkg_signal )) && tau=$PKG_FLOOR_TAU
|
||||||
(( elapsed < 0 )) && elapsed=0
|
|
||||||
span=$(( end - start ))
|
t=$(( NOW - PHASE_T0 ))
|
||||||
(( span < 0 )) && span=0
|
(( t < 0 )) && t=0
|
||||||
(( expected < 1 )) && expected=1
|
span=$(( hi - lo ))
|
||||||
active=$(( elapsed * span / expected ))
|
(( span < 2 )) && span=2
|
||||||
(( active > span - 1 )) && active=$(( span - 1 ))
|
(( tau < 1 )) && tau=1
|
||||||
(( active < 0 )) && active=0
|
|
||||||
pct=$(( start + active ))
|
pos=$(( lo + (span - 1) * t / (t + tau) ))
|
||||||
(( pct > 100 )) && pct=100
|
|
||||||
printf '%s' "$pct"
|
if (( pkg_signal )); then
|
||||||
|
PKG_DB_DIR="$target/var/lib/pacman/local"
|
||||||
|
count_packages
|
||||||
|
# Baseline on first sight: a target that already holds packages would
|
||||||
|
# otherwise pin the bar at the band ceiling for the whole phase.
|
||||||
|
(( PKG_BASE < 0 )) && PKG_BASE=$PKG_COUNT
|
||||||
|
n=$(( PKG_COUNT - PKG_BASE ))
|
||||||
|
(( n < 0 )) && n=0
|
||||||
|
# Running max: a reinstalled package briefly drops its version-stamped dir.
|
||||||
|
if (( n > PKG_SEEN )); then
|
||||||
|
PKG_SEEN=$n
|
||||||
|
PKG_LAST_CHANGE=$NOW
|
||||||
|
fi
|
||||||
|
gap=$(( NOW - PKG_LAST_CHANGE ))
|
||||||
|
(( gap < 0 )) && gap=0
|
||||||
|
(( gap > PKG_BONUS_MAX_PM )) && gap=$PKG_BONUS_MAX_PM
|
||||||
|
(( gap > PKG_BONUS )) && PKG_BONUS=$gap
|
||||||
|
work=$(( lo + (span - 1 - PKG_TAIL_PM) * PKG_SEEN / PKG_TOTAL + PKG_BONUS ))
|
||||||
|
(( work > pos )) && pos=$work
|
||||||
|
fi
|
||||||
|
|
||||||
|
(( pos > hi - 1 )) && pos=$(( hi - 1 ))
|
||||||
|
(( pos > POS )) && POS=$pos
|
||||||
|
(( POS > 1000 )) && POS=1000
|
||||||
|
PROGRESS_PM=$POS
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# One tip per ~8 seconds (frames tick every 0.5s), in order, so each stays
|
# One tip per 8 seconds, in order. On elapsed time, not a frame counter: a frame
|
||||||
# readable and none are skipped when progress jumps.
|
# is 0.5s of sleep plus a render, which stretches under pacstrap I/O — on the
|
||||||
|
# machine where the tips, not the bar, are carrying liveness.
|
||||||
current_tip() {
|
current_tip() {
|
||||||
local frame="$1"
|
printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}"
|
||||||
printf '%s' "${tips[$(( frame / 16 % ${#tips[@]} ))]}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_static() {
|
render_static() {
|
||||||
@@ -259,21 +389,24 @@ render_static() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_dynamic() {
|
render_dynamic() {
|
||||||
local frame="$1" pct
|
local pm
|
||||||
pct="$(install_progress)"
|
set_now
|
||||||
[[ $pct =~ ^[0-9]+$ ]] || pct=$LAST_PCT
|
install_progress
|
||||||
(( pct > 100 )) && pct=100
|
pm=$PROGRESS_PM
|
||||||
(( pct < LAST_PCT )) && pct=$LAST_PCT
|
[[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM
|
||||||
LAST_PCT=$pct
|
(( pm > 1000 )) && pm=1000
|
||||||
|
# POS is monotone by construction; this is a guard, not the mechanism.
|
||||||
|
(( pm < LAST_PM )) && pm=$LAST_PM
|
||||||
|
LAST_PM=$pm
|
||||||
|
|
||||||
printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW"
|
printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW"
|
||||||
center "Installing Omarchy" "$CONTENT_WIDTH"
|
center "Installing Omarchy" "$CONTENT_WIDTH"
|
||||||
blank_line
|
blank_line
|
||||||
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) ""
|
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) ""
|
||||||
progress_bar "$pct" 34
|
progress_bar "$pm" 34
|
||||||
printf '\n'
|
printf '\n'
|
||||||
blank_line
|
blank_line
|
||||||
center "${DIM}Tip:${RESET} ${GREEN}$(current_tip "$frame")${RESET}" "$CONTENT_WIDTH"
|
center "${DIM}Tip:${RESET} ${GREEN}$(current_tip)${RESET}" "$CONTENT_WIDTH"
|
||||||
printf '%s' "$CLEAR_TO_END"
|
printf '%s' "$CLEAR_TO_END"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,12 +674,14 @@ launch_child() {
|
|||||||
[[ -e $TTY_PATH ]] || exit 2
|
[[ -e $TTY_PATH ]] || exit 2
|
||||||
printf '%s' "$HIDE_CURSOR" >"$TTY_PATH"
|
printf '%s' "$HIDE_CURSOR" >"$TTY_PATH"
|
||||||
render_static >"$TTY_PATH"
|
render_static >"$TTY_PATH"
|
||||||
|
set_now
|
||||||
|
DASH_T0=$NOW
|
||||||
|
PHASE_T0=$NOW
|
||||||
|
PKG_LAST_CHANGE=$NOW
|
||||||
launch_child
|
launch_child
|
||||||
|
|
||||||
frame=0
|
|
||||||
while jobs -pr | grep -qx "$child_pid"; do
|
while jobs -pr | grep -qx "$child_pid"; do
|
||||||
render_dynamic "$frame" >"$TTY_PATH" 2>/dev/null || true
|
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
||||||
frame=$((frame + 1))
|
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -559,6 +694,13 @@ child_pgid=""
|
|||||||
|
|
||||||
dashboard_log "installer child exited with status $status"
|
dashboard_log "installer child exited with status $status"
|
||||||
|
|
||||||
|
if (( status == 0 )); then
|
||||||
|
# finished_at lands microseconds before the child exits, so the 0.5s poll
|
||||||
|
# rarely renders a full bar before the finish screen replaces it.
|
||||||
|
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
||||||
|
sleep 0.4
|
||||||
|
fi
|
||||||
|
|
||||||
if (( status != 0 )); then
|
if (( status != 0 )); then
|
||||||
failure_status="$status"
|
failure_status="$status"
|
||||||
failure_summary=""
|
failure_summary=""
|
||||||
|
|||||||
@@ -65,11 +65,15 @@ def main() -> int:
|
|||||||
info(f"Installing Omarchy for {ctx.username} → {ctx.target}")
|
info(f"Installing Omarchy for {ctx.username} → {ctx.target}")
|
||||||
|
|
||||||
from .phases_impl import (
|
from .phases_impl import (
|
||||||
|
boost_cpu_governor,
|
||||||
cleanup_bind_mounts,
|
cleanup_bind_mounts,
|
||||||
cleanup_protected_state,
|
cleanup_protected_state,
|
||||||
cleanup_target_hook_masks,
|
cleanup_target_hook_masks,
|
||||||
|
restore_cpu_governors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
governors = boost_cpu_governor()
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
@@ -85,6 +89,7 @@ def main() -> int:
|
|||||||
info("Installation complete.")
|
info("Installation complete.")
|
||||||
return 0
|
return 0
|
||||||
finally:
|
finally:
|
||||||
|
restore_cpu_governors(governors)
|
||||||
cleanup_bind_mounts(ctx)
|
cleanup_bind_mounts(ctx)
|
||||||
cleanup_target_hook_masks(ctx)
|
cleanup_target_hook_masks(ctx)
|
||||||
if not success:
|
if not success:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ the InstallContext and either return cleanly or raise to abort the install."""
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -25,6 +26,9 @@ def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None:
|
|||||||
state_path = ctx.state_dir / "state.json"
|
state_path = ctx.state_dir / "state.json"
|
||||||
state = {
|
state = {
|
||||||
"started_at": time.time(),
|
"started_at": time.time(),
|
||||||
|
# The dashboard counts packages under <target>/var/lib/pacman/local;
|
||||||
|
# publish the path rather than have the UI assume /mnt.
|
||||||
|
"target": str(ctx.target),
|
||||||
"total_phases": len(phases),
|
"total_phases": len(phases),
|
||||||
"current_index": 0,
|
"current_index": 0,
|
||||||
"current_phase": "Starting installation",
|
"current_phase": "Starting installation",
|
||||||
@@ -63,6 +67,10 @@ def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None:
|
|||||||
state["current_index"] = max(len(phases) - 1, 0)
|
state["current_index"] = max(len(phases) - 1, 0)
|
||||||
state["current_phase"] = "Installation complete"
|
state["current_phase"] = "Installation complete"
|
||||||
state["finished_at"] = time.time()
|
state["finished_at"] = time.time()
|
||||||
|
# Expected vs actual for the bar's denominator, so drift is visible in
|
||||||
|
# acceptance runs rather than only by watching a bar creep.
|
||||||
|
state["installed_packages"] = _installed_package_count(ctx.target)
|
||||||
|
state["expected_packages"] = _expected_package_count()
|
||||||
_write_state(state_path, state)
|
_write_state(state_path, state)
|
||||||
|
|
||||||
timing_path = ctx.target / "var" / "log" / "omarchy-install-timing.json"
|
timing_path = ctx.target / "var" / "log" / "omarchy-install-timing.json"
|
||||||
@@ -70,6 +78,25 @@ def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None:
|
|||||||
_write_state(timing_path, state)
|
_write_state(timing_path, state)
|
||||||
|
|
||||||
|
|
||||||
|
def _installed_package_count(target: Path) -> int:
|
||||||
|
"""Packages libalpm installed into the target — one directory each under
|
||||||
|
local/, which is what the dashboard counts live."""
|
||||||
|
local_db = target / "var" / "lib" / "pacman" / "local"
|
||||||
|
try:
|
||||||
|
with os.scandir(local_db) as entries:
|
||||||
|
return sum(1 for entry in entries if entry.is_dir())
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _expected_package_count() -> int:
|
||||||
|
path = Path("/usr/share/omarchy-iso/expected-packages")
|
||||||
|
try:
|
||||||
|
return int(path.read_text().split()[0])
|
||||||
|
except (OSError, ValueError, IndexError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _write_state(path: Path, state: dict) -> None:
|
def _write_state(path: Path, state: dict) -> None:
|
||||||
# Dashboard polls this file while phases update it. Write atomically so the
|
# Dashboard polls this file while phases update it. Write atomically so the
|
||||||
# reader never observes a truncated/partial JSON document and resets UI.
|
# reader never observes a truncated/partial JSON document and resets UI.
|
||||||
|
|||||||
@@ -1396,6 +1396,40 @@ def _validate_pre_mounted_filesystems(ctx: InstallContext) -> None:
|
|||||||
raise RuntimeError(f"{crypttab} missing LUKS UUID {storage['luks_uuid']}")
|
raise RuntimeError(f"{crypttab} missing LUKS UUID {storage['luks_uuid']}")
|
||||||
|
|
||||||
|
|
||||||
|
CPU_SYSFS = Path("/sys/devices/system/cpu")
|
||||||
|
|
||||||
|
|
||||||
|
def boost_cpu_governor() -> dict[Path, str]:
|
||||||
|
"""Run the live CPUs flat out for the install.
|
||||||
|
|
||||||
|
Package extraction and the UKI build are both CPU-bound, and archiso boots
|
||||||
|
on whatever governor the kernel defaults to. Writing an unsupported
|
||||||
|
governor just fails, so nothing needs probing first, and hosts without
|
||||||
|
cpufreq (most VMs) have no paths at all. Returns the prior governors.
|
||||||
|
"""
|
||||||
|
saved: dict[Path, str] = {}
|
||||||
|
for path in sorted(CPU_SYSFS.glob("cpu*/cpufreq/scaling_governor")):
|
||||||
|
try:
|
||||||
|
saved[path] = path.read_text().strip()
|
||||||
|
path.write_text("performance\n")
|
||||||
|
except OSError:
|
||||||
|
saved.pop(path, None)
|
||||||
|
|
||||||
|
if saved:
|
||||||
|
info(f"› CPU governor set to performance ({len(saved)} CPUs)")
|
||||||
|
return saved
|
||||||
|
|
||||||
|
|
||||||
|
def restore_cpu_governors(saved: dict[Path, str]) -> None:
|
||||||
|
"""Only matters when an install fails and the user keeps using the live
|
||||||
|
environment — a successful one reboots out of it."""
|
||||||
|
for path, governor in saved.items():
|
||||||
|
try:
|
||||||
|
path.write_text(f"{governor}\n")
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# cleanup_bind_mounts: invoked from main()'s finally so bind mounts get
|
# cleanup_bind_mounts: invoked from main()'s finally so bind mounts get
|
||||||
# unwound on success, failure, or interrupt. Idempotent.
|
# unwound on success, failure, or interrupt. Idempotent.
|
||||||
|
|||||||
Reference in new issue
Block a user