Every ISO bundles the cn profile; the first-screen pick decides. --region cn only preselects China and names the artifact. The region value is cn, the cn timezone is preselected when China is chosen, and the profile packages and ArchLinuxCN keyring reach the target only for China.
1370 lines
49 KiB
Bash
1370 lines
49 KiB
Bash
#!/bin/bash
|
|
|
|
# On the live ISO, .automated_script.sh exports OMARCHY_PATH and the package
|
|
# targets before running us. The defaults keep bin/omarchy-iso-configurator
|
|
# dry runs working outside the ISO.
|
|
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
|
OMARCHY_RUNTIME_PACKAGE="${OMARCHY_RUNTIME_PACKAGE:-omarchy}"
|
|
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"
|
|
|
|
# Chinese text for OMARCHY_INSTALL_LANG=zh; disk-step failures stay English for support logs.
|
|
OMARCHY_ZH+=(
|
|
["Aborted installation"]="安装已中止"
|
|
["You can retry later by running: ./.automated_script.sh"]="稍后可运行 ./.automated_script.sh 重试"
|
|
["Let's setup your machine..."]="开始设置这台机器……"
|
|
["Press Ctrl+C to prepare this machine for another owner."]="按 Ctrl+C 为另一位使用者准备这台机器。"
|
|
["This prepares the machine for another owner."]="这会为另一位使用者准备这台机器。"
|
|
["The system installs now, but setup is delayed until first boot."]="系统现在安装,设置留到首次开机。"
|
|
["Yes, prepare for another owner"]="是,为他人准备"
|
|
["No, keep setting up"]="否,继续设置"
|
|
["Prepare this machine for another owner?"]="为另一位使用者准备这台机器?"
|
|
["Let's setup your user account..."]="开始设置用户账户……"
|
|
["Field"]="项目"
|
|
["Value"]="值"
|
|
["Username"]="用户名"
|
|
["Password"]="密码"
|
|
["Full name"]="姓名"
|
|
["Email address"]="邮箱"
|
|
["Hostname"]="主机名"
|
|
["Keyboard"]="键盘"
|
|
["Region"]="区域"
|
|
["Global"]="全球"
|
|
["China"]="中国"
|
|
["[Skipped]"]="[已跳过]"
|
|
["Yes"]="是"
|
|
["No, change it"]="否,去修改"
|
|
["Does this look right?"]="以上信息正确吗?"
|
|
["Not enough free space on %s"]="%s 的空闲空间不足"
|
|
["%s has %s of usable free space; Omarchy needs at least 32GB."]="%s 有 %s 可用空闲空间;Omarchy 至少需要 32GB。"
|
|
["Open the partition tool to free at least 32GB on %s, then try again."]="打开分区工具,在 %s 上腾出至少 32GB,然后重试。"
|
|
["Back"]="返回"
|
|
["Open partition tool"]="打开分区工具"
|
|
["Return to installation mode?"]="返回安装方式选择?"
|
|
["Checking for BitLocker on %s"]="检查 %s 上的 BitLocker"
|
|
["BitLocker signature detected on %s."]="在 %s 上发现 BitLocker 签名。"
|
|
["Turn BitLocker off in Windows and wait for the drive to finish decrypting, then try again."]="在 Windows 中关闭 BitLocker,等磁盘解密完成后重试。"
|
|
["Suspending BitLocker is not enough — the drive stays encrypted."]="只暂停 BitLocker 不够,磁盘仍是加密的。"
|
|
["Aborted: BitLocker is enabled on this disk."]="已中止:这块磁盘开启了 BitLocker。"
|
|
["Checking existing EFI partitions on %s"]="检查 %s 上已有的 EFI 分区"
|
|
["Found a Windows ESP at %s — leaving it untouched."]="在 %s 发现 Windows ESP,保持不动。"
|
|
["Omarchy will create its own dedicated EFI partition in free space."]="Omarchy 会在空闲空间中创建自己的 EFI 分区。"
|
|
["A new EFI partition will be created in free space."]="会在空闲空间中新建 EFI 分区。"
|
|
["Analyzing free space on %s"]="分析 %s 的空闲空间"
|
|
["Install Omarchy in the %s of free space."]="在 %s 空闲空间中安装 Omarchy。"
|
|
["Press Ctrl+C for unencrypted install."]="按 Ctrl+C 改为不加密安装。"
|
|
["Yes, install"]="是,开始安装"
|
|
["Yes, install without encryption"]="是,不加密安装"
|
|
["Confirm installing on %s"]="确认安装到 %s"
|
|
["Creating partitions on %s"]="在 %s 上创建分区"
|
|
["Setting up LUKS2 on %s"]="在 %s 上设置 LUKS2 加密"
|
|
["Creating Btrfs filesystem and subvolumes"]="创建 Btrfs 文件系统和子卷"
|
|
["Let's select where to install Omarchy..."]="选择 Omarchy 的安装位置……"
|
|
["Select install disk"]="选择安装磁盘"
|
|
["Partition tool for %s"]="%s 的分区工具"
|
|
["Create unallocated free space for Omarchy, then write changes and quit."]="为 Omarchy 腾出未分配空间,然后写入更改并退出。"
|
|
["Do not create an Omarchy partition here — leave the target area as Free space."]="不要在这里创建 Omarchy 分区,目标区域保持为 Free space。"
|
|
["Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists."]="提示:空闲空间安装至少需要 32GB 未分配空间;没有 ESP 时再加 2GB。"
|
|
["Open cfdisk"]="打开 cfdisk"
|
|
["Open partition tool for %s?"]="打开 %s 的分区工具?"
|
|
["Full disk install"]="整盘安装"
|
|
["Free space install (alongside existing data)"]="空闲空间安装(保留现有数据)"
|
|
["Choose a different disk"]="换一块磁盘"
|
|
["Let's select how to install Omarchy..."]="选择 Omarchy 的安装方式……"
|
|
["Select installation mode on %s"]="选择 %s 上的安装方式"
|
|
["Everything will be overwritten. There is no recovery possible."]="磁盘上的一切都会被覆盖,无法恢复。"
|
|
["Confirm overwriting %s"]="确认覆盖 %s"
|
|
)
|
|
# Every ISO carries the cn profile here (builder/prepare-region.sh copies it);
|
|
# China preselects its timezone when there is no geo guess, which is every
|
|
# offline install.
|
|
REGION_TIMEZONE="${REGION_TIMEZONE:-/usr/share/omarchy-iso/region/timezone}"
|
|
region_timezone() {
|
|
if [[ $region == "cn" ]]; then
|
|
head -n 1 "$REGION_TIMEZONE"
|
|
fi
|
|
}
|
|
|
|
# Partition creation helpers, shared with test/partition-numbering-test.sh so
|
|
# the numbering logic can be exercised without a real disk.
|
|
DISK_PARTITIONING="${DISK_PARTITIONING:-/usr/share/omarchy-iso/disk-partitioning.sh}"
|
|
[[ -r $DISK_PARTITIONING ]] ||
|
|
DISK_PARTITIONING="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../usr/share/omarchy-iso/disk-partitioning.sh"
|
|
if [[ ! -r $DISK_PARTITIONING ]]; then
|
|
echo "ERROR: disk partitioning helpers not found at $DISK_PARTITIONING" >&2
|
|
exit 1
|
|
fi
|
|
source "$DISK_PARTITIONING"
|
|
|
|
LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH")
|
|
|
|
export GUM_CONFIRM_PROMPT_FOREGROUND="6"
|
|
export GUM_CONFIRM_SELECTED_FOREGROUND="0"
|
|
export GUM_CONFIRM_SELECTED_BACKGROUND="2"
|
|
export GUM_CONFIRM_UNSELECTED_FOREGROUND="7"
|
|
export GUM_CONFIRM_UNSELECTED_BACKGROUND="0"
|
|
|
|
# The console can grow after boot (the framebuffer settles late, especially in
|
|
# VMs), so remeasure at every redraw rather than trusting startup geometry.
|
|
# gum draws on /dev/tty, so measure that rather than inherited COLUMNS.
|
|
measure_terminal() {
|
|
TERM_WIDTH=$(stty size 2>/dev/null </dev/tty | awk '{print $2}')
|
|
(( TERM_WIDTH > 0 )) || TERM_WIDTH=${COLUMNS:-80}
|
|
|
|
PADDING_LEFT=$(((TERM_WIDTH - LOGO_WIDTH) / 2))
|
|
(( PADDING_LEFT < 0 )) && PADDING_LEFT=0
|
|
PADDING_LEFT_SPACES=$(printf "%*s" "$PADDING_LEFT" "")
|
|
|
|
PADDING="0 0 0 $PADDING_LEFT"
|
|
export GUM_CHOOSE_PADDING="$PADDING"
|
|
export GUM_FILTER_PADDING="$PADDING"
|
|
export GUM_INPUT_PADDING="$PADDING"
|
|
export GUM_SPIN_PADDING="$PADDING"
|
|
export GUM_TABLE_PADDING="$PADDING"
|
|
export GUM_CONFIRM_PADDING="$PADDING"
|
|
}
|
|
measure_terminal
|
|
|
|
# The live VT resizes late during boot: tty1 starts as the 80-column boot
|
|
# console and only grows once the kernel framebuffer/KMS takes over (noticeably
|
|
# late in VMs). The logo is wider than 80 columns, so a first draw against that
|
|
# transient narrow console wraps the logo ("smushed") and can't center it
|
|
# (padding clamps to 0, so it hugs the left). A terminal emulator (or sudo's
|
|
# pty) has the same problem the other way: it reports a stale 24x80 for a few
|
|
# hundred ms after start before its real winsize is set. Either way, hold the
|
|
# first draw until the width settles at (or above) the logo width. A real VT
|
|
# that never widens (e.g. nomodeset) still proceeds after the cap.
|
|
wait_for_stable_terminal() {
|
|
local width last="" stable=0 waited=0
|
|
while (( waited < 5000 )); do
|
|
width=$(stty size 2>/dev/null </dev/tty | awk '{print $2}')
|
|
[[ $width =~ ^[0-9]+$ ]] || width=0
|
|
if [[ $width == "$last" ]] && (( width >= LOGO_WIDTH )); then
|
|
(( ++stable >= 3 )) && break
|
|
else
|
|
stable=0
|
|
fi
|
|
last=$width
|
|
sleep 0.2
|
|
(( waited += 200 ))
|
|
done
|
|
}
|
|
|
|
clear_logo() {
|
|
measure_terminal
|
|
printf "\033[H\033[2J"
|
|
gum style --foreground 2 --padding "1 0 0 $PADDING_LEFT" "$(<"$LOGO_PATH")"
|
|
}
|
|
|
|
# Vertically-centered welcome shown once before the keyboard step — the same
|
|
# frame the deferred-provisioning first boot opens on, so install and first boot
|
|
# bookend the setup identically. Logo, the Omarchy tagline, and a single button,
|
|
# centered like the boot logo. No animation.
|
|
greeter() {
|
|
measure_terminal
|
|
|
|
local rows logo_h content_h top cols logo_row tagline_row hint_row
|
|
local tagline hint tpad hpad anim
|
|
cols=$TERM_WIDTH
|
|
rows=$(stty size 2>/dev/null </dev/tty | awk '{print $1}')
|
|
[[ $rows =~ ^[0-9]+$ ]] || rows=${LINES:-24}
|
|
logo_h=$(wc -l <"$LOGO_PATH")
|
|
|
|
# logo + blank + tagline + blank + hint
|
|
content_h=$((logo_h + 4))
|
|
top=$(((rows - content_h) / 2))
|
|
(( top < 0 )) && top=0
|
|
logo_row=$((top + 1))
|
|
tagline_row=$((top + logo_h + 2))
|
|
hint_row=$((top + logo_h + 4))
|
|
|
|
printf '\033[?25l\033[H\033[2J'
|
|
|
|
printf '\033[%d;1H' "$logo_row"
|
|
gum style --foreground 2 --padding "0 0 0 $PADDING_LEFT" "$(<"$LOGO_PATH")"
|
|
|
|
tagline="Beautiful, Fun & Agentic Linux by DHH"
|
|
tpad=$(((cols - ${#tagline}) / 2)); (( tpad < 0 )) && tpad=0
|
|
printf '\033[%d;%dH%s' "$tagline_row" "$((tpad + 1))" "$tagline"
|
|
|
|
hint="Press Return to Start Install"
|
|
hpad=$(((cols - ${#hint}) / 2)); (( hpad < 0 )) && hpad=0
|
|
printf '\033[%d;%dH\033[2m%s\033[0m' "$hint_row" "$((hpad + 1))" "$hint"
|
|
|
|
# ColorShift the logo: a green base (indexed color 2) with a cyan accent (6)
|
|
# drifting through, settling on green. Indexed ANSI colors, not hex — the
|
|
# framebuffer console can't render truecolor faithfully (it crushes the
|
|
# palette to a muddy lavender), but the 16 indexed colors map to the Tokyo
|
|
# Night palette and render true. --xterm-colors is what keeps them indexed:
|
|
# ttfx resolves even indexed stops to truecolor otherwise, which would put the
|
|
# settle color out of step with the `gum style --foreground 2` logo underneath.
|
|
# One long-running invocation (many cycles) so it never restarts — a restart is
|
|
# what flashed. The effect reads from /dev/null so it never swallows the
|
|
# Return; --reuse-canvas paints upward from the saved cursor, anchored one row
|
|
# below the logo.
|
|
printf '\033[%d;1H\0337' "$((logo_row + logo_h))"
|
|
# Run ttfx directly (not inside a `while` subshell) so $anim is ttfx's own
|
|
# PID: killing a wrapping subshell would orphan ttfx, which then keeps
|
|
# painting the logo over the keyboard step. --cycles is high enough that it
|
|
# never ends on its own before Return.
|
|
# Two columns narrower than the console: ttfx centers its text two columns
|
|
# right of plain centering, so a full-width canvas would sit the animated
|
|
# logo a column off the gum-drawn one underneath it.
|
|
ttfx -i "$LOGO_PATH" \
|
|
--canvas-width "$((cols > 2 ? cols - 2 : cols))" \
|
|
--anchor-text c \
|
|
--frame-rate 60 \
|
|
--reuse-canvas \
|
|
--xterm-colors \
|
|
colorshift \
|
|
--gradient-stops 2 10 6 10 \
|
|
--gradient-frames 3 \
|
|
--cycles 1000 \
|
|
--final-gradient-stops 2 \
|
|
</dev/null >/dev/tty 2>/dev/null &
|
|
anim=$!
|
|
|
|
IFS= read -r _ </dev/tty || true
|
|
# Tolerate ttfx's kill signal in `wait` so a future `set -e` here can't abort.
|
|
kill "$anim" 2>/dev/null
|
|
wait "$anim" 2>/dev/null || true
|
|
# ttfx leaves the tty in raw/no-echo mode when killed; restore it or the gum
|
|
# prompts in the keyboard step that follows silently die.
|
|
stty sane </dev/tty 2>/dev/null || true
|
|
# Reset the screen so the leftover animation frame doesn't linger under the
|
|
# keyboard step that follows.
|
|
printf '\033[0m\033[H\033[2J\033[?25h'
|
|
}
|
|
|
|
abort() {
|
|
gum style "${1:-$(omarchy_text "Aborted installation")}"
|
|
echo
|
|
gum style "$(omarchy_text "You can retry later by running: ./.automated_script.sh")"
|
|
exit 1
|
|
}
|
|
|
|
step() {
|
|
clear_logo
|
|
echo
|
|
gum style --padding "0 0 0 $PADDING_LEFT" "$1"
|
|
echo
|
|
}
|
|
|
|
# Padded body text to line up with the logo and gum widgets.
|
|
say() {
|
|
gum style --padding "0 0 0 $PADDING_LEFT" "$@"
|
|
}
|
|
|
|
notice() {
|
|
clear_logo
|
|
echo
|
|
gum spin --spinner "pulse" --title "$1" -- sleep "${2:-2}"
|
|
echo
|
|
}
|
|
|
|
# STEP 1: KEYBOARD LAYOUT
|
|
|
|
# .automated_script.sh moves China into the CJK console and Global back to the VT, which applies keymaps.
|
|
apply_region_language() {
|
|
if [[ $region == "cn" && -n ${OMARCHY_CJK_RELAUNCH:-} ]]; then
|
|
exit "$OMARCHY_CJK_RELAUNCH"
|
|
fi
|
|
if [[ $region == "global" && -n ${OMARCHY_VT_RETURN:-} ]]; then
|
|
exit "$OMARCHY_VT_RETURN"
|
|
fi
|
|
if [[ $region == "cn" && -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
|
export OMARCHY_INSTALL_LANG=zh
|
|
else
|
|
unset OMARCHY_INSTALL_LANG
|
|
fi
|
|
}
|
|
|
|
keyboard_form() {
|
|
# Ctrl+C is the hidden entry into deferred provisioning, offered on every
|
|
# visit: the user step unwinds back here and nothing is on disk yet. Callers
|
|
# re-check defer_provisioning after this returns.
|
|
local status
|
|
|
|
while true; do
|
|
clear_logo
|
|
echo
|
|
say "$(omarchy_text "Let's setup your machine...")"
|
|
say --foreground 8 "$(omarchy_text "Press Ctrl+C to prepare this machine for another owner.")"
|
|
echo
|
|
|
|
omarchy_prompt_region && status=0 || status=$?
|
|
if ((status == 0)); then
|
|
apply_region_language
|
|
# cage keeps its US layout, so the CJK console offers no other.
|
|
if [[ -n ${OMARCHY_CJK_CONSOLE:-} ]]; then
|
|
keyboard="us"
|
|
keyboard_label="English (US)"
|
|
else
|
|
omarchy_prompt_keyboard && status=0 || status=$?
|
|
fi
|
|
fi
|
|
((status == 0)) && break
|
|
|
|
# Esc means "back", and nothing precedes the first screen, so re-ask.
|
|
((status == OMARCHY_FORM_BACK)) && continue
|
|
|
|
if ((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
|
|
if [[ $(tty 2>/dev/null) == "/dev/tty"* ]]; then
|
|
loadkeys "$keyboard" 2>/dev/null
|
|
fi
|
|
}
|
|
|
|
# Ctrl+C on the first screen offers to prepare the machine for another owner.
|
|
# Confirm it before committing, so an accidental Ctrl+C doesn't silently switch
|
|
# the whole install.
|
|
confirm_prepare_for_another_owner() {
|
|
clear_logo
|
|
echo
|
|
say "$(omarchy_text "This prepares the machine for another owner.")"
|
|
say --foreground 8 "$(omarchy_text "The system installs now, but setup is delayed until first boot.")"
|
|
echo
|
|
gum confirm --affirmative "$(omarchy_text "Yes, prepare for another owner")" --negative "$(omarchy_text "No, keep setting up")" \
|
|
"$(omarchy_text "Prepare this machine for another owner?")"
|
|
}
|
|
|
|
# The user step. Deferred-provisioning installs skip it entirely — the machine's first owner
|
|
# picks their keyboard and creates their user in the first-boot setup, so the
|
|
# operator sets nothing user-specific.
|
|
|
|
user_form() {
|
|
step "$(omarchy_text "Let's setup your user account...")"
|
|
|
|
# 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 with SHA-512 crypt ($6$), which is what openssl passwd offers
|
|
password_hash=$(printf '%s' "$password" | openssl passwd -6 -stdin)
|
|
|
|
omarchy_prompt_identity || return $?
|
|
omarchy_prompt_hostname || return $?
|
|
omarchy_prompt_timezone "$(region_timezone)" || return $?
|
|
}
|
|
|
|
# Returns 0 on confirmed account details, or as soon as the keyboard screen
|
|
# arms deferred provisioning.
|
|
user_step() {
|
|
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
|
|
$defer_provisioning && return 0
|
|
continue
|
|
fi
|
|
|
|
# Add manual padding since gum table -p doesn't respect padding
|
|
local skipped region_label=Global
|
|
skipped=$(omarchy_text "[Skipped]")
|
|
[[ $region == "cn" ]] && region_label=China
|
|
echo -e "$(omarchy_text Field),$(omarchy_text Value)
|
|
$(omarchy_text Username),$username
|
|
$(omarchy_text Password),$(printf "%${#password}s" | tr ' ' '*')
|
|
$(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")" |
|
|
gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/"
|
|
|
|
echo
|
|
if gum confirm --affirmative "$(omarchy_text "Yes")" --negative "$(omarchy_text "No, change it")" "$(omarchy_text "Does this look right?")"; then
|
|
break
|
|
else
|
|
keyboard_form
|
|
$defer_provisioning && return 0
|
|
fi
|
|
done
|
|
}
|
|
|
|
# STEP 3: DISK
|
|
|
|
get_disk_info() {
|
|
local device="$1"
|
|
local size vendor model label
|
|
|
|
# Get basic disk information
|
|
size=$(lsblk -dno SIZE "$device" 2>/dev/null)
|
|
vendor=$(lsblk -dno VENDOR "$device" 2>/dev/null | sed 's/ *$//')
|
|
model=$(lsblk -dno MODEL "$device" 2>/dev/null | sed 's/ *$//')
|
|
|
|
# Combine vendor and model, avoiding duplication
|
|
label=""
|
|
if [[ -n $vendor && -n $model ]]; then
|
|
if [[ $model == *$vendor* ]]; then
|
|
label="$model"
|
|
else
|
|
label="$vendor $model"
|
|
fi
|
|
elif [[ -n $model ]]; then
|
|
label="$model"
|
|
elif [[ -n $vendor ]]; then
|
|
label="$vendor"
|
|
fi
|
|
|
|
# Format display string
|
|
local display="$device"
|
|
[[ -n $size ]] && display="$display ($size)"
|
|
[[ -n $label ]] && display="$display - $label"
|
|
|
|
# Append compact partition summary
|
|
local part_summary
|
|
part_summary=$(lsblk -nro TYPE,NAME,FSTYPE,MOUNTPOINT "$device" 2>/dev/null | \
|
|
awk '$1=="part" { printf "%s%s%s", s, ($3==""?"unknown":$3), ($4==""?"":"("$4")"); s=", " }')
|
|
[[ -n $part_summary ]] && display+=" [$part_summary]"
|
|
|
|
echo "$display"
|
|
}
|
|
|
|
get_root_disk() {
|
|
local device="$1"
|
|
local parent
|
|
|
|
[[ -n $device ]] || return 1
|
|
|
|
# /run/archiso/bootmnt usually resolves to the boot partition, but the
|
|
# installer picker works with whole disks. Walk back to the parent disk so
|
|
# the install media itself never shows up as a wipe target.
|
|
device=$(readlink -f "$device" 2>/dev/null || printf "%s\n" "$device")
|
|
|
|
while true; do
|
|
parent=$(lsblk -dno PKNAME "$device" 2>/dev/null | tail -n1)
|
|
[[ -n $parent ]] || break
|
|
device="/dev/$parent"
|
|
done
|
|
|
|
if [[ $(lsblk -dno TYPE "$device" 2>/dev/null) == "disk" ]]; then
|
|
printf "%s\n" "$device"
|
|
fi
|
|
}
|
|
|
|
# partition_path lives in disk-partitioning.sh alongside the creation helpers.
|
|
|
|
# BitLocker writes the "-FVE-FS-" signature at offset 3 of the partition.
|
|
detect_bitlocker() {
|
|
local p sig
|
|
for p in $(lsblk -lnpo NAME,TYPE "$disk" | awk '$2=="part"{print $1}'); do
|
|
sig=$(dd if="$p" bs=1 skip=3 count=8 status=none 2>/dev/null)
|
|
if [[ "$sig" == "-FVE-FS-" ]]; then
|
|
echo "$p"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Look for an existing Windows ESP (FAT partition containing EFI/Microsoft) on $disk.
|
|
detect_windows_esp() {
|
|
local p tmp_mp
|
|
for p in $(blkid -t TYPE=vfat -o device 2>/dev/null); do
|
|
[[ "$p" == "$disk"* ]] || continue
|
|
tmp_mp=$(mktemp -d)
|
|
if mount -o ro "$p" "$tmp_mp" 2>/dev/null; then
|
|
if [[ -d "$tmp_mp/EFI/Microsoft" ]]; then
|
|
umount "$tmp_mp"; rmdir "$tmp_mp"
|
|
echo "$p"
|
|
return 0
|
|
fi
|
|
umount "$tmp_mp"
|
|
fi
|
|
rmdir "$tmp_mp" 2>/dev/null || true
|
|
done
|
|
return 1
|
|
}
|
|
|
|
to_gb() {
|
|
awk -v bytes="$1" 'BEGIN {printf "%.1fGB", bytes/1024/1024/1024}'
|
|
}
|
|
|
|
# T2 Macs need their own kernel for keyboard/wifi drivers.
|
|
detect_kernel() {
|
|
if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then
|
|
echo "linux-t2"
|
|
else
|
|
echo "linux-omarchy"
|
|
fi
|
|
}
|
|
|
|
# wait_for_device lives in disk-partitioning.sh: it now reports failure, and
|
|
# every caller here aborts on it.
|
|
|
|
# disk_step and create_partition fail through this rather than abort() so a
|
|
# half-built layout is undone before the user is handed back their disk.
|
|
disk_abort_hook() {
|
|
# Release the target before reclaiming its partitions: a mounted filesystem
|
|
# or an open LUKS mapping would keep parted from removing them.
|
|
umount -R /mnt 2>/dev/null || true
|
|
umount /mnt/btrfs-root 2>/dev/null || true
|
|
rmdir /mnt/btrfs-root 2>/dev/null || true
|
|
cryptsetup close omarchy_root 2>/dev/null || true
|
|
rollback_created_parts "$disk"
|
|
abort "$1"
|
|
}
|
|
|
|
# Save the configurator answers the installer consumes: git identity and
|
|
# encryption choice as plain files, account details as archinstall credentials.
|
|
# Deferred-provisioning installs defer the user to first boot: no credentials, and on encrypted
|
|
# targets the orchestrator generates a throwaway LUKS passphrase itself.
|
|
write_user_files() {
|
|
echo "$full_name" >user_full_name.txt
|
|
echo "$email_address" >user_email_address.txt
|
|
printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt
|
|
|
|
if $defer_provisioning; then
|
|
cat <<-_EOF_ >user_credentials.json
|
|
{
|
|
"users": []
|
|
}
|
|
_EOF_
|
|
return
|
|
fi
|
|
|
|
local password_escaped password_hash_escaped username_escaped
|
|
password_escaped=$(echo -n "$password" | jq -Rsa)
|
|
password_hash_escaped=$(echo -n "$password_hash" | jq -Rsa)
|
|
username_escaped=$(echo -n "$username" | jq -Rsa)
|
|
|
|
local credentials_encryption_line=""
|
|
if [[ $encrypt_installation == true ]]; then
|
|
credentials_encryption_line=" \"encryption_password\": $password_escaped,"
|
|
fi
|
|
|
|
cat <<-_EOF_ >user_credentials.json
|
|
{
|
|
$credentials_encryption_line
|
|
"root_enc_password": $password_hash_escaped,
|
|
"users": [
|
|
{
|
|
"enc_password": $password_hash_escaped,
|
|
"groups": [],
|
|
"sudo": true,
|
|
"username": $username_escaped
|
|
}
|
|
]
|
|
}
|
|
_EOF_
|
|
}
|
|
|
|
# Show what a dry run would have written, then leave no trace behind.
|
|
print_dry_run_files() {
|
|
echo -e "User Configuration:"
|
|
cat user_configuration.json
|
|
|
|
echo -e "\n\nUser Credentials:"
|
|
cat user_credentials.json
|
|
|
|
echo -e "\n\nUser Full Name:"
|
|
cat user_full_name.txt
|
|
|
|
echo -e "\nUser Email Address:"
|
|
cat user_email_address.txt
|
|
|
|
echo -e "\nDisk Encryption:"
|
|
cat user_encrypt_installation.txt
|
|
|
|
rm -f user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt
|
|
}
|
|
|
|
# Shared error screen for every "can't fit Omarchy here" outcome of the
|
|
# partition install. Offers the partition tool as the meaningful second
|
|
# action so users have a way to actually fix the situation.
|
|
not_enough_space() {
|
|
local available="${1:-0}"
|
|
step "$(omarchy_text "Not enough free space on %s" "$disk")"
|
|
say --foreground 1 "$(omarchy_text "%s has %s of usable free space; Omarchy needs at least 32GB." "$disk" "$(to_gb $available)")"
|
|
say "$(omarchy_text "Open the partition tool to free at least 32GB on %s, then try again." "$disk")"
|
|
echo
|
|
if ! gum confirm --affirmative "$(omarchy_text "Back")" --negative "$(omarchy_text "Open partition tool")" "$(omarchy_text "Return to installation mode?")"; then
|
|
open_partition_tool
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Decide half of the free-space install: checks, free-space analysis, and the
|
|
# encryption confirm. Sets the globals run_partition_execute needs. Returns 1
|
|
# to fall back to the install-mode picker.
|
|
run_partition_decide() {
|
|
step "$(omarchy_text "Checking for BitLocker on %s" "$disk")"
|
|
local bl_part
|
|
bl_part=$(detect_bitlocker || true)
|
|
if [[ -n "$bl_part" ]]; then
|
|
say --foreground 1 "$(omarchy_text "BitLocker signature detected on %s." "$bl_part")"
|
|
say "$(omarchy_text "Turn BitLocker off in Windows and wait for the drive to finish decrypting, then try again.")"
|
|
say "$(omarchy_text "Suspending BitLocker is not enough — the drive stays encrypted.")"
|
|
abort "$(omarchy_text "Aborted: BitLocker is enabled on this disk.")"
|
|
fi
|
|
|
|
# Omarchy always creates its own dedicated ESP in free space and never
|
|
# adopts an existing (Windows) ESP. Sharing the Windows ESP is fragile:
|
|
# Windows Update/repair can reclaim or reformat it, the typical 100-260MiB
|
|
# Windows ESP is far too small for our Unified Kernel Images, and a shared
|
|
# ESP forces the install unencrypted. A separate Linux ESP keeps Windows
|
|
# and Omarchy fully isolated and keeps LUKS on the table.
|
|
step "$(omarchy_text "Checking existing EFI partitions on %s" "$disk")"
|
|
local detected_win_esp
|
|
detected_win_esp=$(detect_windows_esp || true)
|
|
if [[ -n "$detected_win_esp" ]]; then
|
|
say "$(omarchy_text "Found a Windows ESP at %s — leaving it untouched." "$detected_win_esp")"
|
|
say "$(omarchy_text "Omarchy will create its own dedicated EFI partition in free space.")"
|
|
else
|
|
say "$(omarchy_text "A new EFI partition will be created in free space.")"
|
|
fi
|
|
|
|
step "$(omarchy_text "Analyzing free space on %s" "$disk")"
|
|
partprobe "$disk" 2>/dev/null || true
|
|
sleep 1
|
|
|
|
# An unlabeled disk has no partition table for parted to scan, so we
|
|
# synthesize a whole-disk free region and mklabel gpt before mkpart.
|
|
local FREE_SPACE_INFO pt_type disk_size_b
|
|
needs_mklabel=false
|
|
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
|
|
if [[ -z "$pt_type" ]]; then
|
|
disk_size_b=$(lsblk -bdno SIZE "$disk" 2>/dev/null)
|
|
if [[ -n "$disk_size_b" && "$disk_size_b" -gt 0 ]]; then
|
|
local _mib=$((1024 * 1024))
|
|
local _start=$_mib
|
|
local _end=$(( (disk_size_b - _mib) / _mib * _mib ))
|
|
FREE_SPACE_INFO="$_start $_end $((_end - _start))"
|
|
needs_mklabel=true
|
|
fi
|
|
else
|
|
# -s (script mode) keeps parted from prompting on stderr for warnings
|
|
# like a stale GPT backup; without it the configurator hangs invisibly.
|
|
FREE_SPACE_INFO=$(parted -ms "$disk" unit B print free </dev/null 2>/dev/null | \
|
|
awk -F: '
|
|
$NF ~ /free/ {
|
|
start=$2; end=$3; size=$4;
|
|
gsub(/B/, "", start);
|
|
gsub(/B/, "", end);
|
|
gsub(/B/, "", size);
|
|
if (size+0 > max_size+0) {
|
|
max_size=size; max_start=start; max_end=end;
|
|
}
|
|
}
|
|
END {
|
|
if (max_size+0 > 0) printf "%s %s %s\n", max_start, max_end, max_size;
|
|
}')
|
|
fi
|
|
|
|
if [[ -z "$FREE_SPACE_INFO" ]]; then
|
|
not_enough_space 0
|
|
return $?
|
|
fi
|
|
|
|
local FREE_START_B FREE_END_B FREE_SIZE_B
|
|
read -r FREE_START_B FREE_END_B FREE_SIZE_B <<<"$FREE_SPACE_INFO"
|
|
|
|
local mib=$((1024 * 1024)) gib=$((1024 * 1024 * 1024))
|
|
local ALIGN=$mib
|
|
local EFI_SIZE_B=$((2 * gib))
|
|
local MIN_INSTALL_B=$((32 * gib)) # Minimum total Omarchy allocation
|
|
|
|
EFI_START_B=$(( (FREE_START_B + ALIGN - 1) / ALIGN * ALIGN ))
|
|
EFI_END_B=$((EFI_START_B + EFI_SIZE_B))
|
|
ROOT_START_B=$(( (EFI_END_B + 1 + ALIGN - 1) / ALIGN * ALIGN ))
|
|
ROOT_END_B=$((FREE_END_B / ALIGN * ALIGN))
|
|
|
|
if (( ROOT_END_B <= ROOT_START_B )); then
|
|
not_enough_space "$FREE_SIZE_B"
|
|
return $?
|
|
fi
|
|
|
|
local INSTALL_MAX_B=$((ROOT_END_B - EFI_START_B))
|
|
|
|
if (( INSTALL_MAX_B < MIN_INSTALL_B )); then
|
|
not_enough_space "$INSTALL_MAX_B"
|
|
return $?
|
|
fi
|
|
|
|
# Default to encrypted and hide the unencrypted path behind Ctrl+C,
|
|
# matching the full-disk flow. The dedicated Omarchy ESP always supports
|
|
# LUKS, so encryption is always on the table here.
|
|
local mode="encrypted" affirmative confirm_status
|
|
|
|
while true; do
|
|
clear_logo
|
|
echo
|
|
say "$(omarchy_text "Install Omarchy in the %s of free space." "$(to_gb $INSTALL_MAX_B)")"
|
|
|
|
case $mode in
|
|
encrypted)
|
|
say --foreground 8 "$(omarchy_text "Press Ctrl+C for unencrypted install.")"
|
|
affirmative="$(omarchy_text "Yes, install")"
|
|
;;
|
|
unencrypted)
|
|
affirmative="$(omarchy_text "Yes, install without encryption")"
|
|
;;
|
|
esac
|
|
echo
|
|
|
|
gum confirm --affirmative "$affirmative" --negative "$(omarchy_text "No, change it")" \
|
|
"$(omarchy_text "Confirm installing on %s" "$disk")"
|
|
confirm_status=$?
|
|
|
|
case $confirm_status in
|
|
0)
|
|
[[ $mode == "encrypted" ]] && encrypt_installation=true || encrypt_installation=false
|
|
break
|
|
;;
|
|
1)
|
|
return 1
|
|
;;
|
|
130)
|
|
case $mode in
|
|
encrypted) mode="unencrypted" ;;
|
|
unencrypted) mode="encrypted" ;;
|
|
esac
|
|
;;
|
|
*)
|
|
abort
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$encrypt_installation" == "true" ]]; then
|
|
esp_mount_in_target=/boot
|
|
else
|
|
esp_mount_in_target=/efi
|
|
fi
|
|
|
|
kernel_choice=$(detect_kernel)
|
|
|
|
# Partition numbers are not knowable until parted assigns them (it fills the
|
|
# lowest free GPT slot, and freeing space by deleting a partition leaves one
|
|
# below the highest). run_partition_execute reads them back after creation;
|
|
# a dry run has nothing to read.
|
|
efi_part_num=""
|
|
root_part_num=""
|
|
efi_dev="<assigned at creation>"
|
|
root_partition_device="<assigned at creation>"
|
|
root_mapper="<assigned at creation>"
|
|
if [[ "$encrypt_installation" == "true" ]]; then
|
|
root_mapper="/dev/mapper/omarchy_root"
|
|
fi
|
|
}
|
|
|
|
# Execute half of the free-space install: partition, format, mount, and write
|
|
# the pre-mounted (protected) install configuration. Runs after the user step
|
|
# so LUKS can be formatted with the user's password.
|
|
run_partition_execute() {
|
|
local dry=false subvol
|
|
[[ "${1:-}" == "dry" ]] && dry=true
|
|
|
|
if $dry; then
|
|
$needs_mklabel && say "[dry] would initialize GPT on $disk"
|
|
say "[dry] would create an EFI partition and a root partition in the free region"
|
|
say "[dry] partition numbers are assigned by parted at creation, then read back"
|
|
say "[dry] encrypted=$encrypt_installation kernel=$kernel_choice"
|
|
else
|
|
step "$(omarchy_text "Creating partitions on %s" "$disk")"
|
|
if $needs_mklabel; then
|
|
disk_step "initializing GPT on $disk" parted --script "$disk" mklabel gpt
|
|
partprobe "$disk" 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
|
|
# create_partition reports the number parted actually used; nothing here
|
|
# may assume it. It sets created_partition_number rather than printing so
|
|
# the rollback bookkeeping survives (a $(...) would subshell it away).
|
|
create_partition "$disk" "$EFI_START_B" "$EFI_END_B" fat32 OMARCHY_EFI ||
|
|
disk_abort_hook "Could not create the EFI partition on $disk"
|
|
efi_part_num="$created_partition_number"
|
|
|
|
create_partition "$disk" "$ROOT_START_B" "$ROOT_END_B" btrfs OMARCHY_ROOT ||
|
|
disk_abort_hook "Could not create the root partition on $disk"
|
|
root_part_num="$created_partition_number"
|
|
|
|
disk_step "flagging partition $efi_part_num as ESP" \
|
|
parted --script "$disk" set "$efi_part_num" esp on
|
|
|
|
efi_dev=$(partition_path "$disk" "$efi_part_num")
|
|
root_partition_device=$(partition_path "$disk" "$root_part_num")
|
|
|
|
partprobe "$disk"
|
|
sync
|
|
sleep 2
|
|
|
|
wait_for_device "$efi_dev" || disk_abort_hook "EFI partition $efi_dev never appeared"
|
|
wait_for_device "$root_partition_device" ||
|
|
disk_abort_hook "Root partition $root_partition_device never appeared"
|
|
|
|
# Both partitions are ours and brand new, but the space they occupy may
|
|
# carry signatures from whatever was deleted to free it.
|
|
disk_step "clearing stale signatures on $efi_dev" wipefs -af "$efi_dev"
|
|
disk_step "clearing stale signatures on $root_partition_device" wipefs -af "$root_partition_device"
|
|
|
|
if [[ "$encrypt_installation" == "true" ]]; then
|
|
step "$(omarchy_text "Setting up LUKS2 on %s" "$root_partition_device")"
|
|
# Kept as pipes rather than routed through disk_step: the passphrase must
|
|
# not become an argv the process table can show. Folding stderr into
|
|
# stdout still lands any error in the install log.
|
|
if ! printf "%s" "$password" |
|
|
cryptsetup luksFormat --type luks2 --batch-mode "$root_partition_device" - 2>&1; then
|
|
disk_abort_hook "Formatting LUKS2 on $root_partition_device failed"
|
|
fi
|
|
if ! printf "%s" "$password" |
|
|
cryptsetup open "$root_partition_device" omarchy_root - 2>&1; then
|
|
disk_abort_hook "Opening the encrypted root on $root_partition_device failed"
|
|
fi
|
|
root_mapper="/dev/mapper/omarchy_root"
|
|
else
|
|
root_mapper="$root_partition_device"
|
|
fi
|
|
|
|
step "$(omarchy_text "Creating Btrfs filesystem and subvolumes")"
|
|
disk_step "creating the Btrfs filesystem on $root_mapper" \
|
|
mkfs.btrfs -f -L OMARCHY "$root_mapper"
|
|
wait_for_device "$root_mapper" || disk_abort_hook "Root device $root_mapper never appeared"
|
|
|
|
mkdir -p /mnt/btrfs-root
|
|
disk_step "mounting $root_mapper" mount "$root_mapper" /mnt/btrfs-root
|
|
for subvol in @ @home @log @pkg; do
|
|
disk_step "creating subvolume $subvol" btrfs subvolume create "/mnt/btrfs-root/$subvol"
|
|
done
|
|
umount /mnt/btrfs-root
|
|
rmdir /mnt/btrfs-root
|
|
|
|
disk_step "mounting the target root" \
|
|
mount -o noatime,compress=zstd,subvol=@ "$root_mapper" /mnt
|
|
mkdir -p /mnt/home /mnt/var/log /mnt/var/cache/pacman/pkg /mnt"$esp_mount_in_target"
|
|
disk_step "mounting /home" \
|
|
mount -o noatime,compress=zstd,subvol=@home "$root_mapper" /mnt/home
|
|
disk_step "mounting /var/log" \
|
|
mount -o noatime,compress=zstd,subvol=@log "$root_mapper" /mnt/var/log
|
|
disk_step "mounting the package cache" \
|
|
mount -o noatime,compress=zstd,subvol=@pkg "$root_mapper" /mnt/var/cache/pacman/pkg
|
|
|
|
disk_step "creating the ESP filesystem on $efi_dev" mkfs.fat -F32 -n OMARCHY_EFI "$efi_dev"
|
|
disk_step "mounting the ESP" mount "$efi_dev" /mnt"$esp_mount_in_target"
|
|
|
|
# The orchestrator's first act is to verify this handoff. Check it here so
|
|
# a failure names the step that broke rather than surfacing later as
|
|
# "protected mode: /mnt is not a mountpoint".
|
|
mountpoint -q /mnt ||
|
|
disk_abort_hook "Target /mnt is not mounted; not starting the installer"
|
|
mountpoint -q "/mnt$esp_mount_in_target" ||
|
|
disk_abort_hook "ESP is not mounted at /mnt$esp_mount_in_target"
|
|
fi
|
|
|
|
local luks_uuid_json="null" luks_uuid
|
|
if [[ "$encrypt_installation" == "true" ]]; then
|
|
if $dry; then
|
|
luks_uuid="00000000-0000-0000-0000-000000000000"
|
|
else
|
|
luks_uuid=$(blkid -s UUID -o value "$root_partition_device")
|
|
fi
|
|
luks_uuid_json="\"$luks_uuid\""
|
|
fi
|
|
|
|
cat <<-_EOF_ >user_configuration.json
|
|
{
|
|
"app_config": null,
|
|
"archinstall-language": "English",
|
|
"auth_config": {},
|
|
"audio_config": { "audio": "pipewire" },
|
|
"bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
|
|
"custom_commands": [],
|
|
"omarchy_install": {
|
|
"mode": "protected",
|
|
"region": "$region",
|
|
"target_mount": "/mnt",
|
|
"boot": {
|
|
"esp_mount": "$esp_mount_in_target",
|
|
"esp_path": "/EFI/limine",
|
|
"efi_binary": "limine_x64.efi",
|
|
"enable_fallback": false
|
|
},
|
|
"storage": {
|
|
"esp_device": "$efi_dev",
|
|
"root_device": "$root_partition_device",
|
|
"root_mapper": "$root_mapper",
|
|
"luks_uuid": $luks_uuid_json,
|
|
"kernel": "$kernel_choice"
|
|
}
|
|
},
|
|
"disk_config": {
|
|
"config_type": "pre_mounted_config",
|
|
"mountpoint": "/mnt"
|
|
},
|
|
"hostname": "$hostname",
|
|
"kernels": [ "$kernel_choice" ],
|
|
"network_config": { "type": "iso" },
|
|
"ntp": true,
|
|
"parallel_downloads": 8,
|
|
"script": null,
|
|
"services": [],
|
|
"swap": true,
|
|
"timezone": "$timezone",
|
|
"locale_config": {
|
|
"kb_layout": "$keyboard",
|
|
"sys_enc": "UTF-8",
|
|
"sys_lang": "en_US.UTF-8"
|
|
},
|
|
"mirror_config": {
|
|
"custom_repositories": [],
|
|
"custom_servers": [
|
|
{"url": "https://mirror.omarchy.org/\$repo/os/\$arch"},
|
|
{"url": "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch"},
|
|
{"url": "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"}
|
|
],
|
|
"mirror_regions": {},
|
|
"optional_repositories": []
|
|
},
|
|
"packages": [
|
|
"base-devel",
|
|
"git",
|
|
"omarchy-keyring",
|
|
"$OMARCHY_SETTINGS_PACKAGE",
|
|
"$OMARCHY_RUNTIME_PACKAGE"
|
|
],
|
|
"profile_config": {
|
|
"gfx_driver": null,
|
|
"greeter": null,
|
|
"profile": {}
|
|
},
|
|
"version": "3.0.9"
|
|
}
|
|
_EOF_
|
|
}
|
|
|
|
disk_form() {
|
|
step "$(omarchy_text "Let's select where to install Omarchy...")"
|
|
|
|
# Don't offer the install media as an option (Arch ISO mounts it here)
|
|
local boot_source
|
|
local exclude_disk
|
|
boot_source=$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null || true)
|
|
exclude_disk=$(get_root_disk "$boot_source")
|
|
|
|
# List all installable disks, excluding the boot device if present
|
|
local available_disks
|
|
available_disks=$(
|
|
lsblk -dpno NAME,TYPE |
|
|
awk '$2=="disk"{print $1}' |
|
|
grep -E '/dev/(sd|hd|vd|nvme|mmcblk|xv)' |
|
|
{ if [[ -n "$exclude_disk" ]]; then grep -Fvx "$exclude_disk"; else cat; fi; }
|
|
)
|
|
|
|
# Get available disks and format them with info
|
|
local disk_options=""
|
|
while IFS= read -r device; do
|
|
if [[ -n "$device" ]]; then
|
|
disk_info=$(get_disk_info "$device")
|
|
disk_options="$disk_options$disk_info"$'\n'
|
|
fi
|
|
done <<<"$available_disks"
|
|
|
|
selected_display=$(echo "$disk_options" | gum choose --header "$(omarchy_text "Select install disk")") || abort
|
|
disk=$(echo "$selected_display" | awk '{print $1}')
|
|
}
|
|
|
|
# STEP 4: INSTALL MODE
|
|
|
|
open_partition_tool() {
|
|
step "$(omarchy_text "Partition tool for %s" "$disk")"
|
|
gum style "$(omarchy_text "Create unallocated free space for Omarchy, then write changes and quit.")"
|
|
gum style --foreground 8 "$(omarchy_text "Do not create an Omarchy partition here — leave the target area as Free space.")"
|
|
gum style --foreground 8 "$(omarchy_text "Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists.")"
|
|
echo
|
|
gum confirm --affirmative "$(omarchy_text "Open cfdisk")" --negative "$(omarchy_text "Back")" "$(omarchy_text "Open partition tool for %s?" "$disk")" || return 0
|
|
|
|
clear
|
|
cfdisk "$disk" || true
|
|
partprobe "$disk" 2>/dev/null || true
|
|
sync
|
|
sleep 1
|
|
}
|
|
|
|
# If the disk has no partition table or no unallocated free space, the only
|
|
# viable paths write the whole disk, so the free-space mode is not offered.
|
|
requires_full_disk_install() {
|
|
local pt_type free_space_b
|
|
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
|
|
|
|
# a) No partition table: disk is empty
|
|
[[ -z "$pt_type" ]] && return 0
|
|
|
|
# b) No usable free space
|
|
free_space_b=$(parted -ms "$disk" unit B print free </dev/null 2>/dev/null | \
|
|
awk -F: '
|
|
$NF ~ /free/ {
|
|
size=$4;
|
|
gsub(/B/, "", size);
|
|
if (size+0 > max+0) max=size;
|
|
}
|
|
END {
|
|
print max+0;
|
|
}')
|
|
[[ -z "$free_space_b" || "$free_space_b" -eq 0 ]]
|
|
}
|
|
|
|
install_mode_form() {
|
|
local choices=("$(omarchy_text "Full disk install")")
|
|
|
|
# The free-space/protected path registers an EFI boot entry and is UEFI-only.
|
|
# Full-disk installs still support BIOS through the orchestrator's BIOS branch.
|
|
if [[ -d /sys/firmware/efi ]] && ! $full_disk_only; then
|
|
choices+=("$(omarchy_text "Free space install (alongside existing data)")")
|
|
fi
|
|
choices+=("$(omarchy_text "Choose a different disk")")
|
|
|
|
step "$(omarchy_text "Let's select how to install Omarchy...")"
|
|
install_mode=$(gum choose --header "$(omarchy_text "Select installation mode on %s" "$disk")" "${choices[@]}") || abort
|
|
}
|
|
|
|
confirm_disk_overwrite() {
|
|
local mode="encrypted"
|
|
local affirmative confirm_status
|
|
|
|
while true; do
|
|
clear_logo
|
|
echo
|
|
say "$(omarchy_text "Everything will be overwritten. There is no recovery possible.")"
|
|
|
|
if [[ $mode == "encrypted" ]]; then
|
|
say --foreground 8 "$(omarchy_text "Press Ctrl+C for unencrypted install.")"
|
|
affirmative="$(omarchy_text "Yes, install")"
|
|
else
|
|
affirmative="$(omarchy_text "Yes, install without encryption")"
|
|
fi
|
|
|
|
echo
|
|
gum confirm --affirmative "$affirmative" --negative "$(omarchy_text "No, change it")" "$(omarchy_text "Confirm overwriting %s" "$disk")"
|
|
confirm_status=$?
|
|
|
|
case $confirm_status in
|
|
0)
|
|
[[ $mode == "encrypted" ]] && encrypt_installation=true || encrypt_installation=false
|
|
return 0
|
|
;;
|
|
1)
|
|
return 1
|
|
;;
|
|
130)
|
|
[[ $mode == "encrypted" ]] && mode="unencrypted" || mode="encrypted"
|
|
;;
|
|
*)
|
|
abort
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Decide what and how to install: sets install_target (full_disk|free_space),
|
|
# install_target and encrypt_installation. Nothing is written to disk yet —
|
|
# the user step runs between this and the execution below. (deferred provisioning never
|
|
# reaches here; it's armed by Ctrl+C on the keyboard screen and goes straight to
|
|
# disk selection.)
|
|
select_installation() {
|
|
local full_disk_only
|
|
|
|
while true; do
|
|
if requires_full_disk_install; then
|
|
full_disk_only=true
|
|
else
|
|
full_disk_only=false
|
|
fi
|
|
|
|
# With no free-space option to offer, a full-disk install is the only
|
|
# mode, so skip the picker and go straight to the overwrite confirm.
|
|
if $full_disk_only; then
|
|
install_mode=$(omarchy_text "Full disk install")
|
|
else
|
|
install_mode_form
|
|
fi
|
|
|
|
case "$install_mode" in
|
|
"$(omarchy_text "Full disk install")")
|
|
if confirm_disk_overwrite; then
|
|
install_target="full_disk"
|
|
return 0
|
|
fi
|
|
# When the picker was skipped, declining the overwrite means the user
|
|
# wants a different disk.
|
|
$full_disk_only && disk_form
|
|
;;
|
|
"$(omarchy_text "Free space install (alongside existing data)")")
|
|
if run_partition_decide; then
|
|
install_target="free_space"
|
|
return 0
|
|
fi
|
|
;;
|
|
"$(omarchy_text "Choose a different disk")")
|
|
disk_form
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# STEP 1: KEYBOARD (Ctrl+C here arms deferred provisioning and jumps to disk selection)
|
|
|
|
defer_provisioning=false
|
|
install_target="full_disk"
|
|
# A rerun in the other console arrives with the region already chosen; a
|
|
# regional build preselects its own.
|
|
region="${OMARCHY_INSTALL_REGION:-$(<"${ISO_REGION_FILE:-/root/omarchy_region}")}"
|
|
|
|
# Let the live VT reach its settled width before the first screen so the logo
|
|
# isn't measured against the transient 80-column boot console.
|
|
wait_for_stable_terminal
|
|
|
|
if [[ -n ${OMARCHY_INSTALL_REGION:-} ]]; then
|
|
apply_region_language
|
|
else
|
|
greeter
|
|
fi
|
|
|
|
keyboard_form
|
|
|
|
# Normal install: keyboard (loaded above, so the LUKS passphrase is typed under
|
|
# the right layout) → user → disk → install mode. The user step can unwind to
|
|
# the keyboard screen and arm deferral there, hence the second check.
|
|
if ! $defer_provisioning; then
|
|
user_step
|
|
fi
|
|
|
|
if $defer_provisioning; then
|
|
# Deferred provisioning: no user setup, and the keyboard is deferred to the machine's first
|
|
# boot. A defined default keymap so it boots before then; the owner picks
|
|
# their real layout in the OOBE, which re-keys the console and, on encrypted
|
|
# installs, rebuilds the initramfs so the LUKS prompt matches.
|
|
keyboard="us"
|
|
username=""
|
|
password=""
|
|
password_hash=""
|
|
full_name=""
|
|
email_address=""
|
|
# Neutral defaults for the install; the owner overwrites both in the
|
|
# first-boot OOBE (omarchy-provision-owner).
|
|
hostname="omarchy"
|
|
timezone="UTC"
|
|
|
|
# Jump straight to disk selection + overwrite confirmation (encrypted by
|
|
# default, Ctrl+C toggles unencrypted). deferred provisioning is always a full-disk install.
|
|
disk_form
|
|
until confirm_disk_overwrite; do
|
|
disk_form
|
|
done
|
|
install_target="full_disk"
|
|
else
|
|
disk_form
|
|
select_installation
|
|
fi
|
|
|
|
clear
|
|
|
|
write_user_files
|
|
|
|
if [[ $install_target == "free_space" ]]; then
|
|
run_partition_execute "$@"
|
|
|
|
if [[ ${1:-} == "dry" ]]; then
|
|
print_dry_run_files
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Setup partition layout
|
|
disk_size=$(lsblk -bdno SIZE "$disk")
|
|
mib=$((1024 * 1024))
|
|
gib=$((mib * 1024))
|
|
disk_size_in_mib=$((disk_size / mib * mib)) # Rounds to nearest MiB
|
|
|
|
gpt_backup_reserve=$((mib))
|
|
boot_partition_start=$((mib))
|
|
boot_partition_size=$((2 * gib))
|
|
|
|
main_partition_start=$((boot_partition_size + boot_partition_start))
|
|
main_partition_size=$((disk_size_in_mib - main_partition_start - gpt_backup_reserve))
|
|
|
|
kernel_choice=$(detect_kernel)
|
|
|
|
if [[ $encrypt_installation == true ]]; then
|
|
# Deferred-provisioning installs carry no password: the orchestrator generates a throwaway
|
|
# LUKS passphrase and stages it for the first-boot re-key.
|
|
if $defer_provisioning; then
|
|
encryption_password_line=""
|
|
else
|
|
encryption_password_line=",
|
|
\"encryption_password\": $(echo -n "$password" | jq -Rsa)"
|
|
fi
|
|
disk_encryption_config=$(cat <<-_EOF_
|
|
,
|
|
"disk_encryption": {
|
|
"encryption_type": "luks",
|
|
"lvm_volumes": [],
|
|
"iter_time": 2000,
|
|
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ]$encryption_password_line
|
|
}
|
|
_EOF_
|
|
)
|
|
else
|
|
disk_encryption_config=""
|
|
fi
|
|
|
|
cat <<-_EOF_ >user_configuration.json
|
|
{
|
|
"app_config": null,
|
|
"archinstall-language": "English",
|
|
"auth_config": {},
|
|
"audio_config": { "audio": "pipewire" },
|
|
"bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
|
|
"custom_commands": [],
|
|
"omarchy_install": {
|
|
"mode": "full_disk",
|
|
"region": "$region",
|
|
"defer_provisioning": $defer_provisioning,
|
|
"target_mount": "/mnt",
|
|
"boot": {
|
|
"esp_mount": "/boot",
|
|
"esp_path": "/EFI/limine",
|
|
"efi_binary": "limine_x64.efi",
|
|
"enable_fallback": true
|
|
},
|
|
"storage": {
|
|
"kernel": "$kernel_choice"
|
|
}
|
|
},
|
|
"disk_config": {
|
|
"config_type": "default_layout",
|
|
"device_modifications": [
|
|
{
|
|
"device": "$disk",
|
|
"partitions": [
|
|
{
|
|
"btrfs": [],
|
|
"dev_path": null,
|
|
"flags": [ "boot", "esp" ],
|
|
"fs_type": "fat32",
|
|
"mount_options": [],
|
|
"mountpoint": "/boot",
|
|
"obj_id": "ea21d3f2-82bb-49cc-ab5d-6f81ae94e18d",
|
|
"size": {
|
|
"sector_size": { "unit": "B", "value": 512 },
|
|
"unit": "B",
|
|
"value": $boot_partition_size
|
|
},
|
|
"start": {
|
|
"sector_size": { "unit": "B", "value": 512 },
|
|
"unit": "B",
|
|
"value": $boot_partition_start
|
|
},
|
|
"status": "create",
|
|
"type": "primary"
|
|
},
|
|
{
|
|
"btrfs": [
|
|
{ "mountpoint": "/", "name": "@" },
|
|
{ "mountpoint": "/home", "name": "@home" },
|
|
{ "mountpoint": "/var/log", "name": "@log" },
|
|
{ "mountpoint": "/var/cache/pacman/pkg", "name": "@pkg" }
|
|
],
|
|
"dev_path": null,
|
|
"flags": [],
|
|
"fs_type": "btrfs",
|
|
"mount_options": [ "compress=zstd" ],
|
|
"mountpoint": null,
|
|
"obj_id": "8c2c2b92-1070-455d-b76a-56263bab24aa",
|
|
"size": {
|
|
"sector_size": { "unit": "B", "value": 512 },
|
|
"unit": "B",
|
|
"value": $main_partition_size
|
|
},
|
|
"start": {
|
|
"sector_size": { "unit": "B", "value": 512 },
|
|
"unit": "B",
|
|
"value": $main_partition_start
|
|
},
|
|
"status": "create",
|
|
"type": "primary"
|
|
}
|
|
],
|
|
"wipe": true
|
|
}
|
|
]$disk_encryption_config
|
|
},
|
|
"hostname": "$hostname",
|
|
"kernels": [ "$kernel_choice" ],
|
|
"network_config": { "type": "iso" },
|
|
"ntp": true,
|
|
"parallel_downloads": 8,
|
|
"script": null,
|
|
"services": [],
|
|
"swap": true,
|
|
"timezone": "$timezone",
|
|
"locale_config": {
|
|
"kb_layout": "$keyboard",
|
|
"sys_enc": "UTF-8",
|
|
"sys_lang": "en_US.UTF-8"
|
|
},
|
|
"mirror_config": {
|
|
"custom_repositories": [],
|
|
"custom_servers": [
|
|
{"url": "https://mirror.omarchy.org/\$repo/os/\$arch"},
|
|
{"url": "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch"},
|
|
{"url": "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"}
|
|
],
|
|
"mirror_regions": {},
|
|
"optional_repositories": []
|
|
},
|
|
"packages": [
|
|
"base-devel",
|
|
"git",
|
|
"omarchy-keyring",
|
|
"$OMARCHY_SETTINGS_PACKAGE",
|
|
"$OMARCHY_RUNTIME_PACKAGE"
|
|
],
|
|
"profile_config": {
|
|
"gfx_driver": null,
|
|
"greeter": null,
|
|
"profile": {}
|
|
},
|
|
"version": "3.0.9"
|
|
}
|
|
_EOF_
|
|
|
|
if [[ $1 == "dry" ]]; then
|
|
print_dry_run_files
|
|
fi
|