Streamline the installer for fresh Arch installs

This commit is contained in:
SirBlob committed 2026-09-24 12:00:22 -04:00
1 parent 501d066e64
commit d59725f193
14 files changed
+234 -44

No files matched your search

+16 -1
View File
@@ -59,6 +59,17 @@ and nothing to re-apply after somebody else's release.
## Install
On a fresh Arch install, logged in as your own user with sudo and a network
connection:
```bash
curl -fsSL https://git.sirblob.co/SirBlob/blomarchy/raw/branch/main/bootstrap.sh | bash
```
`bootstrap.sh` installs `git`, clones this repository to `~/dotfiles` (or
`BLOB_CHECKOUT_DIR`), and runs `install.sh`, passing any flags through with
`bash -s -- --autologin`.
`BLOB_PATH` is a symlink to this checkout, so `bin/`, `shell/`, `themes/` and
`default/` are always the working tree and edits are live. Only the handful of
files that must sit under `~/.config` get copied.
@@ -75,6 +86,7 @@ It runs these steps, in this order, and every one of them is idempotent:
| Step | What it does |
| --- | --- |
| 0. Preflight | stops if run as root, off Arch, without working sudo, or offline, warns when another network manager is enabled next to NetworkManager, and keeps sudo alive for the whole run |
| 1. Directories | creates the 19 directories the shell and scripts expect, before anything writes into them |
| 2. `BLOB_PATH` | symlinks `~/.local/share/blob` to this checkout |
| 3. Packages | installs what is missing from `packages/`, bootstrapping `yay` first on a machine that has no AUR helper |
@@ -85,7 +97,10 @@ It runs these steps, in this order, and every one of them is idempotent:
| 8. System services | enables NetworkManager, resolved, bluetooth, cups, avahi, docker, power-profiles-daemon, and oomd, masks `NetworkManager-wait-online`, sets the boot splash to `branding/blob_boot.png`, and sets GRUB up with every other installed OS in its menu |
| 9. Login screen | installs the session entry and the SDDM config, enables `sddm.service`, and sets the default target to `graphical.target` |
| 10. Reload | reloads systemd, restarts the running user services, and in a running session re-renders the theme, reloads Hyprland, and restarts the Blob shell |
| 11. Launch | starts the login screen, so the desktop comes up without a reboot |
| 11. Launch | offers a reboot, or starts the login screen straight away if you decline |
Output is grouped into sections, and every warning is repeated in a summary at
the end so a failed AUR build is not lost in the scroll.
Step 9 is the one a plain Arch install cannot do without: SDDM's stock Wayland
greeter runs on `weston`, which Blob does not install, so the greeter is pointed
Executable
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
set -e
repo_url="${BLOB_REPO_URL:-https://git.sirblob.co/SirBlob/blomarchy.git}"
repo_branch="${BLOB_REPO_BRANCH:-main}"
checkout_dir="${BLOB_CHECKOUT_DIR:-$HOME/dotfiles}"
if (( EUID == 0 )); then
echo "Run this as your own user, not as root. It asks for sudo when it needs it." >&2
exit 1
fi
if ! command -v pacman &>/dev/null; then
echo "pacman not found; Blob installs on Arch Linux only." >&2
exit 1
fi
if ! command -v git &>/dev/null; then
echo "Installing git"
sudo pacman -Syu --needed --noconfirm git
fi
if [[ -d $checkout_dir/.git ]]; then
echo "Updating $checkout_dir"
git -C "$checkout_dir" pull --ff-only
else
echo "Cloning $repo_url into $checkout_dir"
git clone --branch "$repo_branch" "$repo_url" "$checkout_dir"
fi
exec "$checkout_dir/install.sh" "$@" </dev/tty
+22 -6
View File
@@ -8,8 +8,10 @@ with `--check`.
| Path | Role |
| --- | --- |
| `bootstrap.sh` | one-command install on a bare machine: installs git, clones, runs `install.sh` |
| `install.sh` | flags and the order the steps run in |
| `install/helpers/lib.sh` | reporting, file copies, unit queries |
| `install/helpers/lib.sh` | reporting, sections, the warning summary, file copies, unit queries |
| `install/steps/preflight.sh` | the checks that stop a run early, and the sudo keepalive |
| `install/helpers/as-root.sh` | `as_root`, a no-op when already root |
| `install/steps/directories.sh` | the directories the shell and scripts expect |
| `install/steps/packages.sh` | repo and AUR packages, and the yay bootstrap |
@@ -23,6 +25,16 @@ with `--check`.
## Steps
0. **Preflight** stops before anything is written when the installer is run as
root (it installs into `$HOME`, and `makepkg` refuses to build as root), when
`pacman` is missing, when `sudo` does not accept the user, or when
`archlinux.org` cannot be reached and packages are wanted. It warns when
`systemd-networkd`, `iwd`, `dhcpcd` or `netctl` is enabled, because
NetworkManager is enabled below and two network managers fight over the same
interface after a reboot. `archinstall`'s "copy ISO network config" option
leaves exactly that behind. Once sudo is accepted it is refreshed every
minute in the background, so a long AUR build does not stop halfway for a
password.
1. **Directories** are created first, because nothing below can write into a
directory that is not there: `~/.config/blob`, `~/.config/systemd/user/app.slice.d`,
`~/.local/state/blob`, `~/.local/share` and `~/.cache/blob`. A fresh Arch
@@ -32,9 +44,10 @@ with `--check`.
create the parent of a link, so this step makes `~/.local/share` itself rather
than assuming step 1 ran.
3. **Packages** come from `packages/blob.packages` and
`packages/blob-aur.packages`. A machine with no AUR helper gets `yay-bin`
built once with `makepkg`. A failed batch refreshes the databases and retries,
then falls back to one package at a time, so a single name that no longer
`packages/blob-aur.packages`. Repo packages go in with a full
`pacman -Syu`, never a partial upgrade. A machine with no AUR helper gets
`yay-bin` built once with `makepkg`. A failed batch upgrades the system on
its own, then falls back to one package at a time, so a single name that no longer
resolves cannot cost you the other hundred and the config, service and login
steps below. See [packages.md](packages.md).
4. **Config** is copied into `~/.config`: `hypr/`, the themed app configs,
@@ -152,8 +165,11 @@ with `--check`.
11. **Launch.** `sddm`, `hyprland`, `quickshell` and `uwsm` are checked one last
time and any that are missing are named, because that is the whole difference
between a desktop and a console. Then, when the installer was run from a
console and nothing graphical is running, it starts `sddm.service` so the
desktop appears without a reboot. `--no-launch` prints the command instead.
console and nothing graphical is running, it offers a reboot, which brings
up NetworkManager, the boot splash and any new kernel modules together.
Declining starts `sddm.service` so the desktop appears without one.
`--no-launch` prints the command instead. Every warning from the run is
listed again just before this step.
`--autologin` writes `/etc/sddm.conf.d/zzz-blob-autologin.conf` for the
current user.
+6 -4
View File
@@ -84,10 +84,12 @@ packages pulled in under a different name no longer come back on every run. AUR
packages are built one at a time, so a build that fails is reported at the end
instead of stopping the install.
Repo packages go in one `pacman` call. If that call fails the databases are
refreshed with `pacman -Sy` and it is tried again, because a stale database is
what turns an installed-and-present package into `target not found`. If it still
fails the packages are installed one at a time, so one unresolvable name does not
Repo packages go in one `pacman -Syu --needed` call, so the databases are
fresh and the system is upgraded in the same transaction. Refreshing the
databases without upgrading (`-Sy` then `-S`) is a partial upgrade, which Arch
does not support and which breaks libraries on a fresh install whose ISO is a
few weeks old. If that call fails the system is upgraded on its own and the
packages are installed one at a time, so one unresolvable name does not
block the other hundred - which is how a missing `sddm` used to turn into a
machine with no login screen. `install.sh` ends by naming any of `sddm`,
`hyprland`, `quickshell` and `uwsm` that are still not installed.
+14 -1
View File
@@ -45,6 +45,7 @@ while (( $# > 0 )); do
done
source "$repo_dir/install/helpers/lib.sh"
source "$repo_dir/install/steps/preflight.sh"
source "$repo_dir/install/steps/directories.sh"
source "$repo_dir/install/steps/packages.sh"
source "$repo_dir/install/steps/config.sh"
@@ -60,12 +61,18 @@ say "repo: $repo_dir"
if [[ $check == true ]]; then
say "mode: check (nothing will be written)"
fi
say ""
section "Preflight"
run_preflight
section "Directories"
create_directories
link_blob_path
section "Packages"
install_packages
section "Config"
install_shipped_config
remove_omarchy_font
refresh_font_cache
@@ -76,16 +83,19 @@ install_systemd_dropins
install_zen_config
install_default_apps
section "Theme"
link_wallpapers
install_default_theme
install_default_background
link_app_themes
section "Services"
enable_system_services
mask_system_services
install_boot_splash
enable_user_units
section "Login screen"
install_login_config
install_session_entry
install_autologin
@@ -94,10 +104,13 @@ report_greeter_compositor
report_login_conflicts
report_desktop_essentials
section "Reload"
reload_system_services
reload_user_services
reload_session
report_warnings
say ""
if [[ $check == true ]]; then
say "$changes item(s) would change."
+22
View File
@@ -4,6 +4,28 @@ say() {
printf '%s\n' "$1"
}
warnings=()
warn() {
warnings+=("$1")
say "WARN $1"
}
section() {
say ""
say "==> $1"
}
report_warnings() {
(( ${#warnings[@]} == 0 )) && return 0
say ""
say "${#warnings[@]} warning(s), details above:"
local message
for message in "${warnings[@]}"; do
say " - $message"
done
}
note_change() {
changes=$((changes + 1))
}
+1 -1
View File
@@ -32,6 +32,6 @@ install_boot_splash() {
if BLOB_PATH="$blob_path" "$repo_dir/bin/blob-boot" "$boot_splash_image"; then
say "set boot splash to branding/blob_boot.png"
else
say "WARN could not set the boot splash; run 'blob boot' to retry"
warn "could not set the boot splash; run 'blob boot' to retry"
fi
}
+1 -1
View File
@@ -15,7 +15,7 @@ link_blob_path() {
fi
if [[ -e $blob_path && ! -L $blob_path ]]; then
say "WARN $blob_path exists and is not a symlink; move it aside first"
warn "$blob_path exists and is not a symlink; move it aside first"
note_change
return 0
fi
+21 -7
View File
@@ -71,7 +71,7 @@ report_greeter_compositor() {
[[ -n $binary ]] || return 0
command -v "$binary" &>/dev/null && return 0
say "WARN the greeter's compositor command '$binary' is not installed"
warn "the greeter's compositor command '$binary' is not installed"
say " SDDM cannot draw a greeter without it; it is set in $blob_greeter_conf"
}
@@ -93,7 +93,7 @@ report_login_conflicts() {
session_entry_exists "$session" && continue
found=true
say "WARN $file autologs into $session, which is not installed"
warn "$file autologs into $session, which is not installed"
done
if [[ $found == true ]]; then
@@ -107,10 +107,10 @@ report_login_conflicts() {
enable_display_manager() {
if ! system_unit_exists sddm.service; then
if package_installed sddm; then
say "WARN sddm is installed but sddm.service is not visible to systemd"
warn "sddm is installed but sddm.service is not visible to systemd"
say " run 'sudo systemctl daemon-reload', then ./install.sh again"
else
say "WARN sddm is not installed; the desktop cannot start at boot"
warn "sddm is not installed; the desktop cannot start at boot"
say " install it with 'sudo pacman -S sddm', then ./install.sh again"
fi
return 0
@@ -144,7 +144,7 @@ enable_display_manager() {
if as_root systemctl enable sddm.service; then
say "enable sddm.service"
else
say "WARN could not enable sddm.service"
warn "could not enable sddm.service"
fi
fi
@@ -152,7 +152,7 @@ enable_display_manager() {
if as_root systemctl set-default graphical.target; then
say "set default target to graphical.target"
else
say "WARN could not set the default target to graphical.target"
warn "could not set the default target to graphical.target"
fi
fi
}
@@ -184,8 +184,22 @@ launch_desktop() {
return 0
fi
if reboot_chosen; then
say "Rebooting into Blob."
as_root systemctl reboot
return 0
fi
say "Starting the login screen. Pick the Blob session to log in."
as_root systemctl start sddm.service || {
say "WARN sddm.service did not start; see 'systemctl status sddm'"
warn "sddm.service did not start; see 'systemctl status sddm'"
}
}
reboot_chosen() {
[[ -r /dev/tty ]] || return 1
local answer
say "A reboot brings up the network, boot splash and new kernel modules together."
read -rp "Reboot now? [Y/n] " answer </dev/tty
[[ -z $answer || $answer == [yY]* ]]
}
+8 -17
View File
@@ -54,8 +54,6 @@ install_packages() {
return 0
fi
blob_sudo_keepalive
if (( ${#repo_missing[@]} )); then
say "install ${#repo_missing[@]} repo package(s)"
install_repo_packages "${repo_missing[@]}"
@@ -63,7 +61,7 @@ install_packages() {
if (( ${#aur_missing[@]} )); then
ensure_aur_helper || {
say "WARN no AUR helper; skipping ${#aur_missing[@]} AUR package(s): ${aur_missing[*]}"
warn "no AUR helper; skipping ${#aur_missing[@]} AUR package(s): ${aur_missing[*]}"
return 0
}
say "install ${#aur_missing[@]} AUR package(s)"
@@ -75,20 +73,17 @@ install_repo_packages() {
local package
local -a failed=()
as_root pacman -S --needed --noconfirm "$@" && return 0
as_root pacman -Syu --needed --noconfirm "$@" && return 0
say "WARN the batch install failed; refreshing the package databases"
as_root pacman -Sy --noconfirm || true
as_root pacman -S --needed --noconfirm "$@" && return 0
say "WARN still failing; retrying one package at a time"
say " the batch install failed; upgrading the system, then one package at a time"
as_root pacman -Syu --noconfirm || true
for package in "$@"; do
as_root pacman -S --needed --noconfirm "$package" || failed+=("$package")
done
(( ${#failed[@]} == 0 )) && return 0
say "WARN ${#failed[@]} repo package(s) failed to install: ${failed[*]}"
warn "${#failed[@]} repo package(s) failed to install: ${failed[*]}"
}
desktop_essentials=(sddm hyprland quickshell uwsm)
@@ -106,7 +101,7 @@ report_desktop_essentials() {
return 0
fi
say "WARN the desktop cannot start without: ${missing[*]}"
warn "the desktop cannot start without: ${missing[*]}"
say " install them with 'sudo pacman -S ${missing[*]}', then ./install.sh again"
}
@@ -120,8 +115,8 @@ install_aur_packages() {
(( ${#failed[@]} == 0 )) && return 0
say "WARN ${#failed[@]} AUR package(s) failed to build: ${failed[*]}"
say "WARN the install continued; retry them with 'yay -S <name>'"
warn "${#failed[@]} AUR package(s) failed to build: ${failed[*]}"
say " the install continued; retry them with 'yay -S <name>'"
}
# A fresh Arch install has no AUR helper, and yay is not in the official
@@ -148,7 +143,3 @@ ensure_aur_helper() {
rm -rf "$build_dir"
command -v yay &>/dev/null
}
blob_sudo_keepalive() {
sudo -v 2>/dev/null || true
}
+85
View File
@@ -0,0 +1,85 @@
conflicting_network_services=(
systemd-networkd.service
iwd.service
dhcpcd.service
netctl.service
)
stop_install() {
printf 'ERROR %s\n' "$1" >&2
shift
local detail
for detail in "$@"; do
printf ' %s\n' "$detail" >&2
done
exit 1
}
require_regular_user() {
(( EUID != 0 )) && return 0
stop_install "run install.sh as your own user, not as root" \
"it installs into \$HOME and builds AUR packages, which makepkg refuses to do as root" \
"it asks for sudo when it needs it"
}
require_arch() {
command -v pacman &>/dev/null && return 0
stop_install "pacman not found; Blob installs on Arch Linux only"
}
require_sudo() {
[[ $check == true ]] && return 0
command -v sudo &>/dev/null || stop_install "sudo is not installed" \
"as root: pacman -S sudo, then add your user with visudo"
sudo -v || stop_install "sudo did not accept this user" \
"as root: add your user to the wheel group and allow it in visudo"
}
require_internet() {
[[ $check == true || $skip_packages == true ]] && return 0
curl -fsS --max-time 15 -o /dev/null https://archlinux.org && return 0
stop_install "no internet connection; packages cannot be downloaded" \
"connect first (iwctl for Wi-Fi), or rerun with --skip-packages"
}
report_network_conflicts() {
local unit
local -a enabled=()
for unit in "${conflicting_network_services[@]}"; do
[[ "$(system_unit_state "$unit")" == enabled ]] && enabled+=("$unit")
done
(( ${#enabled[@]} == 0 )) && return 0
warn "NetworkManager will be enabled, but so is: ${enabled[*]}"
say " two network managers fight over the same interfaces after a reboot"
say " once NetworkManager works: sudo systemctl disable ${enabled[*]}"
}
sudo_keepalive_pid=""
start_sudo_keepalive() {
[[ $check == true ]] && return 0
while true; do
sudo -n true 2>/dev/null
sleep 60
done &
sudo_keepalive_pid=$!
trap stop_sudo_keepalive EXIT
}
stop_sudo_keepalive() {
[[ -n $sudo_keepalive_pid ]] && kill "$sudo_keepalive_pid" 2>/dev/null
sudo_keepalive_pid=""
}
run_preflight() {
require_regular_user
require_arch
require_sudo
require_internet
report_network_conflicts
start_sudo_keepalive
say "ok preflight"
}
+4 -4
View File
@@ -13,14 +13,14 @@ reload_system_services() {
fi
if ! as_root systemctl daemon-reload; then
say "WARN could not reload systemd"
warn "could not reload systemd"
return 0
fi
local unit
for unit in "${reloaded_system_services[@]}"; do
system_unit_exists "$unit" || continue
as_root systemctl try-restart "$unit" || say "WARN could not restart $unit"
as_root systemctl try-restart "$unit" || warn "could not restart $unit"
done
say "ok system services reloaded"
@@ -45,7 +45,7 @@ reload_user_services() {
done
if (( ${#failed[@]} > 0 )); then
say "WARN could not restart: ${failed[*]}"
warn "could not restart: ${failed[*]}"
else
say "ok user services reloaded"
fi
@@ -58,7 +58,7 @@ run_session_reload() {
if BLOB_PATH="$blob_path" PATH="$blob_path/bin:$PATH" "$@" &>/dev/null; then
say "ok $label reloaded"
else
say "WARN could not reload $label"
warn "could not reload $label"
fi
}
+1 -1
View File
@@ -135,7 +135,7 @@ enable_user_units() {
local unit
if ! user_manager_available; then
say "WARN no user systemd manager; skipping user services"
warn "no user systemd manager; skipping user services"
return 0
fi
+1 -1
View File
@@ -55,7 +55,7 @@ install_default_theme() {
blob-theme-set "$default_theme" >/dev/null; then
say "theme $default_theme"
else
say "WARN could not apply the $default_theme theme"
warn "could not apply the $default_theme theme"
say " the session will come up without a palette; run 'blob theme set $default_theme'"
fi
}