max / alloy
- Co-Authored-By
- Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6 files changed,
+377 insertions,
-2 deletions
| @@ -5,8 +5,9 @@ | |||
| 5 | 5 | # alone is a multi-gigabyte ISO, which the build was shipping to itself on | |
| 6 | 6 | # every run. | |
| 7 | 7 | # | |
| 8 | - | # Keep this in step with the COPY lines in the Containerfile: the build | |
| 9 | - | # reads Cargo.toml, Cargo.lock, crates/, etc/, and usr/, and nothing else. | |
| 8 | + | # Keep this in step with the COPY lines in the Containerfiles: the image | |
| 9 | + | # build reads Cargo.toml, Cargo.lock, crates/, etc/ and usr/, and | |
| 10 | + | # build/Containerfile.iso additionally reads build/make-iso.sh. | |
| 10 | 11 | ||
| 11 | 12 | # Cargo artifacts. The rust-build stage compiles from scratch inside the | |
| 12 | 13 | # image on purpose — host artifacts are built against a different libc and | |
| @@ -19,7 +20,10 @@ | |||
| 19 | 20 | ||
| 20 | 21 | # Not read by any COPY. | |
| 21 | 22 | /.git | |
| 23 | + | # Excluded except the one script build/Containerfile.iso copies in. The | |
| 24 | + | # negation has to follow the exclusion; reversed, it does nothing. | |
| 22 | 25 | /build | |
| 26 | + | !/build/make-iso.sh | |
| 23 | 27 | /builds.disabled | |
| 24 | 28 | /docs | |
| 25 | 29 | /schemas |
| @@ -16,5 +16,9 @@ | |||
| 16 | 16 | # a match fall through to /usr/lib/systemd/system-preset/ defaults. | |
| 17 | 17 | ||
| 18 | 18 | enable greetd.service | |
| 19 | + | # Inert unless the kernel command line carries `alloy.installer`, which only | |
| 20 | + | # the installer ISO sets, so enabling it on every install is safe and keeps | |
| 21 | + | # the live medium from needing a modified copy of the image. | |
| 22 | + | enable alloy-installer.service | |
| 19 | 23 | ||
| 20 | 24 | disable tailscaled.service |
| @@ -1,0 +1,33 @@ | |||
| 1 | + | # Builder image for the Alloy installer ISO. | |
| 2 | + | # | |
| 3 | + | # Derived FROM the Alloy image rather than from stock Fedora, so the live | |
| 4 | + | # initramfs is built against exactly the kernel and modules that ship. A | |
| 5 | + | # builder on its own base would produce an initramfs for a different kernel | |
| 6 | + | # and fail at boot with no obvious cause. | |
| 7 | + | # | |
| 8 | + | # The extra packages live here instead of in the image itself because none | |
| 9 | + | # of them belong on an installed machine: dracut-live drives a squashfs | |
| 10 | + | # root, and xorriso only ever assembles ISOs. | |
| 11 | + | ARG BASE=localhost/alloy:local | |
| 12 | + | FROM ${BASE} | |
| 13 | + | ||
| 14 | + | # dnf is only usable here because this stage is thrown away; the shipped | |
| 15 | + | # image disables the third-party repos it layered from at build time, so | |
| 16 | + | # nothing below reaches for them. | |
| 17 | + | RUN dnf install -y \ | |
| 18 | + | dracut-live \ | |
| 19 | + | xorriso \ | |
| 20 | + | squashfs-tools \ | |
| 21 | + | grub2-efi-x64 \ | |
| 22 | + | grub2-efi-x64-modules \ | |
| 23 | + | grub2-tools \ | |
| 24 | + | grub2-tools-extra \ | |
| 25 | + | shim-x64 \ | |
| 26 | + | dosfstools \ | |
| 27 | + | rsync \ | |
| 28 | + | && dnf clean all | |
| 29 | + | ||
| 30 | + | COPY build/make-iso.sh /usr/local/bin/make-iso | |
| 31 | + | RUN chmod +x /usr/local/bin/make-iso | |
| 32 | + | ||
| 33 | + | ENTRYPOINT ["/usr/local/bin/make-iso"] |
| @@ -1,0 +1,119 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # | |
| 3 | + | # build-iso.sh — build an Alloy installer ISO that boots into `alloy install`. | |
| 4 | + | # | |
| 5 | + | # This is the replacement for the bootc-image-builder path in | |
| 6 | + | # build/build-image.sh, which can only produce Anaconda ISOs: every ISO type | |
| 7 | + | # bib offers ends in Anaconda, and the installer binary lives in the bootc | |
| 8 | + | # image rather than in the live root bib composes from RPMs. See GO task | |
| 9 | + | # a1d037f8. build/build-image.sh keeps its disk-image types (raw, qcow2); | |
| 10 | + | # this script owns the ISO. | |
| 11 | + | # | |
| 12 | + | # Shape of the result: | |
| 13 | + | # LiveOS/squashfs.img the Alloy image, as the live system | |
| 14 | + | # source/alloy.oci the same image again, as what gets installed | |
| 15 | + | # EFI/BOOT + boot/ shim, grub, kernel, live initramfs | |
| 16 | + | # | |
| 17 | + | # The live system runs alloy-installer.service, which is inert unless the | |
| 18 | + | # kernel command line carries `alloy.installer`. Only the GRUB entries here | |
| 19 | + | # set it, so the unit cannot fire on an installed machine. | |
| 20 | + | # | |
| 21 | + | # Usage: | |
| 22 | + | # build/build-iso.sh # build image, then ISO | |
| 23 | + | # build/build-iso.sh --skip-build # reuse the current image | |
| 24 | + | # build/build-iso.sh --skip-source # omit the oci-archive (faster; the | |
| 25 | + | # # ISO boots but cannot install) | |
| 26 | + | ||
| 27 | + | set -euo pipefail | |
| 28 | + | ||
| 29 | + | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | |
| 30 | + | IMAGE="localhost/alloy:local" | |
| 31 | + | BUILDER="localhost/alloy-iso-builder:local" | |
| 32 | + | OUTPUT="$REPO_ROOT/output" | |
| 33 | + | WORKDIR="$REPO_ROOT/output/.iso-work" | |
| 34 | + | ||
| 35 | + | SKIP_BUILD=0 | |
| 36 | + | SKIP_SOURCE=0 | |
| 37 | + | ||
| 38 | + | die() { printf 'error: %s\n' "$*" >&2; exit 1; } | |
| 39 | + | say() { printf '==> %s\n' "$*"; } | |
| 40 | + | ||
| 41 | + | while [ $# -gt 0 ]; do | |
| 42 | + | case "$1" in | |
| 43 | + | --skip-build) SKIP_BUILD=1; shift ;; | |
| 44 | + | --skip-source) SKIP_SOURCE=1; shift ;; | |
| 45 | + | -h|--help) sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; | |
| 46 | + | *) die "unknown argument: $1 (see --help)" ;; | |
| 47 | + | esac | |
| 48 | + | done | |
| 49 | + | ||
| 50 | + | command -v podman >/dev/null || die "podman not found" | |
| 51 | + | ||
| 52 | + | # 1. The Alloy image. | |
| 53 | + | if [ "$SKIP_BUILD" -eq 0 ]; then | |
| 54 | + | say "building $IMAGE" | |
| 55 | + | sudo podman build -t "$IMAGE" "$REPO_ROOT" | |
| 56 | + | else | |
| 57 | + | sudo podman image exists "$IMAGE" || die "$IMAGE not in the root store; drop --skip-build" | |
| 58 | + | say "reusing $IMAGE" | |
| 59 | + | fi | |
| 60 | + | ||
| 61 | + | # 2. The builder, derived from it so the initramfs matches the shipped kernel. | |
| 62 | + | say "building $BUILDER" | |
| 63 | + | sudo podman build -t "$BUILDER" \ | |
| 64 | + | --build-arg "BASE=$IMAGE" \ | |
| 65 | + | -f "$REPO_ROOT/build/Containerfile.iso" \ | |
| 66 | + | "$REPO_ROOT" | |
| 67 | + | ||
| 68 | + | # 3. Somewhere to work. Rotated rather than deleted, same as build-image.sh: | |
| 69 | + | # a failed run should not take the last good ISO with it. | |
| 70 | + | if [ -d "$OUTPUT" ] && [ -n "$(sudo ls -A "$OUTPUT" 2>/dev/null)" ]; then | |
| 71 | + | say "rotating previous output to ${OUTPUT}.prev" | |
| 72 | + | sudo rm -rf "${OUTPUT:?}.prev" | |
| 73 | + | sudo mv "$OUTPUT" "${OUTPUT}.prev" | |
| 74 | + | fi | |
| 75 | + | sudo mkdir -p "$OUTPUT" "$WORKDIR/source" | |
| 76 | + | ||
| 77 | + | # 4. The image to install, as an oci-archive. skopeo rather than `podman | |
| 78 | + | # save` because bootc reads skopeo transports, and this is the exact | |
| 79 | + | # format --source-imgref will be handed. | |
| 80 | + | if [ "$SKIP_SOURCE" -eq 0 ]; then | |
| 81 | + | say "exporting the install source (several GB, and slow)" | |
| 82 | + | sudo skopeo copy \ | |
| 83 | + | "containers-storage:$IMAGE" \ | |
| 84 | + | "oci-archive:$WORKDIR/source/alloy.oci:alloy:local" \ | |
| 85 | + | 2>&1 | tail -3 \ | |
| 86 | + | || sudo podman run --rm --privileged \ | |
| 87 | + | -v /var/lib/containers/storage:/var/lib/containers/storage \ | |
| 88 | + | -v "$WORKDIR/source":/source \ | |
| 89 | + | "$BUILDER" \ | |
| 90 | + | --export-only | |
| 91 | + | else | |
| 92 | + | say "skipping the install source" | |
| 93 | + | fi | |
| 94 | + | ||
| 95 | + | # 5. Mount the image's root filesystem and assemble. | |
| 96 | + | # | |
| 97 | + | # `podman image mount` gives a path to the composed rootfs without | |
| 98 | + | # extracting it, which saves writing several GB to disk only to read it | |
| 99 | + | # straight back. Rootful, so it lands in the same store the image is in. | |
| 100 | + | say "mounting $IMAGE" | |
| 101 | + | ROOTFS="$(sudo podman image mount "$IMAGE")" | |
| 102 | + | [ -n "$ROOTFS" ] || die "could not mount $IMAGE" | |
| 103 | + | trap 'sudo podman image umount "$IMAGE" >/dev/null 2>&1 || true' EXIT | |
| 104 | + | say "rootfs at $ROOTFS" | |
| 105 | + | ||
| 106 | + | say "assembling the ISO" | |
| 107 | + | sudo podman run --rm --privileged \ | |
| 108 | + | --security-opt label=type:unconfined_t \ | |
| 109 | + | -v "$ROOTFS":/rootfs:ro \ | |
| 110 | + | -v "$OUTPUT":/output \ | |
| 111 | + | -v "$WORKDIR/source":/source:ro \ | |
| 112 | + | "$BUILDER" | |
| 113 | + | ||
| 114 | + | ARTIFACT="$OUTPUT/install.iso" | |
| 115 | + | sudo test -f "$ARTIFACT" || die "no ISO produced" | |
| 116 | + | say "built: $ARTIFACT ($(sudo du -h "$ARTIFACT" | cut -f1))" | |
| 117 | + | echo | |
| 118 | + | echo " Boot it, or write it with:" | |
| 119 | + | echo " build/build-image.sh --skip-bib --write /dev/sdX" |
| @@ -1,0 +1,175 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # | |
| 3 | + | # make-iso.sh — assemble the Alloy installer ISO. Runs inside the builder | |
| 4 | + | # image from build/Containerfile.iso; build/build-iso.sh is what invokes it. | |
| 5 | + | # | |
| 6 | + | # Expects: | |
| 7 | + | # /rootfs the Alloy image's root filesystem, read-only | |
| 8 | + | # /output where install.iso is written | |
| 9 | + | # /source an oci-archive of the Alloy image, copied onto the ISO | |
| 10 | + | # | |
| 11 | + | # The ISO carries two copies of Alloy for two different jobs. The squashfs | |
| 12 | + | # is the live system the installer runs in. The oci-archive is what gets | |
| 13 | + | # deployed, because `bootc install` installs a container image and cannot | |
| 14 | + | # install a running squashfs. They are the same content and the ISO pays | |
| 15 | + | # for it twice; that is the cost of not needing a network to install. | |
| 16 | + | ||
| 17 | + | set -euo pipefail | |
| 18 | + | ||
| 19 | + | ROOTFS=/rootfs | |
| 20 | + | OUTPUT=/output | |
| 21 | + | SOURCE=/source | |
| 22 | + | WORK=/work | |
| 23 | + | VOLID="ALLOY" | |
| 24 | + | ||
| 25 | + | say() { printf '==> %s\n' "$*"; } | |
| 26 | + | ||
| 27 | + | [ -d "$ROOTFS" ] || { echo "no $ROOTFS" >&2; exit 1; } | |
| 28 | + | [ -d "$OUTPUT" ] || { echo "no $OUTPUT" >&2; exit 1; } | |
| 29 | + | ||
| 30 | + | KVER="$(ls "$ROOTFS/usr/lib/modules" | head -1)" | |
| 31 | + | [ -n "$KVER" ] || { echo "no kernel in $ROOTFS/usr/lib/modules" >&2; exit 1; } | |
| 32 | + | say "kernel $KVER" | |
| 33 | + | ||
| 34 | + | rm -rf "$WORK" | |
| 35 | + | mkdir -p "$WORK/iso/LiveOS" "$WORK/iso/EFI/BOOT" "$WORK/iso/boot/grub" | |
| 36 | + | ||
| 37 | + | # --------------------------------------------------------------------- | |
| 38 | + | # 1. The live root, as a squashfs holding an ext4 image. | |
| 39 | + | # | |
| 40 | + | # dmsquash-live expects LiveOS/squashfs.img to contain LiveOS/rootfs.img, | |
| 41 | + | # not the root tree directly. Handing it a squashfs of the tree boots to a | |
| 42 | + | # dracut shell with "failed to mount live root", which says nothing about | |
| 43 | + | # the layout being the problem. | |
| 44 | + | # --------------------------------------------------------------------- | |
| 45 | + | say "building rootfs.img" | |
| 46 | + | SIZE_KB="$(du -sk "$ROOTFS" | cut -f1)" | |
| 47 | + | # Slack for the ext4 metadata plus room for the overlay's early writes. | |
| 48 | + | IMG_MB=$(( SIZE_KB / 1024 + 1536 )) | |
| 49 | + | mkdir -p "$WORK/LiveOS" | |
| 50 | + | truncate -s "${IMG_MB}M" "$WORK/LiveOS/rootfs.img" | |
| 51 | + | mkfs.ext4 -q -L Alloy -d "$ROOTFS" "$WORK/LiveOS/rootfs.img" | |
| 52 | + | ||
| 53 | + | say "compressing squashfs.img (this is the long part)" | |
| 54 | + | mksquashfs "$WORK/LiveOS" "$WORK/iso/LiveOS/squashfs.img" \ | |
| 55 | + | -noappend -no-progress -comp zstd -Xcompression-level 19 -b 1M \ | |
| 56 | + | -e rootfs.img.tmp | |
| 57 | + | rm -rf "$WORK/LiveOS" | |
| 58 | + | ||
| 59 | + | # --------------------------------------------------------------------- | |
| 60 | + | # 2. Kernel and a live initramfs. | |
| 61 | + | # | |
| 62 | + | # --no-hostonly matters: a host-only initramfs is built for the hardware | |
| 63 | + | # doing the building, and this one has to boot anything. | |
| 64 | + | # --------------------------------------------------------------------- | |
| 65 | + | say "kernel and initramfs" | |
| 66 | + | cp "$ROOTFS/usr/lib/modules/$KVER/vmlinuz" "$WORK/iso/boot/vmlinuz" | |
| 67 | + | ||
| 68 | + | dracut --force --no-hostonly --nomdadmconf --nolvmconf \ | |
| 69 | + | --kver "$KVER" \ | |
| 70 | + | --kmoddir "$ROOTFS/usr/lib/modules/$KVER" \ | |
| 71 | + | --add "dmsquash-live" \ | |
| 72 | + | --add-drivers "squashfs loop overlay iso9660 sr_mod sd_mod usb_storage virtio_blk virtio_scsi virtio_pci" \ | |
| 73 | + | "$WORK/iso/boot/initramfs.img" | |
| 74 | + | ||
| 75 | + | # --------------------------------------------------------------------- | |
| 76 | + | # 3. The image the installer deploys. | |
| 77 | + | # --------------------------------------------------------------------- | |
| 78 | + | if [ -d "$SOURCE" ] && [ -n "$(ls -A "$SOURCE" 2>/dev/null)" ]; then | |
| 79 | + | say "copying the install source onto the ISO" | |
| 80 | + | mkdir -p "$WORK/iso/source" | |
| 81 | + | cp -a "$SOURCE/." "$WORK/iso/source/" | |
| 82 | + | else | |
| 83 | + | echo "warning: no oci-archive at $SOURCE; the ISO will have nothing to install" >&2 | |
| 84 | + | fi | |
| 85 | + | ||
| 86 | + | # --------------------------------------------------------------------- | |
| 87 | + | # 4. GRUB. | |
| 88 | + | # | |
| 89 | + | # `alloy.installer` is what arms alloy-installer.service; without it the | |
| 90 | + | # live system boots to a greeter with no account, which looks like a | |
| 91 | + | # broken ISO. rd.live.image and root=live:CDLABEL are what dmsquash-live | |
| 92 | + | # reads to find the squashfs. | |
| 93 | + | # --------------------------------------------------------------------- | |
| 94 | + | say "grub" | |
| 95 | + | CMDLINE="root=live:CDLABEL=$VOLID rd.live.image rd.live.overlay.overlayfs=1 alloy.installer quiet loglevel=3" | |
| 96 | + | ||
| 97 | + | cat > "$WORK/iso/boot/grub/grub.cfg" <<EOF | |
| 98 | + | set default=0 | |
| 99 | + | set timeout=5 | |
| 100 | + | ||
| 101 | + | menuentry "Install Alloy" { | |
| 102 | + | linux /boot/vmlinuz $CMDLINE | |
| 103 | + | initrd /boot/initramfs.img | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | menuentry "Install Alloy (verbose, for diagnosing a failed boot)" { | |
| 107 | + | linux /boot/vmlinuz root=live:CDLABEL=$VOLID rd.live.image rd.live.overlay.overlayfs=1 alloy.installer rd.debug | |
| 108 | + | initrd /boot/initramfs.img | |
| 109 | + | } | |
| 110 | + | EOF | |
| 111 | + | ||
| 112 | + | # grub.cfg next to the EFI binary is the one the firmware reads first; it | |
| 113 | + | # hands off to the copy above so there is a single place to edit entries. | |
| 114 | + | mkdir -p "$WORK/iso/EFI/BOOT" | |
| 115 | + | cat > "$WORK/iso/EFI/BOOT/grub.cfg" <<EOF | |
| 116 | + | search --no-floppy --set=root --label $VOLID | |
| 117 | + | set prefix=(\$root)/boot/grub | |
| 118 | + | configfile /boot/grub/grub.cfg | |
| 119 | + | EOF | |
| 120 | + | ||
| 121 | + | # Signed shim first, so the ISO boots with Secure Boot on. grubx64.efi is | |
| 122 | + | # what shim chains to and the name is not arbitrary. | |
| 123 | + | cp "$ROOTFS/boot/efi/EFI/BOOT/BOOTX64.EFI" "$WORK/iso/EFI/BOOT/BOOTX64.EFI" 2>/dev/null \ | |
| 124 | + | || cp /boot/efi/EFI/BOOT/BOOTX64.EFI "$WORK/iso/EFI/BOOT/BOOTX64.EFI" 2>/dev/null \ | |
| 125 | + | || cp /usr/share/shim/x64/shimx64.efi "$WORK/iso/EFI/BOOT/BOOTX64.EFI" | |
| 126 | + | cp /usr/share/shim/x64/mmx64.efi "$WORK/iso/EFI/BOOT/mmx64.efi" 2>/dev/null || true | |
| 127 | + | ||
| 128 | + | grub2-mkimage \ | |
| 129 | + | --format=x86_64-efi \ | |
| 130 | + | --prefix="/EFI/BOOT" \ | |
| 131 | + | --output="$WORK/iso/EFI/BOOT/grubx64.efi" \ | |
| 132 | + | part_gpt part_msdos fat iso9660 udf normal linux echo all_video test \ | |
| 133 | + | search search_label search_fs_uuid search_fs_file gfxterm gfxterm_background \ | |
| 134 | + | configfile loadenv chain efi_gop efi_uga ls cat halt reboot minicmd \ | |
| 135 | + | font terminal squash4 loopback probe regexp | |
| 136 | + | ||
| 137 | + | # The EFI system partition the firmware actually mounts. Sized to contents | |
| 138 | + | # rather than a round number, because a fixed size is a future failure the | |
| 139 | + | # day the EFI binaries grow. | |
| 140 | + | say "efiboot.img" | |
| 141 | + | EFI_KB=$(( $(du -sk "$WORK/iso/EFI" | cut -f1) + 2048 )) | |
| 142 | + | truncate -s "${EFI_KB}K" "$WORK/efiboot.img" | |
| 143 | + | mkfs.fat -n ALLOYEFI "$WORK/efiboot.img" >/dev/null | |
| 144 | + | mmd -i "$WORK/efiboot.img" ::/EFI ::/EFI/BOOT 2>/dev/null || { | |
| 145 | + | # mtools absent; fall back to a loop mount, which needs privileges the | |
| 146 | + | # build already has. | |
| 147 | + | mkdir -p "$WORK/efimnt" | |
| 148 | + | mount -o loop "$WORK/efiboot.img" "$WORK/efimnt" | |
| 149 | + | mkdir -p "$WORK/efimnt/EFI/BOOT" | |
| 150 | + | cp -a "$WORK/iso/EFI/BOOT/." "$WORK/efimnt/EFI/BOOT/" | |
| 151 | + | umount "$WORK/efimnt" | |
| 152 | + | } | |
| 153 | + | if command -v mcopy >/dev/null; then | |
| 154 | + | mcopy -i "$WORK/efiboot.img" -s "$WORK/iso/EFI/BOOT/"* ::/EFI/BOOT/ 2>/dev/null || true | |
| 155 | + | fi | |
| 156 | + | cp "$WORK/efiboot.img" "$WORK/iso/efiboot.img" | |
| 157 | + | ||
| 158 | + | # --------------------------------------------------------------------- | |
| 159 | + | # 5. Assemble. UEFI only, matching the images bootc produces: they have no | |
| 160 | + | # legacy BIOS path, so a hybrid MBR would advertise a boot that fails. | |
| 161 | + | # --------------------------------------------------------------------- | |
| 162 | + | say "xorriso" | |
| 163 | + | xorriso -as mkisofs \ | |
| 164 | + | -iso-level 3 \ | |
| 165 | + | -volid "$VOLID" \ | |
| 166 | + | -appid "Alloy Installer" \ | |
| 167 | + | -eltorito-alt-boot \ | |
| 168 | + | -e efiboot.img \ | |
| 169 | + | -no-emul-boot \ | |
| 170 | + | -isohybrid-gpt-basdat \ | |
| 171 | + | -output "$OUTPUT/install.iso" \ | |
| 172 | + | "$WORK/iso" | |
| 173 | + | ||
| 174 | + | chmod 0644 "$OUTPUT/install.iso" | |
| 175 | + | say "built $(du -h "$OUTPUT/install.iso" | cut -f1) at $OUTPUT/install.iso" |
| @@ -1,0 +1,40 @@ | |||
| 1 | + | # Runs the Alloy installer on VT1 when booted from the installer ISO. | |
| 2 | + | # | |
| 3 | + | # Gated on a kernel command line flag rather than shipped disabled and | |
| 4 | + | # enabled by the ISO build: the same image is both the live installer and | |
| 5 | + | # the system it installs, and an installer that could start on an installed | |
| 6 | + | # machine is a way to lose a disk. `alloy.installer` is set only by the | |
| 7 | + | # GRUB entries in build/build-iso.sh, so this unit is inert everywhere else. | |
| 8 | + | # | |
| 9 | + | # Conflicts with greetd because both want VT1. On the live medium there is | |
| 10 | + | # no account to log into yet, so the greeter has nothing to offer and would | |
| 11 | + | # only race the installer for the terminal. | |
| 12 | + | [Unit] | |
| 13 | + | Description=Alloy installer | |
| 14 | + | Documentation=https://git.sr.ht/~maxmj/alloy | |
| 15 | + | ConditionKernelCommandLine=alloy.installer | |
| 16 | + | After=systemd-user-sessions.service systemd-logind.service | |
| 17 | + | Before=getty.target | |
| 18 | + | Conflicts=getty@tty1.service greetd.service | |
| 19 | + | ||
| 20 | + | [Service] | |
| 21 | + | # Type=idle so the boot's own console output has finished before a | |
| 22 | + | # full-screen TUI takes the terminal; without it the first frame draws | |
| 23 | + | # over service status lines and the user sees a torn screen. | |
| 24 | + | Type=idle | |
| 25 | + | ExecStart=/usr/bin/alloy install | |
| 26 | + | TTYPath=/dev/tty1 | |
| 27 | + | StandardInput=tty | |
| 28 | + | StandardOutput=tty | |
| 29 | + | StandardError=journal | |
| 30 | + | TTYReset=yes | |
| 31 | + | TTYVHangup=yes | |
| 32 | + | TTYVTDisallocate=yes | |
| 33 | + | ||
| 34 | + | # A crashed installer must not leave a blank VT with no way forward. It is | |
| 35 | + | # also not something to retry forever: the disk may be half written, so | |
| 36 | + | # coming back to a fresh wizard would invite a second pass over it. | |
| 37 | + | Restart=no | |
| 38 | + | ||
| 39 | + | [Install] | |
| 40 | + | WantedBy=multi-user.target |