diff --git a/.gitignore b/.gitignore index 1fa2b3a..5179df1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ out/ # In-image copy of localrepo/ — populated by localrepo/update.sh and # .gitea/workflows/build-iso.yaml, shipped as /opt/localrepo on the ISO airootfs/opt/localrepo/ + +# Build stamp staged by profiledef.sh at ISO build time (see AGENTS.md); +# consumed by customize_airootfs.sh, never committed +airootfs/root/.iso-build-stamp diff --git a/AGENTS.md b/AGENTS.md index e6b7960..9497644 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,12 +49,12 @@ install them into the target without the network. | File | Purpose | |------|---------| | `packages.x86_64` | Live ISO package list (~402 packages) | -| `profiledef.sh` | ISO metadata, compression, boot modes | +| `profiledef.sh` | ISO metadata, compression, boot modes, build-stamp staging | | `pacman.conf` | Repos: cachyos → core → extra → multilib → chaotic-aur → local-aur | | `bootstrap_packages` | Minimal build-time packages (`base`, `arch-install-scripts`) | | `airootfs/root/customize_airootfs.sh` | Runs inside chroot during build: keyrings, skel dotfiles (incl. end4-pC), qsConfig patch, end-4 `install-deps`, package harvest, uv/font pre-warm | | `airootfs/root/.live-pacman.conf` | `/etc/pacman.conf` of the built image (adds `[local-aur]` = `file:///opt/localrepo`) | -| `airootfs/usr/local/bin/alvin-install` | Installer: partitions, pacstrap, user, end-4 dotfiles (`--skip-alldeps`), boot | +| `airootfs/usr/local/bin/alvin-install` | Installer: partitions, pacstrap, LUKS (systemd initramfs **or** experimental unl0kr + optional TPM2 unlock), user, git identity + GitHub SSH keys, login manager (none/greetd/sddm/gdm) + autologin, end-4 dotfiles (`--skip-alldeps`), build-stamp motd, boot | | `localrepo/build-end4-deps.sh` | Non-interactive AUR builds for end-4 dependencies | | `localrepo/update.sh` | Regenerates localrepo DB, syncs names to packages.x86_64, syncs `airootfs/opt/localrepo/` | @@ -72,3 +72,4 @@ install them into the target without the network. - End-4 metapackages are built from `localrepo/end4-pkgbuilds/` (quickshell-git, bibata-modern-classic-bin, microtex-git). - `packages.x86_64` is sorted alphabetically. Keep it sorted when adding/removing entries. - `airootfs/opt/localrepo/` is a build artifact (gitignored) — never commit it. +- **Build stamp flow**: `profiledef.sh` (sourced by mkarchiso) stages the gitignored `airootfs/root/.iso-build-stamp` when a `.git` dir exists **or** `CI`/`GITEA_ACTIONS` is set — line reads `AlvinArch build | commit | <4-char hash>` (falls back to `commit unknown` if `git -c safe.directory` can't read the repo; without `.git`/CI no file is staged). `customize_airootfs.sh` installs it as `/etc/alvin-build-stamp` on the live medium, and `alvin-install`'s `install_motd` copies the live `/etc/motd` banner to the target and appends the stamp. If the medium has no stamp, the installed system gets no motd from us. diff --git a/README.md b/README.md index 6474550..ff3407d 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ AlvinArch/ ### Key Features - **Hyprland + end-4** — Wayland compositor with illogical-impulse dotfiles (quickshell, bibata cursors, microtex) +- **Automated installer (`alvin-install`)** — gum TUI: LUKS with systemd initramfs or experimental touchscreen unl0kr, optional TPM2 auto-unlock, GitHub SSH key import, login manager choice (none/greetd/sddm/gdm), autologin, and a build stamp in `/etc/motd` - **LazyVim** — Pre-configured Neovim with lazy.nvim and curated plugins - **Fish + Zsh + Starship** — Dual shell setup with oh-my-zsh and a cross-shell prompt - **Local AUR repo** — Pre-built packages (end-4 deps, wallust, grub-customizer, etc.) that aren't in official repos diff --git a/airootfs/root/customize_airootfs.sh b/airootfs/root/customize_airootfs.sh index 0aae764..77e2b3f 100644 --- a/airootfs/root/customize_airootfs.sh +++ b/airootfs/root/customize_airootfs.sh @@ -14,6 +14,13 @@ pacman-key --populate archlinux cachyos chaotic install -m 0644 /root/.live-pacman.conf /etc/pacman.conf rm -f /root/.live-pacman.conf +# Build stamp staged by profiledef.sh (absent for non-git, non-CI builds): +# keep it where alvin-install looks for it, drop the staging copy. +if [[ -f /root/.iso-build-stamp ]]; then + install -m 0644 /root/.iso-build-stamp /etc/alvin-build-stamp + rm -f /root/.iso-build-stamp +fi + # ============================================================================ # Pre-install dotfile dependencies into /etc/skel so new users get them # instantly on first login (no network required). diff --git a/airootfs/usr/local/bin/alvin-install b/airootfs/usr/local/bin/alvin-install index a408f60..def7ddc 100755 --- a/airootfs/usr/local/bin/alvin-install +++ b/airootfs/usr/local/bin/alvin-install @@ -23,12 +23,21 @@ TIMEZONE="" LOCALE="" LUKS="true" FILESYSTEM="ext4" +GH_USERNAME="" +UNL0KR="false" +TPM_STATUS="" +LOGIN_MANAGER="none" +AUTOLOGIN="" +TPM_DISCLAIMER="" +TARGET_DB_SYNCED="false" log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG"; } cleanup() { set +e - rm -f "${TARGET}/etc/sudoers.d/99-end4-install" 2>/dev/null + rm -f "${TARGET}/etc/sudoers.d/99-end4-install" \ + "${TARGET}/etc/sudoers.d/99-ssh-import-id" \ + "${TARGET}/etc/sudoers.d/99-unl0kr-install" 2>/dev/null umount -R "$TARGET" 2>/dev/null cryptsetup close cryptroot 2>/dev/null true @@ -58,6 +67,32 @@ part() { [[ "$DISK" =~ [0-9]$ ]] && echo "${DISK}p$1" || echo "${DISK}$1"; } valid_username() { [[ "$1" =~ ^[a-z_][a-z0-9_-]*$ ]] && (( ${#1} <= 32 )); } +# Numeric uid:gid of $USERNAME as seen inside $TARGET. Always needed for +# chowns from the live medium, whose /etc/passwd has no $USERNAME. Both ids +# come from one arch-chroot call chained with && : separate substitutions +# would only propagate the last status, so a lone `id -u` failure could +# silently produce ':1000' and abort at the wrong line. +target_owner() { + arch-chroot "$TARGET" sh -c "id -u '$USERNAME' && id -g '$USERNAME'" | paste -sd: +} + +# Refresh the target's package databases once before the first target-side +# pacman/yay call (cachyos is appended to the target's pacman.conf after +# pacstrap, so its db is not there yet; keyrings are re-populated as a +# cheap safety net). Failures only warn - later steps fall back on their own. +sync_target_dbs() { + [[ "${TARGET_DB_SYNCED}" == "true" ]] && return 0 + info "Syncing target package databases..." + arch-chroot "$TARGET" pacman-key --populate archlinux cachyos chaotic \ + >>"$LOG" 2>&1 || true + if arch-chroot "$TARGET" pacman -Syy --noconfirm >>"$LOG" 2>&1; then + TARGET_DB_SYNCED="true" + ok "Target databases synced." + else + warn "Target pacman -Syy failed (see $LOG) - continuing with existing databases." + fi +} + check_prereqs() { command -v gum >/dev/null 2>&1 || die "gum is not installed" [[ -d /sys/firmware/efi ]] || die "UEFI not detected. This installer requires UEFI boot." @@ -193,6 +228,39 @@ screen_luks() { log "LUKS: $LUKS" } +screen_unl0kr() { + UNL0KR="false" + if [[ "$LUKS" != "true" ]]; then + log "unl0kr: disabled (LUKS off)" + return + fi + clear + header "Touchscreen Unlock (unl0kr)" + echo "" + gum style --foreground 15 --align center \ + "[EXPERIMENTAL] unl0kr lets you type the LUKS passphrase" \ + "on screen - for tablets and touch-only devices." \ + "" \ + "It puts a modified busybox crypt hook plus touchscreen" \ + "drivers into the initramfs. Test in a VM first: if it" \ + "breaks, the installed system will not boot." \ + "" \ + "Note: enabling unl0kr disables TPM2 auto-unlock" \ + "(the two are mutually exclusive)." + echo "" + # gum confirm selects its affirmative on Enter, which would default this + # experimental feature to ON; gum choose makes Enter pick the first + # (recommended) row instead. + local unl0kr_choice + unl0kr_choice=$(gum choose --header "+ unl0kr (default: no)" \ + "No - standard unlock (recommended)" \ + "Yes - experimental touchscreen unlock") || die "unl0kr question cancelled." + if [[ "$unl0kr_choice" == Yes* ]]; then + UNL0KR="true" + fi + log "unl0kr: $UNL0KR" +} + screen_filesystem() { clear header "Filesystem" @@ -244,14 +312,20 @@ screen_hostname() { screen_git() { clear - header "Git Configuration" + header "Git & SSH Configuration" echo "" - GIT_NAME=$(gum input --value "$USERNAME" --header "+ Git name") \ - || die "No git name entered." - GIT_EMAIL=$(gum input --placeholder "user@example.com" --header "+ Git email") \ - || die "No git email entered." - [[ -z "$GIT_EMAIL" ]] && die "Git email cannot be empty." - log "Git: $GIT_NAME <$GIT_EMAIL>" + gum style --foreground 15 \ + "Optional steps - press Enter on any field to skip it." \ + "Git config sets user.name/user.email for new repositories;" \ + "the GitHub username imports that account's SSH public keys." + echo "" + GIT_NAME=$(gum input --value "$USERNAME" --header "+ Git name (Enter keeps '${USERNAME}')") \ + || die "Git configuration cancelled." + GIT_EMAIL=$(gum input --placeholder "user@example.com (empty to skip)" \ + --header "+ Git email (optional)") || die "Git configuration cancelled." + GH_USERNAME=$(gum input --placeholder "octocat (empty to skip)" \ + --header "+ GitHub username for SSH keys (optional)") || die "Git configuration cancelled." + log "Git: name='${GIT_NAME}' email='${GIT_EMAIL}' github='${GH_USERNAME}'" } screen_timezone() { @@ -275,6 +349,23 @@ screen_locale() { log "Locale: $LOCALE" } +screen_login_manager() { + clear + header "Login Manager" + echo "" + gum style --foreground 15 \ + "How should the desktop session start after boot?" \ + "" \ + " none - plain tty login, Hyprland starts automatically" \ + " greetd - lightweight TUI greeter (tuigreet)" \ + " sddm - Qt display manager (already installed)" \ + " gdm - GNOME display manager (large download)" + echo "" + LOGIN_MANAGER=$(gum choose --header "+ Login manager (Enter = none)" \ + "none" "greetd" "sddm" "gdm") || die "No login manager selected." + log "Login manager: $LOGIN_MANAGER" +} + screen_summary() { clear header "Installation Summary" @@ -283,18 +374,35 @@ screen_summary() { local enc_status="No" [[ "$LUKS" == "true" ]] && enc_status="Yes (uses login password)" + local unl0kr_status="${UNL0KR}" + [[ "$LUKS" != "true" ]] && unl0kr_status="n/a" + + local git_line="skipped" + if [[ -n "$GIT_EMAIL" ]]; then + git_line="${GIT_NAME:-$USERNAME} <${GIT_EMAIL}>" + fi + local ssh_line="skipped" + if [[ -n "$GH_USERNAME" ]]; then + ssh_line="gh:${GH_USERNAME}" + fi + gum style --foreground 15 --border normal --border-foreground 8 \ --padding "0 2" --width 60 \ "Disk: ${DISK}" \ "Encryption: ${enc_status}" \ + "unl0kr: ${unl0kr_status}" \ "Filesystem: ${FILESYSTEM}" \ "Username: ${USERNAME}" \ "Hostname: ${HOSTNAME}" \ "Keymap: ${KEYMAP}" \ "Locale: ${LOCALE}" \ "Timezone: ${TIMEZONE}" \ - "Git: ${GIT_NAME} <${GIT_EMAIL}>" + "Login mgr: ${LOGIN_MANAGER}" \ + "Git: ${git_line}" \ + "SSH keys: ${ssh_line}" + echo "" + echo "TPM2 auto-unlock (if applicable) and autologin are asked about during installation." echo "" gum confirm --affirmative "Install" --negative "Abort" \ "Proceed with installation?" || die "Installation aborted by user." @@ -516,13 +624,117 @@ install_user() { ok "User ${USERNAME} created (wheel group, sudo enabled). Root locked." } -install_git() { - info "Configuring git..." - arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ - git config --global user.name "$GIT_NAME" - arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ - git config --global user.email "$GIT_EMAIL" - ok "Git configured for ${USERNAME}." +install_git_ssh() { + local home_in_target="/home/${USERNAME}" + local owner + owner=$(target_owner) + + # ── git identity (skipped when no email was entered) ───────────────────── + if [[ -n "$GIT_EMAIL" ]]; then + info "Configuring git..." + local git_name="${GIT_NAME:-$USERNAME}" + arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ + git config --global user.name "$git_name" >>"$LOG" 2>&1 \ + || warn "git config user.name failed (see $LOG)." + arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ + git config --global user.email "$GIT_EMAIL" >>"$LOG" 2>&1 \ + || warn "git config user.email failed (see $LOG)." + ok "Git configured for ${USERNAME} (${git_name} <${GIT_EMAIL}>)." + log "Git: ${git_name} <${GIT_EMAIL}>" + else + warn "No git email entered - skipping git identity." + log "Git: skipped" + fi + + # ── SSH keys (skipped when no GitHub username was entered) ─────────────── + if [[ -z "$GH_USERNAME" ]]; then + log "SSH keys: skipped (no GitHub username)" + return 0 + fi + + info "Importing SSH keys for github.com/${GH_USERNAME}..." + local ssh_dir="${TARGET}${home_in_target}/.ssh" + mkdir -p "$ssh_dir" + # ssh-import-id runs as $USERNAME and opens ~/.ssh/authorized_keys itself; + # the mkdir above ran as root, so hand the directory over BEFORE the + # import - otherwise it dies on PermissionError every single time. + chown "$owner" "$ssh_dir" 2>/dev/null || true + chmod 700 "$ssh_dir" 2>/dev/null || true + + sync_target_dbs + + # ssh-import-id from the AUR via yay, as the new user. The AUR PKGBUILD + # does not declare its runtime dependency python-distro, so install that + # first - without it the tool dies with ModuleNotFoundError. update.sh may + # already have put ssh-import-id into the ISO (localrepo), so build only + # when the target does not have it yet. yay needs passwordless sudo here; + # the drop-in is removed immediately afterwards. + local sid_ok="false" + if arch-chroot "$TARGET" pacman -Q ssh-import-id >/dev/null 2>&1; then + sid_ok="true" + log "ssh-import-id: already present in target" + else + echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-ssh-import-id" + chmod 440 "${TARGET}/etc/sudoers.d/99-ssh-import-id" + arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ + env HOME="$home_in_target" yay -Ss ssh-import-id >>"$LOG" 2>&1 \ + || warn "yay -Ss ssh-import-id found nothing (see $LOG)." + if arch-chroot "$TARGET" pacman -S --noconfirm python-distro >>"$LOG" 2>&1 \ + && arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ + env HOME="$home_in_target" yay -S --noconfirm ssh-import-id >>"$LOG" 2>&1; then + sid_ok="true" + log "ssh-import-id: installed via yay" + else + warn "yay could not install ssh-import-id (see $LOG) - falling back to github.com keys." + fi + rm -f "${TARGET}/etc/sudoers.d/99-ssh-import-id" + fi + + if [[ "$sid_ok" == "true" ]]; then + if arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ + env HOME="$home_in_target" ssh-import-id "gh:${GH_USERNAME}" >>"$LOG" 2>&1; then + ok "SSH keys imported via ssh-import-id." + log "SSH keys: ssh-import-id gh:${GH_USERNAME}" + else + warn "ssh-import-id failed (see $LOG) - falling back to github.com keys." + sid_ok="false" + fi + fi + + # Fallback: pull the account's public keys straight from GitHub (per-key + # dedupe keeps re-runs idempotent). + if [[ "$sid_ok" != "true" ]]; then + local keys_url="https://github.com/${GH_USERNAME}.keys" + local keys="" curl_rc=0 + keys=$(curl -fsSL --max-time 30 "$keys_url" 2>>"$LOG") || curl_rc=$? + if (( curl_rc != 0 )); then + warn "Could not fetch ${keys_url} (curl exit ${curl_rc}) - no SSH keys imported." + log "SSH keys: curl failed (exit ${curl_rc}) for ${keys_url}" + elif [[ -z "$keys" ]]; then + # HTTP 200 with an empty body: the account exists but simply has + # no registered keys - not a network problem. + warn "github.com/${GH_USERNAME} has no SSH keys registered - nothing to import." + log "SSH keys: github.com/${GH_USERNAME} has no keys" + else + local auth="${ssh_dir}/authorized_keys" + local line + touch "$auth" + while IFS= read -r line; do + [[ -z "$line" ]] && continue + grep -qxF -- "$line" "$auth" 2>/dev/null || printf '%s\n' "$line" >> "$auth" + done <<< "$keys" + ok "SSH keys appended from ${keys_url}." + log "SSH keys: curl ${keys_url}" + fi + fi + + chmod 700 "$ssh_dir" 2>/dev/null || true + chown "$owner" "$ssh_dir" 2>/dev/null || true + if [[ -f "${ssh_dir}/authorized_keys" ]]; then + chmod 600 "${ssh_dir}/authorized_keys" 2>/dev/null || true + chown "$owner" "${ssh_dir}/authorized_keys" 2>/dev/null || true + fi + ok "SSH setup finished for ${USERNAME}." } install_end4_dots() { @@ -543,11 +755,8 @@ install_end4_dots() { # $USERNAME — the user exists only inside $TARGET — so name lookup fails # ("chown: invalid user"). Resolve the numeric ids from the target and # chown by number; the catch-all at the end of this function does the same. - # One arch-chroot call chained with && : a plain "a=$(id -u):$(id -g)" - # would only propagate the last substitution's status, so a lone id -u - # failure would silently produce ':1000' and abort at the wrong line. local owner - owner=$(arch-chroot "$TARGET" sh -c "id -u '$USERNAME' && id -g '$USERNAME'" | paste -sd:) + owner=$(target_owner) cp -a /opt/dots-hyprland "$repo_on_host" chown -R "$owner" "$repo_on_host" @@ -725,16 +934,65 @@ install_end4_dots() { fi } -install_mkinitcpio() { - info "Generating initramfs..." +# ── Mode A boot configuration (systemd/sd-encrypt) ──────────────────────────── +# Shared by install_mkinitcpio/install_bootloader on the happy path and by +# unl0kr_fallback_mode_a, so the hook list and kernel cmdline are written in +# exactly one shape everywhere - drift between the two places that write them +# is precisely the failure mode the fallback exists to undo. - if [[ "$LUKS" == "true" ]]; then - mkdir -p "${TARGET}/etc/mkinitcpio.conf.d" - cat > "${TARGET}/etc/mkinitcpio.conf.d/alvin.conf" < "${TARGET}/etc/mkinitcpio.conf.d/alvin.conf" <<'HOOKS' +HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt filesystems fsck) HOOKS +} + +# $1 = LUKS partition UUID, $2 = optional extra cmdline option(s), appended +# after root= (install_tpm passes rd.luks.options=...tpm2-device=auto to +# activate an enrolled TPM2 slot; without it systemd-cryptsetup never uses it). +write_mode_a_cmdline() { + local luks_uuid="$1" extra="${2:-}" + sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"rd.luks.name=${luks_uuid}=cryptroot root=/dev/mapper/cryptroot${extra:+ ${extra}}\"|" \ + "${TARGET}/etc/default/grub" +} + +install_mkinitcpio() { + info "Generating initramfs configuration..." + + if [[ "$LUKS" != "true" ]]; then + arch-chroot "$TARGET" mkinitcpio -P + ok "initramfs generated." + return fi + # Keep a pristine copy of the main config; everything this installer + # writes goes through the drop-in below (the image's archiso.conf drop-in + # and both kernel presets are untouched - the fallback image is built + # with `-S autodetect`). + local main_conf="${TARGET}/etc/mkinitcpio.conf" + if [[ -f "$main_conf" && ! -f "${main_conf}.alvin-bak" ]]; then + cp -a "$main_conf" "${main_conf}.alvin-bak" + log "mkinitcpio.conf backed up to mkinitcpio.conf.alvin-bak" + fi + + mkdir -p "${TARGET}/etc/mkinitcpio.conf.d" + + if [[ "$UNL0KR" == "true" ]]; then + # Mode B: busybox hooks + unl0kr (its ash hook parses `cryptdevice=` + # and drives the on-screen keyboard). The build is deferred to + # install_unl0kr: the package must exist first, and grub-mkconfig + # needs the final image for its initrd line. The two hook families + # must never be mixed in one HOOKS list. + cat > "${TARGET}/etc/mkinitcpio.conf.d/alvin.conf" <<'HOOKS' +HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block unl0kr encrypt filesystems fsck) +HOOKS + log "mkinitcpio: Mode B (unl0kr) HOOKS written; build deferred to install_unl0kr" + ok "initramfs hooks staged (image built after unl0kr install)." + return + fi + + # Mode A: systemd initramfs with sd-encrypt (matches the rd.luks.name + # kernel cmdline written by install_bootloader). + write_mode_a_hooks arch-chroot "$TARGET" mkinitcpio -P ok "initramfs generated." @@ -746,17 +1004,632 @@ install_bootloader() { if [[ "$LUKS" == "true" ]]; then local luks_uuid luks_uuid=$(blkid -s UUID -o value "$(part 2)") - sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"cryptdevice=UUID=${luks_uuid}:cryptroot\"|" \ + if [[ "$UNL0KR" == "true" ]]; then + # Mode B: busybox unl0kr/encrypt hooks consume cryptdevice=. + sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"cryptdevice=UUID=${luks_uuid}:cryptroot root=/dev/mapper/cryptroot\"|" \ + "${TARGET}/etc/default/grub" + else + # Mode A: systemd/sd-encrypt consumes rd.luks.name=. + write_mode_a_cmdline "$luks_uuid" + fi + log "GRUB cmdline set for UNL0KR=${UNL0KR} (uuid=${luks_uuid})" + fi + + # Force-disable EFI BootNext menu entries on the installed system: + # /etc/grub.d/31_efi_bootnext exits immediately when this is set, so a + # one-shot firmware boot selection never shows up in the GRUB menu. + if grep -q '^GRUB_DISABLE_BOOTNEXT=' "${TARGET}/etc/default/grub"; then + sed -i 's/^GRUB_DISABLE_BOOTNEXT=.*/GRUB_DISABLE_BOOTNEXT=true/' \ "${TARGET}/etc/default/grub" + else + printf '\nGRUB_DISABLE_BOOTNEXT=true\n' >> "${TARGET}/etc/default/grub" fi arch-chroot "$TARGET" grub-install \ --target=x86_64-efi --efi-directory=/boot --bootloader-id=ALVIN + + if [[ "$UNL0KR" == "true" ]]; then + # Mode B: grub.cfg is written in install_unl0kr together with the + # final initramfs (a config generated now would not match it). + log "grub-mkconfig deferred (Mode B, after unl0kr build)" + ok "Bootloader installed (config finalized after unl0kr)." + return + fi + arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg ok "Bootloader installed." } +# ── TPM2 auto-unlock ────────────────────────────────────────────────────────── + +secure_boot_enabled() { + if bootctl status 2>/dev/null | grep -q 'Secure Boot: enabled'; then + return 0 + fi + # bootctl unavailable or unclear: read the efivar directly + # (bytes 0-3 = attributes, byte 4 = SecureBoot value). + local sbvar byte + for sbvar in /sys/firmware/efi/efivars/SecureBoot-*; do + [[ -f "$sbvar" ]] || continue + byte=$(od -An -tu1 -j4 -N1 "$sbvar" 2>/dev/null | tr -d ' \n') || byte="" + [[ "$byte" == "1" ]] && return 0 + return 1 + done + return 1 +} + +install_tpm() { + TPM_STATUS="" + if [[ "$LUKS" != "true" ]]; then + TPM_STATUS="skipped-no-luks" + log "TPM: skipped (no LUKS)" + return + fi + if [[ "$UNL0KR" == "true" ]]; then + TPM_STATUS="skipped-unl0kr" + info "TPM auto-unlock skipped: unl0kr handles unlock on screen instead." + log "TPM: skipped (unl0kr enabled)" + return + fi + + info "Checking for a TPM2 device..." + # Detected on the live medium: it sees the same /dev, /sys and efivarfs + # the installed system will boot against. + if [[ ! -e /sys/class/tpm/tpm0 ]] || ! systemd-analyze has-tpm2 >>"$LOG" 2>&1; then + TPM_STATUS="unsupported" + log "TPM: unsupported (no usable TPM2 device)" + return + fi + ok "TPM2 device detected." + + local tpm_choice + tpm_choice=$(gum choose --header "+ TPM2 auto-unlock (default: no)" \ + "No - keep the passphrase only" \ + "Yes - enrol TPM2 key (PCR 0+7)") || tpm_choice="No" + if [[ "$tpm_choice" != Yes* ]]; then + TPM_STATUS="skipped" + log "TPM: declined by user" + return + fi + + info "Enrolling TPM2 key (the LUKS passphrase is supplied automatically)..." + # systemd-cryptenroll never reads stdin: the unlock passphrase comes from + # $PASSWORD (stdin would instead trigger the ask-password agent and block + # until someone typed it on a console). It only adds a keyslot - no + # --wipe-slot - so the passphrase slot always stays as a fallback. + if PASSWORD="$PASS" systemd-cryptenroll --tpm2-device=auto \ + --tpm2-pcrs=0+7 "$(part 2)" >>"$LOG" 2>&1; then + TPM_STATUS="enrolled" + # Enrolment alone does nothing at boot: systemd-cryptsetup only uses + # the TPM2 keyslot when the cmdline asks for it. install_bootloader + # (step 7) already wrote rd.luks.name= and ran grub-mkconfig, so + # rewrite the line with rd.luks.options=...tpm2-device=auto appended + # and regenerate grub.cfg now. + local luks_uuid + luks_uuid=$(blkid -s UUID -o value "$(part 2)") + write_mode_a_cmdline "$luks_uuid" "rd.luks.options=${luks_uuid}=tpm2-device=auto" + arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg >>"$LOG" 2>&1 \ + || warn "grub-mkconfig failed after adding rd.luks.options (see $LOG) - TPM2 unlock will not activate at boot." + ok "TPM2 auto-unlock enrolled and activated (rd.luks.options, PCR 0+7)." + else + TPM_STATUS="failed" + warn "TPM enrollment failed (see $LOG) - passphrase unlock still works." + fi + + # TPM-only unlock with an unencrypted /boot is weak: anyone with physical + # access can swap kernel/initrd. Secure Boot is what closes that hole. + if [[ "$TPM_STATUS" == "enrolled" ]] && ! secure_boot_enabled; then + warn "Secure Boot is off: with an unencrypted /boot, anyone can replace the kernel/initrd and reach the decrypted disk. Consider enabling Secure Boot." + log "TPM: enrolled, Secure Boot off" + fi +} + +# ── unl0kr (experimental touchscreen unlock) ────────────────────────────────── + +unl0kr_fallback_mode_a() { + local why="$1" + warn "unl0kr unavailable (${why}) - falling back to the standard systemd initramfs." + log "unl0kr fallback to Mode A: ${why}" + + local main_conf="${TARGET}/etc/mkinitcpio.conf" + if [[ -f "${main_conf}.alvin-bak" ]]; then + cp -a "${main_conf}.alvin-bak" "$main_conf" + fi + # Mode A drop-in + matching cmdline; this also undoes install_mkinitcpio's + # Mode B HOOKS and install_bootloader's cryptdevice= line. + write_mode_a_hooks + if [[ "$LUKS" == "true" ]]; then + local luks_uuid + luks_uuid=$(blkid -s UUID -o value "$(part 2)") + write_mode_a_cmdline "$luks_uuid" + fi + + UNL0KR="false" + if [[ "$TPM_STATUS" == "skipped-unl0kr" ]]; then + # TPM enrollment was skipped because unl0kr was selected; now that the + # system is back on Mode A it would be possible, but the prompt has + # passed. Keep a distinct status so the summary can say so plainly + # instead of the generic "it was skipped". + TPM_STATUS="skipped-unl0kr-fallback" + fi + + arch-chroot "$TARGET" mkinitcpio -P >>"$LOG" 2>&1 \ + || warn "mkinitcpio -P failed during unl0kr fallback (see $LOG)." + arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg >>"$LOG" 2>&1 \ + || warn "grub-mkconfig failed during unl0kr fallback (see $LOG)." + warn "The system will ask for the LUKS passphrase at boot - test a reboot before relying on it." +} + +install_unl0kr() { + [[ "$UNL0KR" == "true" ]] || return 0 + + info "Installing unl0kr (touchscreen LUKS unlock)..." + sync_target_dbs + + local home_in_target="/home/${USERNAME}" + local build_rc=0 + + # yay runs as the new user and needs passwordless sudo for this one call. + echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-unl0kr-install" + chmod 440 "${TARGET}/etc/sudoers.d/99-unl0kr-install" + arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ + env HOME="$home_in_target" yay -S --noconfirm unl0kr >>"$LOG" 2>&1 || build_rc=$? + rm -f "${TARGET}/etc/sudoers.d/99-unl0kr-install" + + if (( build_rc != 0 )); then + unl0kr_fallback_mode_a "yay failed to build it (exit ${build_rc})" + return 0 + fi + + # Record what the AUR actually gave us - advertised and PKGBUILD versions + # have disagreed before, so trust pacman, not the web page. + local built_ver + built_ver=$(arch-chroot "$TARGET" pacman -Qi unl0kr 2>/dev/null \ + | awk -F': ' '/^Version/{print $2}') || built_ver="" + log "unl0kr built version: ${built_ver:-unknown}" + + # The package must ship busybox initcpio hooks (its prepare() strips the + # systemd ones). Read both into the log to confirm hook order and which + # modules the install script pulls in by itself: it adds dm-crypt, crypto + # and DRI libraries but NO touchscreen/HID drivers - those come from the + # MODULES list below. + local hook="${TARGET}/usr/lib/initcpio/hooks/unl0kr" + local hook_install="${TARGET}/usr/lib/initcpio/install/unl0kr" + if [[ ! -f "$hook" || ! -f "$hook_install" ]]; then + unl0kr_fallback_mode_a "initcpio hook files missing after install" + return 0 + fi + { + echo "== unl0kr initcpio files (as installed) ==" + echo "-- hooks/unl0kr --" + cat "$hook" + echo "-- install/unl0kr --" + cat "$hook_install" + } >>"$LOG" 2>&1 || warn "Could not log the unl0kr hook files (see $LOG)." + + # Touchscreen/input stack for the initramfs: mkinitcpio fails the build + # on a MODULES entry it cannot resolve, and -P applies the list to every + # preset - so a module may enter MODULES only if EVERY installed kernel + # knows it (modinfo -k runs inside the target, where /lib/modules holds + # linux + linux-cachyos, not the live kernel). + local -a candidates=( + i2c_hid i2c_hid_acpi hid_multitouch hid_generic usbhid + intel_lpss i2c_i801 i2c_piix4 i2c_amd_mp2 + ) + local -a keep=() kvers=() + local kdir kver m ok_all + for kdir in "${TARGET}"/lib/modules/*; do + [[ -d "$kdir" ]] && kvers+=("$(basename "$kdir")") + done + for m in "${candidates[@]}"; do + ok_all=1 + for kver in "${kvers[@]+"${kvers[@]}"}"; do + if ! arch-chroot "$TARGET" modinfo -k "$kver" "$m" >/dev/null 2>&1; then + ok_all=0 + break + fi + done + if (( ok_all )); then + keep+=("$m") + fi + done + log "unl0kr MODULES: ${keep[*]:-none}" + if ((${#keep[@]} > 0)); then + printf 'MODULES=(%s)\n' "${keep[*]}" \ + >> "${TARGET}/etc/mkinitcpio.conf.d/alvin.conf" + fi + + info "Rebuilding initramfs with unl0kr..." + if ! arch-chroot "$TARGET" mkinitcpio -P >>"$LOG" 2>&1; then + unl0kr_fallback_mode_a "mkinitcpio -P failed" + return 0 + fi + # grub.cfg is written now, next to the final image (Mode B deferred it). + if ! arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg >>"$LOG" 2>&1; then + unl0kr_fallback_mode_a "grub-mkconfig failed" + return 0 + fi + + # Prove the image really carries the hook and every module we asked for: + # `lsinitcpio -a` prints the hook run order and the included module list. + local analysis + if ! analysis=$(lsinitcpio -a "${TARGET}/boot/initramfs-linux.img" 2>>"$LOG"); then + unl0kr_fallback_mode_a "lsinitcpio could not read the image" + return 0 + fi + if ! grep -qw 'unl0kr' <<< "$analysis"; then + unl0kr_fallback_mode_a "unl0kr hook missing from initramfs" + return 0 + fi + # Compare against the image's module list, expecting only modules that + # are LOADABLE for the kernel that built initramfs-linux.img: a builtin + # may legally sit in MODULES (mkinitcpio preloads it) but never appears + # in `lsinitcpio -a`'s list - it is part of the kernel itself - and + # expecting it there would make the check false-fail and discard a + # perfectly good unl0kr install on every run. + local -a expect=() missing=() + local linux_kver mi m_norm mods_section + linux_kver=$(arch-chroot "$TARGET" pacman -Ql linux 2>/dev/null \ + | sed -n 's|.* /usr/lib/modules/\([^/]*\)/modules\.builtin.*|\1|p' \ + | head -n1) || linux_kver="" + for m in "${keep[@]+"${keep[@]}"}"; do + mi=$(arch-chroot "$TARGET" modinfo -k "$linux_kver" "$m" 2>/dev/null || true) + if ! grep -q '(builtin)' <<< "$mi"; then + expect+=("$m") + fi + done + log "unl0kr image check: kernel=${linux_kver:-unknown} expect=${expect[*]:-none}" + mods_section=$(awk '/==> Included modules/{f=1;next} f&&/^==>/{exit} f' <<< "$analysis" \ + | tr -s ' \t' '\n' | tr '_' '-') + for m in "${expect[@]+"${expect[@]}"}"; do + m_norm=${m//_/-} + grep -qxF -- "$m_norm" <<< "$mods_section" || missing+=("$m") + done + if ((${#missing[@]} > 0)); then + unl0kr_fallback_mode_a "modules missing from initramfs: ${missing[*]}" + return 0 + fi + + # Every other preset image from this mkinitcpio -P (linux-cachyos, + # fallbacks) shares the same drop-in, but verify rather than assume. + local img + for img in "${TARGET}"/boot/initramfs-*.img; do + [[ -f "$img" ]] || continue + [[ "$img" == "${TARGET}/boot/initramfs-linux.img" ]] && continue + if ! lsinitcpio -a "$img" 2>/dev/null | grep -qw 'unl0kr'; then + warn "$(basename "$img") lacks the unl0kr hook (see $LOG) - the default image is fine." + fi + done + + ok "unl0kr installed; initramfs verified." + log "unl0kr: done (version=${built_ver:-unknown} modules=${#keep[@]})" +} + +# ── Login manager ───────────────────────────────────────────────────────────── + +ensure_hyprland_session() { + local desktop="${TARGET}/usr/share/wayland-sessions/hyprland.desktop" + if [[ -f "$desktop" ]]; then + return 0 + fi + mkdir -p "$(dirname "$desktop")" + cat > "$desktop" <<'DESKTOP' +[Desktop Entry] +Name=Hyprland +Comment=Hyprland wayland session +Exec=Hyprland +Type=Application +DesktopNames=Hyprland +DESKTOP + log "wayland-sessions/hyprland.desktop was missing - created a minimal one." +} + +install_none_profile() { + local home_in_target="/home/${USERNAME}" + local owner + owner=$(target_owner) + + local shell_name + shell_name=$(arch-chroot "$TARGET" getent passwd "$USERNAME" \ + | awk -F: '{print $NF}') || shell_name="/bin/bash" + + local profile="" + case "$shell_name" in + *zsh) + profile="${home_in_target}/.zprofile" + ;; + *fish) + warn "fish is the login shell - skipping tty autostart; run 'Hyprland' after login." + log "tty profile: skipped (fish shell)" + return 0 + ;; + *) + if [[ -f "${TARGET}${home_in_target}/.bash_profile" \ + || ! -f "${TARGET}${home_in_target}/.profile" ]]; then + profile="${home_in_target}/.bash_profile" + else + profile="${home_in_target}/.profile" + fi + ;; + esac + + # Already launching Hyprland (skel, end-4 dotfiles, a previous run)? + if [[ -f "${TARGET}${profile}" ]] && grep -qi 'hyprland' "${TARGET}${profile}"; then + log "tty profile: ${profile} already launches Hyprland - skipped" + return 0 + fi + + # Launch plain Hyprland - the same way greetd/sddm/gdm paths do, so the + # login method does not silently change with the chosen manager. (The + # hyprland package also ships hyprland-uwsm.desktop for anyone who wants + # a uwsm-managed session; it stays pickable in the DM's session menu.) + { + echo "" + echo "# alvin-install: start Hyprland on tty1 (pam_systemd sets XDG_VTNR)." + echo "if [[ -z \"\${WAYLAND_DISPLAY:-}\" && \"\${XDG_VTNR:-}\" == \"1\" ]]; then" + echo " exec Hyprland" + echo "fi" + } >> "${TARGET}${profile}" + + chown "$owner" "${TARGET}${profile}" 2>/dev/null || true + ok "tty login will start Hyprland via ${profile}." + log "tty profile: ${profile} (exec Hyprland on VT1)" +} + +install_login_manager() { + info "Configuring login manager: ${LOGIN_MANAGER}..." + + # Exactly one way in: disable everything known first (this also clears + # the display-manager.service alias and any previous run's leftovers). + local u + for u in greetd sddm gdm; do + if [[ -f "${TARGET}/usr/lib/systemd/system/${u}.service" ]]; then + arch-chroot "$TARGET" systemctl disable "$u.service" >>"$LOG" 2>&1 \ + || warn "Could not disable ${u}.service (see $LOG)." + fi + done + rm -f "${TARGET}/etc/systemd/system/getty@tty1.service.d/autologin.conf" + + # greetd/sddm/gdm all bind tty1 themselves - getty must not fight them + # for the device ('none' re-enables it in its branch). Each DM branch + # disables getty only AFTER its enable succeeded, so a failed enable + # never leaves the system without any login path. + case "$LOGIN_MANAGER" in + greetd) + sync_target_dbs + if ! arch-chroot "$TARGET" pacman -Si greetd >/dev/null 2>&1; then + warn "greetd is not resolvable in the target repos - falling back to 'none'." + LOGIN_MANAGER="none" + install_none_profile + return + fi + if ! arch-chroot "$TARGET" pacman -S --noconfirm greetd greetd-tuigreet \ + >>"$LOG" 2>&1; then + warn "greetd install failed (see $LOG) - falling back to 'none'." + LOGIN_MANAGER="none" + install_none_profile + return + fi + # The package normally provides the greeter account; create it if + # this version does not (ArchWiki: user = "greeter"). + if ! arch-chroot "$TARGET" id greeter >/dev/null 2>&1; then + arch-chroot "$TARGET" useradd -r -M -s /usr/bin/nologin greeter \ + >>"$LOG" 2>&1 || warn "Could not create the greeter user (see $LOG)." + fi + mkdir -p "${TARGET}/etc/greetd" + cat > "${TARGET}/etc/greetd/config.toml" <<'TOML' +[terminal] +vt = 1 + +[default_session] +command = "tuigreet --cmd Hyprland" +user = "greeter" +TOML + if arch-chroot "$TARGET" systemctl enable greetd.service >>"$LOG" 2>&1; then + arch-chroot "$TARGET" systemctl disable getty@tty1.service >>"$LOG" 2>&1 || true + ok "greetd + tuigreet enabled (Hyprland)." + else + warn "Could not enable greetd.service (see $LOG) - keeping getty@tty1 as the login path." + fi + ;; + sddm) + sync_target_dbs + if ! arch-chroot "$TARGET" pacman -Si sddm >/dev/null 2>&1; then + warn "sddm is not resolvable in the target repos - falling back to 'none'." + LOGIN_MANAGER="none" + install_none_profile + return + fi + if ! arch-chroot "$TARGET" pacman -S --noconfirm sddm >>"$LOG" 2>&1; then + warn "sddm install failed (see $LOG) - falling back to 'none'." + LOGIN_MANAGER="none" + install_none_profile + return + fi + ensure_hyprland_session + if arch-chroot "$TARGET" systemctl enable sddm.service >>"$LOG" 2>&1; then + arch-chroot "$TARGET" systemctl disable getty@tty1.service >>"$LOG" 2>&1 || true + ok "sddm enabled (Hyprland)." + else + warn "Could not enable sddm.service (see $LOG) - keeping getty@tty1 as the login path." + fi + ;; + gdm) + sync_target_dbs + warn "gdm pulls in GNOME components - expect a large download." + if ! arch-chroot "$TARGET" pacman -Si gdm >/dev/null 2>&1; then + warn "gdm is not resolvable in the target repos - falling back to 'none'." + LOGIN_MANAGER="none" + install_none_profile + return + fi + if ! arch-chroot "$TARGET" pacman -S --noconfirm gdm >>"$LOG" 2>&1; then + warn "gdm install failed (see $LOG) - falling back to 'none'." + LOGIN_MANAGER="none" + install_none_profile + return + fi + ensure_hyprland_session + if arch-chroot "$TARGET" systemctl enable gdm.service >>"$LOG" 2>&1; then + arch-chroot "$TARGET" systemctl disable getty@tty1.service >>"$LOG" 2>&1 || true + ok "gdm enabled (Hyprland session)." + else + warn "Could not enable gdm.service (see $LOG) - keeping getty@tty1 as the login path." + fi + ;; + *) + # none: keep getty on tty1; install_none_profile writes the + # login profile that starts the compositor. + arch-chroot "$TARGET" systemctl enable getty@tty1.service >>"$LOG" 2>&1 \ + || true + install_none_profile + ;; + esac + + log "Login manager: ${LOGIN_MANAGER}" +} + +# ── Autologin ───────────────────────────────────────────────────────────────── + +install_autologin() { + AUTOLOGIN="disabled" + + if [[ "$TPM_STATUS" == "enrolled" ]]; then + info "Autologin stays disabled: TPM2 auto-unlock is enrolled (the password is still required at login)." + log "Autologin: disabled (TPM enrolled)" + else + # Default: yes when LUKS is on (TPM did not take over unlock), no + # when nothing is encrypted - then autologin would mean zero auth. + local choice header_text + if [[ "$LUKS" == "true" ]]; then + header_text="+ Autologin (default: yes)" + choice=$(gum choose --header "$header_text" \ + "Yes - autologin to Hyprland" \ + "No - require the password") || choice="No" + else + header_text="+ Autologin (default: no) - no encryption + no password = no auth at all" + choice=$(gum choose --header "$header_text" \ + "No - require the password" \ + "Yes - autologin to Hyprland") || choice="No" + fi + if [[ "$choice" == Yes* ]]; then + AUTOLOGIN="enabled" + fi + log "Autologin: ${AUTOLOGIN} (luks=${LUKS} tpm=${TPM_STATUS})" + fi + + # Apply idempotently: every run either writes its file or removes the one + # a previous run left behind, so turning autologin off stays clean. + case "$LOGIN_MANAGER" in + none) + local dropin="${TARGET}/etc/systemd/system/getty@tty1.service.d/autologin.conf" + if [[ "$AUTOLOGIN" == "enabled" ]]; then + mkdir -p "$(dirname "$dropin")" + cat > "$dropin" < "${TARGET}/etc/greetd/config.toml" < "${TARGET}/etc/greetd/config.toml" <<'TOML' +[terminal] +vt = 1 + +[default_session] +command = "tuigreet --cmd Hyprland" +user = "greeter" +TOML + fi + ;; + sddm) + local sddm_auto="${TARGET}/etc/sddm.conf.d/autologin.conf" + if [[ "$AUTOLOGIN" == "enabled" ]]; then + mkdir -p "$(dirname "$sddm_auto")" + cat > "$sddm_auto" < "$gdm_conf" </dev/null || true + cat "$stamp" >> "${TARGET}/etc/motd" + log "motd: appended build stamp: $(tr -d '\n' < "$stamp")" + ok "Build stamp appended to the installed /etc/motd." +} + install_cleanup() { info "Cleaning up..." cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true @@ -767,6 +1640,14 @@ install_cleanup() { gum style --foreground 10 --align center \ "Arch Linux installed successfully." \ "" \ + "Login manager: ${LOGIN_MANAGER} Autologin: ${AUTOLOGIN:-disabled}" \ + "TPM2 auto-unlock: ${TPM_STATUS:-n/a} unl0kr: ${UNL0KR}" + if [[ -n "${TPM_DISCLAIMER}" ]]; then + echo "" + warn "${TPM_DISCLAIMER}" + fi + echo "" + gum style --foreground 10 --align center \ "Run 'reboot' to boot into your new system." echo "" } @@ -781,6 +1662,7 @@ main() { screen_network screen_disk screen_luks + screen_unl0kr screen_filesystem screen_username screen_password @@ -788,6 +1670,7 @@ main() { screen_git screen_timezone screen_locale + screen_login_manager screen_summary install_partition @@ -801,10 +1684,15 @@ main() { install_hostname install_network install_user - install_git + install_git_ssh install_end4_dots install_mkinitcpio install_bootloader + install_tpm + install_unl0kr + install_login_manager + install_autologin + install_motd install_cleanup } diff --git a/packages.x86_64 b/packages.x86_64 index 481ffa8..c27dab2 100644 --- a/packages.x86_64 +++ b/packages.x86_64 @@ -46,6 +46,7 @@ cpio cronie cryptsetup cups +curl darkhttpd davs2 dconf-editor diff --git a/profiledef.sh b/profiledef.sh index 3fdb8b5..0f1cb45 100644 --- a/profiledef.sh +++ b/profiledef.sh @@ -24,3 +24,32 @@ file_permissions=( ["/usr/local/bin/Installation_guide"]="0:0:755" ["/usr/local/bin/livecd-sound"]="0:0:755" ) + +# ── Build stamp (staged file, never committed) ──────────────────────────────── +# airootfs/root/.iso-build-stamp lands at /root/.iso-build-stamp on the ISO; +# customize_airootfs.sh installs it as /etc/alvin-build-stamp, and alvin-install +# appends it to the installed system's /etc/motd. Only stamp builds from a git +# checkout or CI — a random source-tarball build would record a bogus commit. +# Sourced by mkarchiso under `set -eu`, hence every expansion is defaulted. +_stamp_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +_stamp_file="${_stamp_dir}/airootfs/root/.iso-build-stamp" +rm -f "${_stamp_file}" +if [[ -e "${_stamp_dir}/.git" || -n "${CI:-}" || -n "${GITEA_ACTIONS:-}" ]]; then + _stamp_build="$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)" + # git ≥2.46 honours -c safe.directory even when mkarchiso runs as root + # over a user-owned checkout; older gits ignore it (protected-config + # scope) and fall through to the "unknown" line below instead of failing + # the build (`|| true` keeps set -e happy either way). + _stamp_commit="$(git -C "${_stamp_dir}" -c safe.directory="${_stamp_dir}" \ + log -1 --format='%cs|%h' 2>/dev/null || true)" + if [[ -n "${_stamp_commit}" && "${_stamp_commit}" == *"|"* ]]; then + _stamp_hash="${_stamp_commit##*|}" + printf 'AlvinArch build %s | commit %s | %s\n' \ + "${_stamp_build}" "${_stamp_commit%%|*}" "${_stamp_hash:0:4}" \ + > "${_stamp_file}" + else + printf 'AlvinArch build %s | commit unknown\n' \ + "${_stamp_build}" > "${_stamp_file}" + fi +fi +unset _stamp_dir _stamp_file _stamp_build _stamp_commit _stamp_hash