#!/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"]="区域" ["[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" ) # 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 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 = 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 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/tty 2>/dev/null & anim=$! IFS= read -r _ /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/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 # The target's LANG follows the language picked on the first screen. target_lang() { if [[ $language == "zh" ]]; then echo zh_CN.UTF-8 else echo en_US.UTF-8 fi } # .automated_script.sh moves Chinese into the CJK console and English back to the VT, which applies keymaps. apply_language() { if [[ $language == "zh" && -n ${OMARCHY_CJK_RELAUNCH:-} ]]; then exit "$OMARCHY_CJK_RELAUNCH" fi if [[ $language == "en" && -n ${OMARCHY_VT_RETURN:-} ]]; then exit "$OMARCHY_VT_RETURN" fi if [[ $language == "zh" && -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 status=0 if ! $language_chosen; then omarchy_prompt_language && status=0 || status=$? ((status == 0)) && apply_language fi # Esc, Ctrl+C or a later "No, change it" asks the language again. language_chosen=false if ((status == 0)); then local layouts=$OMARCHY_KEYBOARD_LAYOUTS selected="English (US)" [[ $language == "zh" ]] && selected="Chinese (Simplified, Pinyin)" # cage keeps its US layout, so the CJK console offers only US-keymap keyboards. [[ -n ${OMARCHY_CJK_CONSOLE:-} ]] && layouts=$(awk -F'|' '$2 == "us"' <<<"$layouts") OMARCHY_KEYBOARD_LAYOUTS=$layouts omarchy_prompt_keyboard "$selected" && status=0 || status=$? 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 || 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 # The timezone selects the region (mirrors and repositories), using the # same lookup as the orchestrator. Global installs get no extra row. region=$(cd /usr/share/omarchy-iso 2>/dev/null && python -m orchestrator.region "$timezone") || region="global" region_row="" if [[ $region != "global" ]]; then region_row="\n$(omarchy_text Region),$region" fi # Add manual padding since gum table -p doesn't respect padding local skipped skipped=$(omarchy_text "[Skipped]") 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$region_row $(omarchy_text Keyboard),\"$keyboard_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 | \ 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="" root_partition_device="" root_mapper="" 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": { "input_method": "${keyboard_input_method:-none}", "input_xkb_layout": "${keyboard_xkb_layout:-}", "mode": "protected", "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": "$(target_lang)" }, "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 | \ 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 arrives with the language chosen; otherwise English. language="${OMARCHY_INSTALL_LANGUAGE:-en}" language_chosen=false # 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_LANGUAGE:-} ]]; then apply_language language_chosen=true 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" keyboard_input_method="none" keyboard_xkb_layout="" 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": { "input_method": "${keyboard_input_method:-none}", "input_xkb_layout": "${keyboard_xkb_layout:-}", "mode": "full_disk", "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": "$(target_lang)" }, "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