diff --git a/.ci/publish-prebuilt.sh b/.ci/publish-prebuilt.sh new file mode 100755 index 00000000..4b48abab --- /dev/null +++ b/.ci/publish-prebuilt.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# +# Compress the prebuilt Image and rootfs.cpio in cwd, write a sha1 +# manifest, hash the input files that define the prebuilt's contents, +# and print all three sums in KEY=VAL form on stdout so callers can +# splice them into release notes, GITHUB_OUTPUT, or whatever else. +# +# Inputs (in cwd): +# Image +# rootfs.cpio +# plus the source inputs listed in INPUTS below (config + scripts + +# target/init that define the buildroot/kernel content) +# +# Outputs (in cwd): +# Image.bz2 +# rootfs.cpio.bz2 +# prebuilt.sha1 -- two-line manifest in standard `sha1sum` format +# +# Stdout (machine-readable, one assignment per line): +# kernel_sha1= +# initrd_sha1= +# inputs_sha1= + +set -euo pipefail + +# Pick a SHA1 tool. macOS dropped `sha1sum` from the base system; the +# coreutils-style `shasum -a 1` is the portable fallback. +if command -v sha1sum >/dev/null 2>&1; then + SHA1=(sha1sum) +elif command -v shasum >/dev/null 2>&1; then + SHA1=(shasum -a 1) +else + echo "[!] Need sha1sum (Linux) or shasum (macOS) on PATH" >&2 + exit 1 +fi + +# Keep this list in sync with PREBUILT_INPUTS in mk/external.mk and the +# `paths:` filter in .github/workflows/prebuilt.yml. +INPUTS=( + configs/linux.config + configs/busybox.config + configs/buildroot.config + scripts/build-image.sh + scripts/rootfs_ext4.sh + target/init +) + +for f in Image rootfs.cpio "${INPUTS[@]}"; do + if [ ! -f "$f" ]; then + echo "[!] Missing $f -- run scripts/build-image.sh --all first" >&2 + exit 1 + fi +done + +bzip2 -k -f Image +bzip2 -k -f rootfs.cpio + +KERNEL_SHA1=$("${SHA1[@]}" Image.bz2 | awk '{print $1}') +INITRD_SHA1=$("${SHA1[@]}" rootfs.cpio.bz2 | awk '{print $1}') +# Concatenate inputs in deterministic order and hash the stream. Matches +# the make-time computation in mk/external.mk so they compare directly. +INPUTS_SHA1=$(cat "${INPUTS[@]}" | "${SHA1[@]}" | awk '{print $1}') + +# Write the human-friendly checksum manifest. Format matches `sha1sum -c` +# so the file works as input to that tool unchanged. +{ + echo "$KERNEL_SHA1 Image.bz2" + echo "$INITRD_SHA1 rootfs.cpio.bz2" +} > prebuilt.sha1 + +# Echo the manifest + inputs hash to stderr for visibility in CI logs +# without polluting the parseable stdout block below. +{ + cat prebuilt.sha1 + echo "inputs_sha1: $INPUTS_SHA1" +} >&2 + +echo "kernel_sha1=$KERNEL_SHA1" +echo "initrd_sha1=$INITRD_SHA1" +echo "inputs_sha1=$INPUTS_SHA1" diff --git a/.ci/suggest-format.sh b/.ci/suggest-format.sh new file mode 100755 index 00000000..bf1d6a36 --- /dev/null +++ b/.ci/suggest-format.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# Post clang-format-20 diffs as PR review suggestions via reviewdog. Pairs +# with .ci/check-format.sh: this script *suggests* on pull requests; that +# one *enforces* by failing CI when violations land on master. +# +# Designed for the GitHub Actions `coding_style` job. The workflow must +# provide REVIEWDOG_GITHUB_API_TOKEN in the environment and install +# clang-format-20 + reviewdog beforehand. +# +# The script applies clang-format in-place to surface a diff, then always +# restores the working tree via an EXIT trap so a failed reviewdog run +# does not leave reformatted sources behind. + +set -euo pipefail + +# Collect all C/C++ sources tracked by git (POSIX-compatible array build +# for bash 3.2 compatibility on macOS runners). +SOURCES=() +while IFS= read -r file; do + SOURCES+=("$file") +done < <(git ls-files '*.c' '*.cxx' '*.cpp' '*.h' '*.hpp') + +# Restore files on exit so a failed reviewdog run does not leave +# clang-format-modified sources in the working tree. +cleanup_files() { + if [ ${#SOURCES[@]} -gt 0 ]; then + echo "Restoring files to original state..." + git checkout -- "${SOURCES[@]}" 2>/dev/null || true + fi +} +trap cleanup_files EXIT INT TERM + +# Apply formatting in-place to surface the diff against the working tree. +clang-format-20 -i "${SOURCES[@]}" + +# Pipe the diff into reviewdog. `|| true` keeps the trap from being +# pre-empted by reviewdog's exit status when there are findings. +git diff -u --no-color | reviewdog -f=diff \ + -name="clang-format" \ + -reporter=github-pr-review \ + -filter-mode=added \ + -fail-on-error=false \ + -level=warning || true diff --git a/.github/actions/setup-semu/action.yml b/.github/actions/setup-semu/action.yml index 8ee56604..bf976b55 100644 --- a/.github/actions/setup-semu/action.yml +++ b/.github/actions/setup-semu/action.yml @@ -13,6 +13,7 @@ runs: build-essential \ device-tree-compiler \ expect \ + fakeroot \ libasound2-dev \ libudev-dev \ libsdl2-dev @@ -24,4 +25,4 @@ runs: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_ANALYTICS: 1 run: | - brew install make dtc expect e2fsprogs sdl2 + brew install make dtc expect fakeroot e2fsprogs sdl2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index adb88432..df3338b1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,7 +31,12 @@ jobs: path: | Image rootfs.cpio - key: external-${{ hashFiles('mk/external.mk') }} + # Invalidate the cached prebuilts whenever any input that defines + # their content changes. Keep this list in sync with + # PREBUILT_INPUTS in mk/external.mk and the `paths:` filter in + # .github/workflows/prebuilt.yml; otherwise CI will silently + # restore stale Image / rootfs.cpio after a config bump. + key: external-${{ hashFiles('mk/external.mk', 'configs/linux.config', 'configs/busybox.config', 'configs/buildroot.config', 'scripts/build-image.sh', 'scripts/rootfs_ext4.sh', 'target/**') }} - name: cache submodule builds uses: actions/cache@v4 with: @@ -76,6 +81,55 @@ jobs: shell: bash timeout-minutes: 5 + # Guard the legacy initramfs path so it does not bitrot now that the + # default boot mode is /dev/vda. Single slim job: fresh build with + # ENABLE_EXTERNAL_ROOT=0, then run autorun. + semu-linux-initramfs: + runs-on: ubuntu-24.04 + steps: + - name: checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + - name: cache external downloads + uses: actions/cache@v4 + with: + path: | + Image + rootfs.cpio + # Invalidate the cached prebuilts whenever any input that defines + # their content changes. Keep this list in sync with + # PREBUILT_INPUTS in mk/external.mk and the `paths:` filter in + # .github/workflows/prebuilt.yml; otherwise CI will silently + # restore stale Image / rootfs.cpio after a config bump. + key: external-${{ hashFiles('mk/external.mk', 'configs/linux.config', 'configs/busybox.config', 'configs/buildroot.config', 'scripts/build-image.sh', 'scripts/rootfs_ext4.sh', 'target/**') }} + - name: cache submodule builds + uses: actions/cache@v4 + with: + path: | + mini-gdbstub/build + minislirp/src/*.a + key: ${{ runner.os }}-submodules-${{ hashFiles('.gitmodules', 'mini-gdbstub/**', 'minislirp/**') }} + restore-keys: | + ${{ runner.os }}-submodules- + - name: cache apt packages + uses: actions/cache@v4 + with: + path: /var/cache/apt/archives + key: ${{ runner.os }}-apt-${{ hashFiles('.github/actions/setup-semu/action.yml') }} + restore-keys: | + ${{ runner.os }}-apt- + - name: install-dependencies + uses: ./.github/actions/setup-semu + - name: legacy initramfs build + run: make ENABLE_EXTERNAL_ROOT=0 + shell: bash + timeout-minutes: 5 + - name: legacy initramfs autorun + run: ENABLE_EXTERNAL_ROOT=0 .ci/autorun.sh + shell: bash + timeout-minutes: 10 + semu-macOS: runs-on: macos-latest steps: @@ -88,7 +142,12 @@ jobs: path: | Image rootfs.cpio - key: external-${{ hashFiles('mk/external.mk') }} + # Invalidate the cached prebuilts whenever any input that defines + # their content changes. Keep this list in sync with + # PREBUILT_INPUTS in mk/external.mk and the `paths:` filter in + # .github/workflows/prebuilt.yml; otherwise CI will silently + # restore stale Image / rootfs.cpio after a config bump. + key: external-${{ hashFiles('mk/external.mk', 'configs/linux.config', 'configs/busybox.config', 'configs/buildroot.config', 'scripts/build-image.sh', 'scripts/rootfs_ext4.sh', 'target/**') }} - name: cache submodule builds uses: actions/cache@v4 with: @@ -163,35 +222,7 @@ jobs: if: github.event_name == 'pull_request' env: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - - # Get list of source files into array (POSIX-compatible for macOS bash 3.2) - SOURCES=() - while IFS= read -r file; do - SOURCES+=("$file") - done < <(git ls-files '*.c' '*.cxx' '*.cpp' '*.h' '*.hpp') - - # Register cleanup function to restore files on exit - cleanup_files() { - if [ ${#SOURCES[@]} -gt 0 ]; then - echo "Restoring files to original state..." - git checkout -- "${SOURCES[@]}" 2>/dev/null || true - fi - } - trap cleanup_files EXIT INT TERM - - # Apply clang-format in-place to generate diff - clang-format-20 -i "${SOURCES[@]}" - - # Generate diff and pipe to reviewdog - # Note: reviewdog exit code doesn't affect cleanup due to trap - git diff -u --no-color | reviewdog -f=diff \ - -name="clang-format" \ - -reporter=github-pr-review \ - -filter-mode=added \ - -fail-on-error=false \ - -level=warning || true + run: .ci/suggest-format.sh shell: bash timeout-minutes: 5 - name: Check formatting (fail on violations) diff --git a/.github/workflows/prebuilt.yml b/.github/workflows/prebuilt.yml new file mode 100644 index 00000000..ef02f68f --- /dev/null +++ b/.github/workflows/prebuilt.yml @@ -0,0 +1,110 @@ +name: Publish prebuilt images + +# Builds the Linux kernel and Buildroot rootfs that the rest of CI (and +# `make` on a fresh checkout) consumes, then publishes them as assets on a +# fixed-tag GitHub prerelease so the download URL stays stable across +# rebuilds. This replaces the old `blob` branch convention, which forced +# us to push large binary artifacts into the source tree. +# +# The workflow is manual by default (workflow_dispatch). Run it whenever +# the kernel config, buildroot config, or build-image.sh changes; the +# resulting SHA1 sums must be reflected in mk/external.mk. + +on: + workflow_dispatch: + push: + branches: [master] + # Republish the prebuilt artifacts whenever any input that defines + # what gets baked into Image / rootfs.cpio actually changes. The list + # mirrors PREBUILT_INPUTS in mk/external.mk -- keep them in sync. + paths: + - 'configs/linux.config' + - 'configs/busybox.config' + - 'configs/buildroot.config' + - 'scripts/build-image.sh' + - 'scripts/rootfs_ext4.sh' + - 'target/**' + - '.github/workflows/prebuilt.yml' + +permissions: + contents: write + +concurrency: + group: prebuilt + cancel-in-progress: false + +jobs: + build-and-publish: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install build dependencies + run: | + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ + build-essential \ + bc \ + bison \ + flex \ + cpio \ + fakeroot \ + e2fsprogs \ + git \ + python3 \ + libssl-dev \ + libelf-dev \ + wget + + - name: Build Buildroot and Linux + run: ./scripts/build-image.sh --all + + - name: Compress and checksum artifacts + id: checksum + # The shell logic lives in .ci/publish-prebuilt.sh so it can be + # exercised locally and stays out of the YAML. + run: | + set -euo pipefail + .ci/publish-prebuilt.sh >> "$GITHUB_OUTPUT" + ls -la Image.bz2 rootfs.cpio.bz2 prebuilt.sha1 + + - name: Update prebuilt prerelease + uses: softprops/action-gh-release@v2 + with: + tag_name: prebuilt + name: Prebuilt images (rolling) + prerelease: true + # Replace existing assets at the `prebuilt` tag rather than + # appending duplicates with new names. + fail_on_unmatched_files: true + files: | + Image.bz2 + rootfs.cpio.bz2 + prebuilt.sha1 + body: | + Rolling prerelease of the Linux kernel and Buildroot rootfs + consumed by `mk/external.mk`. Re-published whenever any + input that defines the kernel/rootfs content changes. + + ## Update `mk/external.mk` + + Paste these three lines into `mk/external.mk` to pin a fresh + `make` checkout to this build. `PREBUILT_INPUTS_SHA1` is what + local checkouts compare against to detect when their configs + have drifted from the prebuilt. + + ```make + KERNEL_DATA_SHA1 = ${{ steps.checksum.outputs.kernel_sha1 }} + INITRD_DATA_SHA1 = ${{ steps.checksum.outputs.initrd_sha1 }} + PREBUILT_INPUTS_SHA1 = ${{ steps.checksum.outputs.inputs_sha1 }} + ``` + + ## Raw checksums + + ``` + ${{ steps.checksum.outputs.kernel_sha1 }} Image.bz2 + ${{ steps.checksum.outputs.initrd_sha1 }} rootfs.cpio.bz2 + ${{ steps.checksum.outputs.inputs_sha1 }} inputs (configs + scripts + target/init, concatenated) + ``` diff --git a/.gitignore b/.gitignore index 00f1dfe7..7aa23f79 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ rootfs.cpio # intermediate riscv-harts.dtsi .smp_stamp -rootfs_full.cpio # Build directories buildroot/ diff --git a/Makefile b/Makefile index 8943203d..eebfc59e 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,31 @@ OPTS := LDFLAGS := +# external rootfs: boot from /dev/vda instead of unpacking initramfs. +# Implies VIRTIOBLK and pulls the userland from rootfs.cpio into ext4.img. +# Default-on. If fakeroot is missing or non-functional, fall back to the +# initramfs path so the build still succeeds without forcing a new +# dependency on the user. The probe must exec a real binary -- `fakeroot +# true` looks correct but `true` is a bash builtin, so the wrapper +# script never actually runs the DYLD_INSERT_LIBRARIES path. On macOS +# arm64 the brew fakeroot dylib is built for arm64 and refuses to load +# into arm64e system binaries (cpio, mkfs.ext4); using `/bin/sh -c :` +# forces an exec so we catch that failure here instead of mid-build. +ENABLE_EXTERNAL_ROOT ?= 1 +ifeq ($(call has, EXTERNAL_ROOT), 1) + ifneq (0,$(shell fakeroot /bin/sh -c : >/dev/null 2>&1; echo $$?)) + $(warning fakeroot not usable; falling back to initramfs boot.) + $(warning Install a working fakeroot to enable /dev/vda boot.) + override ENABLE_EXTERNAL_ROOT := 0 + endif +endif +$(call set-feature, EXTERNAL_ROOT) + # virtio-blk ENABLE_VIRTIOBLK ?= 1 +ifeq ($(call has, EXTERNAL_ROOT), 1) + ENABLE_VIRTIOBLK := 1 +endif $(call set-feature, VIRTIOBLK) DISKIMG_FILE := MKFS_EXT4 ?= mkfs.ext4 @@ -186,10 +209,6 @@ endif BIN = semu all: $(BIN) minimal.dtb -.PHONY: bench-login -bench-login: $(BIN) minimal.dtb - $(Q)/usr/bin/time -p expect scripts/bench-login.expect ./$(BIN) -k Image -b minimal.dtb -i rootfs.cpio - OBJS := \ riscv.o \ ram.o \ @@ -202,6 +221,7 @@ OBJS := \ $(OBJS_EXTRA) deps := $(OBJS:%.o=.%.o.d) +BUILD_CONFIG := CC=$(CC) $(strip $(CFLAGS)) GDBSTUB_LIB := mini-gdbstub/build/libgdbstub.a LDFLAGS += $(GDBSTUB_LIB) @@ -232,7 +252,13 @@ $(BIN): $(OBJS) $(VECHO) " LD\t$@\n" $(Q)$(CC) -o $@ $^ $(LDFLAGS) -%.o: %.c +.build-config.stamp: FORCE + @if [ ! -f $@ ] || [ "$$(cat $@ 2>/dev/null)" != "$(BUILD_CONFIG)" ]; then \ + printf '%s\n' "$(BUILD_CONFIG)" > $@; \ + rm -f $(OBJS) $(deps); \ + fi + +%.o: %.c .build-config.stamp $(VECHO) " CC\t$@\n" $(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $< @@ -247,6 +273,11 @@ DTC ?= dtc E := S := $E $E +DT_FEATURE_CPPFLAGS := $(subst ^,$S,$(filter -D^SEMU_FEATURE_%, \ + $(subst -D$(S)SEMU_FEATURE,-D^SEMU_FEATURE,$(CFLAGS)))) +DT_CPPFLAGS := $(DT_CFLAGS) $(DT_FEATURE_CPPFLAGS) +DTB_CONFIG := SMP=$(SMP) CLOCK_FREQ=$(CLOCK_FREQ) $(strip $(DT_CPPFLAGS)) + # During boot process, the emulator manually manages the growth of ticks to # suppress RCU CPU stall warnings. Thus, we need an target time to set the # increment of ticks. According to Using RCU’s CPU Stall Detector[1], the @@ -259,23 +290,22 @@ CFLAGS += -D SEMU_BOOT_TARGET_TIME=10 SMP ?= 1 -# Track SMP value changes to force DTB regeneration -.smp_stamp: FORCE - @if [ ! -f .smp_stamp ] || [ "$$(cat .smp_stamp 2>/dev/null)" != "$(SMP)" ]; then \ - echo "$(SMP)" > .smp_stamp; \ +# Track DTB inputs so config changes regenerate both the hart DTSI and DTB. +.dtb-config.stamp: FORCE + @if [ ! -f $@ ] || [ "$$(cat $@ 2>/dev/null)" != "$(DTB_CONFIG)" ]; then \ + printf '%s\n' "$(DTB_CONFIG)" > $@; \ rm -f riscv-harts.dtsi minimal.dtb; \ fi .PHONY: riscv-harts.dtsi -riscv-harts.dtsi: .smp_stamp +riscv-harts.dtsi: .dtb-config.stamp $(Q)python3 scripts/gen-hart-dts.py $@ $(SMP) $(CLOCK_FREQ) -minimal.dtb: minimal.dts riscv-harts.dtsi +minimal.dtb: minimal.dts riscv-harts.dtsi .dtb-config.stamp $(VECHO) " DTC\t$@\n" $(Q)$(RM) $@ $(Q)$(CC) -nostdinc -E -P -x assembler-with-cpp -undef \ - $(DT_CFLAGS) \ - $(subst ^,$S,$(filter -D^SEMU_FEATURE_%, $(subst -D$(S)SEMU_FEATURE,-D^SEMU_FEATURE,$(CFLAGS)))) $< \ + $(DT_CPPFLAGS) $< \ | $(DTC) - > $@ .PHONY: FORCE @@ -284,9 +314,14 @@ FORCE: # Rules for downloading prebuilt Linux kernel image include mk/external.mk +ifeq ($(call has, EXTERNAL_ROOT), 1) +ext4.img: $(INITRD_DATA) scripts/rootfs_ext4.sh + $(Q)MKFS_EXT4="$(MKFS_EXT4)" scripts/rootfs_ext4.sh $(INITRD_DATA) $@ +else ext4.img: $(Q)dd if=/dev/zero of=$@ bs=4k count=600 $(Q)$(MKFS_EXT4) -F $@ +endif .PHONY: $(DIRECTORY) $(SHARED_DIRECTORY): @@ -295,9 +330,22 @@ $(SHARED_DIRECTORY): mkdir -p $@; \ fi -check: $(BIN) minimal.dtb $(KERNEL_DATA) $(INITRD_DATA) $(DISKIMG_FILE) $(SHARED_DIRECTORY) +ifeq ($(call has, EXTERNAL_ROOT), 1) +INITRD_DEP := +INITRD_OPT := +else +INITRD_DEP := $(INITRD_DATA) +INITRD_OPT := -i $(INITRD_DATA) +endif + +.PHONY: bench-login +bench-login: $(BIN) minimal.dtb $(KERNEL_DATA) $(INITRD_DEP) $(DISKIMG_FILE) + $(Q)/usr/bin/time -p expect scripts/bench-login.expect \ + ./$(BIN) -k $(KERNEL_DATA) -b minimal.dtb -H $(INITRD_OPT) $(OPTS) + +check: $(BIN) minimal.dtb $(KERNEL_DATA) $(INITRD_DEP) $(DISKIMG_FILE) $(SHARED_DIRECTORY) @$(call notice, Ready to launch Linux kernel. Please be patient.) - $(Q)./$(BIN) -k $(KERNEL_DATA) -c $(SMP) -b minimal.dtb -i $(INITRD_DATA) $(if $(NETDEV),-n $(NETDEV)) $(OPTS) + $(Q)./$(BIN) -k $(KERNEL_DATA) -c $(SMP) -b minimal.dtb -H $(INITRD_OPT) $(if $(NETDEV),-n $(NETDEV)) $(OPTS) build-image: scripts/build-image.sh @@ -312,7 +360,8 @@ clean: distclean: clean $(Q)$(RM) riscv-harts.dtsi $(Q)$(RM) minimal.dtb - $(Q)$(RM) .smp_stamp + $(Q)$(RM) .dtb-config.stamp + $(Q)$(RM) .build-config.stamp $(Q)$(RM) Image rootfs.cpio $(Q)$(RM) ext4.img diff --git a/README.md b/README.md index 4ed3c674..fd860103 100644 --- a/README.md +++ b/README.md @@ -79,14 +79,58 @@ You can exit the emulator using: \. (press Ctrl+A, leave it, afterwar ## Usage ```shell -./semu -k linux-image [-b dtb-file] [-i initrd-image] [-d disk-image] [-s shared-directory] +./semu -k linux-image [-b dtb-file] [-d disk-image] [-i initrd-image] [-s shared-directory] [-H] ``` * `linux-image` is the path to the Linux kernel `Image`. * `dtb-file` is optional, as it specifies the user-specified device tree blob. -* `initrd-image` is optional, as it specifies the user-specified initial RAM disk image. -* `disk-image` is optional, as it specifies the path of a disk image in ext4 file system for the virtio-blk device. +* `disk-image` is the ext4 image exposed as `/dev/vda` to the guest. The + default boot path mounts this as the root filesystem; `make` builds it + from `rootfs.cpio` via `scripts/rootfs_ext4.sh`. * `shared-directory` is optional, as it specifies the path of a directory on the host that will be shared with the guest operating system through virtio-fs, enabling file access from the guest via a virtual filesystem mount. +* `-H` (or `--headless`) skips SDL window creation; useful for CI and `make check`. +* `initrd-image` is optional and only used on the *legacy* boot path. + The default `minimal.dtb` built with `ENABLE_EXTERNAL_ROOT=1` does not + advertise initrd placement, so `-i` there requires either + `ENABLE_EXTERNAL_ROOT=0` or a custom DTB passed with `-b`. See *Boot mode* + below. + +### Boot mode + +The default build (`make`) boots the kernel directly from `/dev/vda` and +runs `/sbin/init` from the ext4 root, skipping the initramfs unpack step +entirely. This is faster, avoids the RCU-stall the kernel hits when +unpacking a large cpio, and matches how real systems deploy. The +`ext4.img` is built from `rootfs.cpio` via `scripts/rootfs_ext4.sh`, +which requires `fakeroot` and `mkfs.ext4`. + +If `fakeroot` is missing, the build falls back to the legacy initramfs +path (`-i rootfs.cpio`) automatically and prints a one-line warning. To +force the legacy path explicitly: + +```shell +$ make ENABLE_EXTERNAL_ROOT=0 +$ make ENABLE_EXTERNAL_ROOT=0 check +``` + +The legacy path uses what the flag is still spelled as: `-i initrd-image`. +That is a runtime choice only when the DTB also carries +`linux,initrd-{start,end}`. semu's default external-root build emits a DTB +that always boots from `/dev/vda`, so `-i` is rejected there unless you +replace the DTB with one that describes the initrd layout. +The classical *initrd* (a filesystem image mounted as `/dev/ram0`, +pivoted into via `pivot_root`) is effectively obsolete -- it required +`CONFIG_BLK_DEV_INITRD` plus the legacy ramdisk block driver, an +in-kernel filesystem driver to mount the image before any userspace code +ran, and a `/linuxrc` handoff. Mainstream distros and embedded builds +dropped that path more than a decade ago. Linux 2.6+ kept the flag and +the `linux,initrd-{start,end}` device-tree properties, but the kernel +inspects the loaded blob: a cpio archive is unpacked into the in-memory +`rootfs` (initramfs path, runs `/init`); a filesystem image still falls +back to the legacy initrd path if that driver is configured in. semu +ships and consumes a cpio (`rootfs.cpio`), so the legacy build is +exercising the initramfs path even though the CLI flag spelling stayed +`-i initrd-image`. For detailed networking guidance, see [`docs/networking.md`](docs/networking.md). @@ -125,14 +169,16 @@ This command invokes the underlying script: `scripts/build-image.sh`, which also ### Script Usage ``` -./scripts/build-image.sh [--buildroot] [--linux] [--all] [--external-root] [--clean-build] [--help] +./scripts/build-image.sh [--buildroot] [--linux] [--all] [--no-ext4] [--clean-build] [--help] Options: - --buildroot Build Buildroot rootfs - --linux Build Linux kernel + --buildroot Build Buildroot userland (produces rootfs.cpio and, + unless --no-ext4 is given, ext4.img for vda boot) + --linux Build the Linux kernel --all Build both Buildroot and Linux - --external-root Use external rootfs instead of initramfs - --clean-build Remove entire buildroot/ and/or linux/ directories before build + --no-ext4 Skip ext4.img generation; produce only rootfs.cpio + (matches the legacy ENABLE_EXTERNAL_ROOT=0 path) + --clean-build Remove buildroot/ and/or linux/ before building --help Show this message ``` @@ -144,16 +190,16 @@ Build the Linux kernel only: $ scripts/build-image.sh --linux ``` -Build Buildroot only: +Build Buildroot (produces both `rootfs.cpio` and `ext4.img`): ``` $ scripts/build-image.sh --buildroot ``` -Build Buildroot and generate an external root file system (ext4 image): +Build Buildroot for the legacy initramfs-only path (no ext4): ``` -$ scripts/build-image.sh --buildroot --external-root +$ scripts/build-image.sh --buildroot --no-ext4 ``` Force a clean build: diff --git a/feature.h b/feature.h index b2cda1ed..0ff4e1b0 100644 --- a/feature.h +++ b/feature.h @@ -27,5 +27,10 @@ #define SEMU_FEATURE_VIRTIOINPUT 1 #endif +/* external rootfs: kernel boots /dev/vda, no initramfs. Default off. */ +#ifndef SEMU_FEATURE_EXTERNAL_ROOT +#define SEMU_FEATURE_EXTERNAL_ROOT 0 +#endif + /* Feature test macro */ #define SEMU_HAS(x) SEMU_FEATURE_##x diff --git a/main.c b/main.c index cd15e86e..534b3864 100644 --- a/main.c +++ b/main.c @@ -703,7 +703,7 @@ static void usage(const char *execpath) { fprintf(stderr, "Usage: %s -k linux-image [-b dtb] [-i initrd-image] [-d " - "disk-image] [-s shared-directory]\n", + "disk-image] [-s shared-directory] [-H]\n", execpath); } @@ -716,20 +716,22 @@ static void handle_options(int argc, char **net_dev, int *hart_count, bool *debug, + bool *headless, char **shared_dir) { *kernel_file = *dtb_file = *initrd_file = *disk_file = *net_dev = *shared_dir = NULL; int optidx = 0; - struct option opts[] = {{"kernel", 1, NULL, 'k'}, {"dtb", 1, NULL, 'b'}, - {"initrd", 1, NULL, 'i'}, {"disk", 1, NULL, 'd'}, - {"netdev", 1, NULL, 'n'}, {"smp", 1, NULL, 'c'}, - {"gdbstub", 0, NULL, 'g'}, {"help", 0, NULL, 'h'}, - {"shared_dir", 1, NULL, 's'}}; + struct option opts[] = { + {"kernel", 1, NULL, 'k'}, {"dtb", 1, NULL, 'b'}, + {"initrd", 1, NULL, 'i'}, {"disk", 1, NULL, 'd'}, + {"netdev", 1, NULL, 'n'}, {"smp", 1, NULL, 'c'}, + {"gdbstub", 0, NULL, 'g'}, {"help", 0, NULL, 'h'}, + {"shared_dir", 1, NULL, 's'}, {"headless", 0, NULL, 'H'}}; int c; - while ((c = getopt_long(argc, argv, "k:b:i:d:n:c:s:gh", opts, &optidx)) != + while ((c = getopt_long(argc, argv, "k:b:i:d:n:c:s:ghH", opts, &optidx)) != -1) { switch (c) { case 'k': @@ -747,15 +749,34 @@ static void handle_options(int argc, case 'n': *net_dev = optarg; break; - case 'c': - *hart_count = atoi(optarg); + case 'c': { + /* strtol over atoi: well-defined behavior on overflow plus + * trailing-junk detection. Upper bound is 32 because + * plic_state_t.ie[] is sized for 32 contexts (one per hart); + * see device.h. + */ + char *end; + errno = 0; + long n = strtol(optarg, &end, 10); + if (errno || *end || end == optarg || n < 1 || n > 32) { + fprintf(stderr, + "%s: -c expects an integer hart count in [1,32], " + "got '%s'\n", + argv[0], optarg); + exit(2); + } + *hart_count = (int) n; break; + } case 's': *shared_dir = optarg; break; case 'g': *debug = true; break; + case 'H': + *headless = true; + break; case 'h': usage(argv[0]); exit(0); @@ -776,6 +797,24 @@ static void handle_options(int argc, *dtb_file = "minimal.dtb"; } +#if SEMU_HAS(EXTERNAL_ROOT) +static bool uses_default_minimal_dtb(const char *dtb_file) +{ + const char *name; + + if (!dtb_file) + return false; + + name = strrchr(dtb_file, '/'); + if (name) + name++; + else + name = dtb_file; + + return strcmp(name, "minimal.dtb") == 0; +} +#endif + #define INIT_HART(hart, emu, id) \ do { \ hart->priv = emu; \ @@ -801,12 +840,33 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) char *shared_dir; int hart_count = 1; bool debug = false; + bool headless = false; #if SEMU_HAS(VIRTIONET) bool netdev_ready = false; #endif vm_t *vm = &emu->vm; handle_options(argc, argv, &kernel_file, &dtb_file, &initrd_file, - &disk_file, &netdev, &hart_count, &debug, &shared_dir); + &disk_file, &netdev, &hart_count, &debug, &headless, + &shared_dir); +#if !SEMU_HAS(VIRTIOINPUT) + (void) headless; +#endif + +#if SEMU_HAS(EXTERNAL_ROOT) + if (initrd_file && uses_default_minimal_dtb(dtb_file)) { + fprintf(stderr, + "-i requires a DTB with linux,initrd-start/end. " + "Rebuild with ENABLE_EXTERNAL_ROOT=0 or pass -b " + ".\n"); + return 2; + } + + if (!disk_file && uses_default_minimal_dtb(dtb_file)) { + fprintf(stderr, + "warning: EXTERNAL_ROOT build expects -d ; " + "without it the kernel will hang in rootwait.\n"); + } +#endif /* Initialize the emulator */ memset(emu, 0, sizeof(*emu)); @@ -820,24 +880,38 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) } assert(!(((uintptr_t) emu->ram) & 0b11)); - /* *-----------------------------------------* - * | Memory layout | - * *----------------*----------------*-------* - * | kernel image | initrd image | dtb | - * *----------------*----------------*-------* + /* Memory layout. Two shapes depending on whether `-i` was given: + * + * Default (vda boot, no -i): + * 0 RAM_SIZE - 1MiB + * +------------------+--------//-----+-----+ + * | kernel image | free RAM | dtb | + * +------------------+--------//-----+-----+ + * (dtb at top) + * + * Legacy initramfs (-i present): + * 0 RAM-9MiB RAM-1MiB + * +------------------+----+---------+-----+ + * | kernel image | .. | initrd | dtb | + * +------------------+----+---------+-----+ + * (8 MiB) (1 MiB) + * + * dtb sits in the last 1 MiB and initrd, when present, in the 8 MiB + * just below it -- both placements keep the kernel from clobbering + * them as it allocates downward from RAM_SIZE. */ char *ram_loc = (char *) emu->ram; - /* Load Linux kernel image */ + /* Load Linux kernel image at the base of RAM */ map_file(&ram_loc, kernel_file); - /* Load at last 1 MiB to prevent kernel from overwriting it */ - uint32_t dtb_addr = RAM_SIZE - DTB_SIZE; /* Device tree */ + /* Load dtb at the last 1 MiB so the kernel will not overwrite it */ + uint32_t dtb_addr = RAM_SIZE - DTB_SIZE; ram_loc = ((char *) emu->ram) + dtb_addr; map_file(&ram_loc, dtb_file); - /* Load optional initrd image at last 8 MiB before the dtb region to - * prevent kernel from overwritting it + /* Load optional initrd image in the 8 MiB just below the dtb region + * (legacy boot path; not used when the guest boots from /dev/vda). */ if (initrd_file) { - uint32_t initrd_addr = dtb_addr - INITRD_SIZE; /* Init RAM disk */ + uint32_t initrd_addr = dtb_addr - INITRD_SIZE; ram_loc = ((char *) emu->ram) + initrd_addr; map_file(&ram_loc, initrd_file); } @@ -848,6 +922,10 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) /* Set up RISC-V harts */ vm->n_hart = hart_count; vm->hart = malloc(sizeof(hart_t *) * vm->n_hart); + if (!vm->hart) { + fprintf(stderr, "Failed to allocate %u hart slots.\n", vm->n_hart); + return 1; + } for (uint32_t i = 0; i < vm->n_hart; i++) { hart_t *newhart = calloc(1, sizeof(hart_t)); if (!newhart) { @@ -915,7 +993,7 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) #endif #if SEMU_HAS(VIRTIOINPUT) - g_window.window_init(); + g_window.window_init(headless); emu->vkeyboard.ram = emu->ram; virtio_input_init(&(emu->vkeyboard)); @@ -1127,18 +1205,35 @@ static int semu_step_chunk(emu_state_t *emu, hart_t *hart, int steps) return 0; } -#ifdef MMU_CACHE_STATS +/* Async-signal-safe SIGINT/SIGTERM handler. Setting this flag lets the + * event loops break out cleanly so atexit hooks (e.g., the virtio-blk + * msync) actually run instead of being skipped by an immediate process + * death. The MMU_CACHE_STATS build additionally uses the flag to defer + * stats printing out of the signal handler. + * + * When VIRTIOINPUT runs the emulator in a background thread, the signal + * is typically delivered to the main thread. The emu thread can be + * blocked in poll(-1), so we also write a byte to its wake pipe; that + * guarantees the loop notices `signal_received` and exits, which in + * turn unblocks the window main loop via window_shutdown(). + */ static volatile sig_atomic_t signal_received = 0; - -/* Forward declaration */ -static void print_mmu_cache_stats(vm_t *vm); - -/* Async-signal-safe handler: only set flag, defer printing */ -static void signal_handler_stats(int sig UNUSED) +static volatile sig_atomic_t signal_wake_fd = -1; +static void signal_handler(int sig UNUSED) { signal_received = 1; + int fd = signal_wake_fd; + if (fd >= 0) { + char byte = 1; + /* write() is async-signal-safe; pipe is non-blocking so this + * cannot stall the handler. + */ + ssize_t n = write(fd, &byte, 1); + (void) n; + } } +#ifdef MMU_CACHE_STATS static void print_mmu_cache_stats(vm_t *vm) { fprintf(stderr, "\n=== MMU Cache Statistics ===\n"); @@ -1303,13 +1398,11 @@ static void semu_run(emu_state_t *emu) size_t poll_capacity = 0; while (!emu->stopped) { -#ifdef MMU_CACHE_STATS - /* Check if signal received (SIGINT/SIGTERM). - * Break to cleanup resources, stats printed at end of main(). + /* Break out on SIGINT/SIGTERM so main() returns and atexit + * hooks (e.g., virtio-blk msync) run before the process dies. */ if (signal_received) break; -#endif /* Only need fds for timer and UART (no coroutine I/O), * plus an optional wake pipe when VIRTIOINPUT is enabled. */ @@ -1329,6 +1422,15 @@ static void semu_run(emu_state_t *emu) close(kq); #else close(wfi_timer_fd); +#endif +#if SEMU_HAS(VIRTIOINPUT) + /* Mirror the normal-exit cleanup so the wake pipe + * does not leak across the early return. + */ + if (emu->wake_fd[0] >= 0) + close(emu->wake_fd[0]); + if (emu->wake_fd[1] >= 0) + close(emu->wake_fd[1]); #endif emu->exit_code = -1; return; @@ -1531,6 +1633,11 @@ static void semu_run(emu_state_t *emu) if (emu->wake_fd[1] >= 0) close(emu->wake_fd[1]); #endif + /* Free coroutine stacks/contexts from coro_init() above so the + * graceful-exit path matches what coro_create_hart()'s failure + * path already does. Idempotent against !initialized. + */ + coro_cleanup(); /* A closed window is a normal user action, not an error. */ #if SEMU_HAS(VIRTIOINPUT) @@ -1549,13 +1656,9 @@ static void semu_run(emu_state_t *emu) /* Single-hart mode: use original scheduling */ while (!emu->stopped) { -#ifdef MMU_CACHE_STATS - /* Check if signal received (SIGINT/SIGTERM). - * Break to exit loop, stats printed at end of main(). - */ + /* Break out on SIGINT/SIGTERM so atexit hooks fire on graceful exit. */ if (signal_received) break; -#endif #if SEMU_HAS(VIRTIONET) int i = 0; if (emu->vnet.peer.type == NETDEV_IMPL_user && boot_complete) { @@ -1639,18 +1742,22 @@ static int semu_read_mem(void *args, size_t addr, size_t len, void *val) static gdb_action_t semu_cont(void *args) { emu_state_t *emu = (emu_state_t *) args; + + /* A previous terminal interrupt should stop only the active continue. + * Clear the sticky global flag before resuming so later GDB `continue` + * commands can run guest code again. + */ + signal_received = 0; #if SEMU_HAS(VIRTIOINPUT) while (!semu_is_interrupt(emu) && !g_window.window_is_closed()) { #else while (!semu_is_interrupt(emu)) { #endif -#ifdef MMU_CACHE_STATS - /* Check if signal received (SIGINT/SIGTERM). - * Break to return control to gdbstub, stats printed at end of main(). + /* Break out on SIGINT/SIGTERM so the gdbstub regains control + * and main() returns through the atexit hooks. */ if (signal_received) break; -#endif semu_step(emu); } @@ -1759,12 +1866,32 @@ int main(int argc, char **argv) if (ret) return ret; -#ifdef MMU_CACHE_STATS - signal(SIGINT, signal_handler_stats); - signal(SIGTERM, signal_handler_stats); -#endif + /* Install handlers unconditionally so SIGINT/SIGTERM let us return + * through main() and run atexit hooks (msync of MAP_SHARED disks, + * etc.) instead of being killed mid-flight. Use sigaction so the + * SA_RESTART flag stays clear -- otherwise glibc's `signal()` would + * auto-restart `poll()` and the loops would never see the flag. + */ + /* Use sigaction so the SA_RESTART flag stays clear -- otherwise + * glibc's `signal()` would auto-restart `poll()` and the loops + * would never see the flag. + */ + { + struct sigaction sa = {0}; + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + } #if SEMU_HAS(VIRTIOINPUT) + /* Publish the wake pipe to the signal handler so SIGINT/SIGTERM can + * unblock the emulator thread's poll() in the threaded window path. + */ + if (emu.wake_fd[1] >= 0) + signal_wake_fd = emu.wake_fd[1]; + /* If window backend has a main loop function, run emulator in background * thread and use main thread for window events (required for macOS SDL2). */ diff --git a/minimal.dts b/minimal.dts index c1806ba3..709d1872 100644 --- a/minimal.dts +++ b/minimal.dts @@ -15,10 +15,24 @@ }; chosen { +#if SEMU_FEATURE_EXTERNAL_ROOT && SEMU_FEATURE_VIRTIOBLK + /* External rootfs on virtio-blk; skip initrd unpack entirely. + * - quiet/loglevel=3: suppress non-error printk so the UART path + * (MMIO -> device handler -> fputc) does not dominate boot + * - lpj=260000: skip the BogoMIPS calibration loop; the value + * matches what calibration would print at the current + * timebase-frequency + * Note: rootflags=noatime makes ext4 root mount fail under the + * prebuilt Linux Image; do not add it back without re-testing. + */ + bootargs = "earlycon console=ttyS0 root=/dev/vda rw rootwait quiet loglevel=3 lpj=260000"; + stdout-path = "serial0"; +#else bootargs = "earlycon console=ttyS0"; stdout-path = "serial0"; linux,initrd-start = <0x1f700000>; /* @403 MiB (503 * 1024 * 1024) */ linux,initrd-end = <0x1fefffff>; /* @511 MiB (511 * 1024 * 1024 - 1) */ +#endif }; cpus { diff --git a/mk/external.mk b/mk/external.mk index 1d95dc93..c4b28155 100644 --- a/mk/external.mk +++ b/mk/external.mk @@ -2,6 +2,14 @@ # _DATA_URL : the hyperlink which points to archive. # _DATA : the file to be read by specific executable. # _DATA_SHA1 : the checksum of the content in _DATA +# +# Artifacts live on the orphan `blob` branch of this repository. The +# `Publish prebuilt images` workflow (see .github/workflows/prebuilt.yml) +# will eventually republish them to a fixed-tag GitHub prerelease; once +# that release exists, switch COMMON_URL to +# https://github.com/sysprog21/semu/releases/download/prebuilt and update +# KERNEL_DATA_SHA1, INITRD_DATA_SHA1, and PREBUILT_INPUTS_SHA1 from the +# release body. COMMON_URL = https://github.com/sysprog21/semu/raw/blob @@ -25,3 +33,51 @@ endef EXTERNAL_DATA = KERNEL INITRD $(foreach T,$(EXTERNAL_DATA),$(eval $(download))) + +# --- Stale-prebuilt detection ------------------------------------------- +# +# The prebuilt Image and rootfs.cpio above are baked from a fixed set of +# input files (kernel/buildroot/busybox configs, the build script, and +# the init stub). When any of those change locally the prebuilt may no +# longer reflect the user's intent, so we compute the SHA1 of those +# inputs and compare against PREBUILT_INPUTS_SHA1 -- the value the +# `Publish prebuilt images` workflow recorded for the live release. +# +# Mismatch -> warn but do not auto-rebuild: a buildroot run takes the +# better part of an hour, so we let the user opt in via `make build-image`. +# Keep this list in sync with the INPUTS array in .ci/publish-prebuilt.sh +# and the `paths:` filter in .github/workflows/prebuilt.yml. +PREBUILT_INPUTS := \ + configs/linux.config \ + configs/busybox.config \ + configs/buildroot.config \ + scripts/build-image.sh \ + scripts/rootfs_ext4.sh \ + target/init + +PREBUILT_INPUTS_SHA1 = 1ae09da49a6d7ce44e10d04a682950b295b3b77c + +# Compute the live hash only when *every* input file exists. A partial +# tree would otherwise silently hash the present subset and trip a bogus +# "stale" warning instead of the more useful "your tree is incomplete" +# signal. The shell-side count compare is portable across BSD/GNU. +LIVE_INPUTS_SHA1 := $(shell \ + expected=$(words $(PREBUILT_INPUTS)); \ + found=0; \ + for f in $(PREBUILT_INPUTS); do [ -f "$$f" ] && found=$$((found + 1)); done; \ + if [ "$$found" -eq "$$expected" ]; then \ + cat $(PREBUILT_INPUTS) | $(SHA1SUM) | awk '{print $$1}'; \ + fi) + +# Skip the comparison until PREBUILT_INPUTS_SHA1 is real (the all-zero +# placeholder is the bootstrap state before the first prebuilt run). +ifneq ($(PREBUILT_INPUTS_SHA1),0000000000000000000000000000000000000000) +ifneq ($(LIVE_INPUTS_SHA1),) +ifneq ($(LIVE_INPUTS_SHA1),$(PREBUILT_INPUTS_SHA1)) +$(warning Local kernel/rootfs inputs ($(LIVE_INPUTS_SHA1)) differ from) +$(warning the prebuilt's recorded inputs ($(PREBUILT_INPUTS_SHA1)).) +$(warning The downloaded Image/rootfs.cpio do not reflect your local) +$(warning configs. Run `make build-image` to rebuild from source.) +endif +endif +endif diff --git a/scripts/bench-login.expect b/scripts/bench-login.expect new file mode 100644 index 00000000..066be825 --- /dev/null +++ b/scripts/bench-login.expect @@ -0,0 +1,14 @@ +#!/usr/bin/env expect + +set timeout 40 + +if {$argc < 1} { + puts stderr "usage: bench-login.expect [args...]" + exit 2 +} + +log_user 0 +spawn {*}$argv +expect "buildroot login:" +exec kill [exp_pid] +expect eof diff --git a/scripts/build-image.sh b/scripts/build-image.sh index 0b977a4e..a805443d 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -43,7 +43,7 @@ function do_buildroot safe_copy configs/buildroot.config buildroot/.config safe_copy configs/busybox.config buildroot/busybox.config cp -f target/init buildroot/fs/cpio/init - + # Otherwise, the error below raises: # You seem to have the current working directory in your # LD_LIBRARY_PATH environment variable. This doesn't work. @@ -53,13 +53,20 @@ function do_buildroot ASSERT make $PARALLEL popd - if [[ $EXTERNAL_ROOT -eq 1 ]]; then - echo "Copying rootfs.cpio to rootfs_full.cpio (external root mode)" - cp -f buildroot/output/images/rootfs.cpio ./rootfs_full.cpio - ASSERT ./scripts/rootfs_ext4.sh + # Always publish the cpio. It is the canonical buildroot output and + # serves both as the source for the ext4 image and as the legacy + # initramfs payload (when ENABLE_EXTERNAL_ROOT=0). + echo "Publishing rootfs.cpio" + cp -f buildroot/output/images/rootfs.cpio ./rootfs.cpio + + # Build ext4.img unless --no-ext4 was passed. The make default + # (ENABLE_EXTERNAL_ROOT=1) boots from /dev/vda and needs this image. + # --no-ext4 is the escape hatch for users who only want the legacy + # initramfs path or do not have fakeroot/mkfs.ext4 installed. + if [[ $NO_EXT4 -eq 1 ]]; then + echo "Skipping ext4.img build (--no-ext4)" else - echo "Copying rootfs.cpio to rootfs.cpio (initramfs mode)" - cp -f buildroot/output/images/rootfs.cpio ./rootfs.cpio + ASSERT ./scripts/rootfs_ext4.sh ./rootfs.cpio ./ext4.img fi } @@ -86,14 +93,16 @@ function do_linux function show_help { cat << EOF -Usage: $0 [--buildroot] [--linux] [--all] [--external-root] [--clean-build] [--help] +Usage: $0 [--buildroot] [--linux] [--all] [--no-ext4] [--clean-build] [--help] Options: - --buildroot Build Buildroot rootfs - --linux Build Linux kernel + --buildroot Build Buildroot userland (produces rootfs.cpio and, + unless --no-ext4 is given, ext4.img for vda boot) + --linux Build the Linux kernel --all Build both Buildroot and Linux - --external-root Use external rootfs instead of initramfs - --clean-build Remove entire buildroot/ and/or linux/ directories before build + --no-ext4 Skip ext4.img generation; produce only rootfs.cpio + (matches the legacy ENABLE_EXTERNAL_ROOT=0 path) + --clean-build Remove buildroot/ and/or linux/ before building --help Show this message EOF exit 1 @@ -101,7 +110,7 @@ EOF BUILD_BUILDROOT=0 BUILD_LINUX=0 -EXTERNAL_ROOT=0 +NO_EXT4=0 CLEAN_BUILD=0 while [[ $# -gt 0 ]]; do @@ -116,8 +125,8 @@ while [[ $# -gt 0 ]]; do BUILD_BUILDROOT=1 BUILD_LINUX=1 ;; - --external-root) - EXTERNAL_ROOT=1 + --no-ext4) + NO_EXT4=1 ;; --clean-build) CLEAN_BUILD=1 diff --git a/scripts/rootfs_ext4.sh b/scripts/rootfs_ext4.sh index 0fb91de3..9cb03725 100755 --- a/scripts/rootfs_ext4.sh +++ b/scripts/rootfs_ext4.sh @@ -1,26 +1,57 @@ -#!/usr/bin/bash +#!/usr/bin/env bash +# +# Build an ext4 rootfs image from an existing cpio archive. +# +# Usage: rootfs_ext4.sh [SOURCE_CPIO] [OUT_IMG] [SIZE_MB] +# +# Default values match the EXTROOT make path: read rootfs.cpio, produce +# ext4.img sized at 32 MiB. The 32 MiB default fits the buildroot userland +# with headroom; bump SIZE_MB for larger rootfs payloads. -ROOTFS_CPIO="rootfs_full.cpio" -IMG="ext4.img" -IMG_SIZE=$((1024 * 1024 * 1024)) # 1GB -IMG_SIZE_BLOCKS=$((${IMG_SIZE} / 4096)) # IMG_SIZE / 4k +set -euo pipefail -DIR=rootfs +SRC_CPIO="${1:-rootfs.cpio}" +OUT_IMG="${2:-ext4.img}" +SIZE_MB="${3:-32}" +MKFS_EXT4="${MKFS_EXT4:-mkfs.ext4}" -echo "[*] Remove old rootfs directory..." -rm -rf $DIR -mkdir -p $DIR +if [ ! -f "$SRC_CPIO" ]; then + echo "[!] Source cpio not found: $SRC_CPIO" >&2 + exit 1 +fi -echo "[*] Extract CPIO" -pushd $DIR -cpio -idmv < ../$ROOTFS_CPIO -popd +if ! command -v fakeroot >/dev/null 2>&1; then + echo "[!] fakeroot is required to build the ext4 image" >&2 + exit 1 +fi -echo "[*] Create empty image" -dd if=/dev/zero of=${IMG} bs=4k count=${IMG_SIZE_BLOCKS} +if ! command -v "$MKFS_EXT4" >/dev/null 2>&1; then + echo "[!] mkfs.ext4 is required to build the ext4 image" >&2 + exit 1 +fi -echo "[*] Create ext4 rootfs image" -fakeroot mkfs.ext4 -F ${IMG} -d $DIR +SRC_DIR="$(cd "$(dirname "$SRC_CPIO")" && pwd -P)" +SRC_ABS="$SRC_DIR/$(basename "$SRC_CPIO")" +# `mktemp -d -t PREFIX` differs between GNU (PREFIX is a name) and BSD (PREFIX +# is a template) -- spell out the full template instead. +STAGE="$(mktemp -d "${TMPDIR:-/tmp}/semu-rootfs.XXXXXX")" +trap 'rm -rf "$STAGE"' EXIT -# Show image size -du -h ${IMG} +echo "[*] Extracting $SRC_CPIO -> $STAGE" +( cd "$STAGE" && fakeroot bash -c "cpio -idm < '$SRC_ABS'" ) + +echo "[*] Creating empty image: $OUT_IMG (${SIZE_MB} MiB)" +# bs=1024k works on both GNU and BSD dd; bs=1M is GNU-only and bs=1m is +# BSD-only. +dd if=/dev/zero of="$OUT_IMG" bs=1024k count="$SIZE_MB" >/dev/null 2>&1 + +echo "[*] Building ext4 filesystem" +# -E lazy_*_init=0: do all init at mkfs time so the first guest mount does +# not pay the lazy-init cost. Stripping the journal (-O ^has_journal) +# would also speed mount, but the prebuilt Linux Image is built with +# CONFIG_EXT4_USE_FOR_EXT2=n and refuses to mount a no-journal image. +fakeroot "$MKFS_EXT4" -q -F \ + -E lazy_itable_init=0,lazy_journal_init=0 \ + -d "$STAGE" "$OUT_IMG" + +du -h "$OUT_IMG" diff --git a/virtio-blk.c b/virtio-blk.c index c4027730..56ee5de9 100644 --- a/virtio-blk.c +++ b/virtio-blk.c @@ -67,6 +67,28 @@ PACKED(struct vblk_req_header { static struct virtio_blk_config vblk_configs[VBLK_DEV_CNT_MAX]; static int vblk_dev_cnt = 0; +/* Track each MAP_SHARED disk mapping so we can msync(MS_SYNC) on graceful + * exit. Without this, dirty pages live in the host page cache and rely on + * the kernel's writeback to land on disk. The guest cannot trigger a sync + * via VIRTIO_BLK_T_FLUSH today because we do not advertise + * VIRTIO_BLK_F_FLUSH; this hook is the best-effort substitute for that. + */ +static struct { + void *addr; + size_t size; +} vblk_disks[VBLK_DEV_CNT_MAX]; +static int vblk_disks_cnt = 0; + +static void virtio_blk_sync_all(void) +{ + for (int i = 0; i < vblk_disks_cnt; i++) { + if (!vblk_disks[i].addr) + continue; + if (msync(vblk_disks[i].addr, vblk_disks[i].size, MS_SYNC) < 0) + perror("virtio-blk: msync"); + } +} + static void virtio_blk_set_fail(virtio_blk_state_t *vblk) { vblk->Status |= VIRTIO_STATUS__DEVICE_NEEDS_RESET; @@ -466,7 +488,16 @@ uint32_t *virtio_blk_init(virtio_blk_state_t *vblk, char *disk_file) /* Get the disk image size */ struct stat st; - fstat(disk_fd, &st); + if (fstat(disk_fd, &st) < 0) { + fprintf(stderr, "fstat(%s): %s\n", disk_file, strerror(errno)); + close(disk_fd); + exit(2); + } + if (st.st_size <= 0) { + fprintf(stderr, "%s is empty or has invalid size\n", disk_file); + close(disk_fd); + exit(2); + } size_t disk_size = st.st_size; /* Set up the disk memory */ @@ -482,5 +513,11 @@ uint32_t *virtio_blk_init(virtio_blk_state_t *vblk, char *disk_file) vblk->disk = disk_mem; PRIV(vblk)->capacity = (disk_size - 1) / DISK_BLK_SIZE + 1; + if (vblk_disks_cnt == 0) + atexit(virtio_blk_sync_all); + vblk_disks[vblk_disks_cnt].addr = disk_mem; + vblk_disks[vblk_disks_cnt].size = disk_size; + vblk_disks_cnt++; + return disk_mem; } diff --git a/window-sw.c b/window-sw.c index 940c7f3d..3d464034 100644 --- a/window-sw.c +++ b/window-sw.c @@ -120,8 +120,13 @@ static void window_main_loop_sw(void) } } -static void window_init_sw(void) +static void window_init_sw(bool headless) { + if (headless) { + headless_mode = true; + return; + } + if (SDL_Init(SDL_INIT_VIDEO) < 0) { fprintf(stderr, "window_init_sw(): failed to initialize SDL: %s\n" diff --git a/window.h b/window.h index 7dbe634b..5f2e51e0 100644 --- a/window.h +++ b/window.h @@ -6,7 +6,11 @@ #if SEMU_HAS(VIRTIOINPUT) struct window_backend { - void (*window_init)(void); + /* When headless is true, the backend skips SDL_Init / window creation and + * behaves as if SDL had failed -- useful for batch runs (CI, 'make check') + * that have no display attached. + */ + void (*window_init)(bool headless); /* Main loop function that runs on the main thread (for macOS SDL2). * If non-NULL, the emulator runs in a background thread while this * function handles window events on the main thread.