From 69071efd582a646acf846d011cf00d6105069009 Mon Sep 17 00:00:00 2001 From: "Michal S." Date: Tue, 4 Aug 2026 17:59:44 +0100 Subject: [PATCH] init --- .containerignore | 1 + .gitignore | 3 + Containerfile | 301 ++++++++++ Makefile | 41 ++ README.md | 72 +++ base/cheesed/.cargo/config.toml | 2 + base/cheesed/Cargo.lock | 16 + base/cheesed/Cargo.toml | 17 + base/cheesed/scripts/build-static.sh | 25 + base/cheesed/src/lib.rs | 87 +++ base/cheesed/src/linux.rs | 172 ++++++ base/cheesed/src/linux/boot.rs | 146 +++++ base/cheesed/src/linux/process.rs | 437 ++++++++++++++ base/cheesed/src/linux/services.rs | 458 ++++++++++++++ base/cheesed/src/main.rs | 119 ++++ base/cheesed/src/policy.rs | 568 ++++++++++++++++++ base/kernel/x86_64.config | 122 ++++ base/limine.conf | 16 + base/patches/chimerautils-native-cdefs.patch | 36 ++ base/patches/chimerautils-scanflike.patch | 12 + base/patches/nftables-posix-configure.patch | 15 + base/patches/pkgsrc-bootstrap-mouse.patch | 13 + base/patches/pkgsrc-rc.subr-cgroup.patch | 27 + base/patches/tcsh-gethost-native.patch | 26 + base/rootfs/etc/crontab | 4 + base/rootfs/etc/csh.cshrc | 5 + base/rootfs/etc/csh.login | 4 + base/rootfs/etc/default/useradd | 7 + base/rootfs/etc/depmod.d/mouse.conf | 3 + base/rootfs/etc/dhcpcd.conf | 4 + base/rootfs/etc/group | 6 + base/rootfs/etc/hostname | 1 + base/rootfs/etc/hosts | 2 + base/rootfs/etc/login.defs | 18 + base/rootfs/etc/mdev.conf | 13 + base/rootfs/etc/motd | 2 + base/rootfs/etc/mouse-release | 6 + base/rootfs/etc/ntpd.conf | 1 + base/rootfs/etc/passwd | 4 + base/rootfs/etc/rc.conf | 4 + base/rootfs/etc/rc.d/console_login | 33 + base/rootfs/etc/rc.d/cron | 12 + base/rootfs/etc/rc.d/daemon | 11 + base/rootfs/etc/rc.d/login | 11 + base/rootfs/etc/rc.d/loopback | 20 + base/rootfs/etc/rc.d/machine_identity | 20 + base/rootfs/etc/rc.d/mdevd | 11 + base/rootfs/etc/rc.d/network | 38 ++ base/rootfs/etc/rc.d/ntpd | 12 + base/rootfs/etc/rc.d/servers | 11 + base/rootfs/etc/rc.d/syslogd | 12 + base/rootfs/etc/securetty | 1 + base/rootfs/etc/services | 1 + base/rootfs/etc/shadow | 4 + base/rootfs/etc/shells | 2 + base/rootfs/etc/syslog.conf | 10 + base/rootfs/root/.profile | 8 + base/rootfs/usr/lib/mouse/required-services | 8 + base/rootfs/usr/libexec/mouse-dhcpcd-hook | 22 + base/support/cross-compiler-driver | 137 +++++ base/support/mouse-bootenv | 88 +++ base/support/rust-eh-personality.c | 9 + release/facilities.lock | 11 + release/firmware.lock | 2 + release/kernel.lock | 2 + release/pkgsrc.lock | 2 + release/software.lock | 32 + release/storage.lock | 4 + release/toolchain.lock | 2 + scripts/audit-static-base.sh | 154 +++++ scripts/build-boot-disk.sh | 76 +++ scripts/build-image-commands.sh | 24 + scripts/build-image.sh | 211 +++++++ scripts/build-in-container.sh | 191 ++++++ scripts/build-kernel-test-module.sh | 29 + scripts/build-kernel.sh | 80 +++ scripts/build-pkgsrc-seed.sh | 89 +++ scripts/check-source-locks.sh | 197 ++++++ scripts/configure-cross-toolchain.sh | 51 ++ scripts/fetch-sources.sh | 114 ++++ scripts/run-qemu.sh | 152 +++++ scripts/static-base/bootstrap.sh | 221 +++++++ scripts/static-base/common.sh | 55 ++ scripts/static-base/dynamic-runtime.sh | 125 ++++ scripts/static-base/facilities.sh | 352 +++++++++++ scripts/static-base/finish.sh | 9 + scripts/static-base/libraries-and-accounts.sh | 263 ++++++++ scripts/static-base/native-toolchain.sh | 159 +++++ scripts/static-base/storage.sh | 107 ++++ scripts/static-base/system-services.sh | 489 +++++++++++++++ tests/acceptance/boot.exp | 61 ++ tests/acceptance/boot.sh | 19 + tests/acceptance/btrfs.exp | 71 +++ tests/acceptance/btrfs.sh | 20 + tests/acceptance/firmware.exp | 46 ++ tests/acceptance/firmware.sh | 45 ++ tests/acceptance/harness.exp | 73 +++ tests/acceptance/harness.sh | 67 +++ tests/acceptance/multi-user.exp | 99 +++ tests/acceptance/multi-user.sh | 13 + tests/acceptance/network.exp | 77 +++ tests/acceptance/network.sh | 12 + tests/acceptance/pkgsrc.exp | 66 ++ tests/acceptance/pkgsrc.sh | 33 + tests/acceptance/service-integration.exp | 110 ++++ tests/acceptance/service-integration.sh | 15 + tests/acceptance/storage.exp | 64 ++ tests/acceptance/storage.sh | 20 + tests/acceptance/toolchain.exp | 56 ++ tests/acceptance/toolchain.sh | 11 + tests/acceptance/world.exp | 77 +++ tests/acceptance/world.sh | 14 + tests/fixtures/kernel-module/Makefile | 1 + tests/fixtures/kernel-module/mouse_test.c | 18 + tests/fixtures/mouse-echo/mouse-echo.c | 41 ++ .../mouse-echo/rootfs/etc/rc.d/mouse_echo | 28 + .../pkgsrc/distfiles/mouse-hello-1.0.c | 8 + tests/fixtures/pkgsrc/local/mouse-hello/DESCR | 2 + .../pkgsrc/local/mouse-hello/Makefile | 26 + tests/fixtures/pkgsrc/local/mouse-hello/PLIST | 3 + .../pkgsrc/local/mouse-hello/distinfo | 5 + .../mouse-hello/files/mouse-hello-service | 16 + .../local/mouse-hello/files/mouse_hello.sh | 11 + 123 files changed, 7728 insertions(+) create mode 100644 .containerignore create mode 100644 .gitignore create mode 100644 Containerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 base/cheesed/.cargo/config.toml create mode 100644 base/cheesed/Cargo.lock create mode 100644 base/cheesed/Cargo.toml create mode 100755 base/cheesed/scripts/build-static.sh create mode 100644 base/cheesed/src/lib.rs create mode 100644 base/cheesed/src/linux.rs create mode 100644 base/cheesed/src/linux/boot.rs create mode 100644 base/cheesed/src/linux/process.rs create mode 100644 base/cheesed/src/linux/services.rs create mode 100644 base/cheesed/src/main.rs create mode 100644 base/cheesed/src/policy.rs create mode 100644 base/kernel/x86_64.config create mode 100644 base/limine.conf create mode 100644 base/patches/chimerautils-native-cdefs.patch create mode 100644 base/patches/chimerautils-scanflike.patch create mode 100644 base/patches/nftables-posix-configure.patch create mode 100644 base/patches/pkgsrc-bootstrap-mouse.patch create mode 100644 base/patches/pkgsrc-rc.subr-cgroup.patch create mode 100644 base/patches/tcsh-gethost-native.patch create mode 100644 base/rootfs/etc/crontab create mode 100644 base/rootfs/etc/csh.cshrc create mode 100644 base/rootfs/etc/csh.login create mode 100644 base/rootfs/etc/default/useradd create mode 100644 base/rootfs/etc/depmod.d/mouse.conf create mode 100644 base/rootfs/etc/dhcpcd.conf create mode 100644 base/rootfs/etc/group create mode 100644 base/rootfs/etc/hostname create mode 100644 base/rootfs/etc/hosts create mode 100644 base/rootfs/etc/login.defs create mode 100644 base/rootfs/etc/mdev.conf create mode 100644 base/rootfs/etc/motd create mode 100644 base/rootfs/etc/mouse-release create mode 100644 base/rootfs/etc/ntpd.conf create mode 100644 base/rootfs/etc/passwd create mode 100644 base/rootfs/etc/rc.conf create mode 100644 base/rootfs/etc/rc.d/console_login create mode 100644 base/rootfs/etc/rc.d/cron create mode 100644 base/rootfs/etc/rc.d/daemon create mode 100644 base/rootfs/etc/rc.d/login create mode 100644 base/rootfs/etc/rc.d/loopback create mode 100644 base/rootfs/etc/rc.d/machine_identity create mode 100644 base/rootfs/etc/rc.d/mdevd create mode 100644 base/rootfs/etc/rc.d/network create mode 100644 base/rootfs/etc/rc.d/ntpd create mode 100644 base/rootfs/etc/rc.d/servers create mode 100644 base/rootfs/etc/rc.d/syslogd create mode 100644 base/rootfs/etc/securetty create mode 100644 base/rootfs/etc/services create mode 100644 base/rootfs/etc/shadow create mode 100644 base/rootfs/etc/shells create mode 100644 base/rootfs/etc/syslog.conf create mode 100644 base/rootfs/root/.profile create mode 100644 base/rootfs/usr/lib/mouse/required-services create mode 100755 base/rootfs/usr/libexec/mouse-dhcpcd-hook create mode 100755 base/support/cross-compiler-driver create mode 100755 base/support/mouse-bootenv create mode 100644 base/support/rust-eh-personality.c create mode 100644 release/facilities.lock create mode 100644 release/firmware.lock create mode 100644 release/kernel.lock create mode 100644 release/pkgsrc.lock create mode 100644 release/software.lock create mode 100644 release/storage.lock create mode 100644 release/toolchain.lock create mode 100755 scripts/audit-static-base.sh create mode 100644 scripts/build-boot-disk.sh create mode 100755 scripts/build-image-commands.sh create mode 100755 scripts/build-image.sh create mode 100755 scripts/build-in-container.sh create mode 100755 scripts/build-kernel-test-module.sh create mode 100755 scripts/build-kernel.sh create mode 100755 scripts/build-pkgsrc-seed.sh create mode 100755 scripts/check-source-locks.sh create mode 100755 scripts/configure-cross-toolchain.sh create mode 100755 scripts/fetch-sources.sh create mode 100755 scripts/run-qemu.sh create mode 100644 scripts/static-base/bootstrap.sh create mode 100644 scripts/static-base/common.sh create mode 100755 scripts/static-base/dynamic-runtime.sh create mode 100644 scripts/static-base/facilities.sh create mode 100644 scripts/static-base/finish.sh create mode 100644 scripts/static-base/libraries-and-accounts.sh create mode 100644 scripts/static-base/native-toolchain.sh create mode 100644 scripts/static-base/storage.sh create mode 100644 scripts/static-base/system-services.sh create mode 100755 tests/acceptance/boot.exp create mode 100755 tests/acceptance/boot.sh create mode 100755 tests/acceptance/btrfs.exp create mode 100755 tests/acceptance/btrfs.sh create mode 100755 tests/acceptance/firmware.exp create mode 100755 tests/acceptance/firmware.sh create mode 100644 tests/acceptance/harness.exp create mode 100644 tests/acceptance/harness.sh create mode 100755 tests/acceptance/multi-user.exp create mode 100755 tests/acceptance/multi-user.sh create mode 100755 tests/acceptance/network.exp create mode 100755 tests/acceptance/network.sh create mode 100755 tests/acceptance/pkgsrc.exp create mode 100755 tests/acceptance/pkgsrc.sh create mode 100755 tests/acceptance/service-integration.exp create mode 100755 tests/acceptance/service-integration.sh create mode 100755 tests/acceptance/storage.exp create mode 100755 tests/acceptance/storage.sh create mode 100755 tests/acceptance/toolchain.exp create mode 100755 tests/acceptance/toolchain.sh create mode 100755 tests/acceptance/world.exp create mode 100755 tests/acceptance/world.sh create mode 100644 tests/fixtures/kernel-module/Makefile create mode 100644 tests/fixtures/kernel-module/mouse_test.c create mode 100644 tests/fixtures/mouse-echo/mouse-echo.c create mode 100644 tests/fixtures/mouse-echo/rootfs/etc/rc.d/mouse_echo create mode 100644 tests/fixtures/pkgsrc/distfiles/mouse-hello-1.0.c create mode 100644 tests/fixtures/pkgsrc/local/mouse-hello/DESCR create mode 100644 tests/fixtures/pkgsrc/local/mouse-hello/Makefile create mode 100644 tests/fixtures/pkgsrc/local/mouse-hello/PLIST create mode 100644 tests/fixtures/pkgsrc/local/mouse-hello/distinfo create mode 100644 tests/fixtures/pkgsrc/local/mouse-hello/files/mouse-hello-service create mode 100644 tests/fixtures/pkgsrc/local/mouse-hello/files/mouse_hello.sh diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..0f27300 --- /dev/null +++ b/.containerignore @@ -0,0 +1 @@ +# Apple Container requires this file; the wrapper already cleans the context. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a035951 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/build/ +/base/cheesed/target/ +.DS_Store diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..310951a --- /dev/null +++ b/Containerfile @@ -0,0 +1,301 @@ +ARG MOUSE_ROOTFS_STAGE=rootfs-builder + +FROM alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS toolchain + +RUN apk add --no-cache \ + bc=1.08.2-r1 \ + build-base=0.5-r4 \ + clang22=22.1.3-r2 \ + cmake=4.2.3-r0 \ + curl=8.21.0-r0 \ + diffutils=3.12-r0 \ + elfutils-dev=0.195-r0 \ + file=5.47-r2 \ + findutils=4.10.0-r1 \ + gawk=5.3.2-r2 \ + lld22=22.1.3-r0 \ + linux-headers=7.0.0-r1 \ + llvm=22-r0 \ + patch=2.8-r0 \ + perl=5.42.2-r0 \ + python3=3.14.5-r0 \ + rustup=1.29.0-r0 \ + rsync=3.4.3-r1 \ + openssl-dev=3.5.7-r0 \ + xz=5.8.3-r0 + +RUN apk add --no-cache flex=2.6.4-r8 + +ENV RUSTUP_HOME=/opt/rustup +ENV CARGO_HOME=/opt/cargo +ENV PATH=/opt/cargo/bin:/usr/lib/llvm22/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +RUN rustup-init -y \ + --profile minimal \ + --default-toolchain 1.94.0 \ + --target x86_64-unknown-linux-musl \ + && chmod -R a+rX /opt/rustup /opt/cargo + +WORKDIR /work/mouse-src +ARG MOUSE_BUILD_JOBS=8 +ENV MOUSE_BUILD_JOBS=${MOUSE_BUILD_JOBS} + +FROM scratch AS toolchain-sources +COPY mouse-sources/toolchain/ /opt/mouse-sources/ + +FROM scratch AS kernel-sources +COPY mouse-sources/kernel/ /opt/mouse-sources/ + +FROM scratch AS firmware-sources +COPY mouse-sources/firmware/ /opt/mouse-sources/ + +FROM scratch AS facilities-sources +COPY mouse-sources/facilities/ /opt/mouse-sources/ + +FROM scratch AS storage-sources +COPY mouse-sources/storage/ /opt/mouse-sources/ + +FROM scratch AS pkgsrc-sources +COPY mouse-sources/pkgsrc/ /opt/mouse-sources/ + +FROM scratch AS software-sources +COPY mouse-sources/software/ /opt/mouse-sources/ + +FROM --platform=linux/amd64 alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS pkgsrc-seed-tools + +FROM toolchain AS kernel-builder + +RUN apk add --no-cache bison=3.8.2-r3 + +COPY --from=kernel-sources /opt/mouse-sources/ /opt/mouse-sources/ +COPY mouse-src/base/kernel/ /work/mouse-src/base/kernel/ +COPY mouse-src/scripts/build-kernel.sh /work/mouse-src/scripts/build-kernel.sh + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + /work/mouse-src/scripts/build-kernel.sh + +FROM kernel-builder AS kernel-test-builder + +COPY mouse-src/tests/fixtures/kernel-module/ /work/mouse-src/tests/fixtures/kernel-module/ +COPY mouse-src/scripts/build-kernel-test-module.sh /work/mouse-src/scripts/build-kernel-test-module.sh + +RUN /work/mouse-src/scripts/build-kernel-test-module.sh + +# Keep the resource-heavy kernel and LLVM builds from running concurrently. +# The constant marker adds an ordering edge without passing kernel artifacts +# into the static-base cache key. +FROM kernel-builder AS kernel-ready + +RUN install -m 0444 /dev/null /kernel-ready + +FROM toolchain AS static-base-bootstrap + +RUN apk add --no-cache \ + bison=3.8.2-r3 \ + byacc=20260126-r0 + +COPY --from=kernel-ready /kernel-ready /tmp/kernel-ready + +COPY --from=toolchain-sources /opt/mouse-sources/ /opt/mouse-sources/ +COPY --from=kernel-sources /opt/mouse-sources/ /opt/mouse-sources/ +COPY --from=software-sources /opt/mouse-sources/ /opt/mouse-sources/ +COPY mouse-src/base/support/cross-compiler-driver /work/mouse-src/base/support/cross-compiler-driver +COPY mouse-src/base/support/rust-eh-personality.c /work/mouse-src/base/support/rust-eh-personality.c +COPY mouse-src/scripts/configure-cross-toolchain.sh /work/mouse-src/scripts/configure-cross-toolchain.sh +COPY mouse-src/scripts/static-base/common.sh /work/mouse-src/scripts/static-base/common.sh +COPY mouse-src/scripts/static-base/bootstrap.sh /work/mouse-src/scripts/static-base/bootstrap.sh + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + sh /work/mouse-src/scripts/static-base/bootstrap.sh + +FROM static-base-bootstrap AS static-base-libraries + +COPY mouse-src/scripts/static-base/libraries-and-accounts.sh /work/mouse-src/scripts/static-base/libraries-and-accounts.sh + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + sh /work/mouse-src/scripts/static-base/libraries-and-accounts.sh + +FROM static-base-libraries AS static-base-toolchain + +COPY mouse-src/scripts/static-base/native-toolchain.sh /work/mouse-src/scripts/static-base/native-toolchain.sh + +RUN MOUSE_NATIVE_TOOLCHAIN_PHASE=llvm \ + MOUSE_SOURCE_DIR=/opt/mouse-sources \ + sh /work/mouse-src/scripts/static-base/native-toolchain.sh + +RUN MOUSE_NATIVE_TOOLCHAIN_PHASE=package-tools \ + MOUSE_SOURCE_DIR=/opt/mouse-sources \ + sh /work/mouse-src/scripts/static-base/native-toolchain.sh + +COPY mouse-src/scripts/static-base/dynamic-runtime.sh /work/mouse-src/scripts/static-base/dynamic-runtime.sh + +RUN sh /work/mouse-src/scripts/static-base/dynamic-runtime.sh + +FROM static-base-toolchain AS static-base-system + +COPY mouse-src/base/patches/chimerautils-native-cdefs.patch /work/mouse-src/base/patches/chimerautils-native-cdefs.patch +COPY mouse-src/base/patches/chimerautils-scanflike.patch /work/mouse-src/base/patches/chimerautils-scanflike.patch +COPY mouse-src/base/patches/tcsh-gethost-native.patch /work/mouse-src/base/patches/tcsh-gethost-native.patch +COPY mouse-src/scripts/static-base/system-services.sh /work/mouse-src/scripts/static-base/system-services.sh + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + sh /work/mouse-src/scripts/static-base/system-services.sh + +FROM static-base-system AS static-base-facilities + +COPY --from=facilities-sources /opt/mouse-sources/ /opt/mouse-sources/ +COPY mouse-src/base/patches/nftables-posix-configure.patch /work/mouse-src/base/patches/nftables-posix-configure.patch +COPY mouse-src/scripts/static-base/facilities.sh /work/mouse-src/scripts/static-base/facilities.sh + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + sh /work/mouse-src/scripts/static-base/facilities.sh + +FROM static-base-facilities AS static-base-storage + +COPY --from=storage-sources /opt/mouse-sources/ /opt/mouse-sources/ +COPY mouse-src/scripts/static-base/storage.sh /work/mouse-src/scripts/static-base/storage.sh + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + sh /work/mouse-src/scripts/static-base/storage.sh + +FROM static-base-storage AS static-base-builder + +COPY mouse-src/scripts/static-base/finish.sh /work/mouse-src/scripts/static-base/finish.sh + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + sh /work/mouse-src/scripts/static-base/finish.sh + +FROM --platform=linux/amd64 scratch AS pkgsrc-seed-builder + +ARG MOUSE_BUILD_JOBS=8 + +COPY --from=static-base-builder /work/mouse-src/build/static/rootfs / +COPY --from=pkgsrc-sources /opt/mouse-sources/pkgsrc-2026Q2.tar.gz /usr/share/mouse/pkgsrc-2026Q2.tar.gz +COPY mouse-src/base/rootfs/etc/group /etc/group +COPY mouse-src/base/rootfs/etc/mouse-release /etc/mouse-release +COPY mouse-src/base/rootfs/etc/passwd /etc/passwd +COPY mouse-src/base/patches/pkgsrc-bootstrap-mouse.patch /usr/share/mouse/pkgsrc-bootstrap-mouse.patch +COPY mouse-src/scripts/build-pkgsrc-seed.sh /usr/libexec/mouse-build-pkgsrc-seed +COPY --from=pkgsrc-seed-tools /bin/busybox /usr/libexec/mouse-build-busybox + +RUN MOUSE_BUILD_JOBS="$MOUSE_BUILD_JOBS" \ + sh /usr/libexec/mouse-build-pkgsrc-seed + +FROM toolchain AS btrfs-image-builder + +RUN apk add --no-cache \ + mtools=4.0.49-r0 \ + pkgconf=2.5.1-r0 \ + sfdisk=2.42.1-r0 \ + util-linux-dev=2.42.1-r0 \ + zlib-dev=1.3.2-r0 \ + zstd-dev=1.5.7-r2 + +COPY --from=storage-sources /opt/mouse-sources/btrfs-progs-v7.0.tar.xz /opt/ +COPY --from=storage-sources /opt/mouse-sources/limine-binary-12.3.2.tar.xz /opt/mouse-sources/limine-binary-12.3.2.tar.xz + +RUN tar -C /work -xf /opt/btrfs-progs-v7.0.tar.xz \ + && cd /work/btrfs-progs-v7.0 \ + && ./configure \ + --prefix=/usr \ + --disable-backtrace \ + --disable-documentation \ + --disable-convert \ + --disable-lzo \ + --disable-libudev \ + --disable-python \ + --disable-zoned \ + --with-crypto=builtin \ + && make \ + -j"$MOUSE_BUILD_JOBS" \ + mkfs.btrfs \ + && install -m 0755 \ + /work/btrfs-progs-v7.0/mkfs.btrfs \ + /opt/mkfs.btrfs + +FROM static-base-builder AS image-commands-builder + +COPY mouse-src/base/support/mouse-bootenv /work/mouse-src/base/support/mouse-bootenv +COPY mouse-src/scripts/audit-static-base.sh /work/mouse-src/scripts/audit-static-base.sh +COPY mouse-src/scripts/build-image-commands.sh /work/mouse-src/scripts/build-image-commands.sh + +RUN /work/mouse-src/scripts/build-image-commands.sh \ + && /work/mouse-src/scripts/audit-static-base.sh /work/mouse-src/build/static/rootfs + +FROM image-commands-builder AS test-fixtures-builder + +COPY mouse-src/tests/fixtures/mouse-echo/mouse-echo.c /work/mouse-src/tests/fixtures/mouse-echo/mouse-echo.c + +RUN /work/mouse-src/build/static/cross-tools/bin/x86_64-mouse-linux-musl-cc \ + -O2 -Wall -Wextra -Werror \ + /work/mouse-src/tests/fixtures/mouse-echo/mouse-echo.c \ + -o /opt/mouse-echo + +FROM image-commands-builder AS cheesed-builder + +COPY mouse-src/base/cheesed/ /work/mouse-src/base/cheesed/ + +RUN CHEESED_TARGET=x86_64-unknown-linux-musl \ + /work/mouse-src/base/cheesed/scripts/build-static.sh \ + && install -m 0755 \ + /work/mouse-src/base/cheesed/target/x86_64-unknown-linux-musl/release/cheesed \ + /opt/cheesed + +FROM btrfs-image-builder AS rootfs-common + +COPY --from=cheesed-builder /work/mouse-src/build/static/rootfs /work/mouse-src/build/static/rootfs +COPY --from=cheesed-builder /opt/cheesed /opt/cheesed +COPY --from=kernel-builder /work/mouse-src/build/vmlinuz-virt /work/mouse-src/build/vmlinuz-virt +COPY --from=pkgsrc-seed-builder /usr/local /opt/mouse-pkgsrc-seed/usr/local +COPY --from=pkgsrc-seed-builder /var/db/pkg /opt/mouse-pkgsrc-seed/var/db/pkg +COPY mouse-src/base/rootfs/ /work/mouse-src/base/rootfs/ +COPY mouse-src/base/patches/pkgsrc-rc.subr-cgroup.patch /work/mouse-src/base/patches/pkgsrc-rc.subr-cgroup.patch +COPY mouse-src/scripts/audit-static-base.sh /work/mouse-src/scripts/audit-static-base.sh +COPY mouse-src/scripts/build-image.sh /work/mouse-src/scripts/build-image.sh +COPY mouse-src/scripts/build-boot-disk.sh /work/mouse-src/scripts/build-boot-disk.sh +COPY mouse-src/base/limine.conf /work/mouse-src/base/limine.conf + +FROM rootfs-common AS rootfs-builder + +COPY --from=kernel-builder /work/mouse-src/build/kernel/modules /opt/mouse-kernel-modules + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + MOUSE_CHEESED_BINARY=/opt/cheesed \ + MOUSE_BTRFS_IMAGE_TOOL=/opt/mkfs.btrfs \ + MOUSE_PKGSRC_SEED=/opt/mouse-pkgsrc-seed \ + MOUSE_KERNEL_MODULES=/opt/mouse-kernel-modules \ + /work/mouse-src/scripts/build-image.sh + +FROM rootfs-common AS rootfs-test-builder + +COPY --from=kernel-test-builder /work/mouse-src/build/kernel/test-modules /opt/mouse-kernel-modules +COPY --from=test-fixtures-builder /opt/mouse-echo /opt/mouse-echo +COPY mouse-src/tests/fixtures/ /work/mouse-src/tests/fixtures/ + +RUN MOUSE_SOURCE_DIR=/opt/mouse-sources \ + MOUSE_CHEESED_BINARY=/opt/cheesed \ + MOUSE_BTRFS_IMAGE_TOOL=/opt/mkfs.btrfs \ + MOUSE_PKGSRC_SEED=/opt/mouse-pkgsrc-seed \ + MOUSE_KERNEL_MODULES=/opt/mouse-kernel-modules \ + MOUSE_MOUSE_ECHO_BINARY=/opt/mouse-echo \ + MOUSE_INCLUDE_TEST_FIXTURES=1 \ + /work/mouse-src/scripts/build-image.sh + +FROM ${MOUSE_ROOTFS_STAGE} AS selected-rootfs + +FROM toolchain AS artifact-packer + +COPY --from=selected-rootfs /work/mouse-src/build/mouse-disk.img / +COPY --from=firmware-sources /opt/mouse-sources/ovmf-0.0.202605-r0.apk /opt/ + +RUN mkdir -p /opt/ovmf \ + && tar -xf /opt/ovmf-0.0.202605-r0.apk \ + -C /opt/ovmf \ + usr/share/OVMF/OVMF_CODE.fd \ + && gzip -1 /mouse-disk.img + +FROM scratch AS artifacts + +COPY --from=kernel-builder /work/mouse-src/build/vmlinuz-virt / +COPY --from=artifact-packer /mouse-disk.img.gz / +COPY --from=artifact-packer /opt/ovmf/usr/share/OVMF/OVMF_CODE.fd /ovmf-code.fd diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..553d7e9 --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +.PHONY: all check check-locks clean fetch image run + +MOUSE_BUILDER_CPUS ?= 8 +MOUSE_BUILDER_MEMORY ?= 8G +MOUSE_BUILD_JOBS ?= 8 +MOUSE_BOOT_MODE ?= firmware +MOUSE_NETWORK ?= 1 + +BUILD_ENV = \ + MOUSE_BUILDER_CPUS="$(MOUSE_BUILDER_CPUS)" \ + MOUSE_BUILDER_MEMORY="$(MOUSE_BUILDER_MEMORY)" \ + MOUSE_BUILD_JOBS="$(MOUSE_BUILD_JOBS)" + +CHECKS = boot firmware world service-integration storage btrfs network \ + multi-user toolchain pkgsrc + +all: image + +check-locks: + ./scripts/check-source-locks.sh + +fetch: check-locks + ./scripts/fetch-sources.sh + +image: check-locks + $(BUILD_ENV) ./scripts/build-in-container.sh + +run: + MOUSE_BOOT_MODE="$(MOUSE_BOOT_MODE)" \ + MOUSE_NETWORK="$(MOUSE_NETWORK)" \ + ./scripts/run-qemu.sh + +check: check-locks + $(BUILD_ENV) MOUSE_INCLUDE_TEST_FIXTURES=1 \ + ./scripts/build-in-container.sh + @set -e; for check in $(CHECKS); do ./tests/acceptance/$$check.sh; done + +clean: + rm -f build/vmlinuz-virt \ + build/mouse-disk.img build/ovmf-code.fd \ + build/mouse-rootfs.btrfs diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ff1b69 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# MOUSE + +MOUSE is the **Monolithic Opinionated Unix-like System Environment**: a small +Linux system with a versioned base, pkgsrc for third-party software, and +`cheesed` as PID 1. + +## Build + +The build needs Apple Container, Docker, or Podman, plus QEMU and Expect for +the runtime checks. + +```sh +make image +make run +make check +``` + +Artifacts and downloaded sources are kept in `build/`. The default builder has +8 CPUs, 8 GB of memory, and 8 build jobs; override those with +`MOUSE_BUILDER_CPUS`, `MOUSE_BUILDER_MEMORY`, and `MOUSE_BUILD_JOBS`. +`MOUSE_NETWORK=0 make run` disables QEMU networking. + +Development images contain `root` and `toor`, both with the password +`cheddar`. `root` uses `/bin/sh` for emergency administration; `toor` uses +`/bin/tcsh` for ordinary administration. Change both passwords before exposing +an image to a network. + +## System + +The base owns `/bin`, `/sbin`, `/usr/bin`, and `/usr/sbin`; pkgsrc owns +`/usr/local`. `/var`, `/home`, `/srv`, and `/usr/local` are shared by both root +environments, `/etc` follows the selected root, and `/run` is rebuilt at boot. +The fixed `/usr/local` package prefix keeps pkgsrc out of base-owned paths. + +Limine boots the default or alternate root from the same disk. +`mouse-bootenv prepare` snapshots the running root into the inactive one; +`activate` selects it and `rollback` selects the other root. Automated base +updates and configuration merging are not implemented yet. + +`cheesed` mounts early filesystems, reaps children, supervises services in +cgroups, and handles shutdown. It identifies itself as +`Cheesed to meet you! v0.1.0`. Service definitions live in `/etc/rc.d` and +`/usr/local/etc/rc.d`; dependency cycles, missing facilities, and duplicate +names are rejected. + +Persistent service policy lives only in `/etc/rc.conf`: + +```sh +enable_network=YES +enable_cron=YES +enable_ntpd=YES +``` + +Assignments are exactly `enable_NAME=YES` or `enable_NAME=NO`; shell expansion, +includes, and other keys are invalid. `/sbin/service` controls the running +system without editing boot policy. + +## Packages + +Images contain the pinned pkgsrc 2026Q2 tree in `/usr/local/src/pkgsrc`, with +`bmake` and the package tools under `/usr/local`. Packages use +`/usr/local/etc` for configuration, `/var/db/pkg` for records, and +`/var/cache/pkgsrc` for distfiles and binary packages. This state survives a +base rollback. + +## Checks + +`make check` builds the assembled disk with test fixtures, then boots it under +QEMU. It covers BIOS and UEFI boot, both Btrfs roots, services and cgroup +cleanup, users, networking, storage tools, native C and C++ compilation, pkgsrc +installation, and persistence across reboots. The build also rejects changed +source checksums and dynamic base executables. diff --git a/base/cheesed/.cargo/config.toml b/base/cheesed/.cargo/config.toml new file mode 100644 index 0000000..7f77502 --- /dev/null +++ b/base/cheesed/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-musl] +linker = "rust-lld" diff --git a/base/cheesed/Cargo.lock b/base/cheesed/Cargo.lock new file mode 100644 index 0000000..646be65 --- /dev/null +++ b/base/cheesed/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cheesed" +version = "0.1.0" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" diff --git a/base/cheesed/Cargo.toml b/base/cheesed/Cargo.toml new file mode 100644 index 0000000..cca7371 --- /dev/null +++ b/base/cheesed/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cheesed" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +description = "MOUSE PID 1 and service supervisor" +license = "BSD-2-Clause" + +[dependencies] +libc = "=0.2.189" + +[lints.rust] +unsafe_op_in_unsafe_fn = "deny" + +[lints.clippy] +all = "deny" +pedantic = "deny" diff --git a/base/cheesed/scripts/build-static.sh b/base/cheesed/scripts/build-static.sh new file mode 100755 index 0000000..c3f82d4 --- /dev/null +++ b/base/cheesed/scripts/build-static.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +target=${CHEESED_TARGET:-x86_64-unknown-linux-musl} + +if ! rustup target list --installed | grep -qx "$target"; then + printf '%s\n' "missing Rust target: $target" >&2 + printf '%s\n' "install it with: rustup target add $target" >&2 + exit 1 +fi + +( + cd "$repo_dir" + cargo build --locked --release --target "$target" +) +binary="$repo_dir/target/$target/release/cheesed" + +if ! file "$binary" | grep -Eq 'statically linked|static-pie linked'; then + printf '%s\n' "release binary is not statically linked: $binary" >&2 + exit 1 +fi + +printf '%s\n' "$binary" diff --git a/base/cheesed/src/lib.rs b/base/cheesed/src/lib.rs new file mode 100644 index 0000000..92fdce3 --- /dev/null +++ b/base/cheesed/src/lib.rs @@ -0,0 +1,87 @@ +//! Core process model for the MOUSE init and service supervisor. + +use std::fmt; + +#[cfg(any(target_os = "linux", test))] +mod policy; + +/// The first message emitted after PID 1 bootstrap succeeds. +pub const STARTUP_BANNER: &str = concat!("Cheesed to meet you! v", env!("CARGO_PKG_VERSION")); + +/// The terminal system action requested from PID 1. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ShutdownAction { + Reboot, + Poweroff, +} + +impl fmt::Display for ShutdownAction { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Reboot => formatter.write_str("reboot"), + Self::Poweroff => formatter.write_str("poweroff"), + } + } +} + +/// Maps the two shutdown signals accepted by PID 1. +#[must_use] +pub fn shutdown_action(signal: i32) -> Option { + match signal { + libc::SIGINT => Some(ShutdownAction::Reboot), + libc::SIGTERM => Some(ShutdownAction::Poweroff), + _ => None, + } +} + +#[cfg(target_os = "linux")] +mod linux; + +/// Runs `cheesed` in its normal PID 1 mode. +/// +/// # Errors +/// +/// Returns an error if invoked outside PID 1. Once PID 1 initialization starts, +/// failures are logged and kept alive rather than returned. +#[cfg(target_os = "linux")] +pub fn run() -> Result<(), String> { + linux::run() +} + +/// Rejects normal init mode on non-Linux development hosts. +/// +/// # Errors +/// +/// Always returns an error because PID 1 mode requires Linux. +#[cfg(not(target_os = "linux"))] +pub fn run() -> Result<(), String> { + Err("cheesed PID 1 mode is supported only on Linux".to_owned()) +} + +#[cfg(test)] +mod tests { + use super::{STARTUP_BANNER, ShutdownAction, shutdown_action}; + + #[test] + fn startup_banner_includes_the_package_version() { + assert_eq!(STARTUP_BANNER, "Cheesed to meet you! v0.1.0"); + } + + #[test] + fn sigint_requests_reboot() { + assert_eq!(shutdown_action(libc::SIGINT), Some(ShutdownAction::Reboot)); + } + + #[test] + fn sigterm_requests_poweroff() { + assert_eq!( + shutdown_action(libc::SIGTERM), + Some(ShutdownAction::Poweroff) + ); + } + + #[test] + fn unrelated_signal_is_not_a_shutdown_request() { + assert_eq!(shutdown_action(libc::SIGCHLD), None); + } +} diff --git a/base/cheesed/src/linux.rs b/base/cheesed/src/linux.rs new file mode 100644 index 0000000..156f8f0 --- /dev/null +++ b/base/cheesed/src/linux.rs @@ -0,0 +1,172 @@ +mod boot; +mod process; +mod services; + +use self::boot::{configure_hostname, mount_early_filesystems, mount_mouse_storage}; +use self::process::{ + child_exited, install_signal_handlers, perform_shutdown, reap_children, spawn_emergency_shell, + take_shutdown_action, +}; +use self::services::{ServiceManager, boot_services}; +use crate::STARTUP_BANNER; +use std::fs::{File, OpenOptions}; +use std::io::{self, Write}; +use std::thread; +use std::time::Duration; + +const CONSOLE_PATH: &str = "/dev/console"; +const POLL_INTERVAL: Duration = Duration::from_millis(50); +const SHELL_RESTART_DELAY: Duration = Duration::from_secs(1); + +pub(crate) fn run() -> Result<(), String> { + // SAFETY: getpid has no preconditions and does not mutate memory. + if unsafe { libc::getpid() } != 1 { + return Err("normal mode must run as PID 1".to_owned()); + } + + run_pid1() +} + +fn run_pid1() -> ! { + let mut console = loop { + match Console::open() { + Ok(console) => break console, + Err(error) => { + eprintln!("cheesed: cannot open {CONSOLE_PATH} for early boot logging: {error}"); + thread::sleep(SHELL_RESTART_DELAY); + } + } + }; + + // SAFETY: cheesed is single-threaded here and passes a conventional process umask. + unsafe { + libc::umask(0o022); + } + if let Err(error) = std::env::set_current_dir("/") { + remain_alive_after_fatal_error(&mut console, &format!("cannot chdir to /: {error}")); + } + if let Err(error) = install_signal_handlers() { + remain_alive_after_fatal_error( + &mut console, + &format!("cannot install signal handlers: {error}"), + ); + } + if let Err(error) = mount_early_filesystems() { + remain_alive_after_fatal_error( + &mut console, + &format!("cannot mount early filesystems: {error}"), + ); + } + if let Err(error) = mount_mouse_storage() { + remain_alive_after_fatal_error( + &mut console, + &format!("cannot mount MOUSE storage: {error}"), + ); + } + if let Err(error) = configure_hostname() { + remain_alive_after_fatal_error( + &mut console, + &format!("cannot configure hostname: {error}"), + ); + } + + console.write_line(STARTUP_BANNER); + console.log("base bootstrap complete; cheesed is PID 1"); + let mut services = None; + let emergency = match boot_services(&mut console) { + Ok(manager) => { + services = Some(manager); + false + } + Err(error) => { + console.log(&format!("{error}; entering emergency console")); + true + } + }; + let shell_pid = if emergency { + match spawn_emergency_shell(&mut console) { + Ok(pid) => Some(pid), + Err(error) => { + console.log(&error); + None + } + } + } else { + None + }; + + steady_state(&mut console, services, emergency, shell_pid) +} + +fn steady_state( + console: &mut Console, + mut services: Option, + emergency: bool, + mut shell_pid: Option, +) -> ! { + loop { + if child_exited() { + let shell_exited = reap_children(console, shell_pid); + if shell_exited { + shell_pid = None; + } + } + + if let Some(manager) = services.as_mut() { + manager.poll(console); + } + + if let Some(action) = take_shutdown_action() { + perform_shutdown(console, shell_pid, services.as_mut(), action); + } + + if emergency && shell_pid.is_none() { + console.log("emergency shell exited; restarting it in one second"); + thread::sleep(SHELL_RESTART_DELAY); + shell_pid = Some(match spawn_emergency_shell(console) { + Ok(pid) => pid, + Err(error) => { + console.log(&format!("cannot restart emergency shell: {error}")); + continue; + } + }); + } + + thread::sleep(POLL_INTERVAL); + } +} + +fn remain_alive_after_fatal_error(console: &mut Console, error: &str) -> ! { + console.log(error); + console.log("unrecoverable bootstrap failure; PID 1 will remain alive"); + loop { + thread::sleep(Duration::from_secs(60)); + } +} + +struct Console { + file: File, +} + +impl Console { + fn open() -> io::Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .open(CONSOLE_PATH)?; + Ok(Self { file }) + } + + fn log(&mut self, message: &str) { + self.write_line(&format!("cheesed: {message}")); + } + + fn write_line(&mut self, message: &str) { + let _ = writeln!(self.file, "{message}"); + let _ = self.file.flush(); + } + + fn clone_file(&self) -> io::Result { + self.file.try_clone() + } +} diff --git a/base/cheesed/src/linux/boot.rs b/base/cheesed/src/linux/boot.rs new file mode 100644 index 0000000..0e88e9e --- /dev/null +++ b/base/cheesed/src/linux/boot.rs @@ -0,0 +1,146 @@ +use std::io; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; + +const HOSTNAME_PATH: &str = "/etc/hostname"; +const CGROUP_ROOT: &str = "/sys/fs/cgroup"; +const MOUSE_ROOT_PARTUUID: &str = "4d4f5553-4500-4000-8000-000000000002"; +const BTRFS_SUPER_MAGIC: u64 = 0x9123_683e; + +pub(super) fn mount_early_filesystems() -> io::Result<()> { + mount_filesystem(c"devtmpfs".as_ptr(), c"/dev".as_ptr(), c"devtmpfs".as_ptr())?; + mount_filesystem(c"proc".as_ptr(), c"/proc".as_ptr(), c"proc".as_ptr())?; + mount_filesystem(c"sysfs".as_ptr(), c"/sys".as_ptr(), c"sysfs".as_ptr())?; + std::fs::create_dir_all(CGROUP_ROOT)?; + mount_filesystem( + c"cgroup2".as_ptr(), + c"/sys/fs/cgroup".as_ptr(), + c"cgroup2".as_ptr(), + )?; + mount_filesystem(c"tmpfs".as_ptr(), c"/run".as_ptr(), c"tmpfs".as_ptr()) +} + +fn mount_filesystem( + source: *const libc::c_char, + target: *const libc::c_char, + filesystem_type: *const libc::c_char, +) -> io::Result<()> { + // SAFETY: all pointers are valid, static, NUL-terminated C strings; no data + // argument is required for these pseudo-filesystems. + if unsafe { + libc::mount( + source, + target, + filesystem_type, + 0, + std::ptr::null::(), + ) + } == -1 + { + let error = io::Error::last_os_error(); + if error.raw_os_error() != Some(libc::EBUSY) { + return Err(error); + } + } + Ok(()) +} + +pub(super) fn mount_mouse_storage() -> io::Result<()> { + let device = match mouse_root_device() { + Ok(device) => device, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(error) => return Err(error), + }; + let device = std::ffi::CString::new(device.as_os_str().as_bytes()) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + + std::fs::create_dir_all("/run/mouse-root")?; + mount_btrfs(&device, c"/run/mouse-root", c"subvolid=5")?; + + if root_is_btrfs()? { + for (target, subvolume) in [ + (c"/usr/local", c"subvol=usr-local"), + (c"/var", c"subvol=var"), + (c"/home", c"subvol=home"), + (c"/srv", c"subvol=srv"), + ] { + mount_btrfs(&device, target, subvolume)?; + } + } + Ok(()) +} + +fn mouse_root_device() -> io::Result { + let expected = format!("PARTUUID={MOUSE_ROOT_PARTUUID}"); + for entry in std::fs::read_dir("/sys/class/block")? { + let entry = entry?; + let Ok(uevent) = std::fs::read_to_string(entry.path().join("uevent")) else { + continue; + }; + if uevent.lines().any(|line| line == expected) { + let device = Path::new("/dev").join(entry.file_name()); + if device.exists() { + return Ok(device); + } + } + } + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("MOUSE root partition {MOUSE_ROOT_PARTUUID} is not present"), + )) +} + +fn mount_btrfs( + device: &std::ffi::CStr, + target: &std::ffi::CStr, + options: &std::ffi::CStr, +) -> io::Result<()> { + // SAFETY: all pointers reference NUL-terminated strings for the duration + // of the call. The btrfs device and subvolume layout are fixed MOUSE image + // contracts rather than discovered mount policy. + if unsafe { + libc::mount( + device.as_ptr(), + target.as_ptr(), + c"btrfs".as_ptr(), + 0, + options.as_ptr().cast::(), + ) + } == -1 + { + let error = io::Error::last_os_error(); + if error.raw_os_error() != Some(libc::EBUSY) { + return Err(error); + } + } + Ok(()) +} + +fn root_is_btrfs() -> io::Result { + // SAFETY: status points to writable storage and the path is a static, + // NUL-terminated string. + let mut status: libc::statfs = unsafe { std::mem::zeroed() }; + if unsafe { libc::statfs(c"/".as_ptr(), &raw mut status) } == -1 { + return Err(io::Error::last_os_error()); + } + Ok(status.f_type as u64 == BTRFS_SUPER_MAGIC) +} + +pub(super) fn configure_hostname() -> io::Result<()> { + let hostname = std::fs::read_to_string(HOSTNAME_PATH)?; + let hostname = hostname.trim(); + if hostname.is_empty() || hostname.as_bytes().contains(&0) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("{HOSTNAME_PATH} must contain a non-empty hostname"), + )); + } + + // SAFETY: hostname points to readable bytes for the supplied length, and + // sethostname does not require a terminating NUL. + if unsafe { libc::sethostname(hostname.as_ptr().cast::(), hostname.len()) } == -1 + { + return Err(io::Error::last_os_error()); + } + Ok(()) +} diff --git a/base/cheesed/src/linux/process.rs b/base/cheesed/src/linux/process.rs new file mode 100644 index 0000000..fd609dd --- /dev/null +++ b/base/cheesed/src/linux/process.rs @@ -0,0 +1,437 @@ +use super::services::ServiceManager; +use super::{Console, POLL_INTERVAL}; +use crate::{ShutdownAction, shutdown_action}; +use std::fs::OpenOptions; +use std::io; +use std::os::fd::AsRawFd; +use std::os::unix::process::{CommandExt, ExitStatusExt}; +use std::path::Path; +use std::process::{Command, ExitStatus, Stdio}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::thread; +use std::time::{Duration, Instant}; + +const EMERGENCY_SHELL: &str = "/bin/tcsh"; +pub(super) const TERMINATION_GRACE: Duration = Duration::from_secs(5); + +static CHILD_EXITED: AtomicBool = AtomicBool::new(false); +static SHUTDOWN_SIGNAL: AtomicI32 = AtomicI32::new(0); + +extern "C" fn handle_signal(signal: libc::c_int) { + if signal == libc::SIGCHLD { + CHILD_EXITED.store(true, Ordering::Relaxed); + } else { + let _ = SHUTDOWN_SIGNAL.compare_exchange(0, signal, Ordering::Relaxed, Ordering::Relaxed); + } +} + +pub(super) fn child_exited() -> bool { + CHILD_EXITED.swap(false, Ordering::Relaxed) +} + +pub(super) fn install_signal_handlers() -> io::Result<()> { + for signal in [libc::SIGCHLD, libc::SIGINT, libc::SIGTERM] { + // SAFETY: zero is a valid initial representation for sigaction before its + // fields and mask are initialized below. + let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; + action.sa_sigaction = handle_signal as *const () as usize; + action.sa_flags = libc::SA_RESTART; + if signal == libc::SIGCHLD { + action.sa_flags |= libc::SA_NOCLDSTOP; + } + + // SAFETY: action owns a valid sigset_t and the signal number is known. + if unsafe { libc::sigemptyset(&raw mut action.sa_mask) } == -1 { + return Err(io::Error::last_os_error()); + } + // SAFETY: action remains alive for the call and the old action is not needed. + if unsafe { libc::sigaction(signal, &raw const action, std::ptr::null_mut()) } == -1 { + return Err(io::Error::last_os_error()); + } + } + Ok(()) +} + +pub(super) enum CommandResult { + Exited(ExitStatus), + Shutdown(ShutdownAction), +} + +pub(super) fn command_with_console( + program: &str, + arguments: &[&str], + console: &mut Console, + timeout: Duration, + service_cgroup: Option<&Path>, +) -> Result { + let stdin = console + .clone_file() + .map_err(|error| format!("cannot duplicate console input: {error}"))?; + let stdout = console + .clone_file() + .map_err(|error| format!("cannot duplicate console output: {error}"))?; + let stderr = console + .clone_file() + .map_err(|error| format!("cannot duplicate console error output: {error}"))?; + let mut command = Command::new(program); + command + .args(arguments) + .env("PATH", "/sbin:/bin:/usr/sbin:/usr/bin") + .stdin(Stdio::from(stdin)) + .stdout(Stdio::from(stdout)) + .stderr(Stdio::from(stderr)); + let cgroup_procs = service_cgroup + .map(|cgroup| { + OpenOptions::new() + .write(true) + .open(cgroup.join("cgroup.procs")) + }) + .transpose() + .map_err(|error| format!("cannot open service cgroup: {error}"))?; + if let Some(cgroup) = service_cgroup { + command.env("MOUSE_SERVICE_CGROUP", cgroup); + command.env("MOUSE_SERVICE_PIDS", cgroup_pids(cgroup)?); + } + let cgroup_fd = cgroup_procs.as_ref().map(AsRawFd::as_raw_fd); + // SAFETY: this closure runs between fork and exec, and setpgid is + // async-signal-safe. A separate group lets PID 1 terminate a whole + // transition, including service-script descendants. + unsafe { + command.pre_exec(move || { + if libc::setpgid(0, 0) == -1 { + return Err(io::Error::last_os_error()); + } + if let Some(fd) = cgroup_fd { + let pid = b"0\n"; + if libc::write(fd, pid.as_ptr().cast(), pid.len()) == -1 { + return Err(io::Error::last_os_error()); + } + } + Ok(()) + }); + } + let child = command + .spawn() + .map_err(|error| format!("cannot run {program}: {error}"))?; + let pid = i32::try_from(child.id()).map_err(|_| "child PID does not fit pid_t".to_owned())?; + let deadline = Instant::now() + timeout; + loop { + if let Some(status) = reap_during_command(console, pid)? { + return Ok(CommandResult::Exited(status)); + } + if let Some(action) = take_shutdown_action() { + terminate_process_group(pid, program, console); + return Ok(CommandResult::Shutdown(action)); + } + if Instant::now() >= deadline { + terminate_process_group(pid, program, console); + return Err(format!( + "{program} {} timed out after {} seconds", + arguments.join(" "), + timeout.as_secs() + )); + } + thread::sleep(POLL_INTERVAL); + } +} + +fn cgroup_pids(cgroup: &Path) -> Result { + let contents = std::fs::read_to_string(cgroup.join("cgroup.procs")) + .map_err(|error| format!("cannot read {}: {error}", cgroup.display()))?; + let mut pids = Vec::new(); + for pid in contents.lines() { + if pid.is_empty() || !pid.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(format!("invalid PID in {}", cgroup.display())); + } + pids.push(pid); + } + Ok(pids.join(" ")) +} + +pub(super) fn restore_shutdown_action(action: ShutdownAction) { + SHUTDOWN_SIGNAL.store( + match action { + ShutdownAction::Reboot => libc::SIGINT, + ShutdownAction::Poweroff => libc::SIGTERM, + }, + Ordering::Relaxed, + ); +} + +pub(super) fn take_shutdown_action() -> Option { + shutdown_action(SHUTDOWN_SIGNAL.swap(0, Ordering::Relaxed)) +} + +fn reap_during_command( + console: &mut Console, + command_pid: libc::pid_t, +) -> Result, String> { + CHILD_EXITED.store(false, Ordering::Relaxed); + loop { + let mut status = 0; + // SAFETY: status is writable and WNOHANG makes the call nonblocking. + let pid = unsafe { libc::waitpid(-1, &raw mut status, libc::WNOHANG) }; + match pid { + value if value == command_pid => { + return Ok(Some(ExitStatus::from_raw(status))); + } + value if value > 0 => { + console.log(&format!( + "reaped orphan PID {value} ({})", + describe_wait_status(status) + )); + } + 0 => return Ok(None), + _ => { + let error = io::Error::last_os_error(); + return if error.raw_os_error() == Some(libc::ECHILD) { + Err("transition child disappeared without an exit status".to_owned()) + } else { + Err(format!("waitpid failed during transition: {error}")) + }; + } + } + } +} + +fn terminate_process_group(pid: libc::pid_t, program: &str, console: &mut Console) { + // SAFETY: negative pid addresses the process group created before exec. + let term_result = unsafe { libc::kill(-pid, libc::SIGTERM) }; + if term_result == -1 && io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH) { + console.log(&format!("cannot terminate {program} process group {pid}")); + } + let deadline = Instant::now() + TERMINATION_GRACE; + while Instant::now() < deadline { + match reap_during_command(console, pid) { + Ok(Some(_)) | Err(_) => return, + Ok(None) => thread::sleep(POLL_INTERVAL), + } + } + // SAFETY: negative pid addresses the same known process group. + unsafe { + libc::kill(-pid, libc::SIGKILL); + } + loop { + match reap_during_command(console, pid) { + Ok(Some(_)) | Err(_) => return, + Ok(None) => thread::sleep(POLL_INTERVAL), + } + } +} + +pub(super) fn describe_exit_status(status: ExitStatus) -> String { + status + .code() + .map_or_else(|| "a signal".to_owned(), |code| format!("status {code}")) +} + +pub(super) fn spawn_emergency_shell(console: &mut Console) -> Result { + let stdin = console + .clone_file() + .map_err(|error| format!("cannot duplicate console input: {error}"))?; + let stdout = console + .clone_file() + .map_err(|error| format!("cannot duplicate console output: {error}"))?; + let stderr = console + .clone_file() + .map_err(|error| format!("cannot duplicate console error output: {error}"))?; + + let mut command = Command::new(EMERGENCY_SHELL); + command + .arg("-l") + .env_clear() + .env("HOME", "/root") + .env("LOGNAME", "root") + .env("PATH", "/sbin:/bin:/usr/sbin:/usr/bin") + .env("SHELL", EMERGENCY_SHELL) + .env("TERM", "linux") + .env("USER", "root") + .stdin(Stdio::from(stdin)) + .stdout(Stdio::from(stdout)) + .stderr(Stdio::from(stderr)); + + // SAFETY: this closure runs after fork and before exec in the single-threaded + // child. It invokes only async-signal-safe system calls and reports failures + // through a preallocated io::Error. + unsafe { + command.pre_exec(|| { + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } + if libc::ioctl(libc::STDIN_FILENO, libc::TIOCSCTTY, 0) == -1 { + return Err(io::Error::last_os_error()); + } + Ok(()) + }); + } + + let child = command + .spawn() + .map_err(|error| format!("cannot start {EMERGENCY_SHELL}: {error}"))?; + + let pid = i32::try_from(child.id()).map_err(|_| "child PID does not fit pid_t".to_owned())?; + console.log(&format!("started emergency shell as PID {pid}")); + Ok(pid) +} + +pub(super) fn reap_children(console: &mut Console, shell_pid: Option) -> bool { + let mut shell_exited = false; + loop { + let mut status = 0; + // SAFETY: status points to writable storage and WNOHANG makes this nonblocking. + let pid = unsafe { libc::waitpid(-1, &raw mut status, libc::WNOHANG) }; + match pid { + value if value > 0 => { + let description = describe_wait_status(status); + if Some(value) == shell_pid { + console.log(&format!("emergency shell PID {value} {description}")); + shell_exited = true; + } else { + console.log(&format!("reaped orphan PID {value} ({description})")); + } + } + 0 => break, + _ => { + let error = io::Error::last_os_error(); + if error.raw_os_error() != Some(libc::ECHILD) { + console.log(&format!("waitpid failed: {error}")); + } + break; + } + } + } + shell_exited +} + +fn describe_wait_status(status: libc::c_int) -> String { + if libc::WIFEXITED(status) { + format!("exited with status {}", libc::WEXITSTATUS(status)) + } else if libc::WIFSIGNALED(status) { + format!("was killed by signal {}", libc::WTERMSIG(status)) + } else { + format!("changed state with wait status {status}") + } +} + +pub(super) fn perform_shutdown( + console: &mut Console, + shell_pid: Option, + services: Option<&mut ServiceManager>, + action: ShutdownAction, +) -> ! { + console.log(&format!("received {action} request")); + stop_shell(console, shell_pid); + if let Some(manager) = services { + manager.stop_all(console); + } + + // SAFETY: sync has no preconditions and is required before the terminal reboot call. + unsafe { + libc::sync(); + } + unmount_mouse_storage(console); + remount_root_read_only(console); + + let command = match action { + ShutdownAction::Reboot => libc::RB_AUTOBOOT, + ShutdownAction::Poweroff => libc::RB_POWER_OFF, + }; + // SAFETY: cheesed is PID 1 and passes one of Linux's defined reboot commands. + if unsafe { libc::reboot(command) } == -1 { + console.log(&format!( + "{action} syscall failed: {}; PID 1 will remain alive", + io::Error::last_os_error() + )); + } + + loop { + thread::sleep(Duration::from_secs(60)); + } +} + +fn unmount_mouse_storage(console: &mut Console) { + for target in [ + c"/srv", + c"/home", + c"/var", + c"/usr/local", + c"/run/mouse-root", + ] { + // SAFETY: target is a static NUL-terminated mount path and no flags + // request lazy or forced unmounting. + if unsafe { libc::umount2(target.as_ptr(), 0) } == -1 { + let error = io::Error::last_os_error(); + if !matches!(error.raw_os_error(), Some(libc::EINVAL | libc::ENOENT)) { + console.log(&format!( + "cannot unmount {}: {error}", + target.to_string_lossy() + )); + } + } + } +} + +fn remount_root_read_only(console: &mut Console) { + // SAFETY: all pointers are either null or point to static NUL-terminated + // strings. MS_REMOUNT changes the existing root mount rather than selecting + // or discovering storage. + let result = unsafe { + libc::mount( + std::ptr::null(), + c"/".as_ptr(), + std::ptr::null(), + libc::MS_REMOUNT | libc::MS_RDONLY, + std::ptr::null(), + ) + }; + if result == -1 { + console.log(&format!( + "cannot remount root filesystem read-only: {}", + io::Error::last_os_error() + )); + } else { + console.log("root filesystem remounted read-only"); + } +} + +fn stop_shell(console: &mut Console, shell_pid: Option) { + let Some(pid) = shell_pid else { + return; + }; + + // SAFETY: pid is a direct child PID and SIGTERM is a valid signal. + if unsafe { libc::kill(pid, libc::SIGTERM) } == -1 { + let error = io::Error::last_os_error(); + if error.raw_os_error() != Some(libc::ESRCH) { + console.log(&format!("cannot stop emergency shell PID {pid}: {error}")); + } + return; + } + + let deadline = Instant::now() + TERMINATION_GRACE; + while Instant::now() < deadline { + if wait_for_pid(pid) { + console.log(&format!("emergency shell PID {pid} stopped")); + return; + } + thread::sleep(POLL_INTERVAL); + } + + console.log(&format!( + "emergency shell PID {pid} exceeded shutdown timeout; sending SIGKILL" + )); + // SAFETY: pid is the known shell PID and SIGKILL is a valid signal. + unsafe { + libc::kill(pid, libc::SIGKILL); + } + let _ = wait_for_pid(pid); +} + +fn wait_for_pid(pid: libc::pid_t) -> bool { + let mut status = 0; + // SAFETY: status points to writable storage, pid is a known child, and WNOHANG + // makes the check nonblocking. + let result = unsafe { libc::waitpid(pid, &raw mut status, libc::WNOHANG) }; + result == pid + || (result == -1 && io::Error::last_os_error().raw_os_error() == Some(libc::ECHILD)) +} diff --git a/base/cheesed/src/linux/services.rs b/base/cheesed/src/linux/services.rs new file mode 100644 index 0000000..92985ad --- /dev/null +++ b/base/cheesed/src/linux/services.rs @@ -0,0 +1,458 @@ +use super::process::{ + CommandResult, TERMINATION_GRACE, command_with_console, describe_exit_status, + restore_shutdown_action, +}; +use super::{Console, POLL_INTERVAL}; +use crate::policy::{ServiceCatalog, parse_rc_conf}; +use std::collections::{BTreeMap, VecDeque}; +use std::io::{self, BufRead, BufReader, Write}; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::net::UnixListener; +use std::path::Path; +use std::thread; +use std::time::{Duration, Instant}; + +const RC_CONF_PATH: &str = "/etc/rc.conf"; +const SYSTEM_SERVICE_DEFINITIONS: &str = "/etc/rc.d"; +const LOCAL_SERVICE_DEFINITIONS: &str = "/usr/local/etc/rc.d"; +const REQUIRED_SERVICES: &str = "/usr/lib/mouse/required-services"; +const SERVICE_CGROUP_ROOT: &str = "/sys/fs/cgroup/mouse.services"; +const CONTROL_SOCKET: &str = "/run/cheesed.sock"; +const SERVICE_TRANSITION_TIMEOUT: Duration = Duration::from_secs(120); +const SERVICE_STOP_TIMEOUT: Duration = Duration::from_secs(10); +const RESTART_WINDOW: Duration = Duration::from_secs(10); +const RESTART_DELAY: Duration = Duration::from_secs(1); +const RESTART_LIMIT: usize = 3; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ServiceState { + Stopped, + Started, + Running, + RestartPending, + Failed, +} + +impl ServiceState { + fn description(self) -> &'static str { + match self { + Self::Stopped => "stopped", + Self::Started => "started", + Self::Running => "running", + Self::RestartPending => "restarting", + Self::Failed => "failed", + } + } +} + +struct ManagedService { + state: ServiceState, + restart_times: VecDeque, + restart_at: Option, +} + +impl Default for ManagedService { + fn default() -> Self { + Self { + state: ServiceState::Stopped, + restart_times: VecDeque::new(), + restart_at: None, + } + } +} + +pub(super) struct ServiceManager { + catalog: ServiceCatalog, + services: BTreeMap, + started_order: Vec, + listener: UnixListener, +} + +pub(super) fn boot_services(console: &mut Console) -> Result { + let catalog = ServiceCatalog::discover( + Path::new(SYSTEM_SERVICE_DEFINITIONS), + Path::new(LOCAL_SERVICE_DEFINITIONS), + Path::new(REQUIRED_SERVICES), + ) + .map_err(|error| format!("cannot discover services: {error}"))?; + let contents = std::fs::read_to_string(RC_CONF_PATH) + .map_err(|error| format!("cannot read {RC_CONF_PATH}: {error}"))?; + let policy = parse_rc_conf(&contents, &catalog) + .map_err(|error| format!("invalid service policy: {error}"))?; + let enabled = policy.enabled().collect::>(); + if enabled.is_empty() { + console.log("enabled optional services: (none)"); + } else { + console.log(&format!( + "enabled optional services: {}", + enabled.join(", ") + )); + } + + let order = catalog + .start_order(&policy) + .map_err(|error| format!("cannot order services: {error}"))?; + let mut manager = ServiceManager::new(catalog)?; + for service in order { + console.log(&format!("starting service {service}")); + if let Err(error) = manager.start_service(&service, console, false) { + if manager.catalog.is_required(&service) { + manager.stop_all(console); + return Err(format!("required service {service} failed: {error}")); + } + console.log(&format!("optional service {service} failed: {error}")); + } + } + console.log("service graph started; cgroup supervision active"); + Ok(manager) +} + +impl ServiceManager { + fn new(catalog: ServiceCatalog) -> Result { + std::fs::create_dir_all(SERVICE_CGROUP_ROOT) + .map_err(|error| format!("cannot create service cgroup root: {error}"))?; + let _ = std::fs::remove_file(CONTROL_SOCKET); + let listener = UnixListener::bind(CONTROL_SOCKET) + .map_err(|error| format!("cannot bind {CONTROL_SOCKET}: {error}"))?; + listener + .set_nonblocking(true) + .map_err(|error| format!("cannot make {CONTROL_SOCKET} nonblocking: {error}"))?; + std::fs::set_permissions(CONTROL_SOCKET, std::fs::Permissions::from_mode(0o600)) + .map_err(|error| format!("cannot protect {CONTROL_SOCKET}: {error}"))?; + Ok(Self { + catalog, + services: BTreeMap::new(), + started_order: Vec::new(), + listener, + }) + } + + fn service(&mut self, name: &str) -> &mut ManagedService { + self.services.entry(name.to_owned()).or_default() + } + + fn start_service( + &mut self, + name: &str, + console: &mut Console, + clear_history: bool, + ) -> Result { + let definition = self + .catalog + .definition(name) + .ok_or_else(|| format!("unknown service {name}"))?; + if matches!( + self.services.get(name).map(|service| service.state), + Some(ServiceState::Started | ServiceState::Running) + ) { + return Ok(format!("{name} is already started")); + } + let script = definition.path().to_path_buf(); + let cgroup = service_cgroup(name)?; + std::fs::create_dir_all(&cgroup) + .map_err(|error| format!("cannot create cgroup for {name}: {error}"))?; + kill_cgroup(&cgroup, console); + let result = command_with_console( + script + .to_str() + .ok_or_else(|| format!("service path for {name} is not UTF-8"))?, + &["start"], + console, + SERVICE_TRANSITION_TIMEOUT, + Some(&cgroup), + )?; + let status = match result { + CommandResult::Exited(status) => status, + CommandResult::Shutdown(action) => { + restore_shutdown_action(action); + return Err(format!("start of {name} interrupted by {action} request")); + } + }; + if !status.success() { + kill_cgroup(&cgroup, console); + self.service(name).state = ServiceState::Failed; + return Err(format!( + "start command exited with {}", + describe_exit_status(status) + )); + } + let state = if cgroup_populated(&cgroup)? { + ServiceState::Running + } else { + ServiceState::Started + }; + let service = self.service(name); + service.state = state; + service.restart_at = None; + if clear_history { + service.restart_times.clear(); + } + if !self.started_order.iter().any(|started| started == name) { + self.started_order.push(name.to_owned()); + } + Ok(format!("{name} {}", state.description())) + } + + fn stop_service(&mut self, name: &str, console: &mut Console) -> Result { + let definition = self + .catalog + .definition(name) + .ok_or_else(|| format!("unknown service {name}"))?; + let current = self + .services + .get(name) + .map_or(ServiceState::Stopped, |s| s.state); + if current == ServiceState::Stopped { + return Ok(format!("{name} is already stopped")); + } + let script = definition.path().to_path_buf(); + let cgroup = service_cgroup(name)?; + let result = command_with_console( + script + .to_str() + .ok_or_else(|| format!("service path for {name} is not UTF-8"))?, + &["stop"], + console, + SERVICE_STOP_TIMEOUT, + Some(&cgroup), + ); + let (command_error, allow_grace) = match result { + Ok(CommandResult::Exited(status)) if status.success() => (None, true), + Ok(CommandResult::Exited(status)) => ( + Some(format!( + "stop command exited with {}", + describe_exit_status(status) + )), + true, + ), + Ok(CommandResult::Shutdown(action)) => { + restore_shutdown_action(action); + (Some(format!("stop interrupted by {action} request")), false) + } + Err(error) => (Some(error), false), + }; + if allow_grace { + wait_for_empty_cgroup(&cgroup, TERMINATION_GRACE); + } + kill_cgroup(&cgroup, console); + let service = self.service(name); + service.state = ServiceState::Stopped; + service.restart_at = None; + if let Some(error) = command_error { + Err(error) + } else { + Ok(format!("{name} stopped")) + } + } + + fn run_action( + &mut self, + action: &str, + name: &str, + console: &mut Console, + ) -> Result { + if self.catalog.definition(name).is_none() { + let refreshed = ServiceCatalog::discover( + Path::new(SYSTEM_SERVICE_DEFINITIONS), + Path::new(LOCAL_SERVICE_DEFINITIONS), + Path::new(REQUIRED_SERVICES), + ) + .map_err(|error| format!("cannot refresh service definitions: {error}"))?; + if refreshed.definition(name).is_none() { + return Err(format!("unknown service {name}")); + } + self.catalog = refreshed; + } + match action { + "start" => self.start_service(name, console, true), + "stop" => self.stop_service(name, console), + "restart" => { + let _ = self.stop_service(name, console); + self.start_service(name, console, true) + } + "reload" => { + let definition = self + .catalog + .definition(name) + .ok_or_else(|| format!("unknown service {name}"))?; + let script = definition.path().to_path_buf(); + let cgroup = service_cgroup(name)?; + match command_with_console( + script + .to_str() + .ok_or_else(|| format!("service path for {name} is not UTF-8"))?, + &["reload"], + console, + SERVICE_TRANSITION_TIMEOUT, + Some(&cgroup), + )? { + CommandResult::Exited(status) if status.success() => { + Ok(format!("{name} reloaded")) + } + CommandResult::Exited(status) => Err(format!( + "reload command exited with {}", + describe_exit_status(status) + )), + CommandResult::Shutdown(action) => { + restore_shutdown_action(action); + Err(format!("reload interrupted by {action} request")) + } + } + } + "status" => { + let state = self + .services + .get(name) + .map_or(ServiceState::Stopped, |service| service.state); + if matches!(state, ServiceState::Failed | ServiceState::Stopped) { + Err(format!("{name} {}", state.description())) + } else { + Ok(format!("{name} {}", state.description())) + } + } + _ => Err(format!("unsupported service action {action}")), + } + } + + pub(super) fn poll(&mut self, console: &mut Console) { + loop { + match self.listener.accept() { + Ok((mut stream, _)) => { + let result = (|| { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = String::new(); + BufReader::new(stream.try_clone()?).read_line(&mut request)?; + if request.len() > 256 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "service request is too long", + )); + } + let fields = request.split_ascii_whitespace().collect::>(); + if fields.len() != 2 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "expected ACTION SERVICE", + )); + } + let response = match self.run_action(fields[0], fields[1], console) { + Ok(message) => format!("OK {message}\n"), + Err(error) => format!("ERR {error}\n"), + }; + stream.write_all(response.as_bytes())?; + Ok(()) + })(); + if let Err(error) = result { + let _ = writeln!(stream, "ERR {error}"); + } + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => break, + Err(error) => { + console.log(&format!("service control socket failed: {error}")); + break; + } + } + } + + let now = Instant::now(); + let names = self.services.keys().cloned().collect::>(); + let mut restart = Vec::new(); + for name in names { + let cgroup = match service_cgroup(&name) { + Ok(path) => path, + Err(error) => { + console.log(&error); + continue; + } + }; + let populated = cgroup_populated(&cgroup).unwrap_or(false); + let service = self.service(&name); + if service.state == ServiceState::Running && !populated { + while service + .restart_times + .front() + .is_some_and(|instant| now.duration_since(*instant) > RESTART_WINDOW) + { + service.restart_times.pop_front(); + } + if service.restart_times.len() >= RESTART_LIMIT { + service.state = ServiceState::Failed; + console.log(&format!( + "service {name} exhausted {RESTART_LIMIT} restarts in {} seconds", + RESTART_WINDOW.as_secs() + )); + } else { + service.restart_times.push_back(now); + service.restart_at = Some(now + RESTART_DELAY); + service.state = ServiceState::RestartPending; + console.log(&format!("service {name} exited; restart scheduled")); + } + } + if service.state == ServiceState::RestartPending + && service.restart_at.is_some_and(|deadline| now >= deadline) + { + restart.push(name); + } + } + for name in restart { + match self.start_service(&name, console, false) { + Ok(message) => console.log(&message), + Err(error) => { + self.service(&name).state = ServiceState::Failed; + console.log(&format!("service {name} restart failed: {error}")); + } + } + } + } + + pub(super) fn stop_all(&mut self, console: &mut Console) { + for name in self.started_order.clone().into_iter().rev() { + console.log(&format!("stopping service {name}")); + if let Err(error) = self.stop_service(&name, console) { + console.log(&format!("service {name} stop failed: {error}")); + } + } + } +} + +fn service_cgroup(name: &str) -> Result { + if name.is_empty() + || !name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')) + { + return Err(format!("invalid service name {name:?}")); + } + Ok(Path::new(SERVICE_CGROUP_ROOT).join(name)) +} + +fn cgroup_populated(cgroup: &Path) -> Result { + let contents = std::fs::read_to_string(cgroup.join("cgroup.events")) + .map_err(|error| format!("cannot read {}: {error}", cgroup.display()))?; + Ok(contents + .lines() + .any(|line| line.split_ascii_whitespace().eq(["populated", "1"]))) +} + +fn wait_for_empty_cgroup(cgroup: &Path, timeout: Duration) { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if !cgroup_populated(cgroup).unwrap_or(false) { + return; + } + thread::sleep(POLL_INTERVAL); + } +} + +fn kill_cgroup(cgroup: &Path, console: &mut Console) { + if !cgroup_populated(cgroup).unwrap_or(false) { + return; + } + if let Err(error) = std::fs::write(cgroup.join("cgroup.kill"), b"1\n") { + console.log(&format!( + "cannot kill remaining processes in {}: {error}", + cgroup.display() + )); + } + wait_for_empty_cgroup(cgroup, TERMINATION_GRACE); +} diff --git a/base/cheesed/src/main.rs b/base/cheesed/src/main.rs new file mode 100644 index 0000000..4a1600a --- /dev/null +++ b/base/cheesed/src/main.rs @@ -0,0 +1,119 @@ +use std::io::{BufRead, BufReader, Write}; +use std::os::unix::net::UnixStream; +use std::path::Path; + +const CONTROL_SOCKET: &str = "/run/cheesed.sock"; +const SERVICE_USAGE: &str = "usage: service NAME {start|stop|restart|reload|status}"; + +fn main() { + let mut arguments = std::env::args(); + let executable = arguments.next().unwrap_or_else(|| "cheesed".to_owned()); + let name = Path::new(&executable) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("cheesed"); + let arguments = arguments.collect::>(); + + let result = match name { + "cheesed" => cheesed::run(), + "service" => run_service(&arguments), + "poweroff" | "reboot" | "shutdown" => run_shutdown(name, &arguments), + _ => Err(format!("unknown cheesed invocation name: {name}")), + }; + if let Err(error) = result { + eprintln!("{name}: {error}"); + std::process::exit(1); + } +} + +fn run_service(arguments: &[String]) -> Result<(), String> { + let [name, action] = arguments else { + return Err(SERVICE_USAGE.to_owned()); + }; + if !matches!( + action.as_str(), + "start" | "stop" | "restart" | "reload" | "status" + ) { + return Err(SERVICE_USAGE.to_owned()); + } + + let mut stream = UnixStream::connect(CONTROL_SOCKET) + .map_err(|error| format!("cannot connect to cheesed: {error}"))?; + writeln!(stream, "{action} {name}").map_err(|error| format!("cannot send request: {error}"))?; + stream + .shutdown(std::net::Shutdown::Write) + .map_err(|error| format!("cannot finish request: {error}"))?; + + let mut response = String::new(); + BufReader::new(stream) + .read_line(&mut response) + .map_err(|error| format!("cannot read response: {error}"))?; + let response = response.trim_end(); + if let Some(message) = response.strip_prefix("OK ") { + println!("{message}"); + Ok(()) + } else if let Some(message) = response.strip_prefix("ERR ") { + Err(message.to_owned()) + } else { + Err("invalid response from cheesed".to_owned()) + } +} + +fn run_shutdown(name: &str, arguments: &[String]) -> Result<(), String> { + let signal = shutdown_signal(name, arguments)?; + // SAFETY: PID 1 and the selected signal are fixed by the validated command name. + if unsafe { libc::kill(1, signal) } == -1 { + return Err(format!( + "cannot signal cheesed: {}", + std::io::Error::last_os_error() + )); + } + Ok(()) +} + +fn shutdown_signal(name: &str, arguments: &[String]) -> Result { + match (name, arguments) { + ("poweroff" | "shutdown", []) => Ok(libc::SIGTERM), + ("reboot", []) => Ok(libc::SIGINT), + ("shutdown", [flag]) if matches!(flag.as_str(), "-h" | "-p") => Ok(libc::SIGTERM), + ("shutdown", [argument]) if argument == "now" => Ok(libc::SIGTERM), + ("shutdown", [flag, argument]) + if matches!(flag.as_str(), "-h" | "-p") && argument == "now" => + { + Ok(libc::SIGTERM) + } + ("shutdown", [flag]) if flag == "-r" => Ok(libc::SIGINT), + ("shutdown", [flag, argument]) if flag == "-r" && argument == "now" => Ok(libc::SIGINT), + ("shutdown", _) => Err("usage: shutdown [-h|-p|-r] [now]".to_owned()), + _ => Err(format!("usage: {name}")), + } +} + +#[cfg(test)] +mod tests { + use super::shutdown_signal; + + fn arguments(values: &[&str]) -> Vec { + values.iter().map(ToString::to_string).collect() + } + + #[test] + fn multicall_shutdown_names_select_the_pid1_signal() { + assert_eq!(shutdown_signal("poweroff", &[]), Ok(libc::SIGTERM)); + assert_eq!(shutdown_signal("reboot", &[]), Ok(libc::SIGINT)); + assert_eq!( + shutdown_signal("shutdown", &arguments(&["-r", "now"])), + Ok(libc::SIGINT) + ); + assert_eq!( + shutdown_signal("shutdown", &arguments(&["-p", "now"])), + Ok(libc::SIGTERM) + ); + } + + #[test] + fn multicall_shutdown_rejects_invalid_arguments() { + assert!(shutdown_signal("poweroff", &arguments(&["now"])).is_err()); + assert!(shutdown_signal("shutdown", &arguments(&["later"])).is_err()); + } +} diff --git a/base/cheesed/src/policy.rs b/base/cheesed/src/policy.rs new file mode 100644 index 0000000..ae21e15 --- /dev/null +++ b/base/cheesed/src/policy.rs @@ -0,0 +1,568 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct RcPolicy { + enabled: BTreeSet, +} + +impl RcPolicy { + pub(crate) fn enabled(&self) -> impl Iterator { + self.enabled.iter().map(String::as_str) + } +} + +#[derive(Debug)] +pub(crate) struct ServiceDefinition { + path: PathBuf, + provides: BTreeSet, + requires: BTreeSet, + before: BTreeSet, +} + +impl ServiceDefinition { + pub(crate) fn path(&self) -> &Path { + &self.path + } +} + +#[derive(Debug)] +pub(crate) struct ServiceCatalog { + definitions: BTreeMap, + required: BTreeSet, +} + +impl ServiceCatalog { + pub(crate) fn discover( + system_definitions: &Path, + local_definitions: &Path, + required_services: &Path, + ) -> Result { + let mut definitions = read_service_definitions(system_definitions, false)?; + for (name, definition) in read_service_definitions(local_definitions, true)? { + if let Some(system_definition) = definitions.insert(name.clone(), definition) { + return Err(PolicyError::new(format!( + "duplicate service definition {name}: {} and {}", + system_definition.path.display(), + definitions[&name].path.display() + ))); + } + } + + let required = read_required_services(required_services)?; + for service in &required { + if !definitions.contains_key(service) { + return Err(PolicyError::new(format!( + "required service {service} has no definition" + ))); + } + } + + Ok(Self { + definitions, + required, + }) + } + + pub(crate) fn definition(&self, name: &str) -> Option<&ServiceDefinition> { + self.definitions.get(name) + } + + pub(crate) fn is_required(&self, name: &str) -> bool { + self.required.contains(name) + } + + pub(crate) fn start_order(&self, policy: &RcPolicy) -> Result, PolicyError> { + let selected = self + .required + .iter() + .cloned() + .chain(policy.enabled.iter().cloned()) + .collect::>(); + self.order_selected(&selected) + } + + fn order_selected(&self, selected: &BTreeSet) -> Result, PolicyError> { + let mut all_providers = BTreeMap::>::new(); + for (name, definition) in &self.definitions { + for token in &definition.provides { + all_providers + .entry(token.clone()) + .or_default() + .insert(name.clone()); + } + } + + let mut outgoing = selected + .iter() + .map(|name| (name.clone(), BTreeSet::new())) + .collect::>(); + let mut indegree = selected + .iter() + .map(|name| (name.clone(), 0_usize)) + .collect::>(); + + for name in selected { + let definition = &self.definitions[name]; + for token in &definition.requires { + let Some(providers) = all_providers.get(token) else { + return Err(PolicyError::new(format!( + "service {name} requires missing facility {token}" + ))); + }; + let selected_providers = providers + .intersection(selected) + .filter(|provider| *provider != name) + .cloned() + .collect::>(); + if selected_providers.is_empty() && !providers.contains(name) { + return Err(PolicyError::new(format!( + "service {name} requires disabled facility {token}" + ))); + } + for provider in selected_providers { + add_edge(&mut outgoing, &mut indegree, &provider, name); + } + } + + for token in &definition.before { + if let Some(providers) = all_providers.get(token) { + for target in providers.intersection(selected) { + if target != name { + add_edge(&mut outgoing, &mut indegree, name, target); + } + } + } + } + } + + let mut ready = indegree + .iter() + .filter_map(|(name, degree)| (*degree == 0).then_some(name.clone())) + .collect::>(); + let mut order = Vec::with_capacity(selected.len()); + while let Some(name) = ready.pop_first() { + order.push(name.clone()); + for dependent in outgoing[&name].clone() { + let degree = indegree + .get_mut(&dependent) + .expect("dependency graph contains every selected service"); + *degree -= 1; + if *degree == 0 { + ready.insert(dependent); + } + } + } + + if order.len() != selected.len() { + let cycle = indegree + .into_iter() + .filter_map(|(name, degree)| (degree != 0).then_some(name)) + .collect::>() + .join(", "); + return Err(PolicyError::new(format!( + "service dependency cycle contains: {cycle}" + ))); + } + Ok(order) + } +} + +fn add_edge( + outgoing: &mut BTreeMap>, + indegree: &mut BTreeMap, + from: &str, + to: &str, +) { + if outgoing + .get_mut(from) + .expect("edge source is selected") + .insert(to.to_owned()) + { + *indegree.get_mut(to).expect("edge target is selected") += 1; + } +} + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct PolicyError { + message: String, +} + +impl PolicyError { + fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl fmt::Display for PolicyError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl From for PolicyError { + fn from(error: io::Error) -> Self { + Self::new(error.to_string()) + } +} + +pub(crate) fn parse_rc_conf( + contents: &str, + catalog: &ServiceCatalog, +) -> Result { + let mut assignments = BTreeMap::new(); + + for (index, line) in contents.lines().enumerate() { + let line_number = index + 1; + if line.trim().is_empty() || line.trim_start().starts_with('#') { + continue; + } + + let Some((key, value)) = line.split_once('=') else { + return Err(line_error( + line_number, + "expected enable_name=YES or enable_name=NO", + )); + }; + if key.contains(char::is_whitespace) + || value.contains(char::is_whitespace) + || line.matches('=').count() != 1 + { + return Err(line_error( + line_number, + "whitespace, quoting, comments, and extra assignments are not allowed", + )); + } + + let Some(name) = key.strip_prefix("enable_") else { + return Err(line_error(line_number, "unknown rc.conf key")); + }; + if !valid_service_name(name) { + return Err(line_error(line_number, "invalid service name")); + } + let enabled = match value { + "YES" => true, + "NO" => false, + _ => { + return Err(line_error( + line_number, + "enablement value must be exactly YES or NO", + )); + } + }; + if !catalog.definitions.contains_key(name) { + return Err(line_error( + line_number, + format!("service {name} has no installed definition"), + )); + } + if catalog.required.contains(name) { + return Err(line_error( + line_number, + format!("required service {name} cannot be enabled or disabled"), + )); + } + if assignments.insert(name.to_owned(), enabled).is_some() { + return Err(line_error( + line_number, + format!("duplicate assignment for service {name}"), + )); + } + } + + Ok(RcPolicy { + enabled: assignments + .into_iter() + .filter_map(|(name, enabled)| enabled.then_some(name)) + .collect(), + }) +} + +fn read_service_definitions( + directory: &Path, + optional: bool, +) -> Result, PolicyError> { + let entries = match fs::read_dir(directory) { + Ok(entries) => entries, + Err(error) if optional && error.kind() == io::ErrorKind::NotFound => { + return Ok(BTreeMap::new()); + } + Err(error) => { + return Err(PolicyError::new(format!( + "cannot read {}: {error}", + directory.display() + ))); + } + }; + + let mut definitions = BTreeMap::new(); + for entry in entries { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + let name = entry + .file_name() + .into_string() + .map_err(|_| PolicyError::new("service definition name is not UTF-8"))?; + if !valid_service_name(&name) { + return Err(PolicyError::new(format!( + "service definition {name:?} is not a valid MOUSE service name" + ))); + } + let path = entry.path(); + let mut definition = parse_service_definition(&path)?; + definition.provides.insert(name.clone()); + definitions.insert(name, definition); + } + Ok(definitions) +} + +fn parse_service_definition(path: &Path) -> Result { + let contents = fs::read_to_string(path).map_err(|error| { + PolicyError::new(format!( + "cannot read service definition {}: {error}", + path.display() + )) + })?; + let mut provides = BTreeSet::new(); + let mut requires = BTreeSet::new(); + let mut before = BTreeSet::new(); + for line in contents.lines() { + let line = line.trim_start(); + let Some(comment) = line.strip_prefix('#') else { + continue; + }; + let comment = comment.trim_start(); + for (label, destination) in [ + ("PROVIDE:", &mut provides), + ("REQUIRE:", &mut requires), + ("BEFORE:", &mut before), + ] { + if let Some(tokens) = comment.strip_prefix(label) { + for token in tokens.split_ascii_whitespace() { + if !valid_facility_name(token) { + return Err(PolicyError::new(format!( + "{} contains invalid {label} facility {token:?}", + path.display() + ))); + } + destination.insert(token.to_owned()); + } + } + } + } + Ok(ServiceDefinition { + path: path.to_owned(), + provides, + requires, + before, + }) +} + +fn read_required_services(path: &Path) -> Result, PolicyError> { + let contents = fs::read_to_string(path).map_err(|error| { + PolicyError::new(format!( + "cannot read required service list {}: {error}", + path.display() + )) + })?; + let mut required = BTreeSet::new(); + for (index, line) in contents.lines().enumerate() { + let name = line.trim(); + if name.is_empty() || name.starts_with('#') { + continue; + } + if !valid_service_name(name) { + return Err(PolicyError::new(format!( + "{}:{}: invalid required service name {name:?}", + path.display(), + index + 1 + ))); + } + if !required.insert(name.to_owned()) { + return Err(PolicyError::new(format!( + "{}:{}: duplicate required service {name}", + path.display(), + index + 1 + ))); + } + } + Ok(required) +} + +fn valid_service_name(name: &str) -> bool { + let mut bytes = name.bytes(); + matches!(bytes.next(), Some(b'a'..=b'z')) + && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') +} + +fn valid_facility_name(name: &str) -> bool { + !name.is_empty() + && name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')) +} + +fn line_error(line_number: usize, message: impl fmt::Display) -> PolicyError { + PolicyError::new(format!("/etc/rc.conf:{line_number}: {message}")) +} + +#[cfg(test)] +mod tests { + use super::{RcPolicy, ServiceCatalog, parse_rc_conf}; + use std::collections::BTreeSet; + use std::fs; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, Ordering}; + + static NEXT_TEMP: AtomicU64 = AtomicU64::new(0); + + struct TestLayout { + root: PathBuf, + system_definitions: PathBuf, + local_definitions: PathBuf, + required_services: PathBuf, + } + + impl TestLayout { + fn new() -> Self { + let serial = NEXT_TEMP.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir() + .join(format!("cheesed-policy-{}-{serial}", std::process::id())); + let system_definitions = root.join("etc/rc.d"); + let local_definitions = root.join("usr/local/etc/rc.d"); + let required_services = root.join("required-services"); + fs::create_dir_all(&system_definitions).unwrap(); + fs::create_dir_all(&local_definitions).unwrap(); + fs::write( + system_definitions.join("required"), + "#!/bin/sh\n# PROVIDE: REQUIRED\n", + ) + .unwrap(); + fs::write( + system_definitions.join("optional"), + "#!/bin/sh\n# PROVIDE: OPTIONAL\n# REQUIRE: REQUIRED\n", + ) + .unwrap(); + fs::write( + local_definitions.join("ported"), + "#!/bin/sh\n# PROVIDE: PORTED\n# REQUIRE: OPTIONAL\n", + ) + .unwrap(); + fs::write(&required_services, "required\n").unwrap(); + Self { + root, + system_definitions, + local_definitions, + required_services, + } + } + + fn catalog(&self) -> ServiceCatalog { + ServiceCatalog::discover( + &self.system_definitions, + &self.local_definitions, + &self.required_services, + ) + .unwrap() + } + } + + impl Drop for TestLayout { + fn drop(&mut self) { + fs::remove_dir_all(&self.root).unwrap(); + } + } + + fn enabled(policy: &RcPolicy) -> BTreeSet<&str> { + policy.enabled().collect() + } + + #[test] + fn accepts_comments_empty_lines_and_strict_assignments() { + let layout = TestLayout::new(); + let policy = parse_rc_conf( + "\n # comment\nenable_optional=YES\nenable_ported=YES\n", + &layout.catalog(), + ) + .unwrap(); + assert_eq!(enabled(&policy), BTreeSet::from(["optional", "ported"])); + } + + #[test] + fn rejects_shell_ambiguous_unknown_and_required_assignments() { + let layout = TestLayout::new(); + let catalog = layout.catalog(); + for invalid in [ + "enable_optional =YES", + "enable_optional=\"YES\"", + "enable_optional=YES # comment", + "enable_optional=$(hostname)", + "enable_optional=YES=NO", + "rc_parallel=YES", + "enable_missing=NO", + "enable_required=YES", + ] { + assert!( + parse_rc_conf(invalid, &catalog).is_err(), + "accepted {invalid:?}" + ); + } + } + + #[test] + fn orders_selected_services_from_rcorder_headers() { + let layout = TestLayout::new(); + let catalog = layout.catalog(); + let policy = parse_rc_conf("enable_optional=YES\nenable_ported=YES\n", &catalog).unwrap(); + assert_eq!( + catalog.start_order(&policy).unwrap(), + ["required", "optional", "ported"] + ); + assert!(catalog.is_required("required")); + assert_eq!( + catalog.definition("ported").unwrap().path(), + layout.local_definitions.join("ported") + ); + } + + #[test] + fn rejects_disabled_missing_and_cyclic_dependencies() { + let layout = TestLayout::new(); + let catalog = layout.catalog(); + let disabled = parse_rc_conf("enable_ported=YES\n", &catalog).unwrap(); + assert!(catalog.start_order(&disabled).is_err()); + + fs::write( + layout.system_definitions.join("required"), + "#!/bin/sh\n# PROVIDE: REQUIRED\n# REQUIRE: OPTIONAL\n", + ) + .unwrap(); + let catalog = layout.catalog(); + let cyclic = parse_rc_conf("enable_optional=YES\n", &catalog).unwrap(); + assert!(catalog.start_order(&cyclic).is_err()); + } + + #[test] + fn rejects_duplicate_system_and_local_service_definitions() { + let layout = TestLayout::new(); + fs::write(layout.local_definitions.join("optional"), "#!/bin/sh\n").unwrap(); + let error = ServiceCatalog::discover( + &layout.system_definitions, + &layout.local_definitions, + &layout.required_services, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("duplicate service definition optional") + ); + } +} diff --git a/base/kernel/x86_64.config b/base/kernel/x86_64.config new file mode 100644 index 0000000..0d666a0 --- /dev/null +++ b/base/kernel/x86_64.config @@ -0,0 +1,122 @@ +CONFIG_LOCALVERSION="-mouse" +CONFIG_DEFAULT_HOSTNAME="mouse" + +CONFIG_64BIT=y +CONFIG_X86_64=y +CONFIG_SMP=y +CONFIG_HYPERVISOR_GUEST=y +CONFIG_PARAVIRT=y +CONFIG_ACPI=y +CONFIG_EFI=y +CONFIG_EFI_STUB=y + +CONFIG_MULTIUSER=y +CONFIG_PRINTK=y +CONFIG_BUG=y +CONFIG_FUTEX=y +CONFIG_EPOLL=y +CONFIG_SIGNALFD=y +CONFIG_TIMERFD=y +CONFIG_EVENTFD=y +CONFIG_BINFMT_ELF=y +CONFIG_BINFMT_SCRIPT=y +CONFIG_FILE_LOCKING=y +CONFIG_SWAP=y +CONFIG_CGROUPS=y +CONFIG_KALLSYMS=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_MODVERSIONS=y +CONFIG_BASIC_MODVERSIONS=y + +CONFIG_BLOCK=y +CONFIG_BLK_DEV=y +CONFIG_EFI_PARTITION=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_FW_LOADER=y +CONFIG_PROC_FS=y +CONFIG_PROC_SYSCTL=y +CONFIG_SYSFS=y +CONFIG_TMPFS=y +CONFIG_TMPFS_POSIX_ACL=y + +CONFIG_NET=y +CONFIG_PACKET=y +CONFIG_PACKET_DIAG=y +CONFIG_UNIX=y +CONFIG_UNIX_DIAG=y +CONFIG_INET=y +CONFIG_INET_DIAG=y +CONFIG_INET_TCP_DIAG=y +CONFIG_INET_UDP_DIAG=y +CONFIG_IPV6=y +CONFIG_NETLINK_DIAG=y +CONFIG_NAMESPACES=y +CONFIG_NET_NS=y +CONFIG_NETFILTER=y +CONFIG_NETFILTER_ADVANCED=y +CONFIG_NF_CONNTRACK=y +CONFIG_NF_NAT=y +CONFIG_NF_TABLES=y +CONFIG_NF_TABLES_INET=y +CONFIG_NF_TABLES_NETDEV=y +CONFIG_NFT_CT=y +CONFIG_NFT_LOG=y +CONFIG_NFT_LIMIT=y +CONFIG_NFT_MASQ=y +CONFIG_NFT_REDIR=y +CONFIG_NFT_NAT=y +CONFIG_NFT_REJECT=y +CONFIG_NFT_REJECT_INET=y +CONFIG_NET_SCHED=y +CONFIG_NET_SCH_HTB=y +CONFIG_NET_SCH_FQ_CODEL=y +CONFIG_NET_CLS_BASIC=y +CONFIG_NET_CLS_U32=y +CONFIG_NETDEVICES=y +CONFIG_NET_CORE=y +CONFIG_ETHERNET=y +CONFIG_DUMMY=y +CONFIG_BONDING=y +CONFIG_TUN=y +CONFIG_VETH=y +CONFIG_VLAN_8021Q=y +CONFIG_BRIDGE=y +CONFIG_BRIDGE_VLAN_FILTERING=y + +CONFIG_PCI=y +CONFIG_PCI_MSI=y +CONFIG_VIRTIO_MENU=y +CONFIG_VIRTIO=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_BLK=y +CONFIG_VIRTIO_NET=y +CONFIG_NET_VENDOR_INTEL=y +CONFIG_E1000=y +CONFIG_E1000E=y + +CONFIG_SCSI=y +CONFIG_BLK_DEV_SD=y +CONFIG_ATA=y +CONFIG_SATA_AHCI=y +CONFIG_BLK_DEV_NVME=y + +CONFIG_TTY=y +CONFIG_UNIX98_PTYS=y +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_VT=y +CONFIG_VT_CONSOLE=y +CONFIG_VGA_CONSOLE=y +CONFIG_INPUT=y +CONFIG_INPUT_KEYBOARD=y +CONFIG_KEYBOARD_ATKBD=y +CONFIG_SERIO=y +CONFIG_SERIO_I8042=y +CONFIG_FB=y +CONFIG_FB_EFI=y +CONFIG_FRAMEBUFFER_CONSOLE=y + +CONFIG_BTRFS_FS=y +CONFIG_BTRFS_FS_POSIX_ACL=y diff --git a/base/limine.conf b/base/limine.conf new file mode 100644 index 0000000..3c2946d --- /dev/null +++ b/base/limine.conf @@ -0,0 +1,16 @@ +timeout: 3 +default_entry: 1 +serial: yes +serial_baudrate: 115200 +graphics: yes +interface_branding: MOUSE + +/MOUSE +protocol: linux +path: boot():/vmlinuz-mouse +cmdline: console=tty0 console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed panic=-1 + +/MOUSE alternate +protocol: linux +path: boot():/vmlinuz-mouse +cmdline: console=tty0 console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rootflags=subvol=ROOT/alternate rw rootwait init=/sbin/cheesed panic=-1 diff --git a/base/patches/chimerautils-native-cdefs.patch b/base/patches/chimerautils-native-cdefs.patch new file mode 100644 index 0000000..7c5b548 --- /dev/null +++ b/base/patches/chimerautils-native-cdefs.patch @@ -0,0 +1,36 @@ +diff --git a/include/sys/cdefs.h b/include/sys/cdefs.h +index 2dac3f9..aa16c50 100644 +--- a/include/sys/cdefs.h ++++ b/include/sys/cdefs.h +@@ -28,6 +28,10 @@ + #ifndef SYS_CDEFS_H + #define SYS_CDEFS_H + ++#ifdef __APPLE__ ++#include_next ++#else ++ + #include + + #ifdef __GLIBC__ +@@ -53,4 +57,6 @@ + + #define __unreachable() __builtin_unreachable() + ++#endif ++ + #endif +diff --git a/include/string.h b/include/string.h +index 68108f6..9707bcd 100644 +--- a/include/string.h ++++ b/include/string.h +@@ -38,7 +38,9 @@ + extern "C" { + #endif + ++#ifndef __APPLE__ + void strmode(mode_t, char *); ++#endif + #ifndef HAVE_STRLCPY + size_t strlcpy(char *d, const char *s, size_t n); + #endif diff --git a/base/patches/chimerautils-scanflike.patch b/base/patches/chimerautils-scanflike.patch new file mode 100644 index 0000000..1fbd6ef --- /dev/null +++ b/base/patches/chimerautils-scanflike.patch @@ -0,0 +1,12 @@ +diff --git a/include/sys/cdefs.h b/include/sys/cdefs.h +index aa16c50..4602b26 100644 +--- a/include/sys/cdefs.h ++++ b/include/sys/cdefs.h +@@ -53,6 +53,7 @@ + #define __dead2 + #define __printf0like(x, y) + #define __printflike(x, y) __attribute__((format(printf, x, y))) ++#define __scanflike(x, y) __attribute__((format(scanf, x, y))) + #define __DECONST(a, v) ((a)(v)) + #define __unused __attribute__((unused)) + #define __unreachable() __builtin_unreachable() diff --git a/base/patches/nftables-posix-configure.patch b/base/patches/nftables-posix-configure.patch new file mode 100644 index 0000000..cea26f0 --- /dev/null +++ b/base/patches/nftables-posix-configure.patch @@ -0,0 +1,15 @@ +diff --git a/configure b/configure +--- a/configure ++++ b/configure +@@ -17940,8 +17940,10 @@ $config_commands + echo " ${STABLE_RELEASE}" + echo "};" + echo "static char nftbuildstamp[] = {" +- for ((i = 56; i >= 0; i-= 8)); do ++ i=56 ++ while test "$i" -ge 0; do + echo " ((uint64_t)MAKE_STAMP >> $i) & 0xff," ++ i=$((i - 8)) + done + echo "};" + ) >nftversion.h diff --git a/base/patches/pkgsrc-bootstrap-mouse.patch b/base/patches/pkgsrc-bootstrap-mouse.patch new file mode 100644 index 0000000..ade29c3 --- /dev/null +++ b/base/patches/pkgsrc-bootstrap-mouse.patch @@ -0,0 +1,13 @@ +--- a/bootstrap/bootstrap ++++ b/bootstrap/bootstrap +@@ -683,7 +683,9 @@ Linux) + fi + # Debian/Ubuntu's awk is mawk, and mawk does not understand + # some regexps used in pkgsrc/mk. +- if [ -f /etc/debian_version ]; then ++ if [ -f /etc/mouse-release ]; then ++ bootstrap_awk=/usr/bin/awk ++ elif [ -f /etc/debian_version ]; then + need_awk=yes + # Arch uses gawk 5 that breaks some regexps. It doesn't provide pax + # anymore. diff --git a/base/patches/pkgsrc-rc.subr-cgroup.patch b/base/patches/pkgsrc-rc.subr-cgroup.patch new file mode 100644 index 0000000..5417169 --- /dev/null +++ b/base/patches/pkgsrc-rc.subr-cgroup.patch @@ -0,0 +1,27 @@ +--- rc.subr.orig ++++ rc.subr +@@ -266,6 +266,14 @@ + _procname=$1 + _interpreter=$2 + _psargs=$3 + ++ # cheesed places the rc script and every process it starts in one ++ # service cgroup. That cgroup is the process database on MOUSE, so rc.d ++ # scripts do not need ps or package-specific pidfile interpretation. ++ if [ "${MOUSE_SERVICE_PIDS+x}" = x ]; then ++ ${_RCCMD_echo} -n "$MOUSE_SERVICE_PIDS" ++ return ++ fi ++ + _pref= + if [ $_interpreter != "." ]; then # an interpreted script +@@ -548,7 +556,8 @@ + # checkyesno ${rcvar} + # and return if that failed + # +- if [ -n "${rcvar}" -a "$rc_arg" != "rcvar" ]; then ++ if [ -z "${MOUSE_SERVICE_CGROUP:-}" ] && ++ [ -n "${rcvar}" -a "$rc_arg" != "rcvar" ]; then + if ! checkyesno ${rcvar}; then + return 0 + fi diff --git a/base/patches/tcsh-gethost-native.patch b/base/patches/tcsh-gethost-native.patch new file mode 100644 index 0000000..1665e40 --- /dev/null +++ b/base/patches/tcsh-gethost-native.patch @@ -0,0 +1,26 @@ +--- a/gethost.c ++++ b/gethost.c +@@ -29,7 +29,12 @@ + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +-#include "sh.h" ++#include ++#include ++#include ++#include ++#include ++#include + + #ifdef SCO + # define perror __perror +@@ -45,8 +50,7 @@ + # undef system + #endif + +-#include +-#include ++#define INBUFSIZE 8192 + + #define ISSPACE(p) (isspace((unsigned char) (p)) && (p) != '\n') + diff --git a/base/rootfs/etc/crontab b/base/rootfs/etc/crontab new file mode 100644 index 0000000..0422f28 --- /dev/null +++ b/base/rootfs/etc/crontab @@ -0,0 +1,4 @@ +SHELL=/bin/sh +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin + +# minute hour day month weekday user command diff --git a/base/rootfs/etc/csh.cshrc b/base/rootfs/etc/csh.cshrc new file mode 100644 index 0000000..d1df151 --- /dev/null +++ b/base/rootfs/etc/csh.cshrc @@ -0,0 +1,5 @@ +set path = ( /sbin /bin /usr/sbin /usr/bin /usr/local/sbin /usr/local/bin ) + +if ( $?prompt ) then + set prompt = "mouse:%~%# " +endif diff --git a/base/rootfs/etc/csh.login b/base/rootfs/etc/csh.login new file mode 100644 index 0000000..2920f94 --- /dev/null +++ b/base/rootfs/etc/csh.login @@ -0,0 +1,4 @@ +umask 022 +setenv SHELL /bin/tcsh + +cd diff --git a/base/rootfs/etc/default/useradd b/base/rootfs/etc/default/useradd new file mode 100644 index 0000000..e12dcc1 --- /dev/null +++ b/base/rootfs/etc/default/useradd @@ -0,0 +1,7 @@ +GROUP=100 +HOME=/home +INACTIVE=-1 +EXPIRE= +SHELL=/bin/tcsh +SKEL=/etc/skel +CREATE_MAIL_SPOOL=no diff --git a/base/rootfs/etc/depmod.d/mouse.conf b/base/rootfs/etc/depmod.d/mouse.conf new file mode 100644 index 0000000..a0f3d33 --- /dev/null +++ b/base/rootfs/etc/depmod.d/mouse.conf @@ -0,0 +1,3 @@ +# Package-owned kernel modules live on persistent /usr/local. +external * /usr/local/lib/modules +search external built-in diff --git a/base/rootfs/etc/dhcpcd.conf b/base/rootfs/etc/dhcpcd.conf new file mode 100644 index 0000000..cbe356e --- /dev/null +++ b/base/rootfs/etc/dhcpcd.conf @@ -0,0 +1,4 @@ +# The base image manages one wired interface. +allowinterfaces eth0 +interface eth0 +metric 100 diff --git a/base/rootfs/etc/group b/base/rootfs/etc/group new file mode 100644 index 0000000..f851fcc --- /dev/null +++ b/base/rootfs/etc/group @@ -0,0 +1,6 @@ +root:x:0:root,toor +tty:x:5: +uucp:x:14: +users:x:100: +dhcpcd:x:101: +_ntp:x:102: diff --git a/base/rootfs/etc/hostname b/base/rootfs/etc/hostname new file mode 100644 index 0000000..3e41be9 --- /dev/null +++ b/base/rootfs/etc/hostname @@ -0,0 +1 @@ +mouse diff --git a/base/rootfs/etc/hosts b/base/rootfs/etc/hosts new file mode 100644 index 0000000..9cc33c7 --- /dev/null +++ b/base/rootfs/etc/hosts @@ -0,0 +1,2 @@ +127.0.0.1 localhost mouse +::1 localhost mouse diff --git a/base/rootfs/etc/login.defs b/base/rootfs/etc/login.defs new file mode 100644 index 0000000..e8ee0af --- /dev/null +++ b/base/rootfs/etc/login.defs @@ -0,0 +1,18 @@ +ENV_PATH PATH=/bin:/usr/bin:/usr/local/bin +ENV_SUPATH PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin +MAIL_DIR /var/mail +HUSHLOGIN_FILE .hushlogin +LOGIN_TIMEOUT 60 +MOTD_FILE /etc/motd +PASS_MAX_DAYS 99999 +PASS_MIN_DAYS 0 +PASS_WARN_AGE 7 +TTYGROUP tty +TTYPERM 0600 +UID_MIN 1000 +UID_MAX 60000 +GID_MIN 1000 +GID_MAX 60000 +CREATE_HOME yes +USERGROUPS_ENAB yes +ENCRYPT_METHOD SHA512 diff --git a/base/rootfs/etc/mdev.conf b/base/rootfs/etc/mdev.conf new file mode 100644 index 0000000..3bfdca8 --- /dev/null +++ b/base/rootfs/etc/mdev.conf @@ -0,0 +1,13 @@ +# Load the exact module matching a kernel device alias. Device nodes themselves +# are supplied by devtmpfs and mdevd applies conservative default ownership. +$MODALIAS=.* root:root 0600 @modprobe -qb "$MODALIAS" +null root:root 0666 +zero root:root 0666 +full root:root 0666 +random root:root 0444 +urandom root:root 0444 +console root:tty 0600 +tty root:tty 0666 +tty[0-9]* root:tty 0660 +ttyS[0-9]* root:uucp 0660 +.* root:root 0600 diff --git a/base/rootfs/etc/motd b/base/rootfs/etc/motd new file mode 100644 index 0000000..9af2982 --- /dev/null +++ b/base/rootfs/etc/motd @@ -0,0 +1,2 @@ +MOUSE 0.1.0 (x86_64) +Monolithic Opinionated Unix-like System Environment diff --git a/base/rootfs/etc/mouse-release b/base/rootfs/etc/mouse-release new file mode 100644 index 0000000..4121765 --- /dev/null +++ b/base/rootfs/etc/mouse-release @@ -0,0 +1,6 @@ +NAME=MOUSE +ID=mouse +PRETTY_NAME="MOUSE 0.1.0" +VERSION="0.1.0" +VERSION_ID=0.1.0 +ARCHITECTURE=x86_64 diff --git a/base/rootfs/etc/ntpd.conf b/base/rootfs/etc/ntpd.conf new file mode 100644 index 0000000..66f9e11 --- /dev/null +++ b/base/rootfs/etc/ntpd.conf @@ -0,0 +1 @@ +servers pool.ntp.org diff --git a/base/rootfs/etc/passwd b/base/rootfs/etc/passwd new file mode 100644 index 0000000..f7ecbf7 --- /dev/null +++ b/base/rootfs/etc/passwd @@ -0,0 +1,4 @@ +root:x:0:0:root:/root:/bin/sh +toor:x:0:0:BSD-style root account:/root:/bin/tcsh +dhcpcd:x:101:101:dhcpcd privilege separation:/var/empty:/sbin/nologin +_ntp:x:102:102:OpenNTPD privilege separation:/var/empty:/sbin/nologin diff --git a/base/rootfs/etc/rc.conf b/base/rootfs/etc/rc.conf new file mode 100644 index 0000000..146271c --- /dev/null +++ b/base/rootfs/etc/rc.conf @@ -0,0 +1,4 @@ +# Persistent MOUSE service policy belongs in this file. +enable_network=YES +enable_cron=YES +enable_ntpd=YES diff --git a/base/rootfs/etc/rc.d/console_login b/base/rootfs/etc/rc.d/console_login new file mode 100644 index 0000000..ec790c9 --- /dev/null +++ b/base/rootfs/etc/rc.d/console_login @@ -0,0 +1,33 @@ +#!/bin/sh +# PROVIDE: console_login +# REQUIRE: LOGIN + +. /etc/rc.subr + +name=console_login +start_precmd=console_login_prestart +start_cmd=console_login_start +stop_cmd=: + +autologin= +if grep -qw mouse.autologin=1 /proc/cmdline; then + autologin="--autologin root" +fi + +console_login_prestart() +{ + [ -e /run/utmp ] || : > /run/utmp + [ -e /var/log/wtmp ] || : > /var/log/wtmp + chmod 0664 /run/utmp /var/log/wtmp +} + +console_login_start() +{ + /usr/bin/setsid -f /sbin/agetty $autologin \ + --login-program /bin/login --noclear --keep-baud \ + 115200,38400,9600 ttyS0 vt100 & + /usr/bin/setsid -f /sbin/agetty $autologin \ + --login-program /bin/login --noclear tty1 linux & +} + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/cron b/base/rootfs/etc/rc.d/cron new file mode 100644 index 0000000..443c236 --- /dev/null +++ b/base/rootfs/etc/rc.d/cron @@ -0,0 +1,12 @@ +#!/bin/sh +# PROVIDE: cron +# REQUIRE: DAEMON +# BEFORE: LOGIN + +. /etc/rc.subr + +name=cron +command=/usr/sbin/cron +command_args="-n -s &" + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/daemon b/base/rootfs/etc/rc.d/daemon new file mode 100644 index 0000000..760aae5 --- /dev/null +++ b/base/rootfs/etc/rc.d/daemon @@ -0,0 +1,11 @@ +#!/bin/sh +# PROVIDE: DAEMON +# REQUIRE: machine_identity + +. /etc/rc.subr + +name=daemon +start_cmd=: +stop_cmd=: + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/login b/base/rootfs/etc/rc.d/login new file mode 100644 index 0000000..9f56936 --- /dev/null +++ b/base/rootfs/etc/rc.d/login @@ -0,0 +1,11 @@ +#!/bin/sh +# PROVIDE: LOGIN +# REQUIRE: DAEMON + +. /etc/rc.subr + +name=login +start_cmd=: +stop_cmd=: + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/loopback b/base/rootfs/etc/rc.d/loopback new file mode 100644 index 0000000..99cd334 --- /dev/null +++ b/base/rootfs/etc/rc.d/loopback @@ -0,0 +1,20 @@ +#!/bin/sh +# PROVIDE: LOOPBACK +# BEFORE: DAEMON + +. /etc/rc.subr + +name=loopback + +loopback_start() +{ + /sbin/ip link set lo up + /sbin/ip address replace 127.0.0.1/8 dev lo + /sbin/ip -6 address replace ::1/128 dev lo +} + +start_cmd=loopback_start +stop_cmd=: +status_cmd=: + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/machine_identity b/base/rootfs/etc/rc.d/machine_identity new file mode 100644 index 0000000..077f830 --- /dev/null +++ b/base/rootfs/etc/rc.d/machine_identity @@ -0,0 +1,20 @@ +#!/bin/sh +# PROVIDE: machine_identity + +. /etc/rc.subr + +name=machine_identity +start_cmd=machine_identity_start +stop_cmd=: + +machine_identity_start() +{ + if [ ! -s /etc/machine-id ]; then + sed 's/-//g' /proc/sys/kernel/random/uuid > /etc/machine-id.new || + return 1 + chmod 0444 /etc/machine-id.new || return 1 + mv /etc/machine-id.new /etc/machine-id || return 1 + fi +} + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/mdevd b/base/rootfs/etc/rc.d/mdevd new file mode 100644 index 0000000..c368e0d --- /dev/null +++ b/base/rootfs/etc/rc.d/mdevd @@ -0,0 +1,11 @@ +#!/bin/sh +# PROVIDE: mdevd DEVFS +# BEFORE: machine_identity + +. /etc/rc.subr + +name=mdevd +command=/sbin/mdevd +command_args="-C &" + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/network b/base/rootfs/etc/rc.d/network new file mode 100644 index 0000000..a5fc987 --- /dev/null +++ b/base/rootfs/etc/rc.d/network @@ -0,0 +1,38 @@ +#!/bin/sh +# PROVIDE: network NETWORKING +# REQUIRE: DAEMON +# BEFORE: LOGIN + +. /etc/rc.subr + +name=network +command=/sbin/dhcpcd +command_args="-4 -b -q -c /usr/libexec/mouse-dhcpcd-hook" +start_precmd=network_prestart +start_postcmd=network_poststart +stop_postcmd=network_poststop + +network_prestart() +{ + rm -f /run/mouse-network.ready +} + +network_poststart() +{ + waited=0 + while [ ! -f /run/mouse-network.ready ]; do + if [ "$waited" -ge 20 ]; then + echo "timed out waiting for an IPv4 DHCP lease" >&2 + return 1 + fi + sleep 1 + waited=$((waited + 1)) + done +} + +network_poststop() +{ + rm -f /run/mouse-network.ready +} + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/ntpd b/base/rootfs/etc/rc.d/ntpd new file mode 100644 index 0000000..278887a --- /dev/null +++ b/base/rootfs/etc/rc.d/ntpd @@ -0,0 +1,12 @@ +#!/bin/sh +# PROVIDE: ntpd TIMESYNC +# REQUIRE: DAEMON +# BEFORE: LOGIN + +. /etc/rc.subr + +name=ntpd +command=/usr/sbin/ntpd +command_args="-d &" + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/servers b/base/rootfs/etc/rc.d/servers new file mode 100644 index 0000000..f1cea7e --- /dev/null +++ b/base/rootfs/etc/rc.d/servers @@ -0,0 +1,11 @@ +#!/bin/sh +# PROVIDE: SERVERS +# REQUIRE: DAEMON + +. /etc/rc.subr + +name=servers +start_cmd=: +stop_cmd=: + +run_rc_command "$1" diff --git a/base/rootfs/etc/rc.d/syslogd b/base/rootfs/etc/rc.d/syslogd new file mode 100644 index 0000000..562cb75 --- /dev/null +++ b/base/rootfs/etc/rc.d/syslogd @@ -0,0 +1,12 @@ +#!/bin/sh +# PROVIDE: syslogd LOGGER +# REQUIRE: machine_identity +# BEFORE: DAEMON + +. /etc/rc.subr + +name=syslogd +command=/usr/sbin/syslogd +command_args="-F -ss -f /etc/syslog.conf &" + +run_rc_command "$1" diff --git a/base/rootfs/etc/securetty b/base/rootfs/etc/securetty new file mode 100644 index 0000000..360e925 --- /dev/null +++ b/base/rootfs/etc/securetty @@ -0,0 +1 @@ +ttyS0 diff --git a/base/rootfs/etc/services b/base/rootfs/etc/services new file mode 100644 index 0000000..a0a1344 --- /dev/null +++ b/base/rootfs/etc/services @@ -0,0 +1 @@ +ntp 123/udp diff --git a/base/rootfs/etc/shadow b/base/rootfs/etc/shadow new file mode 100644 index 0000000..29c8380 --- /dev/null +++ b/base/rootfs/etc/shadow @@ -0,0 +1,4 @@ +root:$6$mouse$LofYSsSObtmx/zmS97A.TFONKSQSATXE4bD6dY1H7JSBSayuyAuc4Qwpm7JdfW9lUnri9DrQfUKGiLTNKA3v81:1:0:99999:7::: +toor:$6$mouse$LofYSsSObtmx/zmS97A.TFONKSQSATXE4bD6dY1H7JSBSayuyAuc4Qwpm7JdfW9lUnri9DrQfUKGiLTNKA3v81:1:0:99999:7::: +dhcpcd:!:1:0:99999:7::: +_ntp:!:1:0:99999:7::: diff --git a/base/rootfs/etc/shells b/base/rootfs/etc/shells new file mode 100644 index 0000000..3fc22ac --- /dev/null +++ b/base/rootfs/etc/shells @@ -0,0 +1,2 @@ +/bin/sh +/bin/tcsh diff --git a/base/rootfs/etc/syslog.conf b/base/rootfs/etc/syslog.conf new file mode 100644 index 0000000..1057427 --- /dev/null +++ b/base/rootfs/etc/syslog.conf @@ -0,0 +1,10 @@ +# Keep the base logger local. Remote collection is an explicit package policy. +secure_mode 2 +rotate_size 1M +rotate_count 5 + +auth,authpriv.* /var/log/auth.log +cron.* -/var/log/cron.log +kern.* -/var/log/kern.log +*.*;auth,authpriv.none -/var/log/messages +*.=emerg * diff --git a/base/rootfs/root/.profile b/base/rootfs/root/.profile new file mode 100644 index 0000000..148d9f8 --- /dev/null +++ b/base/rootfs/root/.profile @@ -0,0 +1,8 @@ +# Login defaults for the POSIX root account. +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin +export PATH + +PS1='mouse:~# ' +export PS1 + +umask 022 diff --git a/base/rootfs/usr/lib/mouse/required-services b/base/rootfs/usr/lib/mouse/required-services new file mode 100644 index 0000000..4985ce6 --- /dev/null +++ b/base/rootfs/usr/lib/mouse/required-services @@ -0,0 +1,8 @@ +machine_identity +loopback +mdevd +syslogd +daemon +login +servers +console_login diff --git a/base/rootfs/usr/libexec/mouse-dhcpcd-hook b/base/rootfs/usr/libexec/mouse-dhcpcd-hook new file mode 100755 index 0000000..9a8adc9 --- /dev/null +++ b/base/rootfs/usr/libexec/mouse-dhcpcd-hook @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu + +case "${reason:-}" in + BOUND|REBIND|REBOOT|RENEW) + temporary=/run/resolv.conf.new + : >"$temporary" + for server in ${new_domain_name_servers:-}; do + printf 'nameserver %s\n' "$server" >>"$temporary" + done + if [ ! -s "$temporary" ]; then + rm -f "$temporary" + exit 1 + fi + chmod 0644 "$temporary" + mv "$temporary" /etc/resolv.conf + printf '%s\n' "${new_ip_address:-unknown}" > /run/mouse-network.ready + ;; + EXPIRE|FAIL|NAK|RELEASE|STOP|STOPPED) + rm -f /run/mouse-network.ready + ;; +esac diff --git a/base/support/cross-compiler-driver b/base/support/cross-compiler-driver new file mode 100755 index 0000000..99269d7 --- /dev/null +++ b/base/support/cross-compiler-driver @@ -0,0 +1,137 @@ +#!/bin/sh +set -eu + +toolchain_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +config="$toolchain_dir/compiler.conf" + +if [ ! -r "$config" ]; then + printf '%s\n' "missing cross-compiler configuration: $config" >&2 + exit 1 +fi + +{ + IFS= read -r c_compiler + IFS= read -r cxx_compiler + IFS= read -r sysroot + IFS= read -r linker + IFS= read -r builtins + IFS= read -r rust_eh_personality +} <"$config" + +cxx_runtime=no +case "${0##*/}" in + *-cc) + compiler=$c_compiler + ;; + *-bootstrap-c++) + compiler=$cxx_compiler + ;; + *-c++) + compiler=$cxx_compiler + cxx_runtime=yes + set -- \ + -nostdinc++ \ + -isystem "$sysroot/usr/include/c++/v1" \ + "$@" + ;; + *) + printf '%s\n' "cross compiler invoked with an unknown name: ${0##*/}" >&2 + exit 1 + ;; +esac + +link=executable +caller_startup=no +for argument do + case "$argument" in + -c|-E|-M|-MM|-S|-fsyntax-only) + link=compile + ;; + -shared) + link=shared + ;; + -r) + link=relocatable + ;; + -nostartfiles) + caller_startup=yes + ;; + esac +done + +if [ "$link" = compile ]; then + exec "$compiler" \ + --target=x86_64-unknown-linux-musl \ + --sysroot="$sysroot" \ + "$@" +fi + +# Shared and relocatable links still need the target linker, but Clang should +# select their startup objects and libraries because they are not base-system +# executables and must not inherit the static base link recipe below. +if [ "$link" = shared ] || [ "$link" = relocatable ]; then + exec "$compiler" \ + --target=x86_64-unknown-linux-musl \ + --sysroot="$sysroot" \ + --ld-path="$linker" \ + "$@" +fi + +# Rust's self-contained musl target passes its own CRT objects together with +# -nostartfiles, so the caller supplies everything needed for startup. +if [ "$caller_startup" = yes ]; then + exec "$compiler" \ + --target=x86_64-unknown-linux-musl \ + --sysroot="$sysroot" \ + --ld-path="$linker" \ + -nostdlib \ + -static \ + -L"$sysroot/usr/lib" \ + "$@" \ + -Wl,--start-group \ + ${MOUSE_CROSS_EXTRA_STATIC_LIBS:-} \ + "$builtins" \ + "$sysroot/usr/lib/libc.a" \ + -Wl,--end-group +fi + +if [ "$cxx_runtime" = yes ]; then + exec "$compiler" \ + --target=x86_64-unknown-linux-musl \ + --sysroot="$sysroot" \ + --ld-path="$linker" \ + -nostdlib \ + -static \ + "$sysroot/usr/lib/crt1.o" \ + "$sysroot/usr/lib/crti.o" \ + -L"$sysroot/usr/lib" \ + "$@" \ + -Wl,--start-group \ + ${MOUSE_CROSS_EXTRA_STATIC_LIBS:-} \ + "$sysroot/usr/lib/libc++.a" \ + "$sysroot/usr/lib/libc++abi.a" \ + "$sysroot/usr/lib/libunwind.a" \ + "$builtins" \ + "$rust_eh_personality" \ + "$sysroot/usr/lib/libc.a" \ + -Wl,--end-group \ + "$sysroot/usr/lib/crtn.o" +fi + +exec "$compiler" \ + --target=x86_64-unknown-linux-musl \ + --sysroot="$sysroot" \ + --ld-path="$linker" \ + -nostdlib \ + -static \ + "$sysroot/usr/lib/crt1.o" \ + "$sysroot/usr/lib/crti.o" \ + -L"$sysroot/usr/lib" \ + "$@" \ + -Wl,--start-group \ + ${MOUSE_CROSS_EXTRA_STATIC_LIBS:-} \ + "$builtins" \ + "$rust_eh_personality" \ + "$sysroot/usr/lib/libc.a" \ + -Wl,--end-group \ + "$sysroot/usr/lib/crtn.o" diff --git a/base/support/mouse-bootenv b/base/support/mouse-bootenv new file mode 100755 index 0000000..8cc3e32 --- /dev/null +++ b/base/support/mouse-bootenv @@ -0,0 +1,88 @@ +#!/bin/sh +set -eu + +root=/run/mouse-root +active_file=/etc/mouse-boot-environment +btrfs=/sbin/btrfs + +fail() { + printf '%s\n' "mouse-bootenv: $*" >&2 + exit 1 +} + +read_environment() { + [ -f "$active_file" ] || fail "missing $active_file" + environment=$(cat "$active_file") + case "$environment" in + default|alternate) ;; + *) fail "invalid boot environment: $environment" ;; + esac +} + +other_environment() { + case "$1" in + default) printf '%s\n' alternate ;; + alternate) printf '%s\n' default ;; + *) fail "invalid boot environment: $1" ;; + esac +} + +require_storage() { + [ -d "$root/ROOT/default" ] || fail "MOUSE btrfs root is not mounted" + [ -d "$root/ROOT/alternate" ] || fail "alternate boot environment is missing" +} + +set_default() { + target_path=$1 + target_id=$("$btrfs" inspect-internal rootid "$target_path") + case "$target_id" in + ''|*[!0-9]*) fail "could not resolve the subvolume ID for $target_path" ;; + esac + "$btrfs" subvolume set-default "$target_id" "$root" +} + +activate() { + target=$1 + case "$target" in + default|alternate) ;; + *) fail "invalid boot environment: $target" ;; + esac + [ "$(cat "$root/ROOT/$target/etc/mouse-boot-environment")" = "$target" ] || + fail "$target has an invalid environment marker" + set_default "$root/ROOT/$target" + sync + printf '%s\n' "$target will boot next" +} + +read_environment +require_storage + +case "${1:-status}" in + status) + printf '%s\n' "running: $environment" + "$btrfs" subvolume get-default "$root" + ;; + prepare) + inactive=$(other_environment "$environment") + set_default "$root/ROOT/$environment" + "$btrfs" subvolume delete --commit-after "$root/ROOT/$inactive" + "$btrfs" subvolume snapshot \ + "$root/ROOT/$environment" \ + "$root/ROOT/$inactive" + printf '%s\n' "$inactive" \ + >"$root/ROOT/$inactive/etc/mouse-boot-environment" + sync + printf '%s\n' "$inactive is ready for an update" + ;; + activate) + [ "$#" -eq 2 ] || fail "usage: mouse-bootenv activate default|alternate" + activate "$2" + ;; + rollback) + [ "$#" -eq 1 ] || fail "usage: mouse-bootenv rollback" + activate "$(other_environment "$environment")" + ;; + *) + fail "usage: mouse-bootenv [status|prepare|activate NAME|rollback]" + ;; +esac diff --git a/base/support/rust-eh-personality.c b/base/support/rust-eh-personality.c new file mode 100644 index 0000000..96315c4 --- /dev/null +++ b/base/support/rust-eh-personality.c @@ -0,0 +1,9 @@ +/* + * Rust's compiler-builtins archive carries an unused unwind reference on + * x86_64. Base C programs do not unwind through Rust, but lld still requires + * the personality symbol when it extracts the relevant builtins object. + */ +void +rust_eh_personality(void) +{ +} diff --git a/release/facilities.lock b/release/facilities.lock new file mode 100644 index 0000000..696cfa8 --- /dev/null +++ b/release/facilities.lock @@ -0,0 +1,11 @@ +# SHA-256 URL filename +20a0b0a2bb2525fa53c7eee9beb854b4c9cf172eabb209af7020743547bfe9fb https://www.greenwoodsoftware.com/less/less-704.tar.gz less-704.tar.gz +8bf0d570f01e70a6e124884088870cbed7537f36328d512909eb10cd53179d9c https://mandoc.bsd.lv/snapshots/mandoc-1.14.6.tar.gz mandoc-1.14.6.tar.gz +45672fec165cb4cc1358a2d76b5d57d22876dcb97ab169427ac385cbe1d5597a https://astron.com/pub/file/file-5.47.tar.gz file-5.47.tar.gz +01a7b881bd220bfdf615f97b8718f80bdfd3f6add385b993dcf6efd14e8c0ac6 https://ftp.gnu.org/gnu/gzip/gzip-1.14.tar.xz gzip-1.14.tar.xz +ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269 https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz bzip2-1.0.8.tar.gz +33bf69c0d6c698e83a68f77e6c1f465778e418ca0b3d59860d3ab446f4ac99a6 https://github.com/tukaani-project/xz/releases/download/v5.8.3/xz-5.8.3.tar.bz2 xz-5.8.3.tar.bz2 +fd9fa1b95809417157ca83dd72957e3261bdbce896353cb936f80af0b33a4b5c https://www.kernel.org/pub/linux/utils/net/iproute2/iproute2-7.1.0.tar.xz iproute2-7.1.0.tar.xz +274b9b919ef3152bfb3da3a13c950dd60d6e2bcd54230ffeca298d03b40d0525 https://www.netfilter.org/projects/libmnl/files/libmnl-1.0.5.tar.bz2 libmnl-1.0.5.tar.bz2 +607da28dba66fbdeccf8ef1395dded9077e8d19f2995f9a4d45a9c2f0bcffba8 https://www.netfilter.org/projects/libnftnl/files/libnftnl-1.3.1.tar.xz libnftnl-1.3.1.tar.xz +372931bda8556b310636a2f9020adc710f9bab66f47efe0ce90bff800ac2530c https://www.netfilter.org/projects/nftables/files/nftables-1.1.6.tar.xz nftables-1.1.6.tar.xz diff --git a/release/firmware.lock b/release/firmware.lock new file mode 100644 index 0000000..55796eb --- /dev/null +++ b/release/firmware.lock @@ -0,0 +1,2 @@ +# SHA-256 URL filename +abbfe6ee4a2b2d19a3a7c140c253015deed098d9731a0326db6465740d747846 https://dl-cdn.alpinelinux.org/alpine/edge/community/x86_64/ovmf-0.0.202605-r0.apk ovmf-0.0.202605-r0.apk diff --git a/release/kernel.lock b/release/kernel.lock new file mode 100644 index 0000000..9ef2bbc --- /dev/null +++ b/release/kernel.lock @@ -0,0 +1,2 @@ +# SHA-256 URL filename +f78602932219125e211c5f5bfd84edcfd4ec5ce88fc944f8248413f665bef236 https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.18.35.tar.xz linux-6.18.35.tar.xz diff --git a/release/pkgsrc.lock b/release/pkgsrc.lock new file mode 100644 index 0000000..a226bfb --- /dev/null +++ b/release/pkgsrc.lock @@ -0,0 +1,2 @@ +# sha256 url filename +d8c008b6c786aeb9c7d5260d3fc8d14b15fed32f339be620fe7226f08061fc72 https://cdn.netbsd.org/pub/pkgsrc/pkgsrc-2026Q2/pkgsrc-2026Q2.tar.gz pkgsrc-2026Q2.tar.gz diff --git a/release/software.lock b/release/software.lock new file mode 100644 index 0000000..d09f385 --- /dev/null +++ b/release/software.lock @@ -0,0 +1,32 @@ +# SHA-256 URL filename +d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a https://musl.libc.org/releases/musl-1.2.6.tar.gz musl-1.2.6.tar.gz +bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 https://zlib.net/zlib-1.3.2.tar.gz zlib-1.3.2.tar.gz +d3a8ba457ae25c27c84fd2830a2efdcc5b1d40bf585d4eb0d35f47e99e5d4774 https://github.com/libarchive/libarchive/releases/download/v3.8.7/libarchive-3.8.7.tar.xz libarchive-3.8.7.tar.xz +9d3ebd651e5f70b87b1327b01cbd7e0c01a0f036b4c1371f653b7704b11daf23 https://github.com/sabotage-linux/netbsd-curses/archive/refs/tags/v0.3.2.tar.gz netbsd-curses-0.3.2.tar.gz +39bf67452fa41d0948c2197601053f48b3d78a029389734332a6309a680c6c87 https://download-mirror.savannah.gnu.org/releases/attr/attr-2.5.2.tar.gz attr-2.5.2.tar.gz +5f2bdbad629707aa7d85c623f994aa8a1d2dec55a73de5205bac0bf6058a2f7c https://download-mirror.savannah.gnu.org/releases/acl/acl-2.3.2.tar.gz acl-2.3.2.tar.gz +d12249ffad3ef04b160e6419adf1bbe7e593a60bb23f0a0a077fa780b214934a https://github.com/Juniper/libxo/releases/download/1.7.5/libxo-1.7.5.tar.gz libxo-1.7.5.tar.gz +aa8b6eeca8b93db659a7ebf421a7089b3f7f8457deba34170cd146b92fe60546 https://github.com/tcsh-org/tcsh/archive/refs/tags/TCSH6_24_16.tar.gz tcsh-6.24.16.tar.gz +183ad94242316cad014b54668e0b117ba4d8f6978b1229cde3fc5cd913020751 https://github.com/chimera-linux/chimerautils/archive/refs/tags/v15.0.3.tar.gz chimerautils-15.0.3.tar.gz +a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8 https://github.com/openssl/openssl/releases/download/openssl-3.5.7/openssl-3.5.7.tar.gz openssl-3.5.7.tar.gz +3ff344e30b9b1ed2971044eabb438a08f2e2245ddb5f8ab1a3ad8b63ab4eaf91 https://curl.se/ca/cacert-2026-07-16.pem cacert-2026-07-16.pem +b035c1aeaf82d6988e0feb9be8ac80e1825227ff01d0c7a92e4356f423db0b62 https://raw.githubusercontent.com/chimera-linux/cports/1e8911eb1bb9dc94a6734686b8327206d5ac89e0/main/musl-bsd-headers/files/cdefs.h cdefs.h +3659cd137c320991a78413dd370a92fd18e0a8bc36d017d554f08677a37d7d5a https://raw.githubusercontent.com/chimera-linux/cports/1e8911eb1bb9dc94a6734686b8327206d5ac89e0/main/musl-bsd-headers/files/queue.h queue.h +e1e498a79bf160a5766fa560f2b07b206fe89fe21a62600c77d72e00a6992f92 https://raw.githubusercontent.com/chimera-linux/cports/1e8911eb1bb9dc94a6734686b8327206d5ac89e0/main/musl-bsd-headers/files/tree.h tree.h +a3c6ac4e3a3f7407760f87810e6937a213934bd8d2d7b9ea477489206d80742c https://raw.githubusercontent.com/chimera-linux/cports/1e8911eb1bb9dc94a6734686b8327206d5ac89e0/main/musl-bsd-headers/files/error.h error.h +febaa8f7c1916521c53eb5fd11c0641b5eb4741c2c6e9b42c288ed62d9e4fd2c https://github.com/mesonbuild/meson/archive/refs/tags/1.9.1.tar.gz meson-1.9.1.tar.gz +f0055ad0369bf2e372955ba55128d000cfcc21777057806015b45e4accbebf23 https://github.com/ninja-build/ninja/archive/refs/tags/v1.13.1.tar.gz ninja-1.13.1.tar.gz +79721badcad1987dead9c3609eb4877ab9b58821c06bdacb824f2c8897c11f2a https://github.com/pkgconf/pkgconf/archive/refs/tags/pkgconf-2.5.1.tar.gz pkgconf-2.5.1.tar.gz +03a05d3adf9602ef128f2da05b84b3205ce60c351e5737c0370f74000679ce8a https://www.kernel.org/pub/linux/utils/util-linux/v2.42/util-linux-2.42.2.tar.xz util-linux-2.42.2.tar.xz +71513a31c01a428bccd5367a32fd95f115d6dac50fb5b60c779d5c7942aec071 https://github.com/besser82/libxcrypt/releases/download/v4.5.2/libxcrypt-4.5.2.tar.xz libxcrypt-4.5.2.tar.xz +ce57a313e315a0a7cb04a8f50cc20753e994e487bbe9b78a2a824ca75cb486c0 https://github.com/shadow-maint/shadow/releases/download/4.19.4/shadow-4.19.4.tar.xz shadow-4.19.4.tar.xz +48a89ecf49927ed60b42b73edc07a63f6198d2c47a848f723a495c5cd66e220f https://github.com/NetworkConfiguration/dhcpcd/archive/8b312918d8f1885d7fe8fcc03a7e06ae8a0314b4.tar.gz dhcpcd-8b312918d8f1885d7fe8fcc03a7e06ae8a0314b4.tar.gz +dd16fb1d67bfab79a72f5e8390735c49e3e8e70b4945a15ab1f81ddb78658fb3 https://ftp.gnu.org/gnu/make/make-4.4.1.tar.gz make-4.4.1.tar.gz +f1da374a15ba7605cf378347f96bc8b678d3d7c0765269c8242cfe5b0789c571 https://github.com/cronie-crond/cronie/releases/download/cronie-1.7.2/cronie-1.7.2.tar.gz cronie-1.7.2.tar.gz +091eeb3f4e358e28c3ab2ea58f93d7a0b5758a20d7c8a0418e162e9b2c27addc https://cdn.openbsd.org/pub/OpenBSD/OpenNTPD/openntpd-7.9p1.tar.gz openntpd-7.9p1.tar.gz +c2e6d193cc78f84cd6ddb72aaf6d5c6a9162f0470e5992092057f5ff518562fa https://sourceforge.net/projects/procps-ng/files/Production/procps-ng-4.0.5.tar.xz/download procps-ng-4.0.5.tar.xz +19e680c9eef8c079da4da37040b5f5453763205b4edfb1e2c114de77908927e4 https://github.com/iputils/iputils/archive/refs/tags/20250605.tar.gz iputils-20250605.tar.gz +5a5d5073070cc7e0c7a7a3c6ec2a0e1780850c8b47b3e3892226b93ffcb9cb54 https://mirrors.edge.kernel.org/pub/linux/utils/kernel/kmod/kmod-34.2.tar.xz kmod-34.2.tar.xz +f9c905e74935c6fe911c7e344e3e89d5fbd2014c1a04650b524b15ce9b5635d1 https://skarnet.org/software/skalibs/skalibs-2.15.1.0.tar.gz skalibs-2.15.1.0.tar.gz +ce1ae0149b6a57a34f608218fd6181aa6aa68135cac2f4d931b5b417b072e244 https://skarnet.org/software/mdevd/mdevd-0.1.8.2.tar.gz mdevd-0.1.8.2.tar.gz +bc410ca64551a11fac6518b418fb6b8afbd888a70af2c5eb353334a706727bca https://github.com/troglobit/sysklogd/releases/download/v2.7.2/sysklogd-2.7.2.tar.gz sysklogd-2.7.2.tar.gz diff --git a/release/storage.lock b/release/storage.lock new file mode 100644 index 0000000..b863cae --- /dev/null +++ b/release/storage.lock @@ -0,0 +1,4 @@ +# SHA-256 URL filename +c286d6876cbcd72327a0b417e4cfd280353ec23e37b549fdbcd7800a832d9a99 https://www.kernel.org/pub/linux/kernel/people/kdave/btrfs-progs/btrfs-progs-v7.0.tar.xz btrfs-progs-v7.0.tar.xz +37d7284556b20954e56e1ca85b80226768902e2edabd3b649e9e72c0c9012ee3 https://github.com/facebook/zstd/archive/refs/tags/v1.5.7.tar.gz zstd-1.5.7.tar.gz +26e53e04d562e91403018997afdb5d8c4448a580a3db433d0bccc9b0f02ac134 https://github.com/Limine-Bootloader/Limine/releases/download/v12.3.2/limine-binary.tar.xz limine-binary-12.3.2.tar.xz diff --git a/release/toolchain.lock b/release/toolchain.lock new file mode 100644 index 0000000..19ddd06 --- /dev/null +++ b/release/toolchain.lock @@ -0,0 +1,2 @@ +# SHA-256 URL filename +2488c33a959eafba1c44f253e5bbe7ac958eb53fa626298a3a5f4b87373767cd https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.3/llvm-project-22.1.3.src.tar.xz llvm-project-22.1.3.src.tar.xz diff --git a/scripts/audit-static-base.sh b/scripts/audit-static-base.sh new file mode 100755 index 0000000..9063321 --- /dev/null +++ b/scripts/audit-static-base.sh @@ -0,0 +1,154 @@ +#!/bin/sh +set -eu + +if [ "$#" -ne 1 ]; then + printf '%s\n' "usage: audit-static-base.sh ROOTFS" >&2 + exit 2 +fi + +rootfs=$1 +failed=0 +count=0 + +for directory in bin sbin usr/bin usr/sbin; do + if [ ! -d "$rootfs/$directory" ] || [ -L "$rootfs/$directory" ]; then + printf '%s\n' "FHS command directory must be a real directory: /$directory" >&2 + exit 1 + fi +done + +for executable in \ + /bin/chgrp /bin/login /bin/sh /bin/su /bin/tcsh \ + /sbin/agetty /sbin/blkid /sbin/bridge /sbin/btrfs /sbin/dhcpcd /sbin/dmesg \ + /sbin/depmod /sbin/insmod /sbin/kmod /sbin/lsmod /sbin/mdevd \ + /sbin/mdevd-coldplug /sbin/mkfs.btrfs /sbin/modinfo /sbin/modprobe \ + /sbin/mount /sbin/fdisk /sbin/ip /sbin/losetup /sbin/nft /sbin/rmmod \ + /sbin/sfdisk /sbin/ss /sbin/swapoff /sbin/swapon /sbin/sysctl /sbin/tc \ + /sbin/umount /sbin/wipefs \ + /usr/bin/setsid /usr/sbin/mouse-bootenv \ + /usr/bin/awk /usr/bin/basename /usr/bin/cmp /usr/bin/cut \ + /usr/bin/diff /usr/bin/dirname /usr/bin/fetch /usr/bin/groups /usr/bin/ldd \ + /usr/bin/install /usr/bin/m4 /usr/bin/nc /usr/bin/patch \ + /usr/bin/sort /usr/bin/test /usr/bin/tr /usr/bin/whoami \ + /usr/bin/xargs \ + /usr/bin/apropos /usr/bin/bzip2 /usr/bin/crontab \ + /usr/bin/file /usr/bin/findmnt /usr/bin/flock /usr/bin/free \ + /usr/bin/gzip /usr/bin/less /usr/bin/lsblk /usr/bin/man /usr/bin/mandoc \ + /usr/bin/nvi /usr/bin/openssl /usr/bin/passwd /usr/bin/pgrep \ + /usr/bin/pkill /usr/bin/readlink /usr/bin/sha256 /usr/bin/tar \ + /usr/bin/top /usr/bin/tput /usr/bin/unzip \ + /usr/bin/uptime /usr/bin/vmstat /usr/bin/watch /usr/bin/xz /usr/bin/zstd \ + /bin/ping /bin/ps /usr/bin/vi /usr/sbin/makewhatis \ + /usr/sbin/cron /usr/sbin/groupadd /usr/sbin/ntpd /usr/sbin/syslogd \ + /usr/sbin/useradd +do + if [ ! -x "$rootfs$executable" ]; then + printf '%s\n' "missing executable at its FHS path: $executable" >&2 + exit 1 + fi +done + +if [ ! -f "$rootfs/etc/ssl/cert.pem" ]; then + printf '%s\n' "missing TLS trust bundle: /etc/ssl/cert.pem" >&2 + exit 1 +fi + +if [ ! -f "$rootfs/usr/share/man/mandoc.db" ]; then + printf '%s\n' "missing World manual-page database" >&2 + exit 1 +fi + +if [ -e "$rootfs/etc/mouse-release" ]; then + for executable in /sbin/cheesed /sbin/poweroff /sbin/reboot /sbin/service /sbin/shutdown; do + if [ ! -x "$rootfs$executable" ]; then + printf '%s\n' "missing executable at its FHS path: $executable" >&2 + exit 1 + fi + done +fi + +if [ -e "$rootfs/usr/share/mouse/pkgsrc.version" ]; then + if [ "$(cat "$rootfs/usr/share/mouse/pkgsrc.version")" != 2026Q2 ]; then + printf '%s\n' "unexpected pkgsrc version" >&2 + exit 1 + fi +fi + +if [ -e "$rootfs/usr/bin/cc" ]; then + for tool in \ + /usr/bin/ar /usr/bin/c++ /usr/bin/clang /usr/bin/clang++ \ + /usr/bin/gmake /usr/bin/ld.lld /usr/bin/make /usr/bin/objdump \ + /usr/bin/pkgconf /usr/bin/readelf + do + if [ ! -x "$rootfs$tool" ]; then + printf '%s\n' "incomplete native toolchain: $tool" >&2 + exit 1 + fi + done + for development_file in \ + /usr/include/stdio.h /usr/include/c++/v1/filesystem \ + /usr/include/openssl/ssl.h /usr/include/zlib.h \ + /lib/ld-musl-x86_64.so.1 /usr/lib/libc.so \ + /usr/lib/crt1.o /usr/lib/Scrt1.o /usr/lib/crtbegin.o \ + /usr/lib/crtbeginS.o /usr/lib/crtendS.o \ + /usr/lib/libc.a /usr/lib/libc++.a /usr/lib/libc++.so \ + /usr/lib/libc++abi.so /usr/lib/libunwind.so \ + /usr/lib/libc++abi.a /usr/lib/libcrypto.a /usr/lib/libssl.a \ + /usr/lib/libunwind.a /usr/lib/libz.a + do + if [ ! -f "$rootfs$development_file" ]; then + printf '%s\n' "incomplete development sysroot: $development_file" >&2 + exit 1 + fi + done +fi + +if [ -e "$rootfs/usr/share/mk" ] || [ -e "$rootfs/usr/bin/bmake" ]; then + printf '%s\n' "base image must leave BSD make to pkgsrc under /usr/local" >&2 + exit 1 +fi +if [ -e "$rootfs/usr/bin/make" ]; then + if [ ! -L "$rootfs/usr/bin/gmake" ] || \ + [ "$(readlink "$rootfs/usr/bin/gmake")" != make ]; then + printf '%s\n' "/usr/bin/gmake must alias the base GNU make" >&2 + exit 1 + fi +fi + +if [ -e "$rootfs/usr/bin/rc-update" ] || + [ -e "$rootfs/sbin/rc-update" ]; then + printf '%s\n' "forbidden persistent-policy tool in base image: rc-update" >&2 + exit 1 +fi + +while IFS= read -r executable; do + description=$(file "$executable") + case "$description" in + *ELF*) + if objdump -p "$executable" | + grep -Eq '^[[:space:]]*SONAME[[:space:]]'; then + continue + fi + count=$((count + 1)) + if objdump -p "$executable" | + grep -Eq '^[[:space:]]*(INTERP|NEEDED)[[:space:]]'; then + printf '%s\n' "dynamic base executable: $executable" >&2 + failed=1 + fi + ;; + esac +done <&2 + exit 1 +fi + +if [ "$failed" -ne 0 ]; then + exit 1 +fi + +printf '%s\n' "static ELF audit passed: $count executables" diff --git a/scripts/build-boot-disk.sh b/scripts/build-boot-disk.sh new file mode 100644 index 0000000..5519d2e --- /dev/null +++ b/scripts/build-boot-disk.sh @@ -0,0 +1,76 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} +source_dir=${MOUSE_SOURCE_DIR:-"$build_dir/sources"} +rootfs_image=${MOUSE_ROOTFS_IMAGE:-"$build_dir/mouse-rootfs.btrfs"} +kernel_image=${MOUSE_KERNEL_IMAGE:-"$build_dir/vmlinuz-virt"} +disk_image=${MOUSE_DISK_IMAGE_OUTPUT:-"$build_dir/mouse-disk.img"} +limine_archive=${MOUSE_LIMINE_ARCHIVE:-"$source_dir/limine-binary-12.3.2.tar.xz"} +limine_config=${MOUSE_LIMINE_CONFIG:-"$repo_dir/base/limine.conf"} + +for command in cc make mcopy mformat mmd sfdisk; do + if ! command -v "$command" >/dev/null 2>&1; then + printf '%s\n' "$command is required to build the boot disk" >&2 + exit 1 + fi +done +for input in "$rootfs_image" "$kernel_image" \ + "$limine_archive" "$limine_config"; do + if [ ! -f "$input" ]; then + printf '%s\n' "missing boot-disk input: $input" >&2 + exit 1 + fi +done + +rootfs_bytes=$(stat -c %s "$rootfs_image") + +sector_size=512 +boot_start=2048 +boot_sectors=262144 +root_start=$((boot_start + boot_sectors)) +root_sectors=$(((rootfs_bytes + sector_size - 1) / sector_size)) +disk_sectors=$((root_start + root_sectors + 2048)) + +working=$(mktemp -d "${TMPDIR:-/tmp}/mouse-boot-disk.XXXXXX") +cleanup() { + rm -rf "$working" +} +trap cleanup EXIT HUP INT TERM + +tar -xf "$limine_archive" -C "$working" +limine_source="$working/limine-binary" +make -s -C "$limine_source" + +boot_image="$working/mouse-boot.fat" +truncate -s $((boot_sectors * sector_size)) "$boot_image" +mformat -i "$boot_image" -F -v MOUSE_BOOT :: +mmd -i "$boot_image" ::/EFI ::/EFI/BOOT +mcopy -i "$boot_image" \ + "$limine_source/BOOTX64.EFI" ::/EFI/BOOT/BOOTX64.EFI +mcopy -i "$boot_image" \ + "$limine_source/limine-bios.sys" ::/limine-bios.sys +mcopy -i "$boot_image" "$limine_config" ::/limine.conf +mcopy -i "$boot_image" "$kernel_image" ::/vmlinuz-mouse + +rm -f "$disk_image" +truncate -s $((disk_sectors * sector_size)) "$disk_image" +sfdisk --quiet "$disk_image" <&2 + exit 1 + fi +done + +install -m 0755 \ + "$repo_dir/base/support/mouse-bootenv" \ + "$base_root/usr/sbin/mouse-bootenv" +ln -sfn ../usr/lib/libc.so "$base_root/lib/ld-musl-x86_64.so.1" diff --git a/scripts/build-image.sh b/scripts/build-image.sh new file mode 100755 index 0000000..67f2e7c --- /dev/null +++ b/scripts/build-image.sh @@ -0,0 +1,211 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +cheesed_dir="$repo_dir/base/cheesed" +build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} +target=${CHEESED_TARGET:-x86_64-unknown-linux-musl} +test_fixtures=${MOUSE_INCLUDE_TEST_FIXTURES:-0} +kernel_modules=${MOUSE_KERNEL_MODULES:-"$build_dir/kernel/modules"} +staging= +image_staging= + +cleanup() { + for directory in "$staging" "$image_staging"; do + if [ -n "$directory" ] && [ -d "$directory" ]; then + rm -rf "$directory" + fi + done +} +trap cleanup EXIT HUP INT TERM + +case "$test_fixtures" in + 0|1) ;; + *) + printf '%s\n' "MOUSE_INCLUDE_TEST_FIXTURES must be 0 or 1" >&2 + exit 1 + ;; +esac +base_root="$build_dir/static/rootfs" +pkgsrc_seed=${MOUSE_PKGSRC_SEED:-"$build_dir/pkgsrc-seed"} +for input in "$base_root" "$build_dir/vmlinuz-virt"; do + if [ ! -e "$input" ]; then + printf '%s\n' "missing prepared image input: $input" >&2 + exit 1 + fi +done +for seed_directory in "$pkgsrc_seed/usr/local" "$pkgsrc_seed/var/db/pkg"; do + if [ ! -d "$seed_directory" ]; then + printf '%s\n' "missing pkgsrc seed directory: $seed_directory" >&2 + exit 1 + fi +done +btrfs_image_tool=${MOUSE_BTRFS_IMAGE_TOOL:-"$base_root/sbin/mkfs.btrfs"} +if [ ! -x "$btrfs_image_tool" ]; then + printf '%s\n' "missing btrfs image tool: $btrfs_image_tool" >&2 + exit 1 +fi +if [ -n "${MOUSE_CHEESED_BINARY:-}" ]; then + cheesed_binary=$MOUSE_CHEESED_BINARY +else + cheesed_binary=$(CHEESED_TARGET="$target" "$cheesed_dir/scripts/build-static.sh") +fi +staging=$(mktemp -d "${TMPDIR:-/tmp}/mouse-rootfs.XXXXXX") +image_staging=$(mktemp -d "${TMPDIR:-/tmp}/mouse-btrfs.XXXXXX") + +install -d -m 0755 \ + "$staging/dev" \ + "$staging/bin" \ + "$staging/etc" \ + "$staging/etc/default" \ + "$staging/etc/rc.d" \ + "$staging/etc/skel" \ + "$staging/home" \ + "$staging/media" \ + "$staging/mnt" \ + "$staging/proc" \ + "$staging/root" \ + "$staging/run" \ + "$staging/sbin" \ + "$staging/srv" \ + "$staging/sys" \ + "$staging/usr/bin" \ + "$staging/usr/local/etc/rc.d" \ + "$staging/usr/lib/mouse" \ + "$staging/usr/sbin" \ + "$staging/var/cache/pkgsrc/distfiles" \ + "$staging/var/cache/pkgsrc/packages" \ + "$staging/var/db" \ + "$staging/var/lib/dhcpcd" \ + "$staging/var/empty" \ + "$staging/var/log" \ + "$staging/var/mail" \ + "$staging/var/spool/cron" +chmod 0755 "$staging" +chmod 0700 "$staging/root" +chmod 0755 "$staging/var/empty" +chmod 0700 "$staging/var/spool/cron" +install -d -m 1777 \ + "$staging/tmp" \ + "$staging/var/tmp" \ + "$staging/var/tmp/vi.recover" + +ln -s /run "$staging/var/run" + +cp -R "$base_root/." "$staging/" +cp -R "$pkgsrc_seed/usr/local/." "$staging/usr/local/" +cp -R "$pkgsrc_seed/var/db/pkg" "$staging/var/db/" +if [ ! -d "$kernel_modules/lib/modules" ]; then + printf '%s\n' "missing kernel module tree: $kernel_modules/lib/modules" >&2 + exit 1 +fi +cp -R "$kernel_modules/lib/." "$staging/lib/" +if [ "$test_fixtures" -eq 1 ] && [ -d "$kernel_modules/usr/local" ]; then + cp -R "$kernel_modules/usr/local/." "$staging/usr/local/" +fi +install -m 0755 "$cheesed_binary" "$staging/sbin/cheesed" +for command in service shutdown poweroff reboot; do + ln -s cheesed "$staging/sbin/$command" +done + +cp -R "$repo_dir/base/rootfs/." "$staging/" +if [ "$test_fixtures" -eq 1 ]; then + cp -R "$repo_dir/tests/fixtures/mouse-echo/rootfs/." "$staging/" + install -m 0755 \ + "${MOUSE_MOUSE_ECHO_BINARY:?missing test mouse-echo binary}" \ + "$staging/usr/libexec/mouse-echo" + printf '%s\n' "enable_mouse_echo=YES" >>"$staging/etc/rc.conf" +fi +install -d -m 0755 "$staging/usr/share/mouse" +if [ "$test_fixtures" -eq 1 ]; then + install -d -m 0755 "$staging/usr/share/mouse/pkgsrc-fixture" + cp -R "$repo_dir/tests/fixtures/pkgsrc/local" \ + "$staging/usr/share/mouse/pkgsrc-fixture/" +fi +printf '%s\n' 2026Q2 >"$staging/usr/share/mouse/pkgsrc.version" +rc_subr_source="$staging/usr/local/src/pkgsrc/pkgtools/rc.subr/files/rc.subr" +[ -f "$rc_subr_source" ] || { + printf '%s\n' "pkgsrc seed lacks rc.subr source" >&2 + exit 1 +} +sed 's|@SYSCONFBASE@|/usr/local/etc|g' "$rc_subr_source" \ + >"$staging/etc/rc.subr" +patch "$staging/etc/rc.subr" \ + <"$repo_dir/base/patches/pkgsrc-rc.subr-cgroup.patch" +chmod 0444 "$staging/etc/rc.subr" +ln -s mouse-release "$staging/etc/os-release" +chmod 0755 \ + "$staging/etc/rc.d/console_login" \ + "$staging/etc/rc.d/cron" \ + "$staging/etc/rc.d/daemon" \ + "$staging/etc/rc.d/login" \ + "$staging/etc/rc.d/loopback" \ + "$staging/etc/rc.d/machine_identity" \ + "$staging/etc/rc.d/mdevd" \ + "$staging/etc/rc.d/network" \ + "$staging/etc/rc.d/ntpd" \ + "$staging/etc/rc.d/servers" \ + "$staging/etc/rc.d/syslogd" \ + "$staging/usr/libexec/mouse-dhcpcd-hook" +if [ "$test_fixtures" -eq 1 ]; then + chmod 0755 "$staging/etc/rc.d/mouse_echo" +fi +chmod 0600 "$staging/etc/shadow" + +chmod 0444 "$staging/usr/share/mouse/pkgsrc.version" + +"$script_dir/audit-static-base.sh" "$staging" + +mkdir -p "$build_dir" +btrfs_image="$build_dir/mouse-rootfs.btrfs" +rootfs_size=${MOUSE_ROOTFS_SIZE:-4G} +install -d -m 0755 \ + "$image_staging/ROOT/default" \ + "$image_staging/ROOT/alternate" \ + "$image_staging/usr-local" \ + "$image_staging/var" \ + "$image_staging/home" \ + "$image_staging/srv" + +cp -R "$staging/." "$image_staging/ROOT/default/" +cp -R "$staging/." "$image_staging/ROOT/alternate/" +printf '%s\n' default \ + >"$image_staging/ROOT/default/etc/mouse-boot-environment" +printf '%s\n' alternate \ + >"$image_staging/ROOT/alternate/etc/mouse-boot-environment" + +for persistent in \ + usr/local:usr-local \ + var:var \ + home:home \ + srv:srv +do + root_path=${persistent%%:*} + subvolume=${persistent#*:} + cp -R "$staging/$root_path/." "$image_staging/$subvolume/" + for environment in default alternate; do + rm -rf "$image_staging/ROOT/$environment/$root_path" + install -d -m 0755 \ + "$image_staging/ROOT/$environment/$root_path" + done +done + +rm -f "$btrfs_image" +truncate -s "$rootfs_size" "$btrfs_image" +"$btrfs_image_tool" \ + --force \ + --label MOUSE_ROOT \ + --uuid 4d4f5553-4500-4000-8000-00000000000b \ + --rootdir "$image_staging" \ + --subvol default:ROOT/default \ + --subvol rw:ROOT/alternate \ + --subvol rw:usr-local \ + --subvol rw:var \ + --subvol rw:home \ + --subvol rw:srv \ + "$btrfs_image" + +printf '%s\n' "built $build_dir/vmlinuz-virt" +printf '%s\n' "built $btrfs_image" +sh "$script_dir/build-boot-disk.sh" diff --git a/scripts/build-in-container.sh b/scripts/build-in-container.sh new file mode 100755 index 0000000..e7047db --- /dev/null +++ b/scripts/build-in-container.sh @@ -0,0 +1,191 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +output_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} +test_fixtures=${MOUSE_INCLUDE_TEST_FIXTURES:-0} +builder_cpus=${MOUSE_BUILDER_CPUS:-8} +builder_memory=${MOUSE_BUILDER_MEMORY:-8G} +build_jobs=${MOUSE_BUILD_JOBS:-8} + +die() { + printf '%s\n' "$*" >&2 + exit 1 +} + +require_positive() { + case "$1" in + ''|*[!0-9]*|0) die "$2 must be a positive integer" ;; + esac +} + +memory_mib() { + case "$1" in + *[Kk]) value=${1%?}; divisor=1024 ;; + *[Mm]) value=${1%?}; divisor=1 ;; + *[Gg]) value=${1%?}; divisor=0 ;; + *[Tt]) value=${1%?}; divisor=-1 ;; + *) value=$1; divisor=1048576 ;; + esac + require_positive "$value" MOUSE_BUILDER_MEMORY + case "$divisor" in + 0) printf '%s\n' "$((value * 1024))" ;; + -1) printf '%s\n' "$((value * 1024 * 1024))" ;; + 1) printf '%s\n' "$value" ;; + *) printf '%s\n' "$(((value + divisor - 1) / divisor))" ;; + esac +} + +case "$test_fixtures" in + 0|1) ;; + *) die "MOUSE_INCLUDE_TEST_FIXTURES must be 0 or 1" ;; +esac +case "$test_fixtures" in + 0) rootfs_stage=rootfs-builder ;; + 1) rootfs_stage=rootfs-test-builder ;; +esac +require_positive "$builder_cpus" MOUSE_BUILDER_CPUS +require_positive "$build_jobs" MOUSE_BUILD_JOBS +[ -n "$builder_memory" ] || die "MOUSE_BUILDER_MEMORY must not be empty" + +if [ -n "${MOUSE_BUILDER_PLATFORM:-}" ]; then + builder_platform=$MOUSE_BUILDER_PLATFORM +else + case "$(uname -m)" in + arm64|aarch64) builder_platform=linux/arm64 ;; + x86_64|amd64) builder_platform=linux/amd64 ;; + *) die "unsupported container host architecture: $(uname -m)" ;; + esac +fi + +if [ -n "${CONTAINER_CLI:-}" ]; then + container_cli=$CONTAINER_CLI +elif command -v container >/dev/null 2>&1; then + container_cli=$(command -v container) +elif [ -x /usr/local/bin/container ]; then + container_cli=/usr/local/bin/container +elif command -v docker >/dev/null 2>&1; then + container_cli=$(command -v docker) +elif command -v podman >/dev/null 2>&1; then + container_cli=$(command -v podman) +else + die "no supported container CLI found (container, docker, or podman)" +fi + +container_frontend=$(basename "$container_cli") +case "$container_frontend" in + container) + "$container_cli" system start >/dev/null + builder_resources=$("$container_cli" builder status 2>/dev/null | + awk 'NR == 2 { print $5, $6 }') + if [ -n "$builder_resources" ]; then + set -- $builder_resources + actual_cpus=$1 + actual_memory_mib=$2 + expected_memory_mib=$(memory_mib "$builder_memory") + if [ "$actual_cpus" != "$builder_cpus" ] || + [ "$actual_memory_mib" != "$expected_memory_mib" ]; then + die "Apple Container builder has $actual_cpus CPUs and ${actual_memory_mib} MB; expected $builder_cpus CPUs and ${expected_memory_mib} MB. Recreate it with: container builder stop; container builder delete; container builder start --cpus $builder_cpus --memory $builder_memory (deleting it discards the build cache)" + fi + fi + ;; + docker|podman) ;; + *) die "unsupported container CLI: $container_frontend" ;; +esac + +"$repo_dir/scripts/check-source-locks.sh" +"$repo_dir/scripts/fetch-sources.sh" + +mkdir -p "$output_dir" +build_context=$(mktemp -d "$output_dir/.container-context.XXXXXX") +build_context=$(CDPATH= cd -- "$build_context" && pwd) +export_dir=$(mktemp -d "$output_dir/.container-output.XXXXXX") +export_dir=$(CDPATH= cd -- "$export_dir" && pwd) +artifact_temp= + +cleanup() { + [ -z "$artifact_temp" ] || rm -f "$artifact_temp" + rm -rf "$build_context" "$export_dir" +} +trap cleanup EXIT HUP INT TERM + +( + cd "$repo_dir/.." + tar -cf - \ + --exclude='mouse-src/.git/*' \ + --exclude='mouse-src/build/*' \ + --exclude='mouse-src/Containerfile' \ + --exclude='mouse-src/.containerignore' \ + --exclude='mouse-src/scripts/build-in-container.sh' \ + --exclude='mouse-src/base/cheesed/target/*' \ + mouse-src +) | tar -xf - -C "$build_context" + +cp "$repo_dir/Containerfile" "$build_context/Dockerfile" +cp "$repo_dir/.containerignore" "$build_context/.dockerignore" + +source_dir=${MOUSE_SOURCE_DIR:-"$output_dir/sources"} +for domain in toolchain kernel firmware facilities storage pkgsrc software; do + domain_dir="$build_context/mouse-sources/$domain" + mkdir -p "$domain_dir" + while read -r checksum url filename extra; do + case "$checksum" in + ''|\#*) continue ;; + esac + install -m 0444 "$source_dir/$filename" "$domain_dir/$filename" + done <"$repo_dir/release/$domain.lock" +done + +( + cd "$build_context" + set -- build + case "$container_frontend" in + container) + set -- "$@" --cpus "$builder_cpus" --memory "$builder_memory" + ;; + docker|podman) + set -- "$@" \ + --cpu-period 100000 \ + --cpu-quota "$((builder_cpus * 100000))" \ + --memory "$builder_memory" + ;; + esac + set -- "$@" \ + --platform "$builder_platform" \ + --target artifacts \ + --build-arg "MOUSE_BUILD_JOBS=$build_jobs" \ + --build-arg "MOUSE_ROOTFS_STAGE=$rootfs_stage" \ + --output "type=local,dest=$export_dir" \ + . + "$container_cli" "$@" +) + +artifacts_dir=$export_dir +if [ ! -f "$artifacts_dir/vmlinuz-virt" ]; then + platform_dir=$(printf '%s\n' "$builder_platform" | tr / _) + artifacts_dir="$export_dir/out.tar/$platform_dir" +fi + +for artifact in vmlinuz-virt ovmf-code.fd; do + [ -f "$artifacts_dir/$artifact" ] || \ + die "container build did not export $artifact" + install -m 0644 "$artifacts_dir/$artifact" "$output_dir/$artifact" +done + +artifact=mouse-disk.img +[ -f "$artifacts_dir/$artifact.gz" ] || \ + die "container build did not export $artifact.gz" +artifact_temp="$output_dir/.$artifact.tmp" +gzip -dc "$artifacts_dir/$artifact.gz" >"$artifact_temp" +chmod 0644 "$artifact_temp" +mv -f "$artifact_temp" "$output_dir/$artifact" +artifact_temp= + +for artifact in \ + vmlinuz-virt \ + ovmf-code.fd \ + mouse-disk.img +do + printf '%s\n' "exported $output_dir/$artifact" +done diff --git a/scripts/build-kernel-test-module.sh b/scripts/build-kernel-test-module.sh new file mode 100755 index 0000000..0f50c26 --- /dev/null +++ b/scripts/build-kernel-test-module.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} +kernel_version=$(cat "$build_dir/kernel/release") +kernel_source="$build_dir/kernel/linux-6.18.35" +kernel_output="$build_dir/kernel/output" +module_output="$build_dir/kernel/module-test" +module_root="$build_dir/kernel/test-modules" + +rm -rf "$module_output" "$module_root" +mkdir -p "$module_output" +cp "$repo_dir/tests/fixtures/kernel-module/mouse_test.c" \ + "$repo_dir/tests/fixtures/kernel-module/Makefile" \ + "$module_output/" +make -s -C "$kernel_source" \ + O="$kernel_output" \ + ARCH=x86_64 \ + LLVM=1 \ + M="$module_output" \ + modules + +cp -R "$build_dir/kernel/modules" "$module_root" +install -d -m 0755 \ + "$module_root/usr/local/lib/modules/$kernel_version/extra" +install -m 0644 "$module_output/mouse_test.ko" \ + "$module_root/usr/local/lib/modules/$kernel_version/extra/mouse_test.ko" diff --git a/scripts/build-kernel.sh b/scripts/build-kernel.sh new file mode 100755 index 0000000..9613205 --- /dev/null +++ b/scripts/build-kernel.sh @@ -0,0 +1,80 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} +source_dir=${MOUSE_SOURCE_DIR:-"$build_dir/sources"} +kernel_version=6.18.35 +kernel_archive="$source_dir/linux-$kernel_version.tar.xz" +kernel_source="$build_dir/kernel/linux-$kernel_version" +kernel_output="$build_dir/kernel/output" +kernel_config="$repo_dir/base/kernel/x86_64.config" +jobs=${MOUSE_BUILD_JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '1\n')} + +if [ ! -f "$kernel_archive" ]; then + printf '%s\n' "missing $kernel_archive; run scripts/fetch-sources.sh first" >&2 + exit 1 +fi +if [ ! -f "$kernel_config" ]; then + printf '%s\n' "missing MOUSE kernel config: $kernel_config" >&2 + exit 1 +fi + +mkdir -p "$build_dir/kernel" +rm -rf "$kernel_source" "$kernel_output" +tar -C "$build_dir/kernel" -xf "$kernel_archive" +mkdir -p "$kernel_output" + +make -C "$kernel_source" \ + O="$kernel_output" \ + ARCH=x86_64 \ + LLVM=1 \ + KCONFIG_ALLCONFIG="$kernel_config" \ + allnoconfig + +while IFS= read -r requested || [ -n "$requested" ]; do + case "$requested" in + CONFIG_*=*) ;; + *) continue ;; + esac + if ! grep -Fqx "$requested" "$kernel_output/.config"; then + printf '%s\n' \ + "kernel did not enable requested setting: $requested" >&2 + exit 1 + fi +done <"$kernel_config" + +make -C "$kernel_source" \ + O="$kernel_output" \ + ARCH=x86_64 \ + LLVM=1 \ + KBUILD_BUILD_USER=mouse \ + KBUILD_BUILD_HOST=mouse \ + -j"$jobs" \ + bzImage modules + +kernel_release=$(make -s -C "$kernel_source" \ + O="$kernel_output" \ + ARCH=x86_64 \ + LLVM=1 \ + kernelrelease) +module_root="$build_dir/kernel/modules" +rm -rf "$module_root" +make -s -C "$kernel_source" \ + O="$kernel_output" \ + ARCH=x86_64 \ + LLVM=1 \ + INSTALL_MOD_PATH="$module_root" \ + modules_install + +printf '%s\n' "$kernel_release" >"$build_dir/kernel/release" +install -m 0644 "$kernel_output/.config" \ + "$module_root/lib/modules/$kernel_release/config" +install -m 0644 "$kernel_output/Module.symvers" \ + "$module_root/lib/modules/$kernel_release/Module.symvers" +install -m 0644 \ + "$kernel_output/arch/x86/boot/bzImage" \ + "$build_dir/vmlinuz-virt" +printf '%s\n' \ + "built Linux $kernel_release with built-in btrfs and loadable-module support" diff --git a/scripts/build-pkgsrc-seed.sh b/scripts/build-pkgsrc-seed.sh new file mode 100755 index 0000000..63fc5a8 --- /dev/null +++ b/scripts/build-pkgsrc-seed.sh @@ -0,0 +1,89 @@ +#!/bin/sh +set -eu + +archive=${MOUSE_PKGSRC_ARCHIVE:-/usr/share/mouse/pkgsrc-2026Q2.tar.gz} +patch_file=${MOUSE_PKGSRC_PATCH:-/usr/share/mouse/pkgsrc-bootstrap-mouse.patch} +source_parent=/usr/local/src +pkgsrc=$source_parent/pkgsrc +mk_conf=/usr/local/etc/mk.conf +build_busybox=/usr/libexec/mouse-build-busybox + +fail() { + printf '%s\n' "build-pkgsrc-seed: $*" >&2 + exit 1 +} + +[ "$(id -u)" -eq 0 ] || fail "must run as root" +[ -f "$archive" ] || fail "missing pinned pkgsrc archive: $archive" +[ -f "$patch_file" ] || fail "missing MOUSE pkgsrc bootstrap patch: $patch_file" +[ -x "$build_busybox" ] || fail "missing build-only copy helper" +[ ! -e /usr/local/bin/bmake ] || fail "pkgsrc seed root is not empty" +[ ! -e /var/db/pkg ] || fail "pkgsrc package database already exists" + +mkdir -p \ + "$source_parent" \ + /usr/local/etc/rc.d \ + /tmp \ + /var/cache/pkgsrc/distfiles \ + /var/cache/pkgsrc/packages +chmod 1777 /tmp +tar -xzf "$archive" -C "$source_parent" +[ -x "$pkgsrc/bootstrap/bootstrap" ] || fail "archive did not produce a pkgsrc tree" +[ -f "$pkgsrc/mk/init/bsd.init.mk" ] || fail "pkgsrc tree lacks init-system support" +patch -d "$pkgsrc" -p1 <"$patch_file" +grep -Fq 'bootstrap_awk=/usr/bin/awk' "$pkgsrc/bootstrap/bootstrap" || + fail "pkgsrc tree lacks the MOUSE World awk policy" + +# Apple Rosetta reports ENOSYS for the copy operation used by MOUSE's static +# cp. Use an amd64 build-only applet while pkgsrc populates its work directory; +# only /usr/local and /var/db/pkg leave this stage. +mv /bin/cp /bin/cp.mouse +ln -s "$build_busybox" /bin/cp + +( + cd "$pkgsrc/bootstrap" + env \ + AWK=/usr/bin/awk \ + CC=/usr/bin/cc \ + CXX=/usr/bin/c++ \ + SH=/bin/sh \ + ./bootstrap \ + --compiler clang \ + --make-jobs "${MOUSE_BUILD_JOBS:-1}" \ + --pkgdbdir /var/db/pkg \ + --prefer-pkgsrc yes \ + --prefix /usr/local \ + --sysconfbase /usr/local/etc \ + --sysconfdir /usr/local/etc \ + --varbase /var +) + +rm /bin/cp +mv /bin/cp.mouse /bin/cp +rm "$build_busybox" + +cat >>"$mk_conf" <<'EOF' + +# MOUSE pkgsrc policy +PKGSRC_COMPILER= clang +LOCALBASE= /usr/local +PKG_SYSCONFBASE= /usr/local/etc +PKG_DBDIR= /var/db/pkg +VARBASE= /var +DISTDIR= /var/cache/pkgsrc/distfiles +PACKAGES= /var/cache/pkgsrc/packages +INIT_SYSTEM= rc.d +RCD_SCRIPTS_DIR= /usr/local/etc/rc.d +PKG_RCD_SCRIPTS= YES +PREFER_PKGSRC= yes +EOF + +ln -s /etc/rc.subr /usr/local/etc/rc.subr +rm -rf "$pkgsrc/bootstrap/work" + +[ -x /usr/local/bin/bmake ] || fail "bootstrap did not install bmake" +[ -x /usr/local/sbin/pkg_info ] || fail "bootstrap did not install pkg_info" +[ -d /var/db/pkg ] || fail "bootstrap did not create the package database" +grep -Eq '^TOOLS_PLATFORM\.awk\?=[[:space:]]*/usr/bin/awk$' "$mk_conf" || + fail "bootstrap did not preserve the World awk policy" +printf '%s\n' "built pkgsrc 2026Q2 seed" diff --git a/scripts/check-source-locks.sh b/scripts/check-source-locks.sh new file mode 100755 index 0000000..e6a5c3d --- /dev/null +++ b/scripts/check-source-locks.sh @@ -0,0 +1,197 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +lock_dir="$repo_dir/release" +temporary=$(mktemp -d "${TMPDIR:-/tmp}/mouse-source-lock-check.XXXXXX") + +cleanup() { + rm -rf "$temporary" +} +trap cleanup EXIT HUP INT TERM + +MOUSE_VALIDATE_ONLY=1 "$script_dir/fetch-sources.sh" + +awk 'NF && $1 !~ /^#/ { print $3 }' \ + "$lock_dir/toolchain.lock" \ + "$lock_dir/kernel.lock" \ + "$lock_dir/facilities.lock" \ + "$lock_dir/storage.lock" \ + "$lock_dir/software.lock" | + LC_ALL=C sort -u >"$temporary/locked" + +awk '$1 == "extract" { print $2 }' "$script_dir/static-base"/*.sh | + LC_ALL=C sort -u >"$temporary/referenced-archives" +sed -n 's/.*"$source_dir\/\([^"$]*\)".*/\1/p' \ + "$script_dir/static-base"/*.sh \ + "$script_dir/build-boot-disk.sh" | + LC_ALL=C sort -u >"$temporary/referenced-files" +cat "$temporary/referenced-archives" "$temporary/referenced-files" | + LC_ALL=C sort -u >"$temporary/referenced" + +if ! diff -u "$temporary/locked" "$temporary/referenced"; then + printf '%s\n' "source locks do not match static-base component inputs" >&2 + exit 1 +fi + +toolchain_count=$(awk 'NF && $1 !~ /^#/ { count++ } END { print count + 0 }' \ + "$lock_dir/toolchain.lock") +kernel_count=$(awk 'NF && $1 !~ /^#/ { count++ } END { print count + 0 }' \ + "$lock_dir/kernel.lock") +if [ "$toolchain_count" -ne 1 ] || \ + ! awk 'NF && $1 !~ /^#/ { print $3 }' "$lock_dir/toolchain.lock" | + grep -Eq '^llvm-project-[^/]+$' +then + printf '%s\n' "toolchain.lock must contain exactly the LLVM source" >&2 + exit 1 +fi +if [ "$kernel_count" -ne 1 ] || \ + ! awk 'NF && $1 !~ /^#/ { print $3 }' "$lock_dir/kernel.lock" | + grep -Eq '^linux-[^/]+$' +then + printf '%s\n' "kernel.lock must contain exactly the Linux source" >&2 + exit 1 +fi +pkgsrc_count=$(awk 'NF && $1 !~ /^#/ { count++ } END { print count + 0 }' \ + "$lock_dir/pkgsrc.lock") +if [ "$pkgsrc_count" -ne 1 ] || \ + ! awk 'NF && $1 !~ /^#/ { print $3 }' "$lock_dir/pkgsrc.lock" | + grep -Eq '^pkgsrc-[0-9]{4}Q[1-4]\.tar\.gz$' +then + printf '%s\n' "pkgsrc.lock must contain exactly one stable pkgsrc archive" >&2 + exit 1 +fi +firmware_count=$(awk 'NF && $1 !~ /^#/ { count++ } END { print count + 0 }' \ + "$lock_dir/firmware.lock") +if [ "$firmware_count" -ne 1 ] || \ + ! awk 'NF && $1 !~ /^#/ { print $3 }' "$lock_dir/firmware.lock" | + grep -Eq '^ovmf-[^/]+\.apk$' +then + printf '%s\n' "firmware.lock must contain exactly one OVMF package" >&2 + exit 1 +fi +storage_count=$(awk 'NF && $1 !~ /^#/ { count++ } END { print count + 0 }' \ + "$lock_dir/storage.lock") +if [ "$storage_count" -lt 1 ] || \ + ! awk 'NF && $1 !~ /^#/ { print $3 }' "$lock_dir/storage.lock" | + grep -Eq '^btrfs-progs-[^/]+$' +then + printf '%s\n' "storage.lock must contain the btrfs-progs source" >&2 + exit 1 +fi +if awk 'NF && $1 !~ /^#/ { print $3 }' "$lock_dir/software.lock" | + grep -Eq '^(llvm-project|linux|btrfs-progs|less|mandoc|iproute2|nftables)-' +then + printf '%s\n' "software.lock contains a source owned by another cache domain" >&2 + exit 1 +fi +if awk 'NF && $1 !~ /^#/ { print $3 }' "$lock_dir/facilities.lock" | + grep -Eq '^(llvm-project|linux|btrfs-progs)-' +then + printf '%s\n' "facilities.lock contains a kernel, toolchain, or storage source" >&2 + exit 1 +fi + +awk ' + /^FROM toolchain AS kernel-builder$/ { in_kernel = 1; next } + /^FROM / && in_kernel { exit } + in_kernel { print } +' "$repo_dir/Containerfile" >"$temporary/kernel-stage" + +if ! grep -Fq 'COPY --from=kernel-sources ' "$temporary/kernel-stage"; then + printf '%s\n' "kernel-builder does not import kernel-sources" >&2 + exit 1 +fi +if grep -Eq 'COPY --from=(toolchain|storage|software)-sources ' \ + "$temporary/kernel-stage" +then + printf '%s\n' "kernel-builder imports an unrelated source domain" >&2 + exit 1 +fi +if grep -Fq 'tests/fixtures' "$temporary/kernel-stage"; then + printf '%s\n' "production kernel stage imports test fixtures" >&2 + exit 1 +fi + +awk ' + /^FROM kernel-builder AS kernel-ready$/ { in_marker = 1; next } + /^FROM / && in_marker { exit } + in_marker { print } +' "$repo_dir/Containerfile" >"$temporary/kernel-ready-stage" + +if ! grep -Fq 'RUN install -m 0444 /dev/null /kernel-ready' \ + "$temporary/kernel-ready-stage" +then + printf '%s\n' "kernel-ready does not create its completion marker" >&2 + exit 1 +fi + +for stage_script in \ + static-base-bootstrap:bootstrap.sh \ + static-base-libraries:libraries-and-accounts.sh \ + static-base-system:system-services.sh \ + static-base-facilities:facilities.sh \ + static-base-toolchain:native-toolchain.sh \ + static-base-storage:storage.sh \ + static-base-builder:finish.sh +do + stage=${stage_script%%:*} + component=${stage_script#*:} + awk -v heading="AS $stage" ' + /^FROM / { + if (active) exit + active = index($0, heading) != 0 + next + } + active { print } + ' "$repo_dir/Containerfile" >"$temporary/$stage" + if ! grep -Fq \ + "COPY mouse-src/scripts/static-base/$component " \ + "$temporary/$stage" + then + printf '%s\n' \ + "$stage does not own static-base component $component" >&2 + exit 1 + fi + if [ "$(grep -Fc \ + "COPY mouse-src/scripts/static-base/$component " \ + "$repo_dir/Containerfile")" -ne 1 ] + then + printf '%s\n' \ + "$component must enter exactly one container cache stage" >&2 + exit 1 + fi +done + +if ! grep -Fq \ + 'COPY --from=kernel-ready /kernel-ready /tmp/kernel-ready' \ + "$temporary/static-base-bootstrap" +then + printf '%s\n' "static-base build does not wait for kernel completion" >&2 + exit 1 +fi + +for production_stage in image-commands-builder rootfs-builder; do + awk -v heading="AS $production_stage" ' + /^FROM / { + if (active) exit + active = index($0, heading) != 0 + next + } + active { print } + ' "$repo_dir/Containerfile" >"$temporary/$production_stage" + if grep -Fq 'tests/fixtures' "$temporary/$production_stage"; then + printf '%s\n' "$production_stage imports test fixtures" >&2 + exit 1 + fi +done + +for test_stage in kernel-test-builder test-fixtures-builder rootfs-test-builder; do + if ! grep -Fq "AS $test_stage" "$repo_dir/Containerfile"; then + printf '%s\n' "missing isolated test stage: $test_stage" >&2 + exit 1 + fi +done + +printf '%s\n' "source lock and cache-boundary checks passed" diff --git a/scripts/configure-cross-toolchain.sh b/scripts/configure-cross-toolchain.sh new file mode 100755 index 0000000..f6419de --- /dev/null +++ b/scripts/configure-cross-toolchain.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -eu + +if [ "$#" -ne 9 ]; then + printf '%s\n' \ + "usage: configure-cross-toolchain.sh DEST SYSROOT CLANG CLANGXX LLD BUILTINS RUST_EH_PERSONALITY LLVM_AR LLVM_STRIP" >&2 + exit 2 +fi + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +destination=$1 +sysroot=$2 +clang=$3 +clangxx=$4 +lld=$5 +builtins=$6 +rust_eh_personality=$7 +llvm_ar=$8 +llvm_strip=$9 +target=x86_64-mouse-linux-musl + +for requirement in \ + "$clang" "$clangxx" "$lld" "$builtins" "$rust_eh_personality" "$llvm_ar" "$llvm_strip" +do + if [ ! -f "$requirement" ]; then + printf '%s\n' "missing cross-toolchain input: $requirement" >&2 + exit 1 + fi +done + +install -d -m 0755 "$destination/bin" +install -m 0755 \ + "$repo_dir/base/support/cross-compiler-driver" \ + "$destination/bin/compiler-driver" + +printf '%s\n' \ + "$clang" \ + "$clangxx" \ + "$sysroot" \ + "$lld" \ + "$builtins" \ + "$rust_eh_personality" \ + >"$destination/compiler.conf" + +ln -sf compiler-driver "$destination/bin/$target-cc" +ln -sf compiler-driver "$destination/bin/$target-c++" +ln -sf compiler-driver "$destination/bin/$target-bootstrap-c++" +ln -sf "$llvm_ar" "$destination/bin/$target-ar" +ln -sf "$llvm_ar" "$destination/bin/$target-ranlib" +ln -sf "$llvm_strip" "$destination/bin/$target-strip" diff --git a/scripts/fetch-sources.sh b/scripts/fetch-sources.sh new file mode 100755 index 0000000..494cc65 --- /dev/null +++ b/scripts/fetch-sources.sh @@ -0,0 +1,114 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} +source_dir=${MOUSE_SOURCE_DIR:-"$build_dir/sources"} +lock_dir="$repo_dir/release" + +if [ "$#" -eq 0 ]; then + set -- \ + "$lock_dir/toolchain.lock" \ + "$lock_dir/kernel.lock" \ + "$lock_dir/firmware.lock" \ + "$lock_dir/facilities.lock" \ + "$lock_dir/pkgsrc.lock" \ + "$lock_dir/storage.lock" \ + "$lock_dir/software.lock" +fi + +seen=$(mktemp "${TMPDIR:-/tmp}/mouse-source-locks.XXXXXX") +cleanup() { + rm -f "$seen" +} +trap cleanup EXIT HUP INT TERM + +for lock_file in "$@"; do + if [ ! -f "$lock_file" ]; then + printf '%s\n' "missing source lock: $lock_file" >&2 + exit 1 + fi + line_number=0 + while read -r expected url filename extra; do + line_number=$((line_number + 1)) + case "$expected" in + ""|\#*) continue ;; + esac + if [ -n "${extra:-}" ]; then + printf '%s\n' "$lock_file:$line_number: expected three fields" >&2 + exit 1 + fi + if [ "${#expected}" -ne 64 ]; then + printf '%s\n' "$lock_file:$line_number: SHA-256 must have 64 characters" >&2 + exit 1 + fi + case "$expected" in + *[!0-9a-f]*) + printf '%s\n' "$lock_file:$line_number: SHA-256 must be lowercase hexadecimal" >&2 + exit 1 + ;; + esac + case "$url" in + https://*) ;; + *) + printf '%s\n' "$lock_file:$line_number: source URL must use HTTPS" >&2 + exit 1 + ;; + esac + case "$filename" in + ""|.|..|*/*) + printf '%s\n' "$lock_file:$line_number: invalid source filename: $filename" >&2 + exit 1 + ;; + esac + if grep -Fqx "$filename" "$seen"; then + printf '%s\n' "$lock_file:$line_number: duplicate source filename: $filename" >&2 + exit 1 + fi + printf '%s\n' "$filename" >>"$seen" + done <"$lock_file" +done + +if [ "${MOUSE_VALIDATE_ONLY:-0}" = 1 ]; then + exit 0 +fi + +mkdir -p "$source_dir" + +for lock_file in "$@"; do + while read -r expected url filename extra; do + case "$expected" in + ""|\#*) continue ;; + esac + + destination="$source_dir/$filename" + if [ ! -f "$destination" ]; then + temporary="$destination.part" + printf '%s\n' "fetching $filename" + curl \ + --fail \ + --location \ + --proto '=https' \ + --tlsv1.2 \ + --connect-timeout 20 \ + --retry 4 \ + --retry-all-errors \ + "$url" \ + -o "$temporary" + mv "$temporary" "$destination" + fi + + if command -v sha256sum >/dev/null 2>&1; then + actual=$(sha256sum "$destination" | awk '{print $1}') + else + actual=$(shasum -a 256 "$destination" | awk '{print $1}') + fi + if [ "$actual" != "$expected" ]; then + printf '%s\n' "checksum mismatch: $filename" >&2 + printf '%s\n' "expected: $expected" >&2 + printf '%s\n' "actual: $actual" >&2 + exit 1 + fi + done <"$lock_file" +done diff --git a/scripts/run-qemu.sh b/scripts/run-qemu.sh new file mode 100755 index 0000000..e8d7f76 --- /dev/null +++ b/scripts/run-qemu.sh @@ -0,0 +1,152 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(dirname "$script_dir") +build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} +boot_mode=${MOUSE_BOOT_MODE:-firmware} +kernel_args=${MOUSE_KERNEL_ARGS:-} +disk_image=${MOUSE_DISK_IMAGE:-"$build_dir/mouse-disk.img"} +disk_bus=${MOUSE_DISK_BUS:-virtio} +firmware=${MOUSE_FIRMWARE:-bios} +uefi_code=${MOUSE_UEFI_CODE:-"$build_dir/ovmf-code.fd"} +network=${MOUSE_NETWORK:-0} +network_backend=${MOUSE_NETWORK_BACKEND:-user} +network_device=${MOUSE_NETWORK_DEVICE:-virtio} +memory=${MOUSE_MEMORY:-512M} + +if [ -n "${QEMU_SYSTEM_X86_64:-}" ]; then + qemu=$QEMU_SYSTEM_X86_64 +else + qemu=qemu-system-x86_64 +fi + +case "$qemu" in + */*) + if [ ! -x "$qemu" ]; then + printf '%s\n' "$qemu is not executable" >&2 + exit 1 + fi + ;; + *) + if ! command -v "$qemu" >/dev/null 2>&1; then + printf '%s\n' "qemu-system-x86_64 is required to boot the image" >&2 + exit 1 + fi + ;; +esac + +if [ ! -f "$build_dir/vmlinuz-virt" ]; then + printf '%s\n' "run 'make image' before booting MOUSE" >&2 + exit 1 +fi + +disk_drive="file=$disk_image,format=raw,if=none,id=mouse-root" +disk_controller= +case "$disk_bus" in + virtio) + disk_device="virtio-blk-pci,drive=mouse-root,disable-legacy=on" + ;; + sata) + disk_controller="ich9-ahci,id=mouse-ahci" + disk_device="ide-hd,drive=mouse-root,bus=mouse-ahci.0" + ;; + nvme) + disk_device="nvme,drive=mouse-root,serial=MOUSE0001" + ;; + *) + printf '%s\n' "invalid MOUSE_DISK_BUS: $disk_bus" >&2 + exit 2 + ;; +esac + +set -- "$qemu" \ + -machine q35,accel=tcg \ + -cpu max \ + -m "$memory" \ + -no-reboot \ + -display none \ + -nographic \ + -nic none + +case "$firmware" in + bios) ;; + uefi) + if [ ! -f "$uefi_code" ]; then + printf '%s\n' "missing pinned UEFI firmware: $uefi_code" >&2 + exit 1 + fi + set -- "$@" \ + -drive "if=pflash,format=raw,readonly=on,file=$uefi_code" + ;; + *) + printf '%s\n' "invalid MOUSE_FIRMWARE: $firmware" >&2 + exit 2 + ;; +esac + +case "$boot_mode" in + firmware) + if [ ! -f "$disk_image" ]; then + printf '%s\n' "missing boot disk: $disk_image" >&2 + exit 1 + fi + set -- "$@" -drive "$disk_drive" + if [ -n "$disk_controller" ]; then + set -- "$@" -device "$disk_controller" + fi + set -- "$@" -device "$disk_device" + ;; + disk) + if [ ! -f "$disk_image" ]; then + printf '%s\n' "missing root disk: $disk_image" >&2 + exit 1 + fi + kernel_args=${kernel_args:-"console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed panic=-1"} + set -- "$@" \ + -kernel "$build_dir/vmlinuz-virt" \ + -drive "$disk_drive" + if [ -n "$disk_controller" ]; then + set -- "$@" -device "$disk_controller" + fi + set -- "$@" -device "$disk_device" + ;; + *) + printf '%s\n' "invalid MOUSE_BOOT_MODE: $boot_mode" >&2 + exit 2 + ;; +esac + +case "$network" in + 0) ;; + 1) + if ! "$qemu" -netdev help 2>&1 | + grep -Fxq "$network_backend"; then + printf '%s\n' \ + "QEMU does not provide the requested network backend: $network_backend" \ + "install a QEMU build with that backend or set MOUSE_NETWORK_BACKEND" >&2 + exit 1 + fi + case "$network_device" in + virtio) nic_device="virtio-net-pci,netdev=mouse-net,disable-legacy=on" ;; + e1000) nic_device="e1000,netdev=mouse-net" ;; + e1000e) nic_device="e1000e,netdev=mouse-net" ;; + *) + printf '%s\n' "invalid MOUSE_NETWORK_DEVICE: $network_device" >&2 + exit 2 + ;; + esac + set -- "$@" \ + -netdev "$network_backend,id=mouse-net" \ + -device "$nic_device" + ;; + *) + printf '%s\n' "invalid MOUSE_NETWORK: $network" >&2 + exit 2 + ;; +esac + +if [ "$boot_mode" = disk ]; then + set -- "$@" -append "$kernel_args" +fi +exec "$@" diff --git a/scripts/static-base/bootstrap.sh b/scripts/static-base/bootstrap.sh new file mode 100644 index 0000000..a65ab9d --- /dev/null +++ b/scripts/static-base/bootstrap.sh @@ -0,0 +1,221 @@ +#!/bin/sh +set -eu + +component_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$component_dir/common.sh" + +rm -rf "$static_dir" +install -d -m 0755 \ + "$source_build_dir" \ + "$sysroot" \ + "$base_root/bin" \ + "$base_root/sbin" \ + "$base_root/usr/bin" \ + "$base_root/usr/sbin" \ + "$tools_dir/bin" + +# llvm-ar selects ranlib mode from argv[0]. This gives build systems a real +# ranlib frontend even on the Rust toolchain for macOS, which ships only the +# underlying multi-call binary. +ln -s "$llvm_ar" "$tools_dir/bin/llvm-ranlib" +llvm_ranlib="$tools_dir/bin/llvm-ranlib" + +extract() { + tar -xf "$source_dir/$1" -C "$source_build_dir" +} + +extract meson-1.9.1.tar.gz +extract ninja-1.13.1.tar.gz +extract pkgconf-2.5.1.tar.gz +extract linux-6.18.35.tar.xz +extract musl-1.2.6.tar.gz +extract zlib-1.3.2.tar.gz +extract libarchive-3.8.7.tar.xz +extract llvm-project-22.1.3.src.tar.xz +extract netbsd-curses-0.3.2.tar.gz +extract attr-2.5.2.tar.gz +extract acl-2.3.2.tar.gz +extract libxo-1.7.5.tar.gz +extract tcsh-6.24.16.tar.gz +extract chimerautils-15.0.3.tar.gz +extract openssl-3.5.7.tar.gz +extract util-linux-2.42.2.tar.xz +extract libxcrypt-4.5.2.tar.xz +extract shadow-4.19.4.tar.xz +extract dhcpcd-8b312918d8f1885d7fe8fcc03a7e06ae8a0314b4.tar.gz +extract make-4.4.1.tar.gz +extract cronie-1.7.2.tar.gz +extract openntpd-7.9p1.tar.gz +extract procps-ng-4.0.5.tar.xz +extract iputils-20250605.tar.gz +extract kmod-34.2.tar.xz +extract skalibs-2.15.1.0.tar.gz +extract mdevd-0.1.8.2.tar.gz +extract sysklogd-2.7.2.tar.gz + +( + cd "$source_build_dir/ninja-1.13.1" + python3 configure.py --bootstrap + install -m 0755 ninja "$tools_dir/bin/ninja" +) + +meson_source="$source_build_dir/meson-1.9.1/meson.py" +PATH="$tools_dir/bin:$PATH" +export PATH + +python3 "$meson_source" setup \ + "$source_build_dir/pkgconf-pkgconf-2.5.1/build" \ + "$source_build_dir/pkgconf-pkgconf-2.5.1" \ + --prefix="$tools_dir" \ + --buildtype=release \ + --default-library=static +python3 "$meson_source" compile -C "$source_build_dir/pkgconf-pkgconf-2.5.1/build" +python3 "$meson_source" install -C "$source_build_dir/pkgconf-pkgconf-2.5.1/build" + +musl_cc="$clang --target=x86_64-unknown-linux-musl --ld-path=$lld" +musl_mulxc3="$static_dir/musl-mulxc3.o" +"$clang" \ + --target=x86_64-unknown-linux-musl \ + --sysroot="$sysroot" \ + -fPIC \ + -I"$source_build_dir/llvm-project-22.1.3.src/compiler-rt/lib/builtins" \ + -c "$source_build_dir/llvm-project-22.1.3.src/compiler-rt/lib/builtins/mulxc3.c" \ + -o "$musl_mulxc3" +( + cd "$source_build_dir/musl-1.2.6" + CC="$musl_cc" \ + AR="$llvm_ar" \ + RANLIB="$llvm_ranlib" \ + ./configure \ + --target=x86_64 \ + --prefix=/usr \ + --syslibdir=/lib + make -s -j"$jobs" LIBCC="$builtins $musl_mulxc3" + make -s DESTDIR="$sysroot" LIBCC="$builtins $musl_mulxc3" install +) + +make -s -C "$source_build_dir/linux-6.18.35" \ + ARCH=x86 \ + headers_install \ + INSTALL_HDR_PATH="$sysroot/usr" + +install -d -m 0755 "$sysroot/usr/include/sys" +install -m 0644 "$source_dir/cdefs.h" "$sysroot/usr/include/sys/cdefs.h" +install -m 0644 "$source_dir/queue.h" "$sysroot/usr/include/sys/queue.h" +install -m 0644 "$source_dir/tree.h" "$sysroot/usr/include/sys/tree.h" +install -m 0644 "$source_dir/error.h" "$sysroot/usr/include/error.h" + +rust_eh_personality="$static_dir/rust-eh-personality.o" +"$clang" \ + --target=x86_64-unknown-linux-musl \ + --sysroot="$sysroot" \ + -c "$repo_dir/base/support/rust-eh-personality.c" \ + -o "$rust_eh_personality" + +"$script_dir/configure-cross-toolchain.sh" \ + "$cross_tools_dir" \ + "$sysroot" \ + "$clang" \ + "$clangxx" \ + "$lld" \ + "$builtins" \ + "$rust_eh_personality" \ + "$llvm_ar" \ + "$llvm_strip" + +target=x86_64-mouse-linux-musl +cc="$cross_tools_dir/bin/$target-cc" +cxx="$cross_tools_dir/bin/$target-c++" +bootstrap_cxx="$cross_tools_dir/bin/$target-bootstrap-c++" +ar="$cross_tools_dir/bin/$target-ar" +ranlib="$cross_tools_dir/bin/$target-ranlib" +strip="$cross_tools_dir/bin/$target-strip" + +llvm_runtimes_build="$source_build_dir/llvm-runtimes-build" +cmake \ + -G Ninja \ + -S "$source_build_dir/llvm-project-22.1.3.src/runtimes" \ + -B "$llvm_runtimes_build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_C_COMPILER="$cc" \ + -DCMAKE_C_COMPILER_TARGET=x86_64-unknown-linux-musl \ + -DCMAKE_CXX_COMPILER="$bootstrap_cxx" \ + -DCMAKE_CXX_COMPILER_TARGET=x86_64-unknown-linux-musl \ + -DCMAKE_AR="$ar" \ + -DCMAKE_RANLIB="$ranlib" \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \ + -DLLVM_ENABLE_RUNTIMES="compiler-rt;libcxx;libcxxabi;libunwind" \ + -DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF \ + -DCOMPILER_RT_BUILD_BUILTINS=ON \ + -DCOMPILER_RT_BUILD_CRT=ON \ + -DCOMPILER_RT_BUILD_GWP_ASAN=OFF \ + -DCOMPILER_RT_BUILD_LIBFUZZER=OFF \ + -DCOMPILER_RT_BUILD_MEMPROF=OFF \ + -DCOMPILER_RT_BUILD_ORC=OFF \ + -DCOMPILER_RT_BUILD_PROFILE=OFF \ + -DCOMPILER_RT_BUILD_SANITIZERS=OFF \ + -DCOMPILER_RT_BUILD_XRAY=OFF \ + -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \ + -DLIBCXX_ENABLE_SHARED=OFF \ + -DLIBCXX_ENABLE_STATIC=ON \ + -DLIBCXX_ENABLE_STATIC_ABI_LIBRARY=OFF \ + -DLIBCXX_ENABLE_EXPERIMENTAL_LIBRARY=OFF \ + -DLIBCXX_INCLUDE_BENCHMARKS=OFF \ + -DLIBCXX_INCLUDE_TESTS=OFF \ + -DLIBCXX_HAS_MUSL_LIBC=ON \ + -DLIBCXXABI_ENABLE_SHARED=OFF \ + -DLIBCXXABI_ENABLE_STATIC=ON \ + -DLIBCXXABI_HAS_CXA_THREAD_ATEXIT_IMPL=OFF \ + -DLIBCXXABI_INCLUDE_TESTS=OFF \ + -DLIBCXXABI_USE_LLVM_UNWINDER=ON \ + -DLIBUNWIND_ENABLE_SHARED=OFF \ + -DLIBUNWIND_ENABLE_STATIC=ON \ + -DLIBUNWIND_INCLUDE_TESTS=OFF +cmake --build "$llvm_runtimes_build" -j "$jobs" +DESTDIR="$sysroot" cmake --install "$llvm_runtimes_build" + +clang_runtime_dir="$sysroot/usr/lib/clang/22/lib/x86_64-unknown-linux-musl" +install -d -m 0755 "$clang_runtime_dir" +install -m 0644 \ + "$sysroot/usr/lib/linux/libclang_rt.builtins-x86_64.a" \ + "$clang_runtime_dir/libclang_rt.builtins.a" +for runtime_object in \ + clang_rt.crtbegin-x86_64.o \ + clang_rt.crtend-x86_64.o +do + install -m 0644 \ + "$sysroot/usr/lib/linux/$runtime_object" \ + "$clang_runtime_dir/$runtime_object" +done +install -m 0644 \ + "$sysroot/usr/lib/linux/clang_rt.crtbegin-x86_64.o" \ + "$sysroot/usr/lib/crtbeginT.o" +install -m 0644 \ + "$sysroot/usr/lib/linux/clang_rt.crtend-x86_64.o" \ + "$sysroot/usr/lib/crtend.o" + +{ + printf '%s\n' \ + '[binaries]' \ + "c = '$cc'" \ + "cpp = '$cxx'" \ + "ar = '$ar'" \ + "strip = '$strip'" \ + "pkg-config = '$tools_dir/bin/pkgconf'" \ + '' \ + '[host_machine]' \ + "system = 'linux'" \ + "cpu_family = 'x86_64'" \ + "cpu = 'x86_64'" \ + "endian = 'little'" \ + '' \ + '[built-in options]' \ + "c_args = ['-O2']" \ + "cpp_args = ['-O2']" \ + "c_link_args = ['-static']" \ + "cpp_link_args = ['-static']" \ + "default_library = 'static'" \ + "prefer_static = true" +} >"$cross_file" diff --git a/scripts/static-base/common.sh b/scripts/static-base/common.sh new file mode 100644 index 0000000..4eb1990 --- /dev/null +++ b/scripts/static-base/common.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +component_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +script_dir=$(dirname "$component_dir") +repo_dir=$(dirname "$script_dir") +build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} +source_dir=${MOUSE_SOURCE_DIR:-"$build_dir/sources"} +static_dir="$build_dir/static" +source_build_dir="$static_dir/src" +sysroot="$static_dir/sysroot" +base_root="$static_dir/rootfs" +tools_dir="$static_dir/tools" +cross_tools_dir="$static_dir/cross-tools" +jobs=${MOUSE_BUILD_JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '2')} + +case "$static_dir" in + "$repo_dir"/build/static) ;; + *) + printf '%s\n' "refusing to clear unexpected static build path: $static_dir" >&2 + exit 1 + ;; +esac + +target_libdir=$(rustc --print target-libdir --target x86_64-unknown-linux-musl) +builtins=$(find "$target_libdir" -maxdepth 1 -name 'libcompiler_builtins-*.rlib' -print | head -1) + +clang=$(command -v clang) +clangxx=$(command -v clang++) +lld=$(command -v ld.lld) +llvm_ar=$(command -v llvm-ar) +llvm_strip=$(command -v llvm-strip) +build_triplet=$(cc -dumpmachine) +native_cc=cc + +for requirement in \ + "$clang" "$clangxx" "$lld" "$builtins" "$llvm_ar" "$llvm_strip" +do + if [ ! -f "$requirement" ]; then + printf '%s\n' "missing LLVM/Rust cross-build component: $requirement" >&2 + exit 1 + fi +done + +llvm_ranlib="$tools_dir/bin/llvm-ranlib" +meson_source="$source_build_dir/meson-1.9.1/meson.py" +PATH="$tools_dir/bin:$PATH" +export PATH +target=x86_64-mouse-linux-musl +cc="$cross_tools_dir/bin/$target-cc" +cxx="$cross_tools_dir/bin/$target-c++" +bootstrap_cxx="$cross_tools_dir/bin/$target-bootstrap-c++" +ar="$cross_tools_dir/bin/$target-ar" +ranlib="$cross_tools_dir/bin/$target-ranlib" +strip="$cross_tools_dir/bin/$target-strip" +cross_file="$static_dir/chimerautils.cross" diff --git a/scripts/static-base/dynamic-runtime.sh b/scripts/static-base/dynamic-runtime.sh new file mode 100755 index 0000000..9132542 --- /dev/null +++ b/scripts/static-base/dynamic-runtime.sh @@ -0,0 +1,125 @@ +#!/bin/sh +set -eu + +component_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$component_dir/common.sh" + +for runtime_file in \ + /lib/ld-musl-x86_64.so.1 \ + /usr/lib/libc.so \ + /usr/lib/crt1.o \ + /usr/lib/Scrt1.o \ + /usr/lib/crti.o \ + /usr/lib/crtn.o \ + /usr/lib/crtbeginT.o \ + /usr/lib/crtend.o +do + if [ ! -f "$base_root$runtime_file" ]; then + printf '%s\n' "missing dynamic runtime file: $runtime_file" >&2 + exit 1 + fi +done + +for root in "$sysroot" "$base_root"; do + ln -sf crtbeginT.o "$root/usr/lib/crtbegin.o" + ln -sf crtbeginT.o "$root/usr/lib/crtbeginS.o" + ln -sf crtend.o "$root/usr/lib/crtendS.o" +done + +make_dynamic_compiler() { + wrapper=$1 + compiler=$2 + cat >"$wrapper" <&2 + exit 1 +fi +make_dynamic_compiler "$dynamic_cc" "$cc" +make_dynamic_compiler "$dynamic_cxx" "$bootstrap_cxx" + +shared_runtimes_build="$source_build_dir/llvm-shared-runtimes-build" +cmake \ + -G Ninja \ + -S "$source_build_dir/llvm-project-22.1.3.src/runtimes" \ + -B "$shared_runtimes_build" \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_C_COMPILER="$dynamic_cc" \ + -DCMAKE_C_COMPILER_TARGET=x86_64-unknown-linux-musl \ + -DCMAKE_CXX_COMPILER="$dynamic_cxx" \ + -DCMAKE_CXX_COMPILER_TARGET=x86_64-unknown-linux-musl \ + -DCMAKE_AR="$ar" \ + -DCMAKE_RANLIB="$ranlib" \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \ + -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind" \ + -DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF \ + -DLIBCXX_ENABLE_SHARED=ON \ + -DLIBCXX_ENABLE_STATIC=OFF \ + -DLIBCXX_HAS_MUSL_LIBC=ON \ + -DLIBCXX_HAS_ATOMIC_LIB=NO \ + -DLIBCXX_INCLUDE_BENCHMARKS=OFF \ + -DLIBCXX_INCLUDE_TESTS=OFF \ + -DLIBCXX_USE_COMPILER_RT=ON \ + -DLIBCXXABI_ENABLE_SHARED=ON \ + -DLIBCXXABI_ENABLE_STATIC=OFF \ + -DLIBCXXABI_HAS_CXA_THREAD_ATEXIT_IMPL=OFF \ + -DLIBCXXABI_INCLUDE_TESTS=OFF \ + -DLIBCXXABI_USE_COMPILER_RT=ON \ + -DLIBCXXABI_USE_LLVM_UNWINDER=ON \ + -DLIBUNWIND_ENABLE_SHARED=ON \ + -DLIBUNWIND_ENABLE_STATIC=OFF \ + -DLIBUNWIND_INCLUDE_TESTS=OFF \ + -DLIBUNWIND_USE_COMPILER_RT=ON +cmake --build "$shared_runtimes_build" -j "$jobs" +DESTDIR="$sysroot" cmake --install "$shared_runtimes_build" +cp -R "$sysroot/usr/lib/." "$base_root/usr/lib/" + +cat >"$base_root/usr/bin/clang.cfg" <<'EOF' +--target=x86_64-unknown-linux-musl +--sysroot=/ +--ld-path=/usr/bin/ld.lld +--rtlib=compiler-rt +--unwindlib=libunwind +EOF +cat >"$base_root/usr/bin/clang++.cfg" <<'EOF' +--target=x86_64-unknown-linux-musl +--sysroot=/ +--ld-path=/usr/bin/ld.lld +-stdlib=libc++ +--rtlib=compiler-rt +--unwindlib=libunwind +-lc++abi +EOF diff --git a/scripts/static-base/facilities.sh b/scripts/static-base/facilities.sh new file mode 100644 index 0000000..9aa4c9f --- /dev/null +++ b/scripts/static-base/facilities.sh @@ -0,0 +1,352 @@ +#!/bin/sh +set -eu + +component_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$component_dir/common.sh" + +extract() { + tar -xf "$source_dir/$1" -C "$source_build_dir" +} + +extract less-704.tar.gz +extract mandoc-1.14.6.tar.gz +extract file-5.47.tar.gz +extract gzip-1.14.tar.xz +extract bzip2-1.0.8.tar.gz +extract xz-5.8.3.tar.bz2 +extract iproute2-7.1.0.tar.xz +extract libmnl-1.0.5.tar.bz2 +extract libnftnl-1.3.1.tar.xz +extract nftables-1.1.6.tar.xz + +pkgconfig_path="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" + +install_man_page() { + page=$1 + name=${2:-${page##*/}} + section=${name##*.} + install -d -m 0755 "$base_root/usr/share/man/man$section" + install -m 0644 "$page" "$base_root/usr/share/man/man$section/$name" +} + +install_command_man_page() { + tree=$1 + command=$2 + page=$(find "$tree" -type f -name "$command.[1-9]" -print | head -1) + if [ -n "$page" ]; then + install_man_page "$page" + fi +} + +( + cd "$source_build_dir/less-704" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + LIBS="-lcurses -lterminfo" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --sysconfdir=/etc \ + --with-regex=posix + make -s -j"$jobs" less lesskey lessecho + install -m 0755 less lesskey lessecho "$base_root/usr/bin/" +) +install_man_page "$source_build_dir/less-704/less.nro" less.1 +install_man_page "$source_build_dir/less-704/lesskey.nro" lesskey.1 +install_man_page "$source_build_dir/less-704/lessecho.nro" lessecho.1 + +( + cd "$source_build_dir/bzip2-1.0.8" + make -s -j"$jobs" \ + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + CFLAGS="-O2" \ + LDFLAGS="-static" \ + bzip2 bzip2recover libbz2.a + install -m 0644 bzlib.h "$sysroot/usr/include/bzlib.h" + install -m 0644 libbz2.a "$sysroot/usr/lib/libbz2.a" + install -m 0755 bzip2 bzip2recover "$base_root/usr/bin/" +) +ln -s bzip2 "$base_root/usr/bin/bunzip2" +ln -s bzip2 "$base_root/usr/bin/bzcat" +for page in bzip2.1 bzgrep.1 bzmore.1 bzdiff.1; do + install_man_page "$source_build_dir/bzip2-1.0.8/$page" +done + +( + cd "$source_build_dir/xz-5.8.3" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --disable-nls \ + --disable-doc \ + --disable-xzdec \ + --disable-lzmadec \ + --disable-lzmainfo + make -s -j"$jobs" -C src/liblzma + make -s -C src/liblzma DESTDIR="$sysroot" install + make -s -j"$jobs" -C src/xz xz + install -m 0755 src/xz/xz "$base_root/usr/bin/xz" +) +for command in unxz xzcat lzma unlzma lzcat; do + ln -s xz "$base_root/usr/bin/$command" +done +for page in xz.1; do + install_man_page "$source_build_dir/xz-5.8.3/src/xz/$page" +done + +( + cd "$source_build_dir/gzip-1.14" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + gl_cv_func_getcwd_path_max=yes \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-nls + make -s -j"$jobs" + install -m 0755 gzip "$base_root/usr/bin/gzip" + for script in gunzip zcat zcmp zdiff zgrep zless zmore; do + install -m 0755 "$script" "$base_root/usr/bin/$script" + done +) +for page in gzip.1 gunzip.1 zcat.1 zcmp.1 zdiff.1 zgrep.1 zless.1 zmore.1; do + install_man_page "$source_build_dir/gzip-1.14/$page" +done + +( + cd "$source_build_dir/file-5.47" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + PKG_CONFIG_LIBDIR="$pkgconfig_path" \ + PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --disable-zstdlib \ + --disable-lzlib \ + --disable-libseccomp + make -s -C src magic.h + make -s -j"$jobs" -C src file + make -s -C magic magic.mgc + install -m 0755 src/file "$base_root/usr/bin/file" + install -d -m 0755 "$base_root/usr/share/misc" + install -m 0644 magic/magic.mgc "$base_root/usr/share/misc/magic.mgc" +) +install_man_page "$source_build_dir/file-5.47/doc/file.man" file.1 +install_man_page "$source_build_dir/file-5.47/doc/magic.man" magic.5 + +unzip_source="$source_build_dir/libarchive-unzip-3.8.7" +install -d -m 0755 "$unzip_source" +tar -xf "$source_dir/libarchive-3.8.7.tar.xz" \ + -C "$unzip_source" \ + --strip-components=1 +( + cd "$unzip_source" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + PKG_CONFIG_LIBDIR="$pkgconfig_path" \ + PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --disable-bsdtar \ + --disable-bsdcat \ + --disable-bsdcpio \ + --enable-bsdunzip=static \ + --disable-acl \ + --disable-xattr \ + --without-libb2 \ + --without-iconv \ + --without-lz4 \ + --without-zstd \ + --without-xml2 \ + --without-expat \ + --without-openssl \ + --without-cng + make -s -j"$jobs" bsdunzip + install -m 0755 bsdunzip "$base_root/usr/bin/unzip" +) +install_man_page "$unzip_source/unzip/bsdunzip.1" unzip.1 + +mandoc_source="$source_build_dir/mandoc-1.14.6" +cat >"$mandoc_source/configure.local" <"$host_mandoc_source/configure.local" <"$base_root/usr/bin/clear" +chmod 0755 "$base_root/usr/bin/clear" +printf '%s\n' "$base_root" diff --git a/scripts/static-base/libraries-and-accounts.sh b/scripts/static-base/libraries-and-accounts.sh new file mode 100644 index 0000000..c0119e2 --- /dev/null +++ b/scripts/static-base/libraries-and-accounts.sh @@ -0,0 +1,263 @@ +#!/bin/sh +set -eu + +component_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$component_dir/common.sh" + +( + cd "$source_build_dir/zlib-1.3.2" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ./configure \ + --prefix=/usr \ + --static + make -s -j"$jobs" + make -s DESTDIR="$sysroot" install +) + +( + cd "$source_build_dir/libarchive-3.8.7" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + CPPFLAGS="-I$sysroot/usr/include" \ + LDFLAGS="-L$sysroot/usr/lib" \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig" \ + PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --enable-bsdtar=static \ + --disable-bsdcat \ + --disable-bsdcpio \ + --disable-bsdunzip \ + --disable-acl \ + --disable-xattr \ + --without-bz2lib \ + --without-libb2 \ + --without-iconv \ + --without-lz4 \ + --without-lzma \ + --without-zstd \ + --without-xml2 \ + --without-expat \ + --without-openssl \ + --without-cng + make -s -j"$jobs" bsdtar + install -m 0755 bsdtar "$base_root/usr/bin/tar" +) + +( + cd "$source_build_dir/openssl-3.5.7" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ./Configure \ + linux-x86_64 \ + --prefix=/usr \ + --libdir=lib \ + --openssldir=/etc/ssl \ + no-afalgeng \ + no-apps \ + no-docs \ + no-dso \ + no-module \ + no-secure-memory \ + no-shared \ + no-tests + make -s -j"$jobs" build_libs + make -s DESTDIR="$sysroot" install_sw +) + +install -d -m 0755 "$base_root/etc/ssl" +install -m 0644 \ + "$source_dir/cacert-2026-07-16.pem" \ + "$base_root/etc/ssl/cert.pem" + +( + cd "$source_build_dir/libxcrypt-4.5.2" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --disable-obsolete-api \ + --disable-symvers \ + --enable-hashes=sha512crypt + make -s -j"$jobs" + make -s DESTDIR="$sysroot" install +) + +( + cd "$source_build_dir/shadow-4.19.4" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig" \ + PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --disable-logind \ + --disable-nls \ + --disable-shadowgrp \ + --without-audit \ + --without-libpam \ + --without-selinux \ + --without-acl \ + --without-attr \ + --without-libbsd \ + --without-skey \ + --without-tcb \ + --without-nscd + make -s -j"$jobs" -C lib + shadow_commands=" + groupadd groupdel groupmod login passwd su + useradd userdel usermod + " + MOUSE_CROSS_EXTRA_STATIC_LIBS="$sysroot/usr/lib/libcrypt.a" \ + make -s -j"$jobs" -C src $shadow_commands + for command in $shadow_commands; do + mode=0755 + destination="$base_root/usr/sbin" + case "$command" in + login) destination="$base_root/bin" ;; + passwd) + mode=4755 + destination="$base_root/usr/bin" + ;; + su) + mode=4755 + destination="$base_root/bin" + ;; + esac + install -m "$mode" "src/$command" "$destination/$command" + done +) + +( + cd "$source_build_dir/dhcpcd-8b312918d8f1885d7fe8fcc03a7e06ae8a0314b4" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ./configure \ + --os=linux \ + --target=x86_64-linux-musl \ + --prefix=/usr \ + --sbindir=/usr/bin \ + --sysconfdir=/etc \ + --libexecdir=/usr/libexec \ + --dbdir=/var/lib/dhcpcd \ + --rundir=/run \ + --enable-static \ + --disable-ipv6 \ + --disable-auth \ + --enable-privsep \ + --privsepuser=dhcpcd \ + --disable-ntp \ + --without-udev \ + --without-libpcap \ + --without-openssl \ + --with-hooks= + make -s -j"$jobs" -C src + install -m 0755 src/dhcpcd "$base_root/sbin/dhcpcd" +) + +( + cd "$source_build_dir/netbsd-curses-0.3.2" + make -s -f GNUmakefile -j"$jobs" \ + CC="$cc" \ + HOSTCC="$native_cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + CFLAGS="-O2" \ + CPPFLAGS="-I. -I./libterminfo -DTERMINFO_COMPILE -DTERMINFO_DB -DTERMINFO_COMPAT" \ + LDFLAGS="-static" \ + PREFIX=/usr \ + all-static + make -s -f GNUmakefile \ + CC="$cc" \ + HOSTCC="$native_cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + CFLAGS="-O2" \ + CPPFLAGS="-I. -I./libterminfo -DTERMINFO_COMPILE -DTERMINFO_DB -DTERMINFO_COMPAT" \ + LDFLAGS="-static" \ + PREFIX=/usr \ + DESTDIR="$sysroot" \ + install-static + +) + +build_autoconf_library() { + source_name=$1 + build_target=$2 + shift 2 + ( + cd "$source_build_dir/$source_name" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig" \ + PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + "$@" + make -s -j"$jobs" "$build_target" + make -s DESTDIR="$sysroot" \ + install-libLTLIBRARIES \ + install-data-local \ + install-pkgconfDATA + ) +} + +build_autoconf_library attr-2.5.2 libattr.la +build_autoconf_library acl-2.3.2 libacl.la + +# The projects' install-time EXPORT substitution uses a GNU sed word-boundary +# extension which macOS sed accepts but does not apply. Normalize the installed +# public declarations for consumers of the cross-built static libraries. +find "$sysroot/usr/include/attr" "$sysroot/usr/include/acl" \ + "$sysroot/usr/include/sys/acl.h" \ + -type f -exec perl -pi -e 's/\bEXPORT\b/extern/g' {} + + +( + cd "$source_build_dir/libxo-1.7.5" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ac_cv_func_malloc_0_nonnull=yes \ + ac_cv_func_realloc_0_nonnull=yes \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --disable-libxo-options \ + --disable-gettext \ + --enable-text-only + make -s -C libxo -j"$jobs" libxo.la + make -s -C libxo DESTDIR="$sysroot" \ + install-libLTLIBRARIES \ + install-libxoincHEADERS + make -s DESTDIR="$sysroot" install-pkgconfigDATA +) diff --git a/scripts/static-base/native-toolchain.sh b/scripts/static-base/native-toolchain.sh new file mode 100644 index 0000000..a24d6cc --- /dev/null +++ b/scripts/static-base/native-toolchain.sh @@ -0,0 +1,159 @@ +#!/bin/sh +set -eu + +component_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$component_dir/common.sh" + +build_llvm() { +llvm_host_build="$source_build_dir/llvm-host-tools-build" +cmake \ + -G Ninja \ + -S "$source_build_dir/llvm-project-22.1.3.src/llvm" \ + -B "$llvm_host_build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER="$native_cc" \ + -DCMAKE_CXX_COMPILER=c++ \ + -DLLVM_ENABLE_PROJECTS=clang \ + -DLLVM_TARGETS_TO_BUILD=X86 \ + -DLLVM_INCLUDE_BENCHMARKS=OFF \ + -DLLVM_INCLUDE_EXAMPLES=OFF \ + -DLLVM_INCLUDE_TESTS=OFF \ + -DCLANG_INCLUDE_TESTS=OFF +cmake --build "$llvm_host_build" -j "$jobs" \ + --target llvm-tblgen clang-tblgen + +llvm_target_build="$source_build_dir/llvm-target-build" +llvm_target_install="$static_dir/llvm-target-install" +cmake \ + -G Ninja \ + -S "$source_build_dir/llvm-project-22.1.3.src/llvm" \ + -B "$llvm_target_build" \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_C_COMPILER="$cc" \ + -DCMAKE_CXX_COMPILER="$cxx" \ + -DCMAKE_AR="$ar" \ + -DCMAKE_RANLIB="$ranlib" \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_SYSTEM_PROCESSOR=x86_64 \ + -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \ + -DLLVM_TABLEGEN="$llvm_host_build/bin/llvm-tblgen" \ + -DCLANG_TABLEGEN="$llvm_host_build/bin/clang-tblgen" \ + -DLLVM_ENABLE_PROJECTS="clang;lld" \ + -DLLVM_TARGETS_TO_BUILD=X86 \ + -DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-unknown-linux-musl \ + -DLLVM_HOST_TRIPLE=x86_64-unknown-linux-musl \ + -DLLVM_ENABLE_BINDINGS=OFF \ + -DLLVM_ENABLE_FFI=OFF \ + -DLLVM_ENABLE_LIBEDIT=OFF \ + -DLLVM_ENABLE_LIBXML2=OFF \ + -DLLVM_ENABLE_PIC=OFF \ + -DLLVM_ENABLE_TERMINFO=OFF \ + -DLLVM_ENABLE_ZLIB=OFF \ + -DLLVM_ENABLE_ZSTD=OFF \ + -DLLVM_INCLUDE_BENCHMARKS=OFF \ + -DLLVM_INCLUDE_EXAMPLES=OFF \ + -DLLVM_INCLUDE_TESTS=OFF \ + -DLLVM_INCLUDE_UTILS=ON \ + -DCLANG_ENABLE_ARCMT=OFF \ + -DCLANG_ENABLE_STATIC_ANALYZER=OFF \ + -DCLANG_INCLUDE_TESTS=OFF \ + -DLLVM_DISTRIBUTION_COMPONENTS="clang;clang-resource-headers;lld;llvm-ar;llvm-nm;llvm-objdump;llvm-readelf;llvm-size;llvm-strings;llvm-strip" +DESTDIR="$llvm_target_install" \ + cmake --build "$llvm_target_build" -j "$jobs" \ + --target install-distribution + +cp -R "$sysroot/usr/include" "$base_root/usr/" +cp -R "$sysroot/usr/lib/." "$base_root/usr/lib/" +cp -R "$sysroot/lib" "$base_root/" +cp -R "$llvm_target_install/usr/." "$base_root/usr/" +} + +build_package_tools() { +pkgconf_target_build="$source_build_dir/pkgconf-pkgconf-2.5.1/build-target" +python3 "$meson_source" setup \ + "$pkgconf_target_build" \ + "$source_build_dir/pkgconf-pkgconf-2.5.1" \ + --cross-file="$cross_file" \ + --prefix=/usr \ + --buildtype=release \ + --default-library=static +python3 "$meson_source" compile -C "$pkgconf_target_build" +DESTDIR="$base_root" \ + python3 "$meson_source" install -C "$pkgconf_target_build" +ln -s pkgconf "$base_root/usr/bin/pkg-config" + +( + cd "$source_build_dir/make-4.4.1" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-load \ + --disable-nls \ + --without-guile + make -j"$jobs" + make DESTDIR="$base_root" install +) +ln -s make "$base_root/usr/bin/gmake" + +ln -s clang "$base_root/usr/bin/cc" +ln -s clang++ "$base_root/usr/bin/c++" +ln -s ld.lld "$base_root/usr/bin/ld" +ln -s llvm-ar "$base_root/usr/bin/ar" +ln -s llvm-nm "$base_root/usr/bin/nm" +ln -s llvm-objdump "$base_root/usr/bin/objdump" +ln -s llvm-readelf "$base_root/usr/bin/readelf" +ln -s llvm-ar "$base_root/usr/bin/llvm-ranlib" +cat >"$base_root/usr/bin/ranlib" <<'EOF' +#!/bin/sh +set -eu + +# BSD ranlib accepts -t to refresh an existing archive index. LLVM's ranlib +# does not expose that option, but rebuilding the index is equivalent for the +# package build that requested it. +if [ "${1-}" = "-t" ]; then + shift +fi + +exec /usr/bin/llvm-ranlib "$@" +EOF +chmod 0755 "$base_root/usr/bin/ranlib" +ln -s llvm-strip "$base_root/usr/bin/strip" +ln -s ../../lib/ld-musl-x86_64.so.1 "$base_root/usr/bin/ldd" + +cat >"$base_root/usr/bin/clang.cfg" <<'EOF' +--target=x86_64-unknown-linux-musl +--sysroot=/ +--ld-path=/usr/bin/ld.lld +--rtlib=compiler-rt +--unwindlib=libunwind +-static +EOF +cat >"$base_root/usr/bin/clang++.cfg" <<'EOF' +--target=x86_64-unknown-linux-musl +--sysroot=/ +--ld-path=/usr/bin/ld.lld +-stdlib=libc++ +--rtlib=compiler-rt +--unwindlib=libunwind +-lc++abi +-static +EOF +} + +case "${MOUSE_NATIVE_TOOLCHAIN_PHASE:-all}" in + llvm) build_llvm ;; + package-tools) build_package_tools ;; + all) + build_llvm + build_package_tools + ;; + *) + printf '%s\n' "invalid MOUSE_NATIVE_TOOLCHAIN_PHASE" >&2 + exit 2 + ;; +esac diff --git a/scripts/static-base/storage.sh b/scripts/static-base/storage.sh new file mode 100644 index 0000000..0666881 --- /dev/null +++ b/scripts/static-base/storage.sh @@ -0,0 +1,107 @@ +#!/bin/sh +set -eu + +component_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$component_dir/common.sh" + +extract() { + tar -xf "$source_dir/$1" -C "$source_build_dir" +} + +extract btrfs-progs-v7.0.tar.xz +extract zstd-1.5.7.tar.gz + +install -d -m 0755 \ + "$sysroot/usr/include" \ + "$sysroot/usr/lib" \ + "$sysroot/usr/lib/pkgconfig" + +make -s -C "$source_build_dir/zstd-1.5.7/lib" -j"$jobs" \ + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + libzstd.a +make -s -C "$source_build_dir/zstd-1.5.7/lib" \ + PREFIX=/usr \ + LIBDIR=/usr/lib \ + PKGCONFIGDIR=/usr/lib/pkgconfig \ + DESTDIR="$sysroot" \ + install-static install-includes install-pc + +make -s -C "$source_build_dir/zstd-1.5.7/programs" -j"$jobs" \ + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + LDFLAGS="-static" \ + ZSTD_LEGACY_SUPPORT=0 \ + ZSTD_MULTITHREAD=0 \ + zstd-release +install -m 0755 \ + "$source_build_dir/zstd-1.5.7/programs/zstd" \ + "$base_root/usr/bin/zstd" +ln -s zstd "$base_root/usr/bin/unzstd" +ln -s zstd "$base_root/usr/bin/zstdcat" +install -d -m 0755 "$base_root/usr/share/man/man1" +install -m 0644 \ + "$source_build_dir/zstd-1.5.7/programs/zstd.1" \ + "$base_root/usr/share/man/man1/zstd.1" + +( + cd "$source_build_dir/util-linux-2.42.2" + CC="$cc" \ + CXX="$cxx" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-all-programs \ + --enable-libblkid \ + --enable-libuuid \ + --disable-shared \ + --enable-static + make -s -j"$jobs" + make -s DESTDIR="$sysroot" install +) + +btrfs_source="$source_build_dir/btrfs-progs-v7.0" +pkgconfig_path="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" +compiler_rt="$sysroot/usr/lib/clang/22/lib/x86_64-unknown-linux-musl/libclang_rt.builtins.a" +( + cd "$btrfs_source" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + CFLAGS="-O2" \ + LDFLAGS="-static" \ + ac_cv_func_malloc_0_nonnull=yes \ + ac_cv_func_realloc_0_nonnull=yes \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + PKG_CONFIG_ALL_STATIC=1 \ + PKG_CONFIG_LIBDIR="$pkgconfig_path" \ + PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-backtrace \ + --disable-documentation \ + --disable-shared \ + --enable-static \ + --disable-convert \ + --disable-lzo \ + --disable-libudev \ + --disable-python \ + --disable-zoned \ + --with-crypto=builtin + MOUSE_CROSS_EXTRA_STATIC_LIBS="$compiler_rt" \ + make -s -j"$jobs" btrfs.static mkfs.btrfs.static + "$strip" btrfs.static mkfs.btrfs.static + install -m 0755 btrfs.static "$base_root/sbin/btrfs" + install -m 0755 mkfs.btrfs.static "$base_root/sbin/mkfs.btrfs" +) + +ln -s btrfs "$base_root/sbin/btrfsck" + +"$tools_dir/mandoc-host/makewhatis" "$base_root/usr/share/man" diff --git a/scripts/static-base/system-services.sh b/scripts/static-base/system-services.sh new file mode 100644 index 0000000..cee90ab --- /dev/null +++ b/scripts/static-base/system-services.sh @@ -0,0 +1,489 @@ +#!/bin/sh +set -eu + +component_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$component_dir/common.sh" + +patch -d "$source_build_dir/tcsh-TCSH6_24_16" -p1 \ + <"$repo_dir/base/patches/tcsh-gethost-native.patch" +patch -d "$source_build_dir/chimerautils-15.0.3" -p1 \ + <"$repo_dir/base/patches/chimerautils-native-cdefs.patch" +patch -d "$source_build_dir/chimerautils-15.0.3" -p1 \ + <"$repo_dir/base/patches/chimerautils-scanflike.patch" + +( + cd "$source_build_dir/tcsh-TCSH6_24_16" + CC="$cc" \ + LIBS="-lcurses -lterminfo" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --bindir=/bin \ + --disable-nls-catalogs + make -j"$jobs" + install -m 0755 tcsh "$base_root/bin/tcsh" +) + +util_linux_build="$source_build_dir/util-linux-build" +install -d -m 0755 "$util_linux_build" +( + cd "$util_linux_build" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + CFLAGS="-O2" \ + LDFLAGS="-static" \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" \ + PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + "$source_build_dir/util-linux-2.42.2/configure" \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --disable-nls \ + --disable-all-programs \ + --enable-libuuid \ + --enable-libblkid \ + --enable-libmount \ + --enable-libsmartcols \ + --enable-libfdisk \ + --enable-agetty \ + --enable-nologin \ + --enable-setsid \ + --enable-mount \ + --enable-blkid \ + --enable-fdisks \ + --enable-losetup \ + --enable-wipefs \ + --enable-swapon \ + --enable-lsblk \ + --enable-findmnt \ + --enable-flock \ + --enable-dmesg \ + --enable-static-programs=blkid,fdisk,losetup,mount,sfdisk,umount + make -s -j"$jobs" \ + agetty nologin setsid mount umount blkid fdisk sfdisk losetup \ + wipefs swapon swapoff lsblk findmnt flock dmesg +) +install_util_linux() { + command=$1 + destination=$2 + binary=$(find "$util_linux_build" -type f -name "$command" -perm -0100 -print | head -1) + if [ -z "$binary" ]; then + printf '%s\n' "missing util-linux build output: $command" >&2 + exit 1 + fi + install -m 0755 "$binary" "$destination" +} +install_util_linux agetty "$base_root/sbin/agetty" +install_util_linux nologin "$base_root/sbin/nologin" +install_util_linux setsid "$base_root/usr/bin/setsid" +install_util_linux mount "$base_root/sbin/mount" +install_util_linux umount "$base_root/sbin/umount" +install_util_linux blkid "$base_root/sbin/blkid" +install_util_linux fdisk "$base_root/sbin/fdisk" +install_util_linux sfdisk "$base_root/sbin/sfdisk" +install_util_linux losetup "$base_root/sbin/losetup" +install_util_linux wipefs "$base_root/sbin/wipefs" +install_util_linux swapon "$base_root/sbin/swapon" +install_util_linux swapoff "$base_root/sbin/swapoff" +install_util_linux lsblk "$base_root/usr/bin/lsblk" +install_util_linux findmnt "$base_root/usr/bin/findmnt" +install_util_linux flock "$base_root/usr/bin/flock" +install_util_linux dmesg "$base_root/sbin/dmesg" + +chimerautils_build="$source_build_dir/chimerautils-15.0.3/build" +PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" \ +PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + python3 "$meson_source" setup \ + "$chimerautils_build" \ + "$source_build_dir/chimerautils-15.0.3" \ + --cross-file="$cross_file" \ + --prefix=/usr \ + --buildtype=release \ + -Dopenssl=enabled \ + -Dlibedit=disabled \ + -Dzlib=disabled \ + -Dlzma=disabled \ + -Dbzip2=disabled \ + -Dzstd=disabled \ + -Dpam=disabled \ + -Dselinux=disabled \ + -Dchimera_realpath=disabled + +commands=" +awk basename cat chmod chown chroot cksum cmp col colrm column comm +cp csplit cut date dd df diff diff3 dirname du echo env expand expr false +ed fetch find fmt fold gencat getopt grep head hexdump hostname id join kill +ln logger logname ls m4 md5 mesg mkdir mkfifo mknod mktemp mv nc nice nl nohup +nproc paste patch pathchk pr printenv printf pwd realpath renice rev rm rmdir script +sdiff sed seq sh sleep sort split stat stty sync tail tee time timeout touch tr true +truncate tsort tty ul uname unexpand uniq users vis wc whereis which who xargs yes +vi +" + +targets=" +src.freebsd/awk/awk +src.freebsd/coreutils/basename/basename +src.freebsd/coreutils/cat/cat +src.freebsd/coreutils/chmod/chmod +src.freebsd/coreutils/chown/chown +src.freebsd/coreutils/chroot/chroot +src.freebsd/coreutils/cksum/cksum +src.freebsd/diffutils/cmp/cmp +src.freebsd/miscutils/col/col +src.freebsd/miscutils/colrm/colrm +src.freebsd/miscutils/column/column +src.freebsd/coreutils/comm/comm +src.freebsd/coreutils/cp/cp +src.freebsd/coreutils/csplit/csplit +src.freebsd/coreutils/cut/cut +src.freebsd/coreutils/date/date +src.freebsd/coreutils/dd/dd +src.freebsd/coreutils/df/df +src.freebsd/diffutils/diff/diff +src.freebsd/diffutils/diff3/diff3 +src.freebsd/coreutils/dirname/dirname +src.freebsd/coreutils/du/du +src.freebsd/ed/ed +src.freebsd/coreutils/echo/echo +src.freebsd/coreutils/env/env +src.freebsd/coreutils/expand/expand +src.freebsd/coreutils/expr/expr +src.freebsd/coreutils/false/false +src.freebsd/fetch/fetch +src.freebsd/findutils/find/find +src.freebsd/coreutils/fmt/fmt +src.freebsd/coreutils/fold/fold +src.freebsd/gencat/gencat +src.freebsd/miscutils/getopt/getopt +src.freebsd/grep/grep +src.freebsd/coreutils/head/head +src.freebsd/miscutils/hexdump/hexdump +src.freebsd/coreutils/hostname/hostname +src.freebsd/coreutils/id/id +src.freebsd/coreutils/join/join +src.freebsd/miscutils/kill/kill +src.freebsd/coreutils/ln/ln +src.freebsd/miscutils/logger/logger +src.freebsd/coreutils/logname/logname +src.freebsd/coreutils/ls/ls +src.freebsd/m4/m4 +src.freebsd/coreutils/md5/md5 +src.freebsd/miscutils/mesg/mesg +src.freebsd/coreutils/mkdir/mkdir +src.freebsd/coreutils/mkfifo/mkfifo +src.freebsd/coreutils/mknod/mknod +src.freebsd/coreutils/mktemp/mktemp +src.freebsd/coreutils/mv/mv +src.freebsd/netcat/nc +src.freebsd/coreutils/nice/nice +src.freebsd/coreutils/nl/nl +src.freebsd/coreutils/nohup/nohup +src.freebsd/coreutils/nproc/nproc +src.freebsd/nvi/vi +src.freebsd/coreutils/paste/paste +src.freebsd/patch/patch +src.freebsd/coreutils/pathchk/pathchk +src.freebsd/coreutils/pr/pr +src.freebsd/coreutils/printenv/printenv +src.freebsd/coreutils/printf/printf +src.freebsd/coreutils/pwd/pwd +src.freebsd/coreutils/realpath/realpath +src.freebsd/miscutils/renice/renice +src.freebsd/miscutils/rev/rev +src.freebsd/coreutils/rm/rm +src.freebsd/coreutils/rmdir/rmdir +src.freebsd/miscutils/script/script +src.freebsd/diffutils/sdiff/sdiff +src.freebsd/sed/sed +src.freebsd/coreutils/seq/seq +src.freebsd/sh/sh +src.freebsd/coreutils/sleep/sleep +src.freebsd/coreutils/sort/sort +src.freebsd/coreutils/split/split +src.freebsd/coreutils/stat/stat +src.freebsd/coreutils/stty/stty +src.freebsd/coreutils/sync/sync +src.freebsd/coreutils/tail/tail +src.freebsd/coreutils/tee/tee +src.freebsd/miscutils/time/time +src.freebsd/coreutils/timeout/timeout +src.freebsd/coreutils/touch/touch +src.freebsd/coreutils/tr/tr +src.freebsd/coreutils/true/true +src.freebsd/coreutils/truncate/truncate +src.freebsd/coreutils/tsort/tsort +src.freebsd/coreutils/tty/tty +src.freebsd/miscutils/ul/ul +src.freebsd/coreutils/uname/uname +src.freebsd/coreutils/unexpand/unexpand +src.freebsd/coreutils/uniq/uniq +src.freebsd/coreutils/users/users +src.freebsd/vis/vis +src.freebsd/coreutils/wc/wc +src.freebsd/miscutils/whereis/whereis +src.freebsd/which/which +src.freebsd/coreutils/who/who +src.freebsd/findutils/xargs/xargs +src.freebsd/coreutils/yes/yes +src.freebsd/coreutils/xinstall/xinstall +src.freebsd/coreutils/test/xtest +" + +PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" \ +PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + python3 "$meson_source" compile -C "$chimerautils_build" $targets + +for command in $commands; do + binary=$(find "$chimerautils_build" -type f -name "$command" -perm -0100 -print | head -1) + if [ -z "$binary" ]; then + printf '%s\n' "missing ChimeraUtils build output: $command" >&2 + exit 1 + fi + destination="$base_root/usr/bin" + case "$command" in + cat|chmod|chown|cp|date|dd|df|echo|hostname|kill|ln|ls|mkdir|mv|pwd|rm|rmdir|sed|sh|sleep|stty|sync|touch|uname) + destination="$base_root/bin" + ;; + esac + install -m 0755 "$binary" "$destination/$command" +done + +ln -s chown "$base_root/bin/chgrp" +ln -s vi "$base_root/usr/bin/nvi" +ln -s vi "$base_root/usr/bin/view" + +( + cd "$source_build_dir/procps-ng-4.0.5" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ac_cv_func_malloc_0_nonnull=yes \ + ac_cv_func_realloc_0_nonnull=yes \ + LDFLAGS="-static" \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig" \ + PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --disable-shared \ + --enable-static \ + --disable-nls \ + --disable-pidof \ + --disable-pidwait \ + --disable-kill \ + --disable-w \ + --disable-numa \ + --without-systemd \ + --without-elogind + make -s -j"$jobs" \ + src/ps/pscommand \ + src/pgrep \ + src/pkill \ + src/top/top \ + src/free \ + src/uptime \ + src/vmstat \ + src/watch \ + src/sysctl + install -m 0755 src/ps/pscommand "$base_root/bin/ps" + install -m 0755 src/pgrep "$base_root/usr/bin/pgrep" + install -m 0755 src/pkill "$base_root/usr/bin/pkill" + install -m 0755 src/top/top "$base_root/usr/bin/top" + install -m 0755 src/free "$base_root/usr/bin/free" + install -m 0755 src/uptime "$base_root/usr/bin/uptime" + install -m 0755 src/vmstat "$base_root/usr/bin/vmstat" + install -m 0755 src/watch "$base_root/usr/bin/watch" + install -m 0755 src/sysctl "$base_root/sbin/sysctl" +) + +iputils_build="$source_build_dir/iputils-20250605/build" +PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" \ +PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + python3 "$meson_source" setup \ + "$iputils_build" \ + "$source_build_dir/iputils-20250605" \ + --cross-file="$cross_file" \ + --prefix=/usr \ + --buildtype=release \ + --default-library=static \ + -DUSE_CAP=false \ + -DUSE_IDN=false \ + -DUSE_GETTEXT=false \ + -DBUILD_ARPING=false \ + -DBUILD_CLOCKDIFF=false \ + -DBUILD_TRACEPATH=false \ + -DBUILD_MANS=false \ + -DSKIP_TESTS=true +python3 "$meson_source" compile -C "$iputils_build" ping +install -m 4755 "$iputils_build/ping/ping" "$base_root/bin/ping" + +( + cd "$source_build_dir/cronie-1.7.2" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + LDFLAGS="-static" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --sysconfdir=/etc \ + --localstatedir=/var \ + --runstatedir=/run \ + --disable-anacron \ + --without-pam \ + --without-selinux \ + --without-audit + make -s -j"$jobs" + install -m 0755 src/crond "$base_root/usr/sbin/cron" + install -m 4755 src/crontab "$base_root/usr/bin/crontab" +) + +( + cd "$source_build_dir/openntpd-7.9p1" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + YACC=byacc \ + LDFLAGS="-static" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --sysconfdir=/etc \ + --localstatedir=/var \ + --runstatedir=/run \ + --with-privsep-user=_ntp \ + --disable-https-constraint \ + --disable-shared \ + --enable-static + make -s -j"$jobs" -C compat + make -s -j"$jobs" -C src ntpd + install -m 0755 src/ntpd "$base_root/usr/sbin/ntpd" + ln -s ntpd "$base_root/usr/sbin/ntpctl" +) + +( + cd "$source_build_dir/sysklogd-2.7.2" + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + LDFLAGS="-static" \ + PKG_CONFIG="$tools_dir/bin/pkgconf" \ + ./configure \ + --build="$build_triplet" \ + --host=x86_64-linux-musl \ + --prefix=/usr \ + --sysconfdir=/etc \ + --runstatedir=/run \ + --disable-shared \ + --enable-static \ + --without-logger \ + --with-systemd=no + make -s -j"$jobs" -C src syslogd + install -m 0755 src/syslogd "$base_root/usr/sbin/syslogd" +) + +( + cd "$source_build_dir/skalibs-2.15.1.0" + CC="$cc" AR="$ar" RANLIB="$ranlib" ./configure \ + --prefix=/usr \ + --build="$build_triplet" \ + --target=x86_64-linux-musl \ + --with-include="$sysroot/usr/include" \ + --with-lib="$sysroot/usr/lib" \ + --with-sysdep-devurandom=yes \ + --with-sysdep-posixspawnearlyreturn=no \ + --with-sysdep-procselfexe=/proc/self/exe \ + --with-sysdep-selectinfinite=yes \ + --disable-shared + make -s -j"$jobs" CC="$cc" AR="$ar" RANLIB="$ranlib" + make -s DESTDIR="$sysroot" install +) + +( + cd "$source_build_dir/mdevd-0.1.8.2" + CC="$cc" AR="$ar" RANLIB="$ranlib" ./configure \ + --prefix=/usr \ + --build="$build_triplet" \ + --target=x86_64-linux-musl \ + --with-sysdeps="$sysroot/usr/lib/skalibs/sysdeps" \ + --with-include="$sysroot/usr/include" \ + --with-lib="$sysroot/usr/lib" \ + --enable-static-libc \ + --disable-shared + make -s -j"$jobs" CC="$cc" AR="$ar" RANLIB="$ranlib" + install -m 0755 mdevd "$base_root/sbin/mdevd" + install -m 0755 mdevd-coldplug "$base_root/sbin/mdevd-coldplug" +) + +kmod_build="$source_build_dir/kmod-34.2/build" +PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" \ +PKG_CONFIG_SYSROOT_DIR="$sysroot" \ + python3 "$meson_source" setup \ + "$kmod_build" \ + "$source_build_dir/kmod-34.2" \ + --cross-file="$cross_file" \ + --prefix=/usr \ + --buildtype=release \ + --default-library=static \ + -Dzstd=disabled \ + -Dxz=disabled \ + -Dzlib=disabled \ + -Dopenssl=disabled \ + -Dmanpages=false \ + -Ddocs=false \ + -Dbuild-tests=false +python3 "$meson_source" compile -C "$kmod_build" kmod:executable +install -m 0755 "$kmod_build/kmod" "$base_root/sbin/kmod" +for command in depmod insmod lsmod modinfo modprobe rmmod; do + ln -s kmod "$base_root/sbin/$command" +done + +( + cd "$source_build_dir/openssl-3.5.7" + make -s distclean + CC="$cc" \ + AR="$ar" \ + RANLIB="$ranlib" \ + ./Configure \ + linux-x86_64 \ + --prefix=/usr \ + --libdir=lib \ + --openssldir=/etc/ssl \ + no-afalgeng \ + no-docs \ + no-dso \ + no-module \ + no-secure-memory \ + no-shared \ + no-tests + make -s -j"$jobs" build_generated + make -s depend + make -s -j"$jobs" apps/openssl + install -m 0755 apps/openssl "$base_root/usr/bin/openssl" +) + +install -m 0755 \ + "$chimerautils_build/src.freebsd/coreutils/xinstall/xinstall" \ + "$base_root/usr/bin/install" +install -m 0755 \ + "$chimerautils_build/src.freebsd/coreutils/test/xtest" \ + "$base_root/usr/bin/test" + +ln -s test "$base_root/usr/bin/[" +ln -s install "$base_root/usr/bin/binstall" +ln -s id "$base_root/usr/bin/groups" +ln -s id "$base_root/usr/bin/whoami" +ln -s md5 "$base_root/usr/bin/sha256" +ln -s stat "$base_root/usr/bin/readlink" + +install -m 0755 "$sysroot/usr/bin/tput" "$base_root/usr/bin/tput" +ln -s tcsh "$base_root/bin/csh" diff --git a/tests/acceptance/boot.exp b/tests/acceptance/boot.exp new file mode 100755 index 0000000..c0da336 --- /dev/null +++ b/tests/acceptance/boot.exp @@ -0,0 +1,61 @@ +#!/usr/bin/expect -f + +if {$argc != 1} { + puts stderr "usage: boot.exp RUNNER" + exit 2 +} + +set runner [file normalize [lindex $argv 0]] +set timeout 30 +set acceptance_name "boot" +source [file join [file dirname [info script]] harness.exp] + +proc boot_mouse {runner} { + global spawn_id + spawn -noecho $runner + await_exact "Cheesed to meet you! v0.1.0" "the cheesed startup banner" + await_exact "cheesed: base bootstrap complete; cheesed is PID 1" "PID 1 confirmation" + await_exact "MOUSE 0.1.0 (x86_64)" "the MOUSE login banner" + await_exact "mouse:~# " "the root shell prompt" +} + +boot_mouse $runner + +send -- "hostname\r" +await_exact "mouse\r" "the configured hostname" +await_exact "mouse:~# " "the prompt after hostname" + +send -- "cat /proc/1/comm\r" +await_exact "cheesed\r" "cheesed in /proc/1" +await_exact "mouse:~# " "the prompt after the PID 1 check" + +send -- "sh -c 'test -c /dev/null'; echo DEV_STATUS_\$?\r" +await_exact "DEV_STATUS_0\r" "a mounted devtmpfs" +await_exact "mouse:~# " "the prompt after the devtmpfs check" + +send -- "clear; echo CLEAR_STATUS_\$?\r" +await_exact "CLEAR_STATUS_0\r" "a successful clear command" +await_exact "mouse:~# " "the prompt after clear" + +send -- {sh -c 'i=0; while [ "$i" -lt 8 ]; do (sleep 1) & i=$((i + 1)); done'} +send -- "\r" +await_exact "mouse:~# " "the prompt after starting orphan probes" +await_exact "cheesed: reaped orphan PID " "PID 1 orphan reaping" + +send -- "sleep 2\r" +await_exact "mouse:~# " "all orphan probes to exit" +send -- {sh -c 'if grep -l ") Z " /proc/[0-9]*/stat >/dev/null; then hostname; else cat /proc/1/comm; fi'} +send -- "\r" +await_exact "cheesed\r" "a zombie-free process table" +await_exact "mouse:~# " "the prompt after the zombie check" + +send -- "kill -TERM 1\r" +await_exact "cheesed: received poweroff request" "the poweroff request" +await_clean_exit "poweroff" + +boot_mouse $runner +send -- "kill -INT 1\r" +await_exact "cheesed: received reboot request" "the reboot request" +await_clean_exit "reboot" + +puts "boot acceptance tests passed" diff --git a/tests/acceptance/boot.sh b/tests/acceptance/boot.sh new file mode 100755 index 0000000..badf7db --- /dev/null +++ b/tests/acceptance/boot.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the boot acceptance test" expect +acceptance_copy_disk boot + +MOUSE_KERNEL_ARGS="console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1 quiet loglevel=3" +MOUSE_BOOT_MODE=disk +MOUSE_DISK_IMAGE="$test_disk" +MOUSE_NETWORK=0 +export MOUSE_KERNEL_ARGS +export MOUSE_BOOT_MODE +export MOUSE_DISK_IMAGE +export MOUSE_NETWORK + +exec expect "$acceptance_dir/boot.exp" "$runner" diff --git a/tests/acceptance/btrfs.exp b/tests/acceptance/btrfs.exp new file mode 100755 index 0000000..6104cd3 --- /dev/null +++ b/tests/acceptance/btrfs.exp @@ -0,0 +1,71 @@ +#!/usr/bin/expect -f + +if {$argc != 3} { + puts stderr "usage: btrfs.exp RUNNER DISK MODE" + exit 2 +} + +set runner [file normalize [lindex $argv 0]] +set disk [file normalize [lindex $argv 1]] +set mode [lindex $argv 2] +if {$mode ni {first alternate rollback powerloss-stage powerloss-verify}} { + puts stderr "invalid btrfs mode: $mode" + exit 2 +} +set timeout 180 +set acceptance_name "btrfs" +source [file join [file dirname [info script]] harness.exp] + +acceptance_log +set env(MOUSE_DISK_IMAGE) $disk +set env(MOUSE_MEMORY) 1G + +set env(MOUSE_BOOT_MODE) disk +set env(MOUSE_KERNEL_ARGS) "console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1" +spawn -noecho $runner + +await_exact "cheesed: base bootstrap complete; cheesed is PID 1" "PID 1 confirmation" +await_exact "mouse login: root (automatic login)" "the serial login" +await_prompt +sendline {sh -c "grep -Eq '^[^ ]+ / btrfs ' /proc/mounts && grep -Eq '^[^ ]+ /usr/local btrfs ' /proc/mounts && grep -Eq '^[^ ]+ /var btrfs ' /proc/mounts && grep -Eq '^[^ ]+ /home btrfs ' /proc/mounts && grep -Eq '^[^ ]+ /srv btrfs ' /proc/mounts"; echo MOUNTS_$?} +await_exact "MOUNTS_0\r" "the btrfs root and persistent subvolume mounts" +await_exact "mouse:~# " "the prompt after mount verification" +sendline {btrfs version; echo VERSION_$?} +await_exact "btrfs-progs v7.0\r" "the pinned btrfs userspace version" +await_exact "VERSION_0\r" "the btrfs version status" +await_exact "mouse:~# " "the prompt after version" + +if {$mode in {first powerloss-stage}} { + sendline {sh -c 'test "$(cat /etc/mouse-boot-environment)" = default && mkdir -p /var/lib/mouse /home/mouse-state /usr/local/share/mouse /srv/mouse && echo base-default >/etc/bootenv-state && echo var-shared >/var/lib/mouse/state && echo home-shared >/home/mouse-state/value && echo local-shared >/usr/local/share/mouse/state && echo srv-shared >/srv/mouse/state'; echo WRITE_$?} + await_exact "WRITE_0\r" "boot-environment and persistent state creation" + await_exact "mouse:~# " "the prompt after state creation" + sendline {mouse-bootenv prepare; echo PREPARE_$?} + await_exact "alternate is ready for an update\r" "the inactive snapshot preparation" + await_exact "PREPARE_0\r" "the preparation status" + await_exact "mouse:~# " "the prompt after preparation" + sendline {sh -c 'echo alternate-updated >/run/mouse-root/ROOT/alternate/etc/bootenv-state'; mouse-bootenv activate alternate; echo ACTIVATE_$?} + await_exact "alternate will boot next\r" "the alternate activation" + await_exact "ACTIVATE_0\r" "the activation status" + if {$mode eq "powerloss-stage"} { + exec kill -KILL [exp_pid] + expect eof + catch wait + puts "power-loss interruption staged" + exit 0 + } +} elseif {$mode in {alternate powerloss-verify}} { + sendline {sh -c 'test "$(cat /etc/mouse-boot-environment)" = alternate && test "$(cat /etc/bootenv-state)" = alternate-updated && test "$(cat /var/lib/mouse/state)" = var-shared && test "$(cat /home/mouse-state/value)" = home-shared && test "$(cat /usr/local/share/mouse/state)" = local-shared && test "$(cat /srv/mouse/state)" = srv-shared'; echo ALTERNATE_$?} + await_exact "ALTERNATE_0\r" "the alternate root and shared persistent state" + if {$mode eq "alternate"} { + await_exact "mouse:~# " "the prompt after alternate verification" + sendline {mouse-bootenv rollback; echo ROLLBACK_$?} + await_exact "default will boot next\r" "the default rollback activation" + await_exact "ROLLBACK_0\r" "the rollback status" + } +} else { + sendline {sh -c 'test "$(cat /etc/mouse-boot-environment)" = default && test "$(cat /etc/bootenv-state)" = base-default && test "$(cat /var/lib/mouse/state)" = var-shared && test "$(cat /home/mouse-state/value)" = home-shared && test "$(cat /usr/local/share/mouse/state)" = local-shared && test "$(cat /srv/mouse/state)" = srv-shared'; echo DEFAULT_$?} + await_exact "DEFAULT_0\r" "the rolled-back root and shared persistent state" +} +await_exact "mouse:~# " "the prompt before poweroff" +poweroff +puts "$mode btrfs acceptance passed" diff --git a/tests/acceptance/btrfs.sh b/tests/acceptance/btrfs.sh new file mode 100755 index 0000000..ed52eec --- /dev/null +++ b/tests/acceptance/btrfs.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the btrfs acceptance test" expect +acceptance_copy_disk btrfs + +"$acceptance_dir/btrfs.exp" "$runner" "$test_disk" first +"$acceptance_dir/btrfs.exp" "$runner" "$test_disk" alternate +"$acceptance_dir/btrfs.exp" "$runner" "$test_disk" rollback + +powerloss_disk="$temporary/mouse-disk-powerloss.img" +acceptance_clone_disk "$build_dir/mouse-disk.img" "$powerloss_disk" +"$acceptance_dir/btrfs.exp" "$runner" "$powerloss_disk" powerloss-stage +"$acceptance_dir/btrfs.exp" "$runner" "$powerloss_disk" powerloss-verify + +printf '%s\n' \ + "btrfs root, boot-environment, and power-loss acceptance tests passed" diff --git a/tests/acceptance/firmware.exp b/tests/acceptance/firmware.exp new file mode 100755 index 0000000..e4b8eee --- /dev/null +++ b/tests/acceptance/firmware.exp @@ -0,0 +1,46 @@ +#!/usr/bin/expect -f + +if {$argc != 3} { + puts stderr "usage: firmware.exp RUNNER LABEL CHECK" + exit 2 +} + +set runner [file normalize [lindex $argv 0]] +set label [lindex $argv 1] +set check [lindex $argv 2] +set timeout 90 +set acceptance_name "firmware-$check" +source [file join [file dirname [info script]] harness.exp] + +acceptance_log +spawn -noecho $runner +await_exact "Cheesed to meet you! v0.1.0" "MOUSE through the Limine firmware path" +await_exact "mouse login: " "the authenticated firmware-boot login" +sendline {root} +await_exact "Password: " "the root password prompt" +sendline {cheddar} +await_exact "mouse:~# " "the root shell" +switch -- $check { + raw-virtio { + set check_command {[ -e /sys/block/vda ]} + } + raw-sata { + set check_command {[ -e /sys/block/sda ]} + } + raw-uefi-sata { + set check_command {[ -d /sys/firmware/efi ] && [ -e /sys/block/sda ]} + } + raw-uefi-nvme { + set check_command {[ -d /sys/firmware/efi ] && [ -e /sys/block/nvme0n1 ]} + } + default { + puts stderr "unknown firmware check: $check" + exit 2 + } +} +sendline "sh -c 'grep -q \"root=PARTUUID=4d4f5553-4500-4000-8000-000000000002\" /proc/cmdline && grep -Eq \"^\[^ \]+ / btrfs \" /proc/mounts && $check_command'; echo FIRMWARE_\$?" +await_exact "FIRMWARE_0\r" "the GPT, Limine, PARTUUID, and Btrfs boot contract" +await_exact "mouse:~# " "the prompt after firmware verification" +poweroff + +puts "$label acceptance passed" diff --git a/tests/acceptance/firmware.sh b/tests/acceptance/firmware.sh new file mode 100755 index 0000000..1db6631 --- /dev/null +++ b/tests/acceptance/firmware.sh @@ -0,0 +1,45 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the firmware acceptance test" expect +acceptance_require_file \ + "$build_dir/mouse-disk.img" \ + "run 'make check' before checking BIOS firmware" +acceptance_require_file \ + "$build_dir/ovmf-code.fd" \ + "run 'make check' before checking UEFI firmware" +acceptance_make_temporary firmware + +raw_disk="$temporary/mouse-disk.img" +acceptance_clone_disk "$build_dir/mouse-disk.img" "$raw_disk" + +run_firmware() +{ + label=$1 + check=$2 + disk=$3 + bus=$4 + firmware=$5 + + MOUSE_BOOT_MODE=firmware \ + MOUSE_DISK_IMAGE="$disk" \ + MOUSE_DISK_BUS="$bus" \ + MOUSE_FIRMWARE="$firmware" \ + MOUSE_UEFI_CODE="$build_dir/ovmf-code.fd" \ + MOUSE_MEMORY="${MOUSE_MEMORY:-1G}" \ + expect "$acceptance_dir/firmware.exp" "$runner" "$label" "$check" +} + +run_firmware "BIOS raw VirtIO" raw-virtio \ + "$raw_disk" virtio bios +run_firmware "BIOS raw AHCI" raw-sata \ + "$raw_disk" sata bios +run_firmware "UEFI raw AHCI" raw-uefi-sata \ + "$raw_disk" sata uefi +run_firmware "UEFI raw NVMe" raw-uefi-nvme \ + "$raw_disk" nvme uefi + +printf '%s\n' "BIOS and UEFI firmware acceptance passed" diff --git a/tests/acceptance/harness.exp b/tests/acceptance/harness.exp new file mode 100644 index 0000000..8a9c8f6 --- /dev/null +++ b/tests/acceptance/harness.exp @@ -0,0 +1,73 @@ +if {![info exists acceptance_name]} { + puts stderr "acceptance harness requires acceptance_name" + exit 2 +} + +proc fail {message} { + global acceptance_name + puts stderr "$acceptance_name acceptance failed: $message" + exit 1 +} + +proc await_exact {value description} { + expect { + -exact $value { return } + timeout { fail "timed out waiting for $description" } + eof { fail "QEMU exited while waiting for $description" } + } +} + +proc await {value description} { + await_exact $value $description +} + +proc await_regexp {value description} { + expect { + -re $value { return } + timeout { fail "timed out waiting for $description" } + eof { fail "QEMU exited while waiting for $description" } + } +} + +proc sendline {line} { + send -- $line + send -- "\r" +} + +proc prompt {} { + await_exact "mouse:~# " "the root prompt" +} + +proc await_prompt {} { + send -- "\r" + await_exact "mouse:~# " "the root prompt" +} + +proc await_clean_exit {action} { + expect { + eof { + set result [wait] + set status [lindex $result 3] + if {$status != 0} { + fail "QEMU exited with status $status after $action" + } + } + timeout { fail "QEMU did not exit after $action" } + } +} + +proc poweroff {} { + sendline {poweroff} + await_exact "cheesed: received poweroff request" "the poweroff request" + await_exact "cheesed: root filesystem remounted read-only" "the read-only root remount" + await_clean_exit "poweroff" +} + +proc acceptance_log {} { + global env + if {[info exists env(MOUSE_EXPECT_LOG)]} { + log_user 1 + } else { + log_user 0 + } +} diff --git a/tests/acceptance/harness.sh b/tests/acceptance/harness.sh new file mode 100644 index 0000000..8da567f --- /dev/null +++ b/tests/acceptance/harness.sh @@ -0,0 +1,67 @@ +#!/bin/sh + +acceptance_init() { + acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + repo_dir=$(CDPATH= cd -- "$acceptance_dir/../.." && pwd) + runner="$repo_dir/scripts/run-qemu.sh" + build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"} + temporary= +} + +acceptance_require_commands() { + description=$1 + shift + for command in "$@"; do + if ! command -v "$command" >/dev/null 2>&1; then + printf '%s\n' "$command is required for $description" >&2 + exit 1 + fi + done +} + +acceptance_require_file() { + path=$1 + instruction=$2 + if [ ! -f "$path" ]; then + printf '%s\n' "$instruction" >&2 + exit 1 + fi +} + +acceptance_cleanup_extra() { + : +} + +acceptance_cleanup() { + acceptance_cleanup_extra + if [ -n "$temporary" ] && [ -d "$temporary" ]; then + rm -rf "$temporary" + fi +} + +acceptance_make_temporary() { + name=$1 + temporary=$(mktemp -d "${TMPDIR:-/tmp}/mouse-$name.XXXXXX") + trap acceptance_cleanup EXIT HUP INT TERM +} + +acceptance_copy_disk() { + name=$1 + description=${2:-$1} + base_disk="$build_dir/mouse-disk.img" + acceptance_require_file \ + "$base_disk" \ + "run 'make check' before checking $description" + acceptance_make_temporary "$name" + test_disk="$temporary/mouse-disk.img" + acceptance_clone_disk "$base_disk" "$test_disk" +} + +acceptance_clone_disk() { + source=$1 + destination=$2 + if [ "$(uname -s)" = Darwin ] && cp -c "$source" "$destination" 2>/dev/null; then + return + fi + cp "$source" "$destination" +} diff --git a/tests/acceptance/multi-user.exp b/tests/acceptance/multi-user.exp new file mode 100755 index 0000000..88a8b1a --- /dev/null +++ b/tests/acceptance/multi-user.exp @@ -0,0 +1,99 @@ +#!/usr/bin/expect -f + +if {$argc != 3} { + puts stderr "usage: multi-user.exp RUNNER DISK MODE" + exit 2 +} +set runner [file normalize [lindex $argv 0]] +set disk [file normalize [lindex $argv 1]] +set mode [lindex $argv 2] +if {$mode ni {provision login verify}} { + puts stderr "invalid multi-user mode: $mode" + exit 2 +} +set timeout 75 +set acceptance_name "multi-user" +source [file join [file dirname [info script]] harness.exp] + +acceptance_log +set env(MOUSE_BOOT_MODE) disk +set env(MOUSE_DISK_IMAGE) $disk +set env(MOUSE_NETWORK) 0 +if {$mode eq "provision"} { + set env(MOUSE_KERNEL_ARGS) "console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1 quiet loglevel=3" +} else { + set env(MOUSE_KERNEL_ARGS) "console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed panic=-1 quiet loglevel=3" +} +spawn -noecho $runner + +if {$mode eq "provision"} { + await "mouse login: root (automatic login)" "the provisioning login" + await "mouse:~# " "the provisioning shell" + sendline {groupadd -R /run/mouse-root/ROOT/default operators; useradd -R /run/mouse-root/ROOT/default -M -g operators -s /bin/tcsh alice; mkdir /run/mouse-root/home/alice; chown 1000:1000 /run/mouse-root/home/alice; echo CREATE_$status} + await "CREATE_0\r" "account creation" + await "mouse:~# " "the provisioning prompt" + sendline {sh -c 'grep -q "^root:x:0:0:root:/root:/bin/sh$" /run/mouse-root/ROOT/default/etc/passwd && grep -q "^toor:x:0:0:BSD-style root account:/root:/bin/tcsh$" /run/mouse-root/ROOT/default/etc/passwd'; echo ADMIN_$status} + await "ADMIN_0\r" "the BSD-style administrator accounts" + await "mouse:~# " "the provisioning prompt" + sendline {sh -c 'usermod -R /run/mouse-root/ROOT/default -p "$(head -n 1 /run/mouse-root/ROOT/default/etc/shadow | cut -d: -f2)" alice'; echo PASSWORD_$status} + await "PASSWORD_0\r" "the alternate-root password setup" + await "mouse:~# " "the provisioning prompt" + sendline {sh -c 'test "$(grep "^alice:" /run/mouse-root/ROOT/default/etc/shadow | cut -d: -f2)" = "$(head -n 1 /run/mouse-root/ROOT/default/etc/shadow | cut -d: -f2)" && grep -q "^alice:x:1000:1000::/home/alice:/bin/tcsh$" /run/mouse-root/ROOT/default/etc/passwd'; echo ACCOUNT_$status} + await "ACCOUNT_0\r" "the on-disk login account" + await "mouse:~# " "the provisioning prompt" + sendline {echo enable_mouse_echo=YES >/run/mouse-root/ROOT/default/etc/rc.conf; echo enable_network=NO >>/run/mouse-root/ROOT/default/etc/rc.conf; echo POLICY_$status} + await "POLICY_0\r" "the offline test policy" + await "mouse:~# " "the provisioning prompt" + poweroff + puts "account provisioning acceptance passed" + exit 0 +} + +await "mouse login: " "the authenticated login prompt" +set timeout 2 +expect { + -exact "root (automatic login)" { fail "normal disk enabled root autologin" } + timeout {} +} +set timeout 75 +sendline {alice} +await "Password: " "the password prompt" +if {$mode eq "login"} { + sendline {wrong-password} + await "Login incorrect" "failed authentication rejection" + await "mouse login: " "the retry login prompt" + sendline {alice} + await "Password: " "the second password prompt" +} +sendline {cheddar} +await "mouse:~> " "the ordinary user shell" +sendline {id} +await "uid=1000(alice) gid=1000(operators) groups=1000(operators)\r" "the ordinary identity" +await "mouse:~> " "the user prompt" +sendline {sh -c 'test "$HOME" = /home/alice && test "$SHELL" = /bin/tcsh && test -O /home/alice && ! touch /usr/alice-must-not-write'; echo USER_$status} +await "USER_0\r" "home ownership and base protection" +await "mouse:~> " "the user prompt" +sendline {su - root} +await "Password: " "the explicit elevation password" +sendline {cheddar} +await "mouse:~# " "the elevated root shell" +sendline {id} +await "uid=0(root) gid=0(root) groups=0(root)\r" "authorised root elevation" +await "mouse:~# " "the root prompt" +sendline {sh -c 'test -d /bin && test -d /sbin && test -d /usr/bin && test -d /usr/sbin && test -x /bin/ln && test -x /sbin/cheesed && test -x /usr/bin/passwd && test -x /usr/sbin/useradd'; echo FHS_$?} +await "FHS_0\r" "the FHS command-directory split" +await "mouse:~# " "the root prompt" +sendline {sh -c 'rm -f /tmp/ln-source /tmp/ln-target; echo mouse >/tmp/ln-source; ln /tmp/ln-source /tmp/ln-target; test "$(cat /tmp/ln-target)" = mouse'; echo LN_$?} +await "LN_0\r" "the statically linked ln command" +await "mouse:~# " "the root prompt" +sendline {exit} +await "mouse:~> " "the ordinary user shell after leaving root" +sendline {su - toor} +await "Password: " "the toor password prompt" +sendline {cheddar} +await "mouse:~# " "the toor shell" +sendline {echo $LOGNAME $SHELL} +await "toor /bin/tcsh\r" "the toor login and friendly shell" +await "mouse:~# " "the toor prompt" +poweroff +puts "$mode authenticated multi-user acceptance passed" diff --git a/tests/acceptance/multi-user.sh b/tests/acceptance/multi-user.sh new file mode 100755 index 0000000..d107c5c --- /dev/null +++ b/tests/acceptance/multi-user.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the multi-user acceptance test" expect +acceptance_copy_disk multi-user "multi-user operation" + +"$acceptance_dir/multi-user.exp" "$runner" "$test_disk" provision +"$acceptance_dir/multi-user.exp" "$runner" "$test_disk" login +"$acceptance_dir/multi-user.exp" "$runner" "$test_disk" verify +printf '%s\n' "multi-user acceptance tests passed" diff --git a/tests/acceptance/network.exp b/tests/acceptance/network.exp new file mode 100755 index 0000000..cf940b3 --- /dev/null +++ b/tests/acceptance/network.exp @@ -0,0 +1,77 @@ +#!/usr/bin/expect -f + +if {$argc != 3} { + puts stderr "usage: network.exp RUNNER DISK MODE" + exit 2 +} +set runner [file normalize [lindex $argv 0]] +set disk [file normalize [lindex $argv 1]] +set mode [lindex $argv 2] +if {$mode ni {first verify}} { + puts stderr "invalid network mode: $mode" + exit 2 +} +set timeout 90 +set acceptance_name "network" +source [file join [file dirname [info script]] harness.exp] +proc poweroff {} { + sendline {kill -TERM 1} + await "cheesed: received poweroff request" "the poweroff request" + await "Stopping network" "the network shutdown" + await "cheesed: root filesystem remounted read-only" "the read-only remount" + expect { + eof { + set result [wait] + if {[lindex $result 3] != 0} { + fail "QEMU exited unsuccessfully" + } + } + timeout { fail "QEMU did not power off" } + } +} + +acceptance_log +set env(MOUSE_BOOT_MODE) disk +set env(MOUSE_DISK_IMAGE) $disk +set env(MOUSE_NETWORK) 1 +set env(MOUSE_KERNEL_ARGS) "console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1" +spawn -noecho $runner + +await "Starting network" "the network service" +await "mouse login: root (automatic login)" "the test login" +await "cheesed: service graph started; cgroup supervision active" "the completed service graph" +send -- "\r" +prompt + +sendline {sh -c 'test -s /run/mouse-network.ready && grep -q eth0 /proc/net/route && grep -q 00000000 /proc/net/route && grep -q "^nameserver " /etc/resolv.conf'; echo LEASE_$?} +await "LEASE_0\r" "the DHCP route and resolver state" +prompt +sendline {nc -z -w 5 1.1.1.1 80; echo IP_$?} +await "IP_0\r" "the IP endpoint" +prompt +sendline {nc -z -w 5 example.com 80; echo DNS_$?} +await "DNS_0\r" "the DNS endpoint" +prompt +sendline {service network status; echo STATUS_$?} +await "network running" "accurate service status" +await "STATUS_0\r" "the service status exit code" +prompt +sendline {service network restart} +await "Stopping network" "runtime network stop" +await "Starting network" "runtime network restart" +prompt +sendline {sh -c 'test -s /run/mouse-network.ready'; echo RESTART_$?} +await "RESTART_0\r" "the renewed lease" +prompt + +if {$mode eq "first"} { + sendline {sed 's/metric 100/metric 200/' /etc/dhcpcd.conf >/run/dhcpcd.conf.new && mv /run/dhcpcd.conf.new /etc/dhcpcd.conf; echo POLICY_$?} + await "POLICY_0\r" "the persistent policy update" + prompt +} else { + sendline {sh -c 'grep -q "^metric 200$" /etc/dhcpcd.conf && grep -q "eth0.*200" /proc/net/route'; echo PERSIST_$?} + await "PERSIST_0\r" "the persisted route metric" + prompt +} +poweroff +puts "$mode network acceptance passed" diff --git a/tests/acceptance/network.sh b/tests/acceptance/network.sh new file mode 100755 index 0000000..ac4a201 --- /dev/null +++ b/tests/acceptance/network.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the network acceptance test" expect +acceptance_copy_disk network networking + +"$acceptance_dir/network.exp" "$runner" "$test_disk" first +"$acceptance_dir/network.exp" "$runner" "$test_disk" verify +printf '%s\n' "network acceptance tests passed" diff --git a/tests/acceptance/pkgsrc.exp b/tests/acceptance/pkgsrc.exp new file mode 100755 index 0000000..94f45ed --- /dev/null +++ b/tests/acceptance/pkgsrc.exp @@ -0,0 +1,66 @@ +#!/usr/bin/expect -f + +if {$argc != 3} { + puts stderr "usage: pkgsrc.exp RUNNER DISK MODE" + exit 2 +} +set runner [file normalize [lindex $argv 0]] +set disk [file normalize [lindex $argv 1]] +set mode [lindex $argv 2] +if {$mode ni {install verify}} { + puts stderr "invalid pkgsrc mode: $mode" + exit 2 +} +set timeout 1800 +set acceptance_name "pkgsrc" +source [file join [file dirname [info script]] harness.exp] + +acceptance_log +set env(MOUSE_BOOT_MODE) disk +set env(MOUSE_DISK_IMAGE) $disk +set env(MOUSE_NETWORK) 1 +set env(MOUSE_MEMORY) 2G +set env(MOUSE_KERNEL_ARGS) "console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1" +spawn -noecho $runner + +await_exact "mouse login: root (automatic login)" "the serial login" +await_prompt + +if {$mode eq "install"} { + sendline {sh -c 'test "$(cat /usr/share/mouse/pkgsrc.version)" = 2026Q2 && test ! -e /usr/share/mouse/pkgsrc-2026Q2.tar.gz'; echo SOURCE_$?} + await_exact "SOURCE_0\r" "the pkgsrc release marker without a duplicate archive" + await_exact "mouse:~# " "the prompt after source verification" + + sendline {sh -c "test \"$(command -v make)\" = /usr/bin/make && make --version | grep -q '^GNU Make 4.4.1$' && test \"$(command -v bmake)\" = /usr/local/bin/bmake && test ! -e /usr/share/mk && test \"$(command -v pkg_add)\" = /usr/local/sbin/pkg_add && test \"$(command -v pkg_info)\" = /usr/local/sbin/pkg_info && test \"$(command -v less)\" = /usr/bin/less && test -f /usr/local/share/mk/bsd.own.mk && test -d /var/db/pkg && test -d /usr/local/src/pkgsrc && pkg_info -e 'bootstrap-mk-files-[0-9]*' >/dev/null && pkg_info -e 'bmake-[0-9]*' >/dev/null && pkg_info -e 'pkg_install-[0-9]*' >/dev/null"; echo SEED_$?} + await_exact "SEED_0\r" "the release-built pkgsrc seed" + await_exact "mouse:~# " "the prompt after seed verification" + + sendline {sh -c "grep -Eq '^TOOLS_PLATFORM\.awk\?=[[:space:]]*/usr/bin/awk$' /usr/local/etc/mk.conf && grep -Eq '^LOCALBASE=[[:space:]]*/usr/local$' /usr/local/etc/mk.conf && grep -Eq '^PKG_DBDIR=[[:space:]]*/var/db/pkg$' /usr/local/etc/mk.conf && grep -Eq '^INIT_SYSTEM=[[:space:]]*rc\.d$' /usr/local/etc/mk.conf && grep -Eq '^RCD_SCRIPTS_DIR=[[:space:]]*/usr/local/etc/rc\.d$' /usr/local/etc/mk.conf && grep -Eq '^PKG_RCD_SCRIPTS=[[:space:]]*YES$' /usr/local/etc/mk.conf && test -L /usr/local/etc/rc.subr"; echo POLICY_$?} + await_exact "POLICY_0\r" "the pkgsrc prefix, database, service, and MOUSE ABI policy" + await_exact "mouse:~# " "the prompt after policy verification" + + sendline {cp -R /usr/share/mouse/pkgsrc-fixture/local /usr/local/src/pkgsrc/; bmake -C /usr/local/src/pkgsrc/local/mouse-hello install; echo INSTALL_$?} + await_exact "INSTALL_0\r" "the pkgsrc package installation" + await_exact "mouse:~# " "the prompt after package installation" + sendline {/usr/local/bin/mouse-hello; sh -c "test -x /usr/local/etc/rc.d/mouse_hello && /usr/local/sbin/pkg_info -L mouse-hello | grep -qx /usr/local/share/examples/rc.d/mouse_hello && readelf -l /usr/local/bin/mouse-hello | grep -q INTERP && readelf -d /usr/local/bin/mouse-hello | grep -q NEEDED"; /usr/local/sbin/pkg_info -e 'mouse-hello-[0-9]*'; echo PACKAGE_$?} + await_exact "Hello from pkgsrc on MOUSE\r" "the installed pkgsrc command" + await_regexp {mouse-hello-1\.0\r} "the pkgsrc package record" + await_exact "PACKAGE_0\r" "the installed package status" + await_exact "mouse:~# " "the prompt before starting the newly installed service" + sendline {service mouse_hello start; sh -c 'test -f /run/mouse-hello-service.ready'; service mouse_hello status; echo RUNTIME_$?} + await_exact "mouse_hello running\r" "the newly discovered pkgsrc service start" + await_exact "mouse_hello running\r" "the newly discovered pkgsrc service status" + await_exact "RUNTIME_0\r" "the newly installed service runtime control" + await_exact "mouse:~# " "the prompt before enabling the pkgsrc service" + sendline {printf '%s\n' 'enable_mouse_hello=YES' >>/etc/rc.conf; echo ENABLE_$?} + await_exact "ENABLE_0\r" "the persistent pkgsrc service policy" +} else { + sendline {sh -c 'test -x /usr/local/bin/mouse-hello && test -d /var/db/pkg && test -f /run/mouse-hello-service.ready && service mouse_hello status >/dev/null && grep -Eq "^LOCALBASE=[[:space:]]*/usr/local$" /usr/local/etc/mk.conf'; /usr/local/bin/mouse-hello; /usr/local/sbin/pkg_info -e 'mouse-hello-[0-9]*'; echo PERSIST_$?} + await_exact "Hello from pkgsrc on MOUSE\r" "the persistent pkgsrc command" + await_regexp {mouse-hello-1\.0\r} "the persistent package record" + await_exact "PERSIST_0\r" "the persistent pkgsrc state" +} + +await_exact "mouse:~# " "the prompt before poweroff" +poweroff +puts "$mode pkgsrc acceptance passed" diff --git a/tests/acceptance/pkgsrc.sh b/tests/acceptance/pkgsrc.sh new file mode 100755 index 0000000..cd0faa9 --- /dev/null +++ b/tests/acceptance/pkgsrc.sh @@ -0,0 +1,33 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +server_pid= + +acceptance_cleanup_extra() { + if [ -n "$server_pid" ]; then + kill "$server_pid" 2>/dev/null || : + wait "$server_pid" 2>/dev/null || : + fi +} + +acceptance_require_commands "the pkgsrc acceptance test" expect python3 +acceptance_copy_disk pkgsrc + +python3 -m http.server 18080 \ + --bind 127.0.0.1 \ + --directory "$repo_dir/tests/fixtures/pkgsrc/distfiles" \ + >"$temporary/http.log" 2>&1 & +server_pid=$! +sleep 1 +if ! kill -0 "$server_pid" 2>/dev/null; then + printf '%s\n' "pkgsrc source server failed to start" >&2 + cat "$temporary/http.log" >&2 + exit 1 +fi + +"$acceptance_dir/pkgsrc.exp" "$runner" "$test_disk" install +"$acceptance_dir/pkgsrc.exp" "$runner" "$test_disk" verify +printf '%s\n' "pkgsrc seed, rc.d installation, and persistence tests passed" diff --git a/tests/acceptance/service-integration.exp b/tests/acceptance/service-integration.exp new file mode 100755 index 0000000..30a55ad --- /dev/null +++ b/tests/acceptance/service-integration.exp @@ -0,0 +1,110 @@ +#!/usr/bin/expect -f + +if {$argc != 3} { + puts stderr "usage: service-integration.exp RUNNER DISK MODE" + exit 2 +} + +set runner [file normalize [lindex $argv 0]] +set disk [file normalize [lindex $argv 1]] +set mode [lindex $argv 2] +if {$mode ni {enabled disabled failure}} { + puts stderr "invalid service-integration mode: $mode" + exit 2 +} +set timeout 180 +set acceptance_name "service-integration" +source [file join [file dirname [info script]] harness.exp] + +acceptance_log +set env(MOUSE_BOOT_MODE) disk +set env(MOUSE_DISK_IMAGE) $disk +set env(MOUSE_NETWORK) 0 +set env(MOUSE_MEMORY) 1G +set env(MOUSE_KERNEL_ARGS) "console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1" +spawn -noecho $runner + +if {$mode eq "failure"} { + await_regexp {required service console_login failed: start command exited with status [0-9]+; entering emergency console} "the required-service failure" + await_regexp {cheesed: started emergency shell as PID [0-9]+} "the emergency shell" + await_exact "mouse:~# " "the emergency prompt" + sendline {sh -c 'set -- `cat /proc/$PPID/stat`; cat /proc/$4/comm'} + await_exact "cheesed\r" "the emergency shell parent" + await_exact "mouse:~# " "the prompt after the emergency parent check" + sendline {mv /etc/rc.d/console_login.good /etc/rc.d/console_login; chmod 0755 /etc/rc.d/console_login} + await_exact "mouse:~# " "the prompt after restoring console login" + poweroff + puts "required-service failure acceptance passed" + exit 0 +} + +await_exact "cheesed: base bootstrap complete; cheesed is PID 1" "PID 1 confirmation" +if {$mode eq "disabled"} { + await_exact "cheesed: enabled optional services: (none)" "disabled service policy" +} else { + await_exact "cheesed: enabled optional services: mouse_echo" "enabled service policy" +} +await_exact "cheesed: service graph started; cgroup supervision active" "the supervised service graph" +await_prompt + +sendline {sh -c 'test "$(cat /proc/1/comm)" = cheesed && test -S /run/cheesed.sock && test -f /sys/fs/cgroup/mouse.services/console_login/cgroup.events; echo BASE_$?'} +await_exact "BASE_0\r" "the cheesed and cgroup service contract" +await_exact "mouse:~# " "the prompt after the base contract check" +sendline {service console_login status} +await_exact "console_login running\r" "the required console service status" +await_exact "mouse:~# " "the prompt after the console status" + +if {$mode eq "disabled"} { + sendline {sh -c 'service mouse_echo status >/dev/null 2>&1; test $? -ne 0 && test ! -e /sys/fs/cgroup/mouse.services/mouse_echo; echo DISABLED_$?'} + await_exact "DISABLED_0\r" "the disabled service state" + await_exact "mouse:~# " "the prompt after the disabled check" + sendline {sh -c 'cksum /etc/rc.conf > /run/rc.conf.cksum'; service mouse_echo start} + await_exact "mouse_echo running\r" "the runtime-only service start" + await_exact "mouse:~# " "the prompt after the runtime start" + sendline {sh -c 'test "$(cksum /etc/rc.conf)" = "$(cat /run/rc.conf.cksum)" && test -f /sys/fs/cgroup/mouse.services/mouse_echo/cgroup.events; echo POLICY_$?'} + await_exact "POLICY_0\r" "unchanged persistent policy and live cgroup" + await_exact "mouse:~# " "the prompt after the persistent-policy check" + sendline {service mouse_echo stop} + await_exact "mouse_echo stopped\r" "the runtime-only service stop" + await_exact "mouse:~# " "the prompt after the runtime stop" + sendline {sh -c 'cp /etc/rc.d/console_login /etc/rc.d/console_login.good && printf "#!/bin/sh\nexit 42\n" >/etc/rc.d/console_login && chmod 0755 /etc/rc.d/console_login'; echo FAILURE_$?} + await_exact "FAILURE_0\r" "the required-service failure setup" + await_exact "mouse:~# " "the prompt after failure setup" + poweroff + puts "disabled service acceptance passed" + exit 0 +} + +sendline {sh -c 'test -f /run/mouse-echo.ready && grep -qx "populated 1" /sys/fs/cgroup/mouse.services/mouse_echo/cgroup.events; echo ENABLED_$?'} +await_exact "ENABLED_0\r" "the enabled service cgroup" +await_exact "mouse:~# " "the prompt after the enabled check" +sendline {sh -c 'i=0; while [ "$i" -lt 4 ]; do set -- $(cat /sys/fs/cgroup/mouse.services/mouse_echo/cgroup.procs); kill -KILL "$1"; i=$((i + 1)); sleep 2; done; echo EXHAUST_$?'} +await_exact "EXHAUST_0\r" "four supervised service crashes" +await_exact "mouse:~# " "the prompt after exhausting restarts" +sendline {sh -c 'service mouse_echo status >/dev/null 2>&1; echo FAILED_STATUS_$?'} +await_exact "FAILED_STATUS_1\r" "the failed service status" +await_exact "mouse:~# " "the prompt after failed status" +sendline {service mouse_echo restart} +await_exact "mouse_echo running\r" "administrative service restart" +await_exact "mouse:~# " "the prompt after the administrative restart" +sendline {service mouse_echo stop} +await_exact "mouse_echo stopped\r" "the bounded service stop" +await_exact "mouse:~# " "the prompt after the stop" +sendline {sh -c 'test -f /run/mouse-echo.term && grep -qx "populated 0" /sys/fs/cgroup/mouse.services/mouse_echo/cgroup.events; echo STOPPED_$?'} +await_exact "STOPPED_0\r" "SIGTERM delivery and cgroup cleanup" +await_exact "mouse:~# " "the prompt after cgroup cleanup" +sendline {service mouse_echo start} +await_exact "mouse_echo running\r" "the final service start" +await_exact "mouse:~# " "the prompt after the final start" +sendline {sh -c 'sed "s/enable_mouse_echo=YES/enable_mouse_echo=NO/" /etc/rc.conf >/run/rc.conf.next && mv /run/rc.conf.next /etc/rc.conf'; echo NEXT_$?} +await_exact "NEXT_0\r" "the disabled policy for the next boot" +await_exact "mouse:~# " "the prompt after the next-boot policy update" + +sendline {kill -TERM 1} +await_exact "cheesed: received poweroff request" "the poweroff request" +await_exact "cheesed: stopping service mouse_echo" "the dependent service stopping first" +await_exact "cheesed: stopping service console_login" "the console service stopping" +await_exact "cheesed: root filesystem remounted read-only" "the read-only root remount" +await_clean_exit "poweroff" + +puts "enabled service-integration acceptance passed" diff --git a/tests/acceptance/service-integration.sh b/tests/acceptance/service-integration.sh new file mode 100755 index 0000000..1054506 --- /dev/null +++ b/tests/acceptance/service-integration.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the service-integration acceptance test" expect +acceptance_copy_disk service-integration + +requested_modes=" ${MOUSE_SERVICE_INTEGRATION_MODES:-enabled disabled failure} " +for mode in enabled disabled failure; do + case "$requested_modes" in + *" $mode "*) "$acceptance_dir/service-integration.exp" "$runner" "$test_disk" "$mode" ;; + esac +done diff --git a/tests/acceptance/storage.exp b/tests/acceptance/storage.exp new file mode 100755 index 0000000..2dd581f --- /dev/null +++ b/tests/acceptance/storage.exp @@ -0,0 +1,64 @@ +#!/usr/bin/expect -f + +if {$argc != 3} { + puts stderr "usage: storage.exp RUNNER DISK MODE" + exit 2 +} + +set runner [file normalize [lindex $argv 0]] +set disk [file normalize [lindex $argv 1]] +set mode [lindex $argv 2] +if {$mode ni {first verify failure}} { + puts stderr "invalid storage mode: $mode" + exit 2 +} +set timeout 60 +set acceptance_name "storage" +source [file join [file dirname [info script]] harness.exp] + +acceptance_log +set env(MOUSE_BOOT_MODE) disk +set env(MOUSE_DISK_IMAGE) $disk +set env(MOUSE_KERNEL_ARGS) "console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1" + +spawn -noecho $runner + +if {$mode eq "failure"} { + await_regexp {VFS: Unable to mount root fs} "the failed-root diagnostic" + expect { + eof { + set result [wait] + if {[lindex $result 3] != 0} { + fail "QEMU exited unsuccessfully after the expected kernel panic" + } + } + timeout { + fail "QEMU did not exit after the failed-root diagnostic" + } + } + puts "failed-root diagnostic acceptance passed" + exit 0 +} + +await_exact "cheesed: base bootstrap complete; cheesed is PID 1" "PID 1 confirmation" +if {$mode eq "verify"} { + await_exact "cheesed: enabled optional services: (none)" "persisted service policy" +} +await_exact "mouse login: root (automatic login)" "the serial login" +await_prompt + +if {$mode eq "first"} { + sendline {sh -c 'test -s /etc/machine-id && mkdir -p /var/lib/mouse /home/mouse-state && echo etc-state > /etc/mouse-state && echo var-state > /var/lib/mouse/state && echo home-state > /home/mouse-state/value && cp /etc/machine-id /var/lib/mouse/machine-id.first'; echo STATE_$?} + await_exact "STATE_0\r" "persistent state creation" + await_exact "mouse:~# " "the prompt after state creation" + sendline {sh -c 'printf "%s\n" "# Persistent MOUSE service policy belongs in this file." "enable_network=NO" > /etc/rc.conf'; echo POLICY_$?} + await_exact "POLICY_0\r" "persistent policy update" + await_exact "mouse:~# " "the prompt after the policy update" +} else { + sendline {sh -c 'test "$(cat /etc/mouse-state)" = etc-state && test "$(cat /var/lib/mouse/state)" = var-state && test "$(cat /home/mouse-state/value)" = home-state && test "$(cat /etc/machine-id)" = "$(cat /var/lib/mouse/machine-id.first)" && test "$(cat /etc/rc.conf | tail -1)" = enable_network=NO'; echo PERSIST_$?} + await_exact "PERSIST_0\r" "persistent state after reboot" + await_exact "mouse:~# " "the prompt after persistence verification" +} + +poweroff +puts "$mode persistent-root acceptance passed" diff --git a/tests/acceptance/storage.sh b/tests/acceptance/storage.sh new file mode 100755 index 0000000..e2b3228 --- /dev/null +++ b/tests/acceptance/storage.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the storage acceptance test" expect dd +acceptance_copy_disk storage + +"$acceptance_dir/storage.exp" "$runner" "$test_disk" first +"$acceptance_dir/storage.exp" "$runner" "$test_disk" verify +"$acceptance_dir/storage.exp" "$runner" "$test_disk" verify + +failed_disk="$test_disk" +root_start=264192 +dd if=/dev/zero of="$failed_disk" bs=512 seek="$root_start" \ + count=262144 conv=notrunc 2>/dev/null +"$acceptance_dir/storage.exp" "$runner" "$failed_disk" failure + +printf '%s\n' "storage acceptance tests passed" diff --git a/tests/acceptance/toolchain.exp b/tests/acceptance/toolchain.exp new file mode 100755 index 0000000..f99f562 --- /dev/null +++ b/tests/acceptance/toolchain.exp @@ -0,0 +1,56 @@ +#!/usr/bin/expect -f + +if {$argc != 2} { + puts stderr "usage: toolchain.exp RUNNER DISK" + exit 2 +} +set runner [file normalize [lindex $argv 0]] +set disk [file normalize [lindex $argv 1]] +set timeout 300 +set acceptance_name "toolchain" +source [file join [file dirname [info script]] harness.exp] + +acceptance_log +set env(MOUSE_BOOT_MODE) disk +set env(MOUSE_DISK_IMAGE) $disk +set env(MOUSE_NETWORK) 0 +set env(MOUSE_MEMORY) 1G +set env(MOUSE_KERNEL_ARGS) "console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1" +spawn -noecho $runner + +await "mouse:~# " "the root shell" +sendline {cc --version; cc -dumpmachine; make --version; sh -c 'test "$(command -v make)" = /usr/bin/make && test "$(command -v gmake)" = /usr/bin/gmake && test "$(readlink /usr/bin/gmake)" = make && test ! -e /usr/share/mk'; echo MAKE_ID_$status} +await "clang version 22.1.3" "the native Clang version" +await "x86_64-unknown-linux-musl\r" "the MOUSE compiler target" +await "GNU Make 4.4.1" "the base GNU make version" +await "MAKE_ID_0\r" "the base make ownership" +await "mouse:~# " "the prompt after compiler identification" + +sendline {echo 'int answer(void) { return 42; }' >/tmp/answer.c; echo 'int answer(void);' >/tmp/main.c; echo 'int main(void) { return answer() != 42; }' >>/tmp/main.c; cc -c /tmp/answer.c -o /tmp/answer.o; ar rcs /tmp/libanswer.a /tmp/answer.o; cc /tmp/main.c /tmp/libanswer.a -o /tmp/hello-c; cc -static /tmp/main.c /tmp/libanswer.a -o /tmp/hello-c-static; /tmp/hello-c; /tmp/hello-c-static; echo C_$status} +await "C_0\r" "the native C build" +await "mouse:~# " "the prompt after the C build" + +sendline {cd /tmp; echo '#include ' >make-main.c; echo 'int main(void) { puts("made"); return 0; }' >>make-main.c; sh -c 'printf "CC=cc\nall: made\nmade: make-main.c\n\t\$(CC) make-main.c -o made\n" >Makefile'; make && ./made && pkgconf --exists libcrypt; echo BUILD_$status} +await "made\r" "the make-built program output" +await "BUILD_0\r" "make and pkgconf" +await "mouse:/tmp# " "the prompt after the make build" + +sendline {echo '#include ' >/tmp/hello.cpp; echo '#include ' >>/tmp/hello.cpp; echo '#include ' >>/tmp/hello.cpp; echo 'int main() { try { if (std::filesystem::path("/tmp").filename() != "tmp") throw std::runtime_error("filesystem"); } catch (...) { return 1; } return 0; }' >>/tmp/hello.cpp; c++ /tmp/hello.cpp -o /tmp/hello-cxx; c++ -static /tmp/hello.cpp -o /tmp/hello-cxx-static; /tmp/hello-cxx; /tmp/hello-cxx-static; echo CXX_$status} +await "CXX_0\r" "the native C++ build" +await "mouse:/tmp# " "the prompt after the C++ build" + +sendline {sh -c 'for f in /tmp/hello-c /tmp/hello-cxx; do readelf -l "$f" | grep -q INTERP || { echo "missing INTERP: $f"; exit 1; }; readelf -d "$f" | grep -q NEEDED || { echo "missing NEEDED: $f"; exit 1; }; done; for f in /tmp/hello-c-static /tmp/hello-cxx-static; do if readelf -l "$f" | grep -q INTERP; then echo "unexpected INTERP: $f"; exit 1; fi; if readelf -d "$f" 2>/dev/null | grep -q NEEDED; then echo "unexpected NEEDED: $f"; exit 1; fi; done; exit 0'; echo LINKAGE_$status} +await "LINKAGE_0\r" "dynamic defaults and explicit static builds" +await "mouse:/tmp# " "the prompt after the linkage audit" + +sendline {echo 'int mouse_shared(void) { return 7; }' >/tmp/shared.c; echo 'int mouse_shared(void); int main(void) { return mouse_shared() != 7; }' >/tmp/shared-user.c; cc -fPIC -shared /tmp/shared.c -o /tmp/libmouse.so; cc /tmp/shared-user.c -L/tmp -Wl,-rpath,/tmp -lmouse -o /tmp/shared-user; /tmp/shared-user; sh -c 'test -f /usr/lib/crtbeginS.o && test -f /usr/lib/crtendS.o && file /tmp/libmouse.so | grep -q "shared object"'; echo SHARED_$status} +await "SHARED_0\r" "building and consuming a shared library" +await "mouse:/tmp# " "the prompt after the shared-library build" + +sendline {sh -c 'mkdir -p /tmp/tar-source /tmp/tar-output; echo archived >/tmp/tar-source/value; tar -czf /tmp/value.tar.gz -C /tmp/tar-source value; tar -xf /tmp/value.tar.gz -C /tmp/tar-output; test "$(cat /tmp/tar-output/value)" = archived'; echo TAR_$status} +await "TAR_0\r" "the gzip-compressed archive round trip" +await "mouse:/tmp# " "the prompt after the archive round trip" + +poweroff + +puts "native toolchain guest acceptance passed" diff --git a/tests/acceptance/toolchain.sh b/tests/acceptance/toolchain.sh new file mode 100755 index 0000000..cb2aa6a --- /dev/null +++ b/tests/acceptance/toolchain.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the toolchain acceptance test" expect +acceptance_copy_disk toolchain "the toolchain" + +"$acceptance_dir/toolchain.exp" "$runner" "$test_disk" +printf '%s\n' "toolchain acceptance tests passed" diff --git a/tests/acceptance/world.exp b/tests/acceptance/world.exp new file mode 100755 index 0000000..d6889cb --- /dev/null +++ b/tests/acceptance/world.exp @@ -0,0 +1,77 @@ +#!/usr/bin/expect -f + +if {$argc != 1} { + puts stderr "usage: world.exp RUNNER" + exit 2 +} + +set runner [file normalize [lindex $argv 0]] +set timeout 90 +set acceptance_name "world" +source [file join [file dirname [info script]] harness.exp] + +acceptance_log +spawn -noecho $runner +await_exact "cheesed: base bootstrap complete; cheesed is PID 1" "the World boot" +await_exact "mouse login: root (automatic login)" "the automatic test login" +await_prompt + +sendline {sh -c 'man -w ls | grep -q /usr/share/man && man ls | grep -q "list directory contents" && apropos "list directory contents" | grep -q "ls(1)"'; echo MANUAL_$?} +await_exact "MANUAL_0\r" "the installed World manuals and apropos database" +await_exact "mouse:~# " "the prompt after the manual check" + +sendline {sh -c 'printf mouse-world >/tmp/plain && gzip -c /tmp/plain | gzip -dc | cmp - /tmp/plain && bzip2 -c /tmp/plain | bzip2 -dc | cmp - /tmp/plain && xz -c /tmp/plain | xz -dc | cmp - /tmp/plain && zstd -q -c /tmp/plain | zstd -q -dc | cmp - /tmp/plain && tar -a -cf /tmp/plain.zip -C /tmp plain && mkdir /tmp/unzip && unzip -q /tmp/plain.zip -d /tmp/unzip && cmp /tmp/plain /tmp/unzip/plain && file /tmp/plain | grep -q text'; echo ARCHIVE_$?} +await_exact "ARCHIVE_0\r" "the World compression, archive, and file-identification tools" +await_exact "mouse:~# " "the prompt after the archive check" + +sendline {sh -c 'openssl version | grep -q OpenSSL && free >/dev/null && uptime >/dev/null && vmstat 1 2 >/dev/null && watch --version >/dev/null && sysctl kernel.hostname | grep -q mouse && lsblk >/dev/null && findmnt / >/dev/null && flock /tmp/world.lock true'; echo RUNTIME_$?} +await_exact "RUNTIME_0\r" "the crypto, process, sysctl, and storage administration tools" +await_exact "mouse:~# " "the prompt after the runtime administration check" + +sendline {sh -c 'ip link show lo >/dev/null && ip -6 address show dev lo | grep -q ::1 && ss -l >/dev/null && tc qdisc show dev lo >/dev/null && bridge link >/dev/null && nft add table inet mouse_acceptance && nft list table inet mouse_acceptance >/dev/null && nft delete table inet mouse_acceptance'; echo NETWORK_TOOLS_$?} +await_exact "NETWORK_TOOLS_0\r" "the IPv6, socket, route, traffic-control, and nftables tools" +await_exact "mouse:~# " "the prompt after the network-tool check" + +sendline {sh -c 'ip link add mouse-dummy type dummy && ip link add mouse-bridge type bridge && ip link set mouse-dummy master mouse-bridge && ip link add link mouse-dummy name mouse-vlan type vlan id 42 && ip link delete mouse-bridge && ip link delete mouse-vlan && ip link delete mouse-dummy'; echo VIRTUAL_LINKS_$?} +await_exact "VIRTUAL_LINKS_0\r" "the dummy, bridge, and VLAN kernel interfaces" +await_exact "mouse:~# " "the prompt after the virtual-link check" + +sendline {sh -c 'service loopback status >/dev/null && service mdevd status >/dev/null && service syslogd status >/dev/null && service cron status >/dev/null && service ntpd status >/dev/null'; echo SERVICES_$?} +await_exact "SERVICES_0\r" "the device, logging, job, and time services" +await_exact "mouse:~# " "the prompt after the World service check" + +sendline {sh -c 'sleep 8 && service ntpd status >/dev/null && test -S /run/ntpd.sock && ntpctl -s status >/dev/null'; echo NTPD_$?} +await_exact "NTPD_0\r" "the supervised time service after the restart window" +await_exact "mouse:~# " "the prompt after the time-service stability check" + +sendline {sh -c 'sleep 30 & probe=$!; ps -p 1 | grep -q cheesed && pgrep cheesed >/dev/null && pkill -P $$ sleep && wait "$probe" 2>/dev/null; killed=$?; test "$killed" -ne 0 && dmesg | grep -q "Linux version" && ping -c 1 127.0.0.1 >/dev/null'; echo ADMIN_$?} +await_exact "ADMIN_0\r" "process, kernel-log, and network diagnosis" +await_exact "mouse:~# " "the prompt after administration checks" + +sendline {sh -c 'mkdir -p /tmp/mouse-mount; mount -t tmpfs tmpfs /tmp/mouse-mount && echo mounted >/tmp/mouse-mount/state && umount /tmp/mouse-mount && test ! -e /tmp/mouse-mount/state'; echo MOUNT_$?} +await_exact "MOUNT_0\r" "manual mount and unmount" +await_exact "mouse:~# " "the prompt after mount checks" + +sendline {sh -c 'printf "a\nworld-editor\n.\nwq\n" | vi -e /tmp/mouse-vi && grep -qx world-editor /tmp/mouse-vi'; echo VI_$?} +await_exact "VI_0\r" "the base editor" +await_exact "mouse:~# " "the prompt after editor verification" + +sendline {logger -p user.notice mouse-world-syslog; sleep 1; grep -q mouse-world-syslog /var/log/messages; echo SYSLOG_$?} +await_exact "SYSLOG_0\r" "durable base logging" +await_exact "mouse:~# " "the prompt after logging verification" + +sendline {sh -c 'kill -USR2 "$(cat /run/syslogd.pid)" && sleep 1 && logger -p user.notice mouse-world-after-rotation && sleep 1 && test -f /var/log/messages.0 && grep -q mouse-world-syslog /var/log/messages.0 && grep -q mouse-world-after-rotation /var/log/messages'; echo ROTATE_$?} +await_exact "ROTATE_0\r" "base log rotation and descriptor reopening" +await_exact "mouse:~# " "the prompt after log rotation" + +sendline {sh -c 'printf "* * * * * /bin/true\n" | crontab - && crontab -l | grep -q "/bin/true"'; echo CRON_$?} +await_exact "CRON_0\r" "the base crontab interface" +await_exact "mouse:~# " "the prompt after crontab verification" + +sendline {depmod -a; modinfo mouse_test >/dev/null && modprobe mouse_test && grep -q '^mouse_test ' /proc/modules && rmmod mouse_test; echo MODULE_$?} +await_exact "MODULE_0\r" "loadable external kernel modules" +await_exact "mouse:~# " "the prompt after module verification" + +poweroff + +puts "World facilities acceptance passed" diff --git a/tests/acceptance/world.sh b/tests/acceptance/world.sh new file mode 100755 index 0000000..237bdcf --- /dev/null +++ b/tests/acceptance/world.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu + +acceptance_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$acceptance_dir/harness.sh" +acceptance_init +acceptance_require_commands "the World acceptance test" expect +acceptance_copy_disk world + +MOUSE_BOOT_MODE=disk \ +MOUSE_DISK_IMAGE="$test_disk" \ +MOUSE_KERNEL_ARGS="console=ttyS0 root=PARTUUID=4d4f5553-4500-4000-8000-000000000002 rootfstype=btrfs rw rootwait init=/sbin/cheesed mouse.autologin=1 panic=-1" \ +MOUSE_MEMORY="${MOUSE_MEMORY:-1G}" \ + exec expect "$acceptance_dir/world.exp" "$runner" diff --git a/tests/fixtures/kernel-module/Makefile b/tests/fixtures/kernel-module/Makefile new file mode 100644 index 0000000..a049fda --- /dev/null +++ b/tests/fixtures/kernel-module/Makefile @@ -0,0 +1 @@ +obj-m += mouse_test.o diff --git a/tests/fixtures/kernel-module/mouse_test.c b/tests/fixtures/kernel-module/mouse_test.c new file mode 100644 index 0000000..776a2bc --- /dev/null +++ b/tests/fixtures/kernel-module/mouse_test.c @@ -0,0 +1,18 @@ +#include +#include + +static int __init mouse_test_init(void) +{ + pr_info("MOUSE external module loaded\n"); + return 0; +} + +static void __exit mouse_test_exit(void) +{ + pr_info("MOUSE external module unloaded\n"); +} + +module_init(mouse_test_init); +module_exit(mouse_test_exit); +MODULE_DESCRIPTION("MOUSE external module acceptance fixture"); +MODULE_LICENSE("BSD"); diff --git a/tests/fixtures/mouse-echo/mouse-echo.c b/tests/fixtures/mouse-echo/mouse-echo.c new file mode 100644 index 0000000..2762547 --- /dev/null +++ b/tests/fixtures/mouse-echo/mouse-echo.c @@ -0,0 +1,41 @@ +#include +#include +#include + +static const char ready_path[] = "/run/mouse-echo.ready"; +static const char term_path[] = "/run/mouse-echo.term"; + +static void write_marker(const char *path, const char *contents, size_t length) +{ + int descriptor = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + + if (descriptor == -1) + return; + (void)write(descriptor, contents, length); + (void)close(descriptor); +} + +static void handle_term(int signal_number) +{ + static const char marker[] = "SIGTERM\n"; + + (void)signal_number; + write_marker(term_path, marker, sizeof(marker) - 1); +} + +int main(void) +{ + static const char marker[] = "ready\n"; + struct sigaction action = { + .sa_handler = handle_term, + }; + + if (sigemptyset(&action.sa_mask) == -1) + return 1; + if (sigaction(SIGTERM, &action, NULL) == -1) + return 1; + write_marker(ready_path, marker, sizeof(marker) - 1); + + for (;;) + pause(); +} diff --git a/tests/fixtures/mouse-echo/rootfs/etc/rc.d/mouse_echo b/tests/fixtures/mouse-echo/rootfs/etc/rc.d/mouse_echo new file mode 100644 index 0000000..6aebef2 --- /dev/null +++ b/tests/fixtures/mouse-echo/rootfs/etc/rc.d/mouse_echo @@ -0,0 +1,28 @@ +#!/bin/sh +# PROVIDE: mouse_echo +# REQUIRE: console_login + +. /etc/rc.subr + +name=mouse_echo +command=/usr/libexec/mouse-echo +command_args="&" +start_precmd=mouse_echo_prestart +stop_cmd=mouse_echo_stop + +mouse_echo_prestart() +{ + rm -f /run/mouse-echo.term +} + +mouse_echo_stop() +{ + while read pid; do + case "$pid" in + ""|$$|$PPID) continue ;; + esac + kill -TERM "$pid" 2>/dev/null || : + done < "$MOUSE_SERVICE_CGROUP/cgroup.procs" +} + +run_rc_command "$1" diff --git a/tests/fixtures/pkgsrc/distfiles/mouse-hello-1.0.c b/tests/fixtures/pkgsrc/distfiles/mouse-hello-1.0.c new file mode 100644 index 0000000..ea5926a --- /dev/null +++ b/tests/fixtures/pkgsrc/distfiles/mouse-hello-1.0.c @@ -0,0 +1,8 @@ +#include + +int +main(void) +{ + puts("Hello from pkgsrc on MOUSE"); + return 0; +} diff --git a/tests/fixtures/pkgsrc/local/mouse-hello/DESCR b/tests/fixtures/pkgsrc/local/mouse-hello/DESCR new file mode 100644 index 0000000..e34ac69 --- /dev/null +++ b/tests/fixtures/pkgsrc/local/mouse-hello/DESCR @@ -0,0 +1,2 @@ +Small command and rc.d service used to exercise pkgsrc installation and +cheesed supervision on MOUSE. diff --git a/tests/fixtures/pkgsrc/local/mouse-hello/Makefile b/tests/fixtures/pkgsrc/local/mouse-hello/Makefile new file mode 100644 index 0000000..a54e445 --- /dev/null +++ b/tests/fixtures/pkgsrc/local/mouse-hello/Makefile @@ -0,0 +1,26 @@ +DISTNAME= mouse-hello-1.0 +CATEGORIES= local +MASTER_SITES= http://10.0.2.2:18080/ +DISTFILES= ${DISTNAME}.c +EXTRACT_ONLY= + +MAINTAINER= mouse@localhost +COMMENT= MOUSE pkgsrc acceptance command +LICENSE= 2-clause-bsd + +NO_CONFIGURE= yes +INSTALLATION_DIRS= bin libexec + +RCD_SCRIPTS= mouse_hello + +do-build: + ${CC} ${CFLAGS} ${LDFLAGS} ${DISTDIR}/${DISTNAME}.c \ + -o ${WRKSRC}/mouse-hello + +do-install: + ${INSTALL_PROGRAM} ${WRKSRC}/mouse-hello \ + ${DESTDIR}${PREFIX}/bin/mouse-hello + ${INSTALL_SCRIPT} ${FILESDIR}/mouse-hello-service \ + ${DESTDIR}${PREFIX}/libexec/mouse-hello-service + +.include "../../mk/bsd.pkg.mk" diff --git a/tests/fixtures/pkgsrc/local/mouse-hello/PLIST b/tests/fixtures/pkgsrc/local/mouse-hello/PLIST new file mode 100644 index 0000000..bc25a82 --- /dev/null +++ b/tests/fixtures/pkgsrc/local/mouse-hello/PLIST @@ -0,0 +1,3 @@ +@comment $NetBSD$ +bin/mouse-hello +libexec/mouse-hello-service diff --git a/tests/fixtures/pkgsrc/local/mouse-hello/distinfo b/tests/fixtures/pkgsrc/local/mouse-hello/distinfo new file mode 100644 index 0000000..7ec8316 --- /dev/null +++ b/tests/fixtures/pkgsrc/local/mouse-hello/distinfo @@ -0,0 +1,5 @@ +$NetBSD$ + +BLAKE2s (mouse-hello-1.0.c) = 0b050fc1474665f3aa59da2afe70b7bce1d04acfa8de8f7a62fe5b6b40bc0503 +SHA512 (mouse-hello-1.0.c) = ab0bd1c0c3a4c622029ef9495c58df31b462067b9ff997925fff7047df9a0cdbf1462817e5f9195f73c7994b47d2ddc2c4af06aea78c9f31cb5813d770d2eb50 +Size (mouse-hello-1.0.c) = 87 bytes diff --git a/tests/fixtures/pkgsrc/local/mouse-hello/files/mouse-hello-service b/tests/fixtures/pkgsrc/local/mouse-hello/files/mouse-hello-service new file mode 100644 index 0000000..d33d79d --- /dev/null +++ b/tests/fixtures/pkgsrc/local/mouse-hello/files/mouse-hello-service @@ -0,0 +1,16 @@ +#!/bin/sh +set -eu + +ready=/run/mouse-hello-service.ready + +cleanup() { + rm -f "$ready" + exit 0 +} +trap cleanup INT TERM + +: >"$ready" +while :; do + sleep 3600 & + wait "$!" +done diff --git a/tests/fixtures/pkgsrc/local/mouse-hello/files/mouse_hello.sh b/tests/fixtures/pkgsrc/local/mouse-hello/files/mouse_hello.sh new file mode 100644 index 0000000..bf9d640 --- /dev/null +++ b/tests/fixtures/pkgsrc/local/mouse-hello/files/mouse_hello.sh @@ -0,0 +1,11 @@ +#!@RCD_SCRIPTS_SHELL@ +# PROVIDE: mouse_hello +# REQUIRE: NETWORKING + +. /etc/rc.subr + +name=mouse_hello +command=@PREFIX@/libexec/mouse-hello-service +command_args="&" + +run_rc_command "$1"