summaryrefslogtreecommitdiff
path: root/bin/install
blob: 2f4dc3b410ef47873785fd91cea824a2ad763b1b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
#!/bin/sh
#
# install - rawnix.org Netinstaller
# https://rawnix.org
#
# Usage: install --target <mountpoint> <group> [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-<ver>-generic): every in-tree driver, wifi, GPU,
#             bluetooth and ethernet as modules, for real hardware
#   kernel-cloud
#             the same, but the cloud kernel (linux-<ver>-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 ... <group>' 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 <mountpoint> <group> [group2 ...]
Groups: base core opt libs kernel kernel-cloud" ;;
    esac
done

[ -n "$MNT" ]    || die "Usage: install --target <mountpoint> <group> [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/<collection>/, 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-<ver>-<flavor>/linux-<ver>-<flavor>#<ver>-<rel>.pkg.tar.gz
#
# where <flavor> is generic or cloud: the same string as the kernel's
# CONFIG_LOCALVERSION suffix, so the port name, the image
# (/boot/vmlinuz-<ver>-<flavor>) and the module tree
# (/lib/modules/<ver>-<flavor>) 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-<version>-<flavor> are considered, so the
# flavors never mix, and neither do the old layouts: a leftover
# linux-<ver> or linux-cloud-<ver> 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-<ver>-$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-<ver>-${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-<ver>.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 ""