|
1 | 1 | #!/usr/bin/env bash |
2 | | -# MiOS Bootstrap -- Total Root Merge Mode |
| 2 | +# |
| 3 | +# MiOS Bootstrap -- Interactive Ignition Installer (Total Root Merge Mode) |
| 4 | +# |
| 5 | +# SSOT: This script installs EVERYTHING a fully built MiOS system has. |
| 6 | +# It transforms a bare Fedora host into a self-building MiOS workstation. |
| 7 | +# |
3 | 8 | set -euo pipefail |
4 | 9 |
|
| 10 | +# ============================================================================ |
| 11 | +# Defaults |
| 12 | +# ============================================================================ |
5 | 13 | DEFAULT_USER="mios" |
6 | 14 | DEFAULT_HOST="mios" |
7 | 15 | DEFAULT_USER_FULLNAME="MiOS User" |
| 16 | +DEFAULT_USER_SHELL="/bin/bash" |
8 | 17 | DEFAULT_USER_GROUPS="wheel,libvirt,kvm,video,render,input,dialout" |
| 18 | +DEFAULT_SSH_KEY_TYPE="ed25519" |
| 19 | +DEFAULT_BRANCH="main" |
| 20 | + |
9 | 21 | MIOS_REPO="https://github.com/mios-dev/MiOS.git" |
| 22 | +PROFILE_DIR="/etc/mios" |
| 23 | +PROFILE_FILE="${PROFILE_DIR}/install.env" |
| 24 | + |
| 25 | +# ============================================================================ |
| 26 | +# Logging & UI |
| 27 | +# ============================================================================ |
| 28 | +_BOLD=$(tput bold 2>/dev/null || echo "") |
| 29 | +_RED=$(tput setaf 1 2>/dev/null || echo "") |
| 30 | +_GREEN=$(tput setaf 2 2>/dev/null || echo "") |
| 31 | +_YELLOW=$(tput setaf 3 2>/dev/null || echo "") |
| 32 | +_CYAN=$(tput setaf 6 2>/dev/null || echo "") |
| 33 | +_DIM=$(tput dim 2>/dev/null || echo "") |
| 34 | +_RESET=$(tput sgr0 2>/dev/null || echo "") |
| 35 | + |
| 36 | +log_info() { printf '%s[INFO]%s %s\n' "${_CYAN}" "${_RESET}" "$*"; } |
| 37 | +log_ok() { printf '%s[ OK ]%s %s\n' "${_GREEN}" "${_RESET}" "$*"; } |
| 38 | +log_warn() { printf '%s[WARN]%s %s\n' "${_YELLOW}" "${_RESET}" "$*" >&2; } |
| 39 | +log_err() { printf '%s[ERR ]%s %s\n' "${_RED}" "${_RESET}" "$*" >&2; } |
| 40 | +log_phase() { printf '\n%s%s== %s ==%s\n\n' "${_BOLD}" "${_CYAN}" "$*" "${_RESET}"; } |
| 41 | + |
| 42 | +require_root() { |
| 43 | + if [[ $EUID -ne 0 ]]; then |
| 44 | + log_err "Bootstrap must run as root: sudo $0" |
| 45 | + exit 1 |
| 46 | + fi |
| 47 | +} |
| 48 | + |
| 49 | +detect_host_kind() { |
| 50 | + if command -v bootc >/dev/null 2>&1 && bootc status --format=json 2>/dev/null | grep -q '"booted"'; then |
| 51 | + echo "bootc" |
| 52 | + elif [[ -f /etc/os-release ]] && grep -qE '^ID(_LIKE)?=.*fedora' /etc/os-release; then |
| 53 | + echo "fhs-fedora" |
| 54 | + else |
| 55 | + echo "unsupported" |
| 56 | + fi |
| 57 | +} |
| 58 | + |
| 59 | +check_network() { |
| 60 | + local host |
| 61 | + for host in github.com; do |
| 62 | + if ! curl -fsSL --max-time 5 -o /dev/null "https://${host}/" 2>/dev/null; then |
| 63 | + log_err "No network reachability to ${host}." |
| 64 | + exit 1 |
| 65 | + fi |
| 66 | + done |
| 67 | + log_ok "Network reachability verified" |
| 68 | +} |
10 | 69 |
|
11 | | -log_info() { printf '\033[36m[INFO]\033[0m %s\n' "$*"; } |
12 | | -log_ok() { printf '\033[32m[ OK ]\033[0m %s\n' "$*"; } |
13 | | -log_err() { printf '\033[31m[ERR ]\033[0m %s\n' "$*" >&2; } |
| 70 | +prompt_default() { |
| 71 | + local question="$1" default="$2" answer |
| 72 | + read -r -p "$(printf '%s%s%s [%s%s%s]: ' "${_BOLD}" "${question}" "${_RESET}" "${_DIM}" "${default}" "${_RESET}")" answer |
| 73 | + echo "${answer:-$default}" |
| 74 | +} |
14 | 75 |
|
15 | | -require_root() { [[ $EUID -eq 0 ]] || { log_err "Must run as root"; exit 1; }; } |
| 76 | +prompt_password() { |
| 77 | + local prompt="$1" pw1 pw2 |
| 78 | + while :; do |
| 79 | + printf '%s%s%s: ' "${_BOLD}" "${prompt}" "${_RESET}" >&2 |
| 80 | + read -rs pw1; echo >&2 |
| 81 | + printf '%sConfirm:%s ' "${_BOLD}" "${_RESET}" >&2 |
| 82 | + read -rs pw2; echo >&2 |
| 83 | + if [[ "$pw1" == "$pw2" && -n "$pw1" ]]; then |
| 84 | + echo "$pw1" |
| 85 | + return 0 |
| 86 | + fi |
| 87 | + log_warn "Passwords don't match or are empty." |
| 88 | + done |
| 89 | +} |
16 | 90 |
|
| 91 | +prompt_yesno() { |
| 92 | + local question="$1" default="${2:-y}" answer hint |
| 93 | + if [[ "$default" == "y" ]]; then hint="[Y/n]"; else hint="[y/N]"; fi |
| 94 | + read -r -p "$(printf '%s%s%s %s: ' "${_BOLD}" "${question}" "${_RESET}" "${hint}")" answer |
| 95 | + answer="${answer:-$default}" |
| 96 | + case "${answer,,}" in |
| 97 | + y|yes) return 0 ;; |
| 98 | + *) return 1 ;; |
| 99 | + esac |
| 100 | +} |
| 101 | + |
| 102 | +# ============================================================================ |
| 103 | +# Core Logic |
| 104 | +# ============================================================================ |
17 | 105 | main() { |
18 | 106 | require_root |
19 | | - log_info "Starting MiOS Bootstrap..." |
| 107 | + log_phase "MiOS Bootstrap Installer (Full Build Mode)" |
| 108 | + |
| 109 | + local hostkind=$(detect_host_kind) |
| 110 | + if [[ "$hostkind" == "unsupported" ]]; then |
| 111 | + log_err "Host is not Fedora. MiOS requires a Fedora-based host." |
| 112 | + exit 1 |
| 113 | + fi |
| 114 | + log_info "Detected host: ${hostkind}" |
| 115 | + |
| 116 | + check_network |
| 117 | + |
| 118 | + # --- 1. Gather Profile --- |
| 119 | + LINUX_USER="$(prompt_default 'Linux username' "${DEFAULT_USER}")" |
| 120 | + HOSTNAME_VAL="$(prompt_default 'Hostname' "${DEFAULT_HOST}")" |
| 121 | + USER_FULLNAME="$(prompt_default 'Full name (GECOS)' "${DEFAULT_USER_FULLNAME}")" |
| 122 | + USER_PASSWORD="$(prompt_password 'Password')" |
| 123 | + |
| 124 | + log_phase "Review profile" |
| 125 | + printf " User: %s\n Host: %s\n Mode: Total Root Overlay\n\n" "$LINUX_USER" "$HOSTNAME_VAL" |
| 126 | + if ! prompt_yesno 'Proceed with these settings?' y; then exit 0; fi |
| 127 | + |
| 128 | + # --- 2. Apply Profile --- |
| 129 | + log_phase "Applying system profile" |
| 130 | + hostnamectl set-hostname "$HOSTNAME_VAL" |
20 | 131 |
|
21 | | - # 1. User/Group handling (GRACEFUL) |
22 | 132 | local existing_groups="" |
23 | | - IFS=',' read -ra ADDR <<< "$DEFAULT_USER_GROUPS" |
24 | | - for g in "${ADDR[@]}"; do |
25 | | - getent group "$g" >/dev/null && { [[ -n "$existing_groups" ]] && existing_groups+=","; existing_groups+="$g"; } |
| 133 | + IFS=',' read -ra ADDR <<< "${DEFAULT_USER_GROUPS}" |
| 134 | + for group in "${ADDR[@]}"; do |
| 135 | + if getent group "$group" >/dev/null; then |
| 136 | + [[ -n "$existing_groups" ]] && existing_groups+="," |
| 137 | + existing_groups+="$group" |
| 138 | + else |
| 139 | + log_warn "Group '$group' missing on host, skipping." |
| 140 | + fi |
26 | 141 | done |
27 | 142 |
|
28 | | - if id -u "$DEFAULT_USER" >/dev/null 2>&1; then |
29 | | - usermod -aG "$existing_groups" "$DEFAULT_USER" |
| 143 | + if id -u "$LINUX_USER" >/dev/null 2>&1; then |
| 144 | + log_info "User '$LINUX_USER' exists; updating groups + password" |
| 145 | + usermod -aG "$existing_groups" "$LINUX_USER" |
| 146 | + usermod -c "$USER_FULLNAME" "$LINUX_USER" |
30 | 147 | else |
31 | | - useradd -m -G "$existing_groups" -s /bin/bash -c "$DEFAULT_USER_FULLNAME" "$DEFAULT_USER" |
| 148 | + log_info "Creating '$LINUX_USER' (groups: $existing_groups)" |
| 149 | + useradd -m -G "$existing_groups" -s "$DEFAULT_USER_SHELL" -c "$USER_FULLNAME" "$LINUX_USER" |
32 | 150 | fi |
33 | | - log_ok "User configured with available groups: $existing_groups" |
| 151 | + echo "$LINUX_USER:$USER_PASSWORD" | chpasswd |
| 152 | + log_ok "User profile applied." |
34 | 153 |
|
35 | | - # 2. Total Root Merge |
36 | | - log_info "Merging MiOS repository onto / ..." |
37 | | - [[ -d "/.git" ]] || git init / |
38 | | - git -C / remote add origin "$MIOS_REPO" 2>/dev/null || git -C / remote set-url origin "$MIOS_REPO" |
39 | | - git -C / fetch --depth=1 origin main |
40 | | - git -C / checkout -f main |
41 | | - log_ok "Root merge complete." |
| 154 | + # --- 3. Total Root Merge --- |
| 155 | + log_phase "MiOS Core Installation (Root Merge)" |
| 156 | + log_info "Merging MiOS repository onto system root (/) ..." |
| 157 | + if [[ ! -d "/.git" ]]; then |
| 158 | + git init / |
| 159 | + git -C / remote add origin "$MIOS_REPO" 2>/dev/null || git -C / remote set-url origin "$MIOS_REPO" |
| 160 | + fi |
| 161 | + git -C / fetch --depth=1 origin "$DEFAULT_BRANCH" |
| 162 | + git -C / checkout -f "$DEFAULT_BRANCH" |
| 163 | + log_ok "MiOS source tree merged to root." |
| 164 | + |
| 165 | + # --- 4. Package Installation --- |
| 166 | + log_phase "Installing MiOS System Stack" |
| 167 | + if [[ -f "/usr/share/mios/PACKAGES.md" ]]; then |
| 168 | + log_info "Extracting package list from /usr/share/mios/PACKAGES.md..." |
| 169 | + local pkgs |
| 170 | + pkgs=$(sed -n '/^```packages-/,/^```$/{/^```/d;/^#/d;/^$/d;p}' /usr/share/mios/PACKAGES.md | tr '\n' ' ') |
| 171 | + |
| 172 | + if [[ -n "$pkgs" ]]; then |
| 173 | + local dnf_cmd="dnf" |
| 174 | + command -v dnf5 >/dev/null 2>&1 && dnf_cmd="dnf5" |
| 175 | + log_info "Executing: $dnf_cmd install -y --skip-unavailable --best [PACKAGES]" |
| 176 | + $dnf_cmd install -y --skip-unavailable --best $pkgs || log_warn "Some packages failed to install." |
| 177 | + log_ok "Package stack installation complete." |
| 178 | + else |
| 179 | + log_err "No packages found in manifest!" |
| 180 | + fi |
| 181 | + else |
| 182 | + log_err "CRITICAL: /usr/share/mios/PACKAGES.md not found!" |
| 183 | + exit 1 |
| 184 | + fi |
42 | 185 |
|
43 | | - # 3. System installer |
| 186 | + # --- 5. System Initialization --- |
| 187 | + log_phase "System Initialization" |
44 | 188 | if [[ -x "/install.sh" ]]; then |
45 | | - log_info "Running /install.sh ..." |
| 189 | + log_info "Running /install.sh to finalize FHS overlay..." |
46 | 190 | /install.sh |
| 191 | + log_ok "Initialization complete." |
| 192 | + else |
| 193 | + log_err "/install.sh not found or not executable!" |
| 194 | + exit 1 |
| 195 | + fi |
| 196 | + |
| 197 | + log_phase "MiOS Installation Complete" |
| 198 | + if prompt_yesno 'Reboot now to enter MiOS?' y; then |
| 199 | + systemctl reboot |
47 | 200 | fi |
48 | | - log_ok "MiOS Installation Finished." |
49 | 201 | } |
50 | 202 |
|
51 | 203 | main "$@" |
0 commit comments