#!/bin/sh # # install - rawnix.org Netinstaller # https://rawnix.org # # Usage: install --target [group2 ...] # # Groups: # base full install: libs + core + opt + kernel + devel # core base system packages (pulls libs and opt too) # opt udevd, limine, tinyrd # libs runtime libraries only # kernel fetch and install the prebuilt generic kernel # (linux--generic): every in-tree driver, wifi, GPU, # bluetooth and ethernet as modules, for real hardware # kernel-cloud # the same, but the cloud kernel (linux--cloud): no # wifi, GPU, bluetooth or sound, for virtual machines # # NOTE: this script is /bin/install in the image and PATH is # /sbin:/bin:/usr/bin, so a bare `install` from inside here re-executes # *this* script, not coreutils install(1) -- it parses -m600 as a group # name and dies with the usage message. Use cp + chmod instead. # # devel is not selectable on its own: it is part of base. A toolchain # without the core system underneath it has nothing to build against, # and installing it separately only ever meant a second run. set -e die() { echo "ERROR: $*" >&2; exit 1; } say() { printf ">>> %s\n" "$*"; } info() { printf " + %s\n" "$*"; } warn() { printf " ! %s\n" "$*"; } [ -f /etc/ports/ports.rsync ] || die "ports.rsync not found — is this the rawnix.org installer?" TOKEN=$(grep '^token=' /etc/ports/ports.rsync | cut -d= -f2) [ -n "$TOKEN" ] || die "No token found in /etc/ports/ports.rsync" RSYNC_HOST="$TOKEN@rsync.rawnix.org::bulk" export RSYNC_PASSWORD="$TOKEN" [ "$(id -u)" = 0 ] || die "Must run as root" # ── Group → collection + package list ───────────────────────── # Each group selects specific ports from a bulk collection. The # collection naming matches /etc/ports/ports.rsync on the build # server; "udevd" is in opt/ because the udevd port lives in # /usr/ports/opt/udevd. Add or remove names here to change what # 'install --target ... ' pulls down. CORE_COL="core" # musl and shadow lead the list, and the order is load-bearing. # # addpkg's meta_add_groups chroots into the target to run groupadd, # useradd and install -d. Until musl is there the target has no # /lib/ld-musl-x86_64.so.1, so nothing inside it can exec at all, and # until shadow is there useradd does not exist. A package carrying a # groups= line that lands before either of them fails twice: # # addpkg: useradd dhcpcd failed (exit 127) # addpkg: install -d /var/lib/dhcpcd failed (exit 127) # # Both are exit 127 -- execvp could not run it -- and neither says why. # The account is simply never created and the install looks fine. # mkinstall.sh solves this the same way for the image. CORE_PKGS=" musl shadow acl argon2 attr bash bzip2 coreutils cryptsetup dash device-mapper dhcpcd diffutils dosfstools e2fsprogs efibootmgr efivar file findutils gmp grep gzip iproute2 kmod libcap2 libelf libmnl libnl libressl libseccomp lz4 mawk pigz pkg mkpkg ncurses openresolv openvi patch pcre2 popt procps readline rsync runit sed signify-openbsd tar tzdata revdep util-linux wget wpa_supplicant xxhash xz zlib zstd " # limine owns the bootloader on the ESP and ships limine-sync; tinyrd is # the initramfs. Both are needed on every install now that the target # boots through limine rather than an EFI stub, so they are not optional # and not a separate group. OPT_COL="opt" OPT_PKGS="udevd limine tinyrd" # libucontext: udevd's libsystemd-shared needs it, and udevd's hwdb # trigger runs at addpkg time -- so libs must be installed before opt, # not after. json-c: cryptsetup links it; without it cryptsetup exits # on the first run, which on an encrypted install is at boot. LIBS_COL="libs" LIBS_PKGS="libffi libedit libucontext json-c" DEVEL_COL="devel" DEVEL_PKGS=" llvm rust gcc-compat cmake python3 make meson samurai pkgconf autoconf automake libtool m4 perl " # ── Arguments ───────────────────────────────────────────────── MNT="" GROUPS="" while [ $# -gt 0 ]; do case "$1" in --target) MNT="$2"; shift 2 ;; base|core|opt|libs|kernel|kernel-cloud) GROUPS="$GROUPS $1"; shift ;; udevd) GROUPS="$GROUPS opt"; shift ;; # old name *) die "Usage: install --target [group2 ...] Groups: base core opt libs kernel kernel-cloud" ;; esac done [ -n "$MNT" ] || die "Usage: install --target [group2 ...]" [ -n "$GROUPS" ] || die "No group specified. Choose: base core opt libs kernel kernel-cloud" [ -d "$MNT" ] || die "Target not found: $MNT" mountpoint -q "$MNT" || die "$MNT is not mounted" [ -f "$MNT/var/lib/pkg/db" ] || die "Run setup-filesystem $MNT first" # The ESP being an unmounted empty directory is the failure that # looks like success: everything installs, the bootloader files land # on the root filesystem, and the machine boots whatever was on the # real ESP before. Warn rather than die -- an install with no ESP is # legitimate, an install that silently wrote into the mountpoint is # not. if [ -d "$MNT/boot/efi" ] && ! mountpoint -q "$MNT/boot/efi"; then warn "$MNT/boot/efi exists but is not a mountpoint" warn "mount the ESP there before installing a bootloader" fi FAILED="" TMP="$MNT/tmp/rawnix-install" echo "" echo " rawnix.org Netinstaller" echo " Target : $MNT" echo " Groups : $GROUPS" echo " Server : rsync.rawnix.org" echo "" # ── Verify network ──────────────────────────────────────────── say "Checking network..." rsync --quiet --list-only "$RSYNC_HOST/" > /dev/null 2>&1 || \ die "Cannot reach rsync.rawnix.org — check your network connection" mkdir -p "$TMP" # ── install_pkg: addpkg a single package dir ────────────────── # addpkg's stderr is deliberately not discarded. It carries the # "reserved group X is missing from $MNT — broken filesystem # skeleton" warning, which is the only thing that catches a target # whose /etc/group does not match the reserved table compiled into # mkpkg. A silent install onto a broken skeleton leaves service # accounts with dynamically allocated GIDs that will not match any # other machine. install_pkg() { pkgdir="$1" name=$(basename "$pkgdir") # sort -V, not `ls -t`: rsync -a preserves the server's mtimes, so # "newest file" is whichever the server touched last, not the # highest version. Same rule install_kernel already uses. pkg=$(ls "$pkgdir"/*.pkg.tar.gz 2>/dev/null | sort -V | tail -1) [ -f "$pkg" ] || { warn "No package found in $pkgdir"; FAILED="$FAILED $name"; return; } info "$name" addpkg -f -r "$MNT" "$pkg" || { warn "addpkg failed: $name"; FAILED="$FAILED $name"; } rm -f "$pkg" } # ── install_group: pull listed pkgs from a collection ───────── # Pulls only the named ports from bulk//, not the whole # collection. Uses rsync --files-from for a single transfer # regardless of list length. install_group() { group="$1"; col="$2"; pkgs="$3" say "Installing: $group (from bulk/$col/)" [ -n "$pkgs" ] || { warn "empty package list for $group"; return; } mkdir -p "$TMP/$group" list="$TMP/$group.list" : > "$list" for name in $pkgs; do echo "$name/" >> "$list" done # rsync reports a missing name on stderr and carries on with the # rest, so a port published under the wrong collection used to # vanish here: no directory, nothing to install, nothing said. # # --no-owner/--no-group: the bulk server is an ordinary account # (ubuntu, uid 1001), and -a alone reproduces 1001:1001 on whatever # it writes. Here that lands in $TMP and is then read by addpkg, so # it is cosmetic rather than dangerous -- but the same flags are in # ports.c for the same reason, and a package staged under an # unknown uid is not something to leave to chance. rsync -a --no-owner --no-group --info=progress2 --files-from="$list" \ "$RSYNC_HOST/$col/" "$TMP/$group/" || true rm -f "$list" for name in $pkgs; do if [ -d "$TMP/$group/$name" ]; then install_pkg "$TMP/$group/$name" else warn "not found in bulk/$col/: $name" FAILED="$FAILED $name" fi done rm -rf "$TMP/$group" echo "" } # ── Install the kernel package ──────────────────────────────── # The kernel is an ordinary package; sync-bulk.sh publishes it as # # bulk/linux/linux--/linux--#-.pkg.tar.gz # # where is generic or cloud: the same string as the kernel's # CONFIG_LOCALVERSION suffix, so the port name, the image # (/boot/vmlinuz--) and the module tree # (/lib/modules/-) all agree. # # The port name carries the version, so each kernel is a distinct package # rather than a new release of one. That is what lets two kernels be # installed at once, which is what the limine fallback entries and their # module trees depend on. It also means a kernel is never an upgrade: # addpkg, never addpkg -u. # # Installing it is deliberately the last thing that happens. Its # boot/ payload fires depmod and then limine-sync, and limine-sync is # what writes limine.conf on the ESP -- so everything it reads # (limine.conf.in from the limine port, limine-sync.conf from # setup-boot, tinyrd.img in /boot) has to be in place first. # # Note this is the only kernel the target gets. `pkg sysup` will # never offer another: /usr/src is not in pkg.conf's portdir list, on # purpose, so a kernel is only ever installed by hand. # # $1 is the flavor: generic (the default) or cloud. Only directories # that are exactly linux-- are considered, so the # flavors never mix, and neither do the old layouts: a leftover # linux- or linux-cloud- directory on the server matches # nothing and is never picked. The version part must be digits and # dots, so another flavor's directory (linux-7.2.11-msi) cannot be read # as a version either. install_kernel() { kflavor="${1:-generic}" say "Installing kernel package linux--$kflavor (from bulk/linux/)" [ -f "$MNT/etc/limine-sync.conf" ] || die "no $MNT/etc/limine-sync.conf — run: setup-boot $MNT" [ -f "$MNT/boot/tinyrd.img" ] || warn "no $MNT/boot/tinyrd.img — install the core group first, or the entries will have no initramfs" mountpoint -q "$MNT/boot/efi" || die "$MNT/boot/efi is not a mountpoint — limine-sync will refuse" kver=$(rsync --list-only "$RSYNC_HOST/linux/" 2>/dev/null | awk '/^d/ { print $NF }' | sed -n "s/^linux-\([0-9][0-9.]*\)-${kflavor}\$/\1/p" | sort -V | tail -1) [ -n "$kver" ] || die "no linux--${kflavor}/ directories in bulk/linux/" kname="linux-$kver-$kflavor" say "Kernel: $kname" # Only the package. A kernel port directory also holds config and, # once it has been built there, the upstream linux-.tar.xz -- # ~150MB of source pulled over the network to be deleted unread. mkdir -p "$TMP/linux" rsync -a --no-owner --no-group --info=progress2 \ --include='*.pkg.tar.gz' --exclude='*' \ "$RSYNC_HOST/linux/$kname/" "$TMP/linux/" kpkg=$(ls "$TMP/linux/$kname"#"$kver"-*.pkg.tar.gz 2>/dev/null | sort -V | tail -1) [ -f "$kpkg" ] || die "no $kname#$kver-*.pkg.tar.gz in bulk/linux/$kname/" info "$(basename "$kpkg")" addpkg -f -r "$MNT" "$kpkg" || { warn "addpkg failed: kernel"; FAILED="$FAILED linux"; } rm -rf "$TMP/linux" echo "" } # ── Dispatch groups ─────────────────────────────────────────── # ensure_libs rather than an inline `[ $libs_done -eq 0 ] && ...`: # under set -e that test is the last command of the `libs` branch, so # asking for a group whose libraries were already installed made the # whole case exit 1 and took the script with it. libs_done=0 ensure_libs() { [ "$libs_done" -eq 0 ] || return 0 install_group "libs" "$LIBS_COL" "$LIBS_PKGS" libs_done=1 } for group in $GROUPS; do case "$group" in base) ensure_libs install_group "core" "$CORE_COL" "$CORE_PKGS" install_group "opt" "$OPT_COL" "$OPT_PKGS" install_group "devel" "$DEVEL_COL" "$DEVEL_PKGS" install_kernel generic ;; core) ensure_libs install_group "core" "$CORE_COL" "$CORE_PKGS" install_group "opt" "$OPT_COL" "$OPT_PKGS" ;; opt) ensure_libs install_group "opt" "$OPT_COL" "$OPT_PKGS" ;; libs) ensure_libs ;; kernel) install_kernel generic ;; kernel-cloud) install_kernel cloud ;; esac done # ── Copy ports.rsync and the trust store into target ────────── # cp + chmod, never `install -m600`: see the note at the top of this # file. 600 because the file carries the rsync token, and it has been # world-readable on every installed system so far. cp /etc/ports/ports.rsync "$MNT/etc/ports/ports.rsync" chmod 600 "$MNT/etc/ports/ports.rsync" # Without a public key the target can rsync the ports tree but cannot # trust it: mkpkg's trust anchor has nothing to match a port's signify() # block against, and `pkg sysup` cannot verify a .signature. That fails # at the first build or upgrade, long after the install looked fine. # # Copied from the installer's own store rather than fetched, so the key # never arrives over the same unauthenticated channel as the content it # vouches for. say "Installing trust store" mkdir -p "$MNT/etc/ports/keys" keys=0 for pub in /etc/ports/keys/*.pub; do [ -f "$pub" ] || continue cp "$pub" "$MNT/etc/ports/keys/" chmod 644 "$MNT/etc/ports/keys/$(basename "$pub")" info "$(basename "$pub")" keys=$((keys + 1)) done [ "$keys" -gt 0 ] || warn "no public key installed — the target cannot verify ports" echo "" # ── Cleanup ─────────────────────────────────────────────────── rm -rf "$TMP" # ── Report failures ─────────────────────────────────────────── # A half-installed system that exits 0 is worse than one that # stops: the next step is a chroot, and the missing package does # not surface until something in there cannot find it. if [ -n "$FAILED" ]; then echo "" warn "the following packages did not install:$FAILED" warn "the target is incomplete — fix these before chrooting" exit 1 fi # ── Done ────────────────────────────────────────────────────── say "Done. Next steps:" echo "" echo " genfstab /mnt > /mnt/etc/fstab" echo " enter-chroot /mnt" echo "" echo " Inside chroot:" echo " passwd root" echo " echo myhostname > /etc/hostname" echo " ln -sf /usr/share/zoneinfo/Region/City /etc/localtime" echo " ports -u (sync the tree before any other pkg command)" echo "" echo " UEFI: the bootloader is installed; limine-sync wrote" echo " /boot/efi/limine.conf when the kernel landed." echo "" echo " BIOS: inside the chroot, once:" echo " limine bios-install /dev/sdX 1" echo " cp /usr/share/limine/limine-bios.sys /boot/efi/" echo "" echo " Serial (VPS, qemu -nographic), UEFI or BIOS:" echo " vi /etc/limine-sync.conf" echo " add console=tty0 console=ttyS0,115200 to CMDLINE" echo " limine-sync" echo "" echo " Before rebooting, and for firmware that needs a boot entry:" echo " man bootloader" echo ""