diff --git a/apps/starry/selfhost/build-x86_64-unknown-none.toml b/apps/starry/selfhost/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..0e636768bf --- /dev/null +++ b/apps/starry/selfhost/build-x86_64-unknown-none.toml @@ -0,0 +1,10 @@ +target = "x86_64-unknown-none" +log = "Warn" +features = [ + "axplat-dyn/efi", + "ax-driver/virtio-blk", + "ax-driver/virtio-net", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", +] diff --git a/apps/starry/selfhost/selfhost-full-kernel/guest-selfbuild-reboot-guard.sh b/apps/starry/selfhost/selfhost-full-kernel/guest-selfbuild-reboot-guard.sh new file mode 100644 index 0000000000..70ab56e59a --- /dev/null +++ b/apps/starry/selfhost/selfhost-full-kernel/guest-selfbuild-reboot-guard.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# This file is sourced by Alpine's login profile on every Starry boot. The +# app runner writes its current phase to persistent ext4 before doing work, so +# a kernel reset can be distinguished from the initial ready state. + +state_file="${SELFHOST_STATE_FILE:-/opt/starry-selfhost.state}" + +if [ -r "$state_file" ]; then + state="" + run_id="" + phase="unknown" + IFS=' ' read -r state run_id phase <"$state_file" + + if [ "$state" = "running" ]; then + echo "SELF_COMPILE_FAILED: unexpected guest reboot during $phase (run_id=$run_id)" + sync 2>/dev/null || true + + poweroff -f 2>/dev/null || poweroff 2>/dev/null || true + return 1 2>/dev/null || exit 1 + fi +fi + +unset state_file state run_id phase +true diff --git a/apps/starry/selfhost/selfhost-full-kernel/guest-selfbuild.sh b/apps/starry/selfhost/selfhost-full-kernel/guest-selfbuild.sh new file mode 100644 index 0000000000..6ec0a080ba --- /dev/null +++ b/apps/starry/selfhost/selfhost-full-kernel/guest-selfbuild.sh @@ -0,0 +1,294 @@ +#!/bin/sh + +set -eu + +TOOLCHAIN="nightly-2026-05-28" +HOST_TRIPLE="x86_64-unknown-linux-musl" +TOOLCHAIN_DIR_NAME="${TOOLCHAIN}-${HOST_TRIPLE}" +RUSTUP_TOOLCHAIN="starry-selfhost-${TOOLCHAIN#nightly-}-${HOST_TRIPLE}" +SOURCE_TAR="${SELFHOST_SOURCE_TAR:-/opt/tgoskits-src.tar}" +SOURCE_META="${SELFHOST_SOURCE_META:-/opt/tgoskits-src.meta}" +SOURCE_DIR="${SELFHOST_SOURCE_DIR:-/tmp/tgoskits-src}" +TARGET_DIR="${SELFHOST_TARGET_DIR:-/opt/starry-selfhost-target}" +ARTIFACT="${SELFHOST_ARTIFACT:-/opt/starryos-selfbuilt}" +STATE_FILE="${SELFHOST_STATE_FILE:-/opt/starry-selfhost.state}" +RUN_ID_FILE="${SELFHOST_RUN_ID_FILE:-/opt/starry-selfhost.run-id}" +FAILURE_REASON="guest command failed" +CURRENT_PHASE="bootstrap" +RUN_ID="unknown" + +write_state() { + state="$1" + phase="$2" + state_tmp="${STATE_FILE}.tmp" + + printf '%s %s %s\n' "$state" "$RUN_ID" "$phase" >"$state_tmp" + mv "$state_tmp" "$STATE_FILE" + sync +} + +mark_phase() { + CURRENT_PHASE="$1" + write_state running "$CURRENT_PHASE" + echo "SELF_COMPILE_PHASE=$CURRENT_PHASE" +} + +finish_failure() { + status="$1" + trap - EXIT + write_state failed "$CURRENT_PHASE" 2>/dev/null || true + echo "SELF_COMPILE_FAILED: $FAILURE_REASON (phase=$CURRENT_PHASE, status=$status)" + sync 2>/dev/null || true + poweroff -f 2>/dev/null || poweroff 2>/dev/null || true + exit "$status" +} + +handle_exit() { + status="$?" + if [ "$status" -ne 0 ]; then + finish_failure "$status" + fi +} + +fail() { + FAILURE_REASON="$1" + exit 1 +} + +finish_success() { + artifact_size="$1" + + trap - EXIT + write_state success publish + echo "SELF_COMPILE_ARTIFACT=$ARTIFACT" + echo "SELF_COMPILE_ARTIFACT_SIZE=$artifact_size" + echo "SELF_COMPILE_SUCCESS" + poweroff -f 2>/dev/null || poweroff 2>/dev/null || true + exit 0 +} + +load_run_id() { + [ -s "$RUN_ID_FILE" ] || fail "run id is missing: $RUN_ID_FILE" + IFS= read -r RUN_ID <"$RUN_ID_FILE" + [ -n "$RUN_ID" ] || fail "run id is empty: $RUN_ID_FILE" +} + +install_build_packages() { + # Ensure busybox symlinks exist BEFORE apk touches the rootfs. The + # base image's busybox is statically linked and known-good; after apk + # upgrades musl libc / libcrypto / libssl, exec-ing the new busybox + # binary may SIGSEGV when the dynamic linker loads the just-upgraded + # shared libraries. Running this step first avoids that crash. + /bin/busybox --install -s /bin 2>/dev/null || true + + # apk upgrades libcrypto3 and libssl3 first (packages 1-2/53). The + # new .so files are written to disk and all 53 packages install + # successfully, but apk's atexit / libc _fini cleanup may SIGSEGV + # when the dynamic linker touches the just-upgraded libraries during + # process exit. This is a known StarryOS ELF-loader limitation, + # not an rsext4 data corruption — apk reports "OK: … in 97 packages" + # before the crash. Absorb the non-fatal exit signal and validate + # the installed tools below. + apk add --no-cache --no-scripts \ + bash build-base ca-certificates clang clang-dev cmake curl git libudev-zero-dev \ + linux-headers musl-dev openssl-dev perl pkgconf python3 tar xz \ + || true + + sync + + # Re-create busybox symlinks (apk may have replaced busybox). + /bin/busybox --install -s /bin 2>/dev/null || true + update-ca-certificates 2>/dev/null || true + + # Verify the build toolchain actually landed — if any critical binary + # is missing, fail explicitly rather than crashing later with a + # confusing "command not found". + for bin in bash gcc g++ make git curl perl python3 tar xz pkg-config cmake; do + command -v "$bin" >/dev/null 2>&1 || fail "$bin missing after apk" + done + [ -x /bin/bash ] || fail "/bin/bash not executable after apk" + [ -x /usr/bin/gcc ] || fail "/usr/bin/gcc not executable after apk" +} + +verify_network() { + curl --fail --silent --show-error --location --retry 3 \ + --connect-timeout 20 --max-time 120 \ + https://static.rust-lang.org/dist/channel-rust-nightly.toml \ + -o /tmp/channel-rust-nightly.toml +} + +configure_musl_toolchain_aliases() { + gcc_path="$(command -v gcc)" || fail "gcc is unavailable after apk install" + ar_path="$(command -v ar)" || fail "ar is unavailable after apk install" + + mkdir -p /usr/local/bin + ln -sf "$gcc_path" /usr/local/bin/x86_64-linux-musl-cc + ln -sf "$gcc_path" /usr/local/bin/x86_64-linux-musl-gcc + ln -sf "$ar_path" /usr/local/bin/x86_64-linux-musl-ar +} + +install_rust() { + # The prebuild host extracts all six rustup component tarballs and + # bundles them into a single uncompressed tar (~2.5 GiB). We extract + # it directly to the ext4 rootfs — rsext4 is slow for small-file writes + # (~1-4 KiB/s during tar extraction) but `tar xf` streams the data + # through a pipe and does not deadlock the way `cp -a` from tmpfs does. + # Once the toolchain is on ext4, reads (rustc, std libs) are fast. + local toolchain_tar="/opt/rust-toolchain.tar" + local toolchain_name="$TOOLCHAIN_DIR_NAME" + local toolchain_dir="/root/.rustup/toolchains/$toolchain_name" + + [ -f "$toolchain_tar" ] || fail "rust toolchain tar is missing: $toolchain_tar" + + echo "[self-compile] extracting pre-built Rust toolchain to ext4 rootfs..." + mkdir -p /root/.rustup/toolchains + tar xf "$toolchain_tar" -C /root/.rustup/toolchains/ \ + || fail "failed to extract toolchain tar to ext4" + rm -f "$toolchain_tar" + [ -x "$toolchain_dir/bin/rustc" ] \ + || fail "pre-extracted rustc is missing: $toolchain_dir/bin/rustc" + [ -x "$toolchain_dir/bin/cargo" ] \ + || fail "pre-extracted cargo is missing: $toolchain_dir/bin/cargo" + + # Install rustup to tmpfs (rsext4 is too slow for rustup's many small + # writes during cargo install of cargo-binutils / ksym). + local tmp_rustup=/tmp/rustup-home + local tmp_cargo=/tmp/cargo-home + mkdir -p "$tmp_rustup" "$tmp_cargo" + rm -f "$tmp_rustup/settings.toml" + + export RUSTUP_HOME="$tmp_rustup" + export CARGO_HOME="$tmp_cargo" + export PATH="$tmp_cargo/bin:/usr/local/bin:/usr/bin:/bin" + export RUSTUP_IO_THREADS="${SELFHOST_RUSTUP_IO_THREADS:-4}" + export RUSTUP_MAX_RETRIES="${SELFHOST_RUSTUP_MAX_RETRIES:-5}" + + if [ ! -x "$tmp_cargo/bin/rustup" ]; then + curl --fail --silent --show-error --location https://sh.rustup.rs \ + -o /tmp/rustup-init.sh + sh /tmp/rustup-init.sh -y --no-modify-path --default-host "$HOST_TRIPLE" \ + --default-toolchain none \ + || fail "rustup-init failed" + fi + + # Official channel-like names are not valid custom toolchain aliases. + # Link the pre-extracted directory under a distinct name, then export the + # alias so the workspace rust-toolchain.toml cannot trigger a download. + rustup toolchain link "$RUSTUP_TOOLCHAIN" "$toolchain_dir" \ + || fail "rustup toolchain link failed" + rustup default "$RUSTUP_TOOLCHAIN" \ + || fail "rustup default failed" + export RUSTUP_TOOLCHAIN + + command -v rustc >/dev/null 2>&1 || fail "rustc not found after install" + command -v cargo >/dev/null 2>&1 || fail "cargo not found after install" + rustc --version || fail "rustc --version failed" + cargo --version || fail "cargo --version failed" + + echo "[self-compile] Rust toolchain ready." + echo "[self-compile] $(rustc --version)" + echo "[self-compile] $(cargo --version)" +} + +install_kallsyms_tools() { + # cargo install downloads source + compiles — small enough to tolerate + # rsext4 throughput and slirp latency. + if ! cargo install --list | grep -q '^cargo-binutils v0.4.0:'; then + cargo install cargo-binutils --version 0.4.0 --locked + fi + if ! cargo install --list | grep -q '^ksym v0.6.0:'; then + cargo install ksym --version 0.6.0 --locked + fi + + command -v rust-nm >/dev/null 2>&1 || fail "cargo-binutils did not install rust-nm" + command -v rust-objcopy >/dev/null 2>&1 || fail "cargo-binutils did not install rust-objcopy" + command -v gen_ksym >/dev/null 2>&1 || fail "ksym did not install gen_ksym" +} + +prepare_source_tree() { + [ -f "$SOURCE_TAR" ] || fail "source archive is missing: $SOURCE_TAR" + rm -rf "$SOURCE_DIR" + mkdir -p "$SOURCE_DIR" + tar -xf "$SOURCE_TAR" -C "$SOURCE_DIR" + [ -f "$SOURCE_DIR/Cargo.toml" ] || fail "source archive does not contain Cargo.toml" + + mkdir -p "$TARGET_DIR" + rm -rf "$SOURCE_DIR/target" + ln -s "$TARGET_DIR" "$SOURCE_DIR/target" + [ -d "$SOURCE_DIR/target" ] || fail "persistent target directory is unavailable" + + if [ -f "$SOURCE_META" ]; then + echo "SELF_COMPILE_SOURCE_METADATA_BEGIN" + cat "$SOURCE_META" + echo "SELF_COMPILE_SOURCE_METADATA_END" + fi +} + +report_build_storage() { + echo "SELF_COMPILE_STORAGE_BEGIN" + mount | grep -E ' on /(tmp|opt) ' || true + df -T / /tmp "$TARGET_DIR" 2>/dev/null || df -h / /tmp "$TARGET_DIR" + free -m 2>/dev/null || sed -n '1,5p' /proc/meminfo + echo "SELF_COMPILE_STORAGE_END" +} + +detect_rust_host() { + DETECTED_HOST="$(rustc -vV | sed -n 's/^host: //p')" + [ "$DETECTED_HOST" = "$HOST_TRIPLE" ] \ + || fail "Rust host must be $HOST_TRIPLE, got ${DETECTED_HOST:-unknown}" + echo "SELF_COMPILE_RUST_HOST=$DETECTED_HOST" +} + +build_host_xtask() { + cd "$SOURCE_DIR" + RUSTFLAGS= CARGO_ENCODED_RUSTFLAGS= \ + cargo "+$RUSTUP_TOOLCHAIN" build --locked -p tg-xtask --target "$DETECTED_HOST" + XTASK="$SOURCE_DIR/target/$DETECTED_HOST/debug/tg-xtask" + [ -x "$XTASK" ] || fail "tg-xtask was not built for $DETECTED_HOST" +} + +build_kernel() { + cd "$SOURCE_DIR" + "$XTASK" starry build \ + -c apps/starry/selfhost/build-x86_64-unknown-none.toml \ + --arch x86_64 +} + +publish_artifact() { + built_artifact="$SOURCE_DIR/target/x86_64-unknown-linux-musl/release/starryos" + [ -s "$built_artifact" ] || fail "x86_64 kernel artifact is missing: $built_artifact" + + cp "$built_artifact" "$ARTIFACT" + chmod 0755 "$ARTIFACT" + [ -s "$ARTIFACT" ] || fail "failed to persist self-built kernel" + wc -c <"$ARTIFACT" +} + +trap handle_exit EXIT + +echo "SELF_COMPILE_START" +load_run_id +export CARGO_BUILD_JOBS="${SELFHOST_CARGO_BUILD_JOBS:-2}" +export AXBUILD_STARRY_KALLSYMS_AUTO_INSTALL=0 +unset CARGO_BUILD_TARGET + +mark_phase packages +install_build_packages +mark_phase network +verify_network +mark_phase rust +configure_musl_toolchain_aliases +install_rust +mark_phase tools +install_kallsyms_tools +mark_phase source +prepare_source_tree +report_build_storage +detect_rust_host +mark_phase xtask-host +build_host_xtask +mark_phase kernel +build_kernel +mark_phase publish +artifact_size="$(publish_artifact)" +finish_success "$artifact_size" diff --git a/apps/starry/selfhost/selfhost-full-kernel/prebuild.sh b/apps/starry/selfhost/selfhost-full-kernel/prebuild.sh new file mode 100644 index 0000000000..fc0e5285a9 --- /dev/null +++ b/apps/starry/selfhost/selfhost-full-kernel/prebuild.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env bash +# +# Prepares the persistent rootfs overlay for the x86_64 StarryOS self-build app. +# The app runner owns rootfs injection and QEMU startup; this script only stages +# the exact current checkout and the guest-side runner. + +set -euo pipefail + +app_dir="${STARRY_APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +workspace="${STARRY_WORKSPACE:?STARRY_WORKSPACE is required}" +rootfs="${STARRY_ROOTFS:?STARRY_ROOTFS is required}" +overlay_dir="${STARRY_OVERLAY_DIR:?STARRY_OVERLAY_DIR is required}" +arch="${STARRY_ARCH:?STARRY_ARCH is required}" +rootfs_size_mib="${SELFHOST_ROOTFS_SIZE_MIB:-32768}" +output_dir="$workspace/target/starry-selfhost-x86_64" + +require_x86_64() { + if [[ "$arch" != "x86_64" ]]; then + echo "selfhost prebuild only supports x86_64, got $arch" >&2 + exit 2 + fi +} + +git_value() { + local fallback="$1" + shift + git -C "$workspace" "$@" 2>/dev/null || printf '%s\n' "$fallback" +} + +source_dirty() { + if [[ -n "$(git -C "$workspace" status --porcelain --untracked-files=all)" ]]; then + printf '%s\n' true + else + printf '%s\n' false + fi +} + +resize_rootfs() { + cargo xtask image resize "$rootfs" --size-mib "$rootfs_size_mib" +} + +stage_source_archive() { + local source_tar="$output_dir/tgoskits-src.tar" + + tar -C "$workspace" \ + --exclude=.git \ + --exclude=target \ + --exclude=tmp \ + --exclude=.tgos-images \ + --exclude=.cache \ + --exclude=download \ + --exclude=.idea \ + --exclude=.vscode \ + -cf "$source_tar" . + install -m 0644 "$source_tar" "$overlay_dir/opt/tgoskits-src.tar" +} + +stage_source_metadata() { + local metadata="$output_dir/tgoskits-src.meta" + + cat >"$metadata" <"$resolver" + for resolver_source in /run/systemd/resolve/resolv.conf /etc/resolv.conf; do + [[ -f "$resolver_source" ]] || continue + awk ' + $1 == "nameserver" && $2 !~ /^127\./ && $2 !~ /:/ && $2 != "10.0.2.3" { + print + } + ' "$resolver_source" >>"$resolver" + if [[ -s "$resolver" ]]; then + return + fi + done + + cat >"$resolver" <<'EOF' +nameserver 1.1.1.1 +nameserver 8.8.8.8 +EOF +} + +stage_guest_runner() { + install -m 0755 "$app_dir/guest-selfbuild.sh" "$overlay_dir/opt/starry-selfhost-run.sh" +} + +stage_guest_reboot_guard() { + local profile_dir="$overlay_dir/etc/profile.d" + + mkdir -p "$profile_dir" + install -m 0644 \ + "$app_dir/guest-selfbuild-reboot-guard.sh" \ + "$profile_dir/starry-selfhost-reboot-guard.sh" +} + +stage_run_state() { + local run_id + + run_id="$(git_value unknown rev-parse --short=12 HEAD)-$(date -u +%Y%m%dT%H%M%SZ)-$$" + printf '%s\n' "$run_id" >"$overlay_dir/opt/starry-selfhost.run-id" + printf 'ready %s prebuild\n' "$run_id" >"$overlay_dir/opt/starry-selfhost.state" +} + +# Build a pre-extracted Rust toolchain tarball from host-downloaded components. +# The guest cannot reliably extract .tar.xz files because: (1) QEMU slirp +# degrades catastrophically for large downloads, and (2) StarryOS MemoryFs +# (/tmp) has a limited size — filling it during XZ extraction freezes the +# guest. We download the six component tarballs on the host, extract them +# into a merged toolchain tree, and inject a single uncompressed tar so the +# guest only needs `tar xf` (no XZ, no network, no tmpfs pressure). +stage_rust_toolchain() { + local rust_date="2026-05-28" + local rust_dl="https://static.rust-lang.org/dist/${rust_date}" + local toolchain_name="nightly-2026-05-28-x86_64-unknown-linux-musl" + local toolchain_version="2" + local version_file=".starry-selfhost-toolchain-version" + local stage_dir="$output_dir/toolchain-stage" + local toolchain_dir="$stage_dir/$toolchain_name" + local component_stage="$stage_dir/component" + local cache_dir="$output_dir/toolchain-downloads" + local output_tar="$output_dir/rust-toolchain.tar" + + # component → sha256 from channel-rust-nightly.toml + local pairs=( + "rustc:b03dac6f955cf5e8075d4187e2579bad0737cbc96caaa7e76c9a949a47bae0ff" + "cargo:4180435487dadf1593925f11e1dd4b02dbd5315d7a4813b8c214b96410957c3d" + "rust-std:783e922fb28ff74488db25ef0c62ef8147ba509b7e7d19ac8adfadfc3924bf41" + "rust-src:3ef29c6fe273c9c1fc210a53c461a1f984fc8857be508aa7aa3e8f82f23652b2" + "llvm-tools:13bdcad985200f19188537e629bb80a7cd104237ad4469deebb53eb32b4a29ec" + "rust-std-none:2e67b503d145f68ab474fc7070bac3a1d936d5dd78f96a8bc3a2c5d98baa190d" + ) + + if [[ -f "$output_tar" ]] \ + && [[ "$(tar -xOf "$output_tar" "$toolchain_name/$version_file" 2>/dev/null)" \ + == "$toolchain_version" ]]; then + echo "[prebuild] rust toolchain tar already built ($(du -h "$output_tar" | cut -f1)) — skipping" + install -m 0644 "$output_tar" "$overlay_dir/opt/rust-toolchain.tar" + return + fi + + rm -rf "$stage_dir" + mkdir -p "$toolchain_dir" "$cache_dir" + + for pair in "${pairs[@]}"; do + component="${pair%%:*}" + hash="${pair##*:}" + case "$component" in + rust-src) tarball="rust-src-nightly.tar.xz" ;; + llvm-tools) tarball="llvm-tools-nightly-x86_64-unknown-linux-musl.tar.xz" ;; + rust-std-none) tarball="rust-std-nightly-x86_64-unknown-none.tar.xz" ;; + *) tarball="${component}-nightly-x86_64-unknown-linux-musl.tar.xz" ;; + esac + url="${rust_dl}/${tarball}" + dest="$cache_dir/$hash" + + if [[ -f "$dest" ]] \ + && printf '%s %s\n' "$hash" "$dest" | sha256sum --check --status; then + echo "[prebuild] ${component} already downloaded ($(du -h "$dest" | cut -f1))" + else + rm -f "$dest" + echo "[prebuild] downloading ${tarball}..." + curl -fsSL --retry 3 --connect-timeout 30 --max-time 600 \ + "$url" -o "${dest}.tmp" 2>/dev/null || { + rm -f "${dest}.tmp" + echo "[prebuild] ERROR: failed to download ${tarball}" >&2 + exit 1 + } + mv "${dest}.tmp" "$dest" + fi + + if ! printf '%s %s\n' "$hash" "$dest" | sha256sum --check --status; then + rm -f "$dest" + echo "[prebuild] ERROR: checksum mismatch for ${tarball}" >&2 + exit 1 + fi + + echo "[prebuild] installing ${tarball}..." + rm -rf "$component_stage" + mkdir -p "$component_stage" + tar xf "$dest" -C "$component_stage" 2>/dev/null || { + echo "[prebuild] ERROR: failed to extract ${tarball}" >&2 + exit 1 + } + installer="$(find "$component_stage" -mindepth 2 -maxdepth 2 -type f -name install.sh -print -quit)" + if [[ -z "$installer" ]]; then + echo "[prebuild] ERROR: installer missing from ${tarball}" >&2 + exit 1 + fi + bash "$installer" --prefix="$toolchain_dir" --disable-ldconfig >/dev/null || { + echo "[prebuild] ERROR: failed to install ${tarball}" >&2 + exit 1 + } + done + + [[ -x "$toolchain_dir/bin/rustc" ]] \ + || { echo "[prebuild] ERROR: rustc missing from assembled toolchain" >&2; exit 1; } + [[ -x "$toolchain_dir/bin/cargo" ]] \ + || { echo "[prebuild] ERROR: cargo missing from assembled toolchain" >&2; exit 1; } + [[ -d "$toolchain_dir/lib/rustlib/src/rust/library" ]] \ + || { echo "[prebuild] ERROR: rust-src missing from assembled toolchain" >&2; exit 1; } + [[ -d "$toolchain_dir/lib/rustlib/x86_64-unknown-none/lib" ]] \ + || { echo "[prebuild] ERROR: x86_64-unknown-none missing from assembled toolchain" >&2; exit 1; } + printf '%s\n' "$toolchain_version" >"$toolchain_dir/$version_file" + + echo "[prebuild] creating uncompressed toolchain tar..." + rm -f "$output_tar" + tar -C "$stage_dir" -cf "$output_tar" "$toolchain_name" + echo "[prebuild] rust toolchain tar built ($(du -h "$output_tar" | cut -f1))" + + rm -rf "$stage_dir" + install -m 0644 "$output_tar" "$overlay_dir/opt/rust-toolchain.tar" +} + +require_x86_64 +mkdir -p "$output_dir" "$overlay_dir/opt" +resize_rootfs +stage_source_archive +stage_source_metadata +stage_guest_resolver +stage_guest_runner +stage_guest_reboot_guard +stage_run_state +stage_rust_toolchain + +echo "selfhost x86_64 overlay ready in $overlay_dir" +echo "rootfs=$rootfs" +echo "rootfs_size_mib=$rootfs_size_mib" diff --git a/apps/starry/selfhost/selfhost-full-kernel/qemu-x86_64.toml b/apps/starry/selfhost/selfhost-full-kernel/qemu-x86_64.toml new file mode 100644 index 0000000000..1c9d521838 --- /dev/null +++ b/apps/starry/selfhost/selfhost-full-kernel/qemu-x86_64.toml @@ -0,0 +1,21 @@ +args = [ + "-nographic", + "-no-shutdown", + "-cpu", "host", + "-smp", "4", + "-m", "16G", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-selfhost.img,file.locking=off,cache=writeback", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +# The guest writes both its provisioned toolchain and /opt/starryos-selfbuilt to +# this app-specific rootfs, so the runner must not discard writes on exit. +snapshot = false +shell_prefix = "root@starry:" +shell_init_cmd = "/bin/sh /opt/starry-selfhost-run.sh" +success_regex = ["(?m)^SELF_COMPILE_SUCCESS\\s*$"] +fail_regex = ['(?i)\bpanicked\b', 'SELF_COMPILE_FAILED'] +timeout = 28800 diff --git a/components/rsext4/src/blockdev/journal.rs b/components/rsext4/src/blockdev/journal.rs index 6a7dd0c422..413dc9a6e6 100644 --- a/components/rsext4/src/blockdev/journal.rs +++ b/components/rsext4/src/blockdev/journal.rs @@ -98,6 +98,23 @@ mod tests { .expect("read target after auto commit"); assert_eq!(dev.buffer()[0], 1); } + + #[test] + fn bulk_read_overlays_pending_journal_update() { + let mut dev = Jbd2Dev::initial_jbd2dev(0, MemBlockDev::new(256), true); + dev.set_journal_superblock(JournalSuperBllockS::default(), AbsoluteBN::new(128)); + + let target = AbsoluteBN::new(10); + let pending = vec![0x5a; BLOCK_SIZE]; + dev.write_blocks(&pending, target, 1, true) + .expect("queue metadata update"); + + let mut observed = vec![0; BLOCK_SIZE]; + dev.read_blocks(&mut observed, target, 1) + .expect("bulk read pending metadata"); + + assert_eq!(observed, pending); + } } /// Block device proxy that optionally routes metadata writes through JBD2. @@ -276,9 +293,8 @@ impl Jbd2Dev { }; let raw_dev = self.inner.device_mut(); - let committed = Self::enqueue_journal_update(system, raw_dev, updates)?; - if committed { - let _ = self.inner.invalidate_cache(); + if Self::enqueue_journal_update(system, raw_dev, updates)? { + self.inner.invalidate_cache()?; } trace!("[JBD2 buffer] queued metadata block {block_id}"); Ok(()) @@ -317,7 +333,28 @@ impl Jbd2Dev { block_id: AbsoluteBN, count: u32, ) -> Ext4Result<()> { - self.inner.read_blocks(buf, block_id, count) + if !self.journal_use || count == 0 { + return self.inner.read_blocks(buf, block_id, count); + } + + let required = BLOCK_SIZE * count as usize; + if buf.len() < required { + return Err(Ext4Error::buffer_too_small(buf.len(), required)); + } + + self.inner.read_blocks(buf, block_id, count)?; + + let Some(system) = self.system.as_ref() else { + return Ok(()); + }; + for i in 0..count { + let bid = block_id.checked_add(i)?; + if let Some(update) = system.commit_queue.iter().find(|queued| queued.0 == bid) { + let off = (i as usize) * BLOCK_SIZE; + buf[off..off + BLOCK_SIZE].copy_from_slice(&update.1[..BLOCK_SIZE]); + } + } + Ok(()) } /// Writes multiple blocks, optionally journaling metadata buffers. @@ -355,7 +392,7 @@ impl Jbd2Dev { committed_any |= Self::enqueue_journal_update(system, raw_dev, updates)?; } if committed_any { - let _ = self.inner.invalidate_cache(); + self.inner.invalidate_cache()?; } Ok(()) diff --git a/components/rsext4/src/dir/insert.rs b/components/rsext4/src/dir/insert.rs index 42bddeeb8c..37a0f6ef3a 100644 --- a/components/rsext4/src/dir/insert.rs +++ b/components/rsext4/src/dir/insert.rs @@ -3,9 +3,19 @@ use log::{error, warn}; use crate::{ - blockdev::*, bmalloc::InodeNumber, checksum::update_ext4_dirblock_csum32, config::*, - crc32c::ext4_superblock_has_metadata_csum, disknode::*, endian::DiskFormat, entries::*, - error::*, ext4::*, extents_tree::*, loopfile::*, metadata::Ext4InodeMetadataUpdate, + blockdev::*, + bmalloc::{AbsoluteBN, InodeNumber}, + checksum::update_ext4_dirblock_csum32, + config::*, + crc32c::ext4_superblock_has_metadata_csum, + disknode::*, + endian::DiskFormat, + entries::*, + error::*, + ext4::*, + extents_tree::*, + loopfile::*, + metadata::Ext4InodeMetadataUpdate, }; /// Inserts a child entry into a parent directory, extending the directory if needed. @@ -41,12 +51,13 @@ pub fn insert_dir_entry( }; let mut inserted = false; + let mut modified_phys: Option = None; // Try to satisfy the insertion inside already mapped directory blocks first. let blocks = resolve_inode_block_allextend(fs, device, parent_inode)?; for lbn in 0..total_blocks { - if inserted { + if modified_phys.is_some() { break; } @@ -100,6 +111,7 @@ pub fn insert_dir_entry( data[offset + 8..offset + 8 + nlen] .copy_from_slice(&full_entry.name[..nlen]); inserted = true; + modified_phys = Some(phys); update_ext4_dirblock_csum32( &fs.superblock, parent_ino_num.raw(), @@ -128,6 +140,7 @@ pub fn insert_dir_entry( data[new_off + 8..new_off + 8 + nlen] .copy_from_slice(&full_entry.name[..nlen]); inserted = true; + modified_phys = Some(phys); update_ext4_dirblock_csum32( &fs.superblock, parent_ino_num.raw(), @@ -151,7 +164,17 @@ pub fn insert_dir_entry( } } - if inserted { + if let Some(modified_block) = modified_phys { + // Publish the modified directory block before subsequent lookup. + fs.datablock_cache.flush(device, modified_block)?; + // The hash tree is stale after insertion; force subsequent lookups + // through the authoritative linear directory scan. + if parent_inode.i_flags & Ext4Inode::EXT4_INDEX_FL != 0 { + parent_inode.i_flags &= !Ext4Inode::EXT4_INDEX_FL; + fs.modify_inode(device, parent_ino_num, |ino| { + ino.i_flags &= !Ext4Inode::EXT4_INDEX_FL; + })?; + } fs.touch_parent_dir_for_entry_change(device, parent_ino_num)?; return Ok(()); } @@ -216,6 +239,17 @@ pub fn insert_dir_entry( } })?; + // Immediately write the new directory block to disk so it is visible + // to subsequent lookups. + fs.datablock_cache.flush(device, new_block)?; + + // Clear the hash-tree index flag: the hash tree is stale after + // adding a directory entry, and forcing linear scan guarantees + // correct lookups. + if parent_inode.i_flags & Ext4Inode::EXT4_INDEX_FL != 0 { + parent_inode.i_flags &= !Ext4Inode::EXT4_INDEX_FL; + } + fs.finalize_inode_update( device, parent_ino_num, diff --git a/components/rsext4/src/dir/mkdir.rs b/components/rsext4/src/dir/mkdir.rs index 671266c8d2..cbb04e5dd6 100644 --- a/components/rsext4/src/dir/mkdir.rs +++ b/components/rsext4/src/dir/mkdir.rs @@ -84,7 +84,7 @@ fn mkdir_internal( p }; - let (parent_ino_num, mut parent_inode) = + let (parent_ino_num, parent_inode) = get_inode_with_num(fs, device, &parent)?.ok_or(Ext4Error::not_found())?; if !parent_inode.is_dir() { return Err(Ext4Error::not_dir()); @@ -183,6 +183,9 @@ fn mkdir_internal( desc.bg_used_dirs_count_hi = ((newc >> 16) & 0xFFFF) as u16; } + // set_inode_links_count updated the inode cache, so reload the parent + // before insert_dir_entry can persist directory block growth. + let mut parent_inode = fs.get_inode_by_num(device, parent_ino_num)?; insert_dir_entry( fs, device, diff --git a/components/rsext4/tests/cache_coherence_repro.rs b/components/rsext4/tests/cache_coherence_repro.rs new file mode 100644 index 0000000000..6809462a8f --- /dev/null +++ b/components/rsext4/tests/cache_coherence_repro.rs @@ -0,0 +1,187 @@ +//! Deterministic directory cache-coherence regressions. + +use std::cell::Cell; + +use rsext4::{ + bmalloc::AbsoluteBN, + dir::{get_inode_with_num, insert_dir_entry}, + disknode::Ext4Inode, + entries::Ext4DirEntry2, + error::{Ext4Error, Ext4Result}, + file::{read_inode_data_into, truncate_inode, write_inode_data}, + *, +}; + +struct MockBlockDevice { + data: Vec, + block_size: u32, + now: Cell, +} + +impl MockBlockDevice { + fn new(size: usize) -> Self { + Self { + data: vec![0; size], + block_size: BLOCK_SIZE as u32, + now: Cell::new(1_700_000_000), + } + } +} + +impl BlockDevice for MockBlockDevice { + fn read(&mut self, buffer: &mut [u8], block_id: AbsoluteBN, _count: u32) -> Ext4Result<()> { + let start = block_id.as_usize()? * self.block_size as usize; + let end = start + buffer.len(); + if end > self.data.len() { + return Err(Ext4Error::block_out_of_range( + block_id.to_u32()?, + self.total_blocks(), + )); + } + buffer.copy_from_slice(&self.data[start..end]); + Ok(()) + } + + fn write(&mut self, buffer: &[u8], block_id: AbsoluteBN, _count: u32) -> Ext4Result<()> { + let start = block_id.as_usize()? * self.block_size as usize; + let end = start + buffer.len(); + if end > self.data.len() { + return Err(Ext4Error::block_out_of_range( + block_id.to_u32()?, + self.total_blocks(), + )); + } + self.data[start..end].copy_from_slice(buffer); + Ok(()) + } + + fn open(&mut self) -> Ext4Result<()> { + Ok(()) + } + + fn close(&mut self) -> Ext4Result<()> { + Ok(()) + } + + fn total_blocks(&self) -> u64 { + (self.data.len() / self.block_size as usize) as u64 + } + + fn block_size(&self) -> u32 { + self.block_size + } + + fn current_time(&self) -> Ext4Result { + let sec = self.now.get(); + self.now.set(sec + 1); + Ok(Ext4Timestamp::new(sec, 0)) + } +} + +fn setup() -> (Jbd2Dev, Ext4FileSystem) { + let device = MockBlockDevice::new(100 * 1024 * 1024); + let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, device, true); + mkfs(&mut jbd2_dev).expect("mkfs failed"); + let fs = mount(&mut jbd2_dev).expect("mount failed"); + (jbd2_dev, fs) +} + +fn long_name(prefix: &str) -> String { + format!("{prefix}{}", "x".repeat(248)) +} + +#[test] +fn directory_growth_preserves_parent_link_count() { + let (mut dev, mut fs) = setup(); + mkdir(&mut dev, &mut fs, "/parent").expect("mkdir /parent"); + + for index in 0..15 { + let path = format!("/parent/{}", long_name(&format!("f{index:02}"))); + mkfile(&mut dev, &mut fs, &path, Some(b"x"), None) + .unwrap_or_else(|error| panic!("mkfile {path} failed: {error:?}")); + } + + let (_, before) = get_inode_with_num(&mut fs, &mut dev, "/parent") + .expect("lookup parent") + .expect("parent exists"); + assert_eq!(before.size(), BLOCK_SIZE as u64); + + let child = long_name("dir"); + mkdir(&mut dev, &mut fs, &format!("/parent/{child}")) + .expect("mkdir that expands parent directory"); + + let (_, after) = get_inode_with_num(&mut fs, &mut dev, "/parent") + .expect("lookup expanded parent") + .expect("expanded parent exists"); + assert_eq!(after.size(), (2 * BLOCK_SIZE) as u64); + assert_eq!(after.i_links_count, before.i_links_count + 1); +} + +#[test] +fn insertion_clears_stale_directory_index_flag() { + let (mut dev, mut fs) = setup(); + mkdir(&mut dev, &mut fs, "/indexed").expect("mkdir /indexed"); + mkfile(&mut dev, &mut fs, "/target", Some(b"x"), None).expect("mkfile /target"); + + let (parent_ino, mut parent_inode) = get_inode_with_num(&mut fs, &mut dev, "/indexed") + .expect("lookup indexed directory") + .expect("indexed directory exists"); + let (target_ino, _) = get_inode_with_num(&mut fs, &mut dev, "/target") + .expect("lookup target") + .expect("target exists"); + + parent_inode.i_flags |= Ext4Inode::EXT4_INDEX_FL; + fs.modify_inode(&mut dev, parent_ino, |inode| { + inode.i_flags |= Ext4Inode::EXT4_INDEX_FL; + }) + .expect("mark directory index stale"); + + insert_dir_entry( + &mut fs, + &mut dev, + parent_ino, + &mut parent_inode, + target_ino, + "entry", + Ext4DirEntry2::EXT4_FT_REG_FILE, + ) + .expect("insert entry into indexed directory"); + + let (_, updated_parent) = get_inode_with_num(&mut fs, &mut dev, "/indexed") + .expect("lookup updated directory") + .expect("updated directory exists"); + assert_eq!(updated_parent.i_flags & Ext4Inode::EXT4_INDEX_FL, 0); + + let (entry_ino, _) = get_inode_with_num(&mut fs, &mut dev, "/indexed/entry") + .expect("lookup inserted entry") + .expect("inserted entry exists"); + assert_eq!(entry_ino, target_ino); +} + +#[test] +fn truncate_rewrite_reread_is_coherent() { + let (mut dev, mut fs) = setup(); + let path = "/libfoo.so.3"; + + // Phase 1: write initial data + mkfile(&mut dev, &mut fs, path, Some(b"old data - v1.0"), None).expect("create libfoo"); + + // Phase 2: truncate to 0 (simulates apk upgrading the .so) + let (ino, _) = get_inode_with_num(&mut fs, &mut dev, path) + .expect("lookup") + .expect("exists"); + truncate_inode(&mut dev, &mut fs, ino, 0).expect("truncate to 0"); + + // Phase 3: write new data (simulates apk installing new version) + let new_content: Vec = (0..8192u16).flat_map(|i| i.to_le_bytes()).collect(); + write_inode_data(&mut dev, &mut fs, ino, 0, &new_content).expect("write new data"); + + // Phase 4: read back and verify — must see the new data, not old + let mut buf = vec![0u8; new_content.len()]; + let n = read_inode_data_into(&mut dev, &mut fs, ino, 0, &mut buf).expect("read back"); + assert_eq!(n, new_content.len(), "read length"); + assert_eq!( + buf, new_content, + "data mismatch — truncate+rewrite not visible to reader" + ); +} diff --git a/docs/docs/build/starry/app.md b/docs/docs/build/starry/app.md index da5b067105..1be0cf1ea2 100644 --- a/docs/docs/build/starry/app.md +++ b/docs/docs/build/starry/app.md @@ -95,6 +95,12 @@ cargo starry app qemu --all --cap board:OrangePi-5-Plus # 运行单个应用 cargo starry app qemu -t dual-net +# 在 x86_64 QEMU 来宾中自编译 StarryOS +cargo starry app qemu -t selfhost/selfhost-full-kernel --arch x86_64 + # 板端应用 cargo starry app board -t my-board-app -b OrangePi-5-Plus ``` + +自编译应用需要独立的持久化 rootfs、KVM 和较长运行时间,完整环境与验证边界见 +[StarryOS 自编译](./self-compilation)。 diff --git a/docs/docs/build/starry/overview.md b/docs/docs/build/starry/overview.md index ab8dd3a7e6..cdf00b26ed 100644 --- a/docs/docs/build/starry/overview.md +++ b/docs/docs/build/starry/overview.md @@ -16,6 +16,7 @@ StarryOS 在三大子系统中**命令面最广**:它编译整个内核(无 - [性能剖析](./perf):qperf 火焰图与 callchain - [内核模块](./kmod):可加载内核模块(`.ko`)编译 - [rootfs 准备](./rootfs):独立预拉取 rootfs 镜像 +- [自编译](./self-compilation):在 x86_64 QEMU 来宾中直接构建 StarryOS 通用的参数解析、Snapshot、Build Info 和动态平台构建约定详见 [参数与配置](../configuration)。 @@ -37,6 +38,7 @@ cargo xtask starry [options] | `perf` | qperf 性能剖析(火焰图/callchain) | [性能剖析](./perf) | | `kmod build` | 编译内核模块(`.ko`) | [内核模块](./kmod) | | `rootfs` | 按架构准备默认 managed rootfs | [rootfs 准备](./rootfs) | +| `app qemu -t selfhost/selfhost-full-kernel` | 在 x86_64 QEMU 来宾中自编译 StarryOS | [自编译](./self-compilation) | | `defconfig ` | 生成默认板卡配置 | 见下文 | | `config ls` | 列出可用板卡名称 | 见下文 | | `quick-start ...` | 旧版常见平台便捷入口,后续会废弃 | 见下文 | diff --git a/docs/docs/build/starry/self-compilation.md b/docs/docs/build/starry/self-compilation.md new file mode 100644 index 0000000000..b12693f50f --- /dev/null +++ b/docs/docs/build/starry/self-compilation.md @@ -0,0 +1,81 @@ +--- +sidebar_position: 9 +sidebar_label: "自编译" +--- + +# StarryOS 自编译 + +## x86_64:直接通过 Starry App 运行 + +x86_64 自编译的唯一主入口是: + +```bash +cargo starry app qemu -t selfhost/selfhost-full-kernel --arch x86_64 +``` + +当前验收状态:清理前的提交已在启用 KVM 的 QEMU 中完成来宾工具链安装,并将 +`tg-xtask` 构建推进到 `453/454`、即最终大型静态链接前;该次链接因耗时过长被人工终止。 +因此目前能够证明绝大多数 Rust crate 可编译,但尚未证明最终链接完成、 +`/opt/starryos-selfbuilt` 已发布或自编译 ELF 能通过 OVMF 启动。本次清理后的提交只运行定向 +回归测试,不重复数小时端到端构建。最终链接性能优化和启动烟测留待后续完成。 + +该命令使用项目的 Starry app runner:构建种子内核、创建或复用 app 专用 rootfs、执行 +`prebuild.sh`、注入 overlay,并通过 `shell_init_cmd` 启动来宾 runner。它不依赖 +`scripts/self-compile.sh`、`expect`、loop mount 或 host sudo。 + +### 来宾流程 + +首次运行时,app runner 从默认 Alpine rootfs 创建受管理的 +`rootfs-x86_64-selfhost.img`,并由 prebuild 扩容到 32 GiB。prebuild 会把当前 checkout +(包含未提交修改)和 source metadata 打包注入 rootfs。它还会从 Rust 官方发布站点下载并 +校验固定 nightly 的六个组件,在 host 侧预解压为单一非压缩 tar 后注入;不复制 host 已安装 +的 Rust 或 GNU 工具链。 + +QEMU 通过 user-mode networking 联网。来宾 runner 会: + +1. 用 `apk` 安装构建依赖、`libudev-zero-dev`、git 和 curl; +2. 将预处理的 `nightly-2026-05-28-x86_64-unknown-linux-musl` Rust toolchain 解包到 rootfs, + 并在 MemoryFs 中安装 rustup; +3. 在线安装固定版本的 `cargo-binutils 0.4.0` 和 `ksym 0.6.0`; +4. 将源码解包到 `/tmp`,但把 canonical `target/` 链接到持久化的 + `/opt/starry-selfhost-target`,使用两个 Cargo jobs 编译 musl-host `tg-xtask`,然后执行 + `tg-xtask starry build -c apps/starry/selfhost/build-x86_64-unknown-none.toml --arch x86_64`; +5. 将生成的 ELF 持久化为 `/opt/starryos-selfbuilt`,并输出 `SELF_COMPILE_SUCCESS`。 + +失败会输出 `SELF_COMPILE_FAILED` 并让 app 命令以非零状态退出。成功前会先 `sync`,因此 +app runner 检测到成功标记后可安全结束 QEMU。来宾还会把当前构建阶段写入 rootfs;若内核 +在编译中途异常重启,下一次登录会输出包含中断阶段的失败标记,而不会等待到全局超时。 + +### 前置条件 + +- Linux x86_64 host,并可读写 `/dev/kvm`;app runner 检测到后会自动加入 `-accel kvm`, + 上述长时间构建实际启用了 KVM; +- `qemu-system-x86_64`、OVMF、`debugfs` 和 ext4 resize 工具; +- host 和 QEMU guest 都可访问网络。 + +正常流程不需要 sudo。第一次运行需要下载 Rust 官方组件、Alpine 包和 Cargo crates,后续运行 +复用 app rootfs 中的系统包、Rust 和 Cargo 缓存。 + +### 验证并启动自编译产物 + +app 成功后,先确认 rootfs 中的产物非空: + +```bash +ROOTFS="${TGOS_IMAGE_LOCAL_STORAGE:-$PWD/tmp/axbuild/rootfs}/rootfs-x86_64-selfhost.img/rootfs-x86_64-selfhost.img" +debugfs -R 'stat /opt/starryos-selfbuilt' "$ROOTFS" +``` + +可选的二次启动烟测仍使用现有脚本,但它只消费 app 生成的 rootfs,不再调用 +`self-compile.sh`: + +```bash +scripts/run-selfbuilt-kernel.sh --arch x86_64 +``` + +脚本会重新从 rootfs 提取 ELF,转换为 EFI payload,并通过 OVMF 启动。该脚本当前保留为 +后续烟测工具;本 PR 尚未完成产物生成和启动验证,不能据此宣称已经到达 Starry shell。 + +## riscv64 旧流程 + +riscv64 的离线 Debian selfhost 流程和 `scripts/self-compile.sh` 保持不变。它是与 x86_64 +app runner 独立的兼容路径;不要用该脚本准备或运行 x86_64 自编译。 diff --git a/docs/starryos-self-compilation.md b/docs/starryos-self-compilation.md deleted file mode 100644 index 79a05d39fe..0000000000 --- a/docs/starryos-self-compilation.md +++ /dev/null @@ -1,225 +0,0 @@ -# StarryOS 自编译:问题与解决方案 - -在 StarryOS 内部使用 cargo 编译 StarryOS 自身——支持 riscv64 和 x86_64 架构。 - -## 概览 - -### 跨架构自编译状态 - -| 架构 | 种子内核 | QEMU 启动 | rootfs | 自编译 | 耗时 | 备注 | -|------|---------|----------|--------|--------|------|------| -| riscv64 | ✅ | ✅ | 已就绪 | ✅ | ~100 min | TCG 模拟,SMP=1 | -| x86_64 | ✅ | ✅ (KVM) | 已就绪 | ✅ | 6m53s | KVM + SMP=1, 301 crates | - -### 测试链路 - -``` -Host (Linux) - └─ scripts/self-compile.sh --arch - ├─ cargo xtask starry build (种子内核) - ├─ loopback mount → inject files (脚本/配置注入) - └─ expect + QEMU (-m 8G) - └─ StarryOS 内核 - └─ Debian rootfs (ext4) - └─ /usr/bin/self-compile-inner.sh - ├─ mount tmpfs (8G) - ├─ filter-workspace.sh (架构过滤) - └─ cargo build -p starryos --offline -``` - -## 前置依赖 PR - -| PR | 内容 | 关联 | -|----|------|------| -| #797 | 信号传递修复:`interrupt_waker.wake()` 唤醒被 `future_blocked_resched` 移出运行队列的任务 | 无此修复,cargo 子进程(build script)挂起,父进程 waitpid 永远阻塞 | -| #1007 | 页回收:内存压力下驱逐干净文件支持页面,`try_page_reclaim()` 最多重试 4 次 | 无此修复,编译 `syn` 时 OOM panic(大量源码/产物占满文件缓存) | -| #971 | rsext4 clock LRU 缓存(4 入口/16 KiB),减少 virtio 块设备 round-trip | 加速离线 registry 读取,将依赖解析从分钟级降到秒级 | - -## 共通阻塞点(riscv64 + x86_64) - -### 1. 内存检测仅识别 512MB - -**现象**: QEMU `-m 8G` 但内核只识别 ~510MB。 - -**根因**: 早期已淘汰的平台配置路径在 `axconfig.toml` 中硬编码 `phys-memory-size = 0x2000_0000`。 - -**修复**: 当时改为 `phys-memory-size = "0x2_0000_0000"` (8GB)。当前构建链固定使用 `axplat-dyn` + `somehal::mem::memory_map()` 动态检测,无此问题。 - -**注**: PR #987 重构了 ax-alloc,移除了旧 bitmap 页分配器(及 `page-alloc-*` 特性),改用 TLSF/buddy-slab。TLSF 无硬编码容量限制,不再需要 `page-alloc-64g` passthrough。 - -### 2. TMPFS 挂载失败 - -**现象**: `mount -t tmpfs -o size=8G tmpfs /tmp` 失败。 - -**根因**: mount(8) 优先使用新版 mount API(`fsopen`/`fsconfig`/`fsmount`)。StarryOS 将 `fsopen` 实现为 `sys_dummy_fd`(返回伪 fd),mount(8) 误以为挂载成功但后续操作失败,不会回退到传统 `mount(2)`。 - -**修复**: 将 `fsopen`/`fspick`/`open_tree` 返回 `ENOSYS`,mount(8) 收到后回退到传统 `mount(2)` 调用。 - -```rust -Sysno::fsopen | Sysno::fspick | Sysno::open_tree => Err(AxError::Unsupported), -``` - -### 3. 链接器 `_ex_table_end` 未定义 - -**现象**: 所有 crate 编译通过,但最终链接失败: `undefined symbol: _ex_table_end`。 - -**根因**: 自编译环境中 `.cargo/config.toml` 未传递 `-Tlinker.x`。`ext_linker.ld` 使用 `INSERT AFTER .data;` 期望 `linker.x` 先定义 `.data` 段(含 `_ex_table_end`),但缺少 linker.x 时符号未定义。 - -**修复** (`os/StarryOS/starryos/ext_linker.ld`): -```ld -PROVIDE(_ex_table_start = 0); -PROVIDE(_ex_table_end = 0); - -SECTIONS { /* 原有内容 */ } -INSERT AFTER .data; -``` - -### 4. 测试正则误匹配 - -**现象**: 编译 crate `axpanic` 时 cargo 输出 `panic v0.1.0`,触发 fail_regex。 - -**修复**: `\bpanic` → `\bpanicked\b`(仅匹配内核 panic 消息)。 - -### 5. Workspace 架构过滤 - -**现象**: `cargo build --offline` 解析失败——workspace 包含其他架构的 crate(如 `arm_vcpu`、`loongarch_vcpu`)。 - -**根因**: 这些 crate 依赖当前目标架构不可用的平台库(如 `aarch64-cpu`),在 `--offline` 模式下无法解析。 - -**修复**: `scripts/filter-workspace.sh` — 基于目标架构从 `Cargo.toml` 的 workspace members 中精确移除不兼容的行: - -```bash -filter-workspace.sh x86_64 Cargo.toml -# 移除: arm_vcpu, arm_vgic, aarch64_sysreg, kasm-aarch64, riscv_*, loongarch_vcpu 等 -# 保留: x86_vcpu, x86_vlapic 及所有公共 crate -``` - -## x86_64 专有阻塞点 - -### 6. PCI BAR 64位地址导致 Page Fault - -**现象**: 内核启动时 `#PF` panic——64-bit PCI BAR(28GB+ 地址)未映射到页表。 - -**根因**: QEMU q35 机型的 PCI 设备 BAR 可分配在 4GB 以上地址空间,但页表未建立相应映射。 - -**修复**: 驱动初始化调用 `ax_mm::iomap()` 动态映射 BAR 物理地址到虚拟地址空间。 - -### 7. QEMU 镜像文件排他锁 - -**现象**: QEMU 启动时报文件锁冲突。 - -**修复**: `-drive id=disk0,if=none,format=raw,file=$IMG,file.locking=off` - -### 8. ext4 兼容性 Bug 系列 - -Linux host 内核 ext4 驱动与 StarryOS rsext4 之间存在多个不兼容点: - -| Bug | 现象 | 根因 | 修复 | -|-----|------|------|------| -| #1 | mount 后 checksum 失败 | `metadata_csum` 被 `debugfs -w` 破坏 | `mkfs.ext4 -O ^metadata_csum,^metadata_csum_seed` | -| #2 | Cargo.toml 被截断为 0 字节 | busybox grep 不支持 `[[:space:]]` | 用 `[ ]` 替代 + `[ -s ]` 安全检查 | -| #3 | 目录项读取 ENOENT | `debugfs -w` 写入目录项不可靠 | 使用 loopback mount + `cp` 替代 debugfs | -| #4 | 反复 mount/e2fsck 累积损坏 | 多次循环后目录结构不一致 | prepare 阶段(nspawn)完成所有写入,minimize host 修改 | -| #5 | `--offline` 缺少 crate | Cargo.lock 引用所有平台依赖 | 全量 `cargo fetch`(无 `--target` 过滤) | -| #6 | init 进程退出,QEMU 终止 | POSIX shell 重定向失败导致 shell 退出 | `: > file` → `touch file 2>/dev/null \|\| true` | - -### 9. SMP 死锁 - -**现象**: SMP=4 + KVM 时,ext4 写入操作后系统冻结;SMP=1 正常完成。 - -**根因**: `axfs-ng` ext4 状态使用 `SpinNoPreempt` mutex。多 vCPU 并发访问时发生锁顺序死锁:线程 A 持锁等 I/O,线程 B 自旋等锁。 - -**当前解决方案**: SMP=1 + KVM(性能足够:6m53s 编译 301 crates)。 - -**未来方向**: 将 `SpinNoPreempt` 替换为 `Spin`(允许抢占),或审计 `sync_to_disk()` 消除锁重入。 - -### 10. rustc 版本不满足 MSRV - -**现象**: Debian 系统 rustc (1.85) 无法编译要求 nightly 特性的代码。 - -**修复**: 在 rootfs 准备阶段通过 `rustup` 安装 nightly-2026-04-27 工具链(~6.9GB)。 - -### 11. USB UVC 未供应商化依赖 - -**现象**: `cargo build --offline` 报 `no matching package named 'qoi' found`。 - -**根因**: `drivers/usb/usb-device/uvc` (crab-uvc) 的 dev-dependency 引用了未缓存的 `qoi` crate。 - -**修复**: `filter-workspace.sh` 中移除 `drivers/usb/usb-device/uvc` member 行。该驱动不参与内核编译。 - -## riscv64 专有阻塞点 - -### 12. Bitmap 容量溢出(已淘汰) - -**现象**: 8GB RAM 下 panic: `need 3145728 pages but CAP is 1048576`。 - -**根因**: 旧 `page-alloc-4g` 使用 `BitAlloc1M`(1M bits = 4GB 最大容量)。 - -**状态**: PR #987 移除了整个 bitmap 分配器,改用 TLSF/buddy-slab。TLSF 无硬编码容量限制,**此问题已不存在**。 - -### 13. 动态 RAM 检测失败(早期静态平台路径,已淘汰) - -**现象**: 早期静态平台路径无法通过 someboot 传递实际 FDT 内存大小,导致 RAM 大小只能来自固定配置。 - -**根因**: someboot(MMU 关闭阶段)写入共享内存的地址,在 StarryOS(MMU 开启阶段)无法直接访问——地址空间不一致。 - -**当前方案**: 当前构建链固定走 `axplat-dyn`,由 `somehal::mem::memory_map()` 提供运行时内存图,不再依赖静态平台 axconfig 或 `.axconfig.toml`。 - -## 脚本编排 - -| 脚本 | 功能 | 运行环境 | -|------|------|---------| -| `scripts/prepare-selfhost-rootfs.sh` | 创建 Debian rootfs(debootstrap + rustup + cargo fetch + 预解压 .crate) | Host (sudo) | -| `scripts/self-compile.sh` | 构建种子内核 → 注入文件 → QEMU expect 自动化 → 验证产物 | Host | -| `scripts/run-selfbuilt-kernel.sh` | 提取并启动自编译的内核 | Host | -| `scripts/filter-workspace.sh` | 从 Cargo.toml 移除架构不兼容的 workspace members | Host + Guest | - -### 使用流程 - -```bash -# 1. 准备 rootfs(首次,每架构一次) -sudo ./scripts/prepare-selfhost-rootfs.sh --arch x86_64 - -# 2. 自编译 -./scripts/self-compile.sh --arch x86_64 --smp 4 - -# 3. 启动自编译内核 -./scripts/run-selfbuilt-kernel.sh --arch x86_64 -``` - -## 测试配置 - -测试用例位于 `test-suit/starryos/selfhost-manual/`,**不在** `normal/` 目录下,不参与标准 CI。 - -``` -test-suit/starryos/selfhost-manual/ -├── build-riscv64gc-unknown-none-elf.toml # 构建配置 -├── selfhost-full-kernel/ # 完整编译测试(timeout=7200s) -│ ├── qemu-riscv64.toml -│ └── sh/self-compile.sh # Guest 内执行的编译脚本 -└── test-selfhost-check/ # 快速工具检查(timeout=120s) - └── qemu-riscv64.toml -``` - -**CI 不运行的原因**: Debian rootfs 镜像(~8-12GB)未上传到 tgosimages release,CI 容器无法下载。 - -**手动运行**: -```bash -cargo xtask starry test qemu --arch riscv64 --test-suite test-suit/starryos/selfhost-manual -c selfhost-full-kernel -``` - -## 已知限制 - -1. **`phys-memory-size` 硬编码 8GB**: 动态 RAM 检测因启动阶段地址空间不一致无法实现。标准 CI 测试使用默认内存配置,自编译需要 `-m 8G`。 -2. **自编译测试不在标准 CI 中运行**: 需要 8-12GB rootfs 镜像,仅支持本地手动测试。 -3. **SMP > 1 未验证**: ext4 `SpinNoPreempt` 死锁 workaround 为 SMP=1。x86_64 通过 KVM 加速弥补单核性能。 -4. **aarch64 引导已验证**: rootfs 准备 + 种子内核引导 + shell 可用均通过,完整编译因 TCG 模拟性能限制(预计 4-8h)未运行。需动态平台默认配置 + PIE 目标(`--config test-suit/starryos/qemu-smp1/build-aarch64-unknown-none-softfloat.toml`)。 -5. **页面回收仅支持干净页**: 脏页在极端压力下作为最后手段回收(记录 warning),缺少脏页写回机制。 - -## 环境要求 - -- **QEMU**: riscv64 (TCG) / x86_64 (KVM) / aarch64 (TCG), `-m 8G` -- **内核**: StarryOS (dev 分支) -- **根文件系统**: Debian (per-arch), ext4, rustc nightly-2026-04-27 -- **Host 依赖**: `qemu-system-*`, `expect`, `sudo`(免密), `systemd-nspawn` -- **源码**: StarryOS monorepo (离线,预取依赖) diff --git a/os/StarryOS/docs/starryos-self-compilation.md b/os/StarryOS/docs/starryos-self-compilation.md index af8f7e49cd..f78415f52a 100644 --- a/os/StarryOS/docs/starryos-self-compilation.md +++ b/os/StarryOS/docs/starryos-self-compilation.md @@ -1,5 +1,14 @@ # StarryOS 自编译全过程 +> ## 当前 x86_64 入口 +> +> x86_64 自编译现在通过 `cargo starry app qemu -t selfhost/selfhost-full-kernel --arch x86_64` +> 直接运行。它在联网 QEMU guest 中安装工具链和依赖,不需要 `self-compile.sh`、host +> sudo 或 loop mount。该流程使用 32 GiB 的稀疏 rootfs,并把 Cargo target 保存在 +> `/opt/starry-selfhost-target`,避免 Starry 的 `/tmp` MemoryFs 承载完整编译缓存。推荐流程与产物启动方式见 +> [`docs/starryos-self-compilation.md`](../../../docs/starryos-self-compilation.md)。本文其余内容 +> 保留为 riscv64/Debian 旧流程和历史排障记录,不能作为 x86_64 的当前操作说明。 + 在 riscv64 Debian Linux 上运行 StarryOS,并在 StarryOS 内部使用 cargo 编译 StarryOS 自身。前置依赖 PR 实现自编译需要两个基础设施 PR 作为前置条件。 diff --git a/os/arceos/modules/axfs-ng/src/fs/ext4/rsext4/inode.rs b/os/arceos/modules/axfs-ng/src/fs/ext4/rsext4/inode.rs index 248d39140f..e6c86771bc 100644 --- a/os/arceos/modules/axfs-ng/src/fs/ext4/rsext4/inode.rs +++ b/os/arceos/modules/axfs-ng/src/fs/ext4/rsext4/inode.rs @@ -224,6 +224,7 @@ impl FileNodeOps for Inode { // Path-based write_file() fails with NotFound after unlink. rsext4::write_inode_data(dev, fs, self.ino, offset, buf).map_err(into_vfs_err)?; } + self.fs.sync_to_disk()?; Ok(buf.len()) } @@ -236,6 +237,7 @@ impl FileNodeOps for Inode { rsext4::write_inode_data(dev, fs, self.ino, length, buf).map_err(into_vfs_err)?; length }; + self.fs.sync_to_disk()?; Ok((buf.len(), length + buf.len() as u64)) } @@ -522,7 +524,6 @@ impl DirNodeOps for Inode { let target_ino = InodeNumber::new(node.inode() as u32).map_err(into_vfs_err)?; Self::update_ctime_with(fs, dev, target_ino)?; } - self.fs.sync_to_disk()?; self.lookup_locked(name) } diff --git a/scripts/axbuild/src/build/std_build.rs b/scripts/axbuild/src/build/std_build.rs index c0571c5753..24191a5d6e 100644 --- a/scripts/axbuild/src/build/std_build.rs +++ b/scripts/axbuild/src/build/std_build.rs @@ -96,7 +96,13 @@ pub(super) fn std_c_toolchain_env(target_name: &str, tool_prefix: &str) -> HashM let target_env = target_name.replace('-', "_"); let cc = format!("{tool_prefix}-cc"); let ar = format!("{tool_prefix}-ar"); - let c_flags = std_c_target_flags(target_name).join(" "); + // The kernel links these freestanding C objects without a stack-protector + // runtime (no __stack_chk_fail / __stack_chk_guard). GCC 16 enables stack + // protection by default, which breaks the static-PIE link; disable it for + // all kernel C compiles, matching the in-guest self-compile build. + let mut c_flag_list = std_c_target_flags(target_name); + c_flag_list.push("-fno-stack-protector"); + let c_flags = c_flag_list.join(" "); env.insert(format!("CC_{target_env}"), cc.clone()); env.insert(format!("AR_{target_env}"), ar); if !c_flags.is_empty() { diff --git a/scripts/axbuild/src/build/tests/target_specs.rs b/scripts/axbuild/src/build/tests/target_specs.rs index e5cfc21e7b..7f8186d13c 100644 --- a/scripts/axbuild/src/build/tests/target_specs.rs +++ b/scripts/axbuild/src/build/tests/target_specs.rs @@ -14,11 +14,11 @@ fn std_c_toolchain_env_does_not_require_installed_cross_compiler() { ); assert_eq!( env.get("CFLAGS_riscv64gc_unknown_linux_musl"), - Some(&"-march=rv64gc -mabi=lp64d -mcmodel=medany".to_string()) + Some(&"-march=rv64gc -mabi=lp64d -mcmodel=medany -fno-stack-protector".to_string()) ); assert_eq!( env.get("CXXFLAGS_riscv64gc_unknown_linux_musl"), - Some(&"-march=rv64gc -mabi=lp64d -mcmodel=medany".to_string()) + Some(&"-march=rv64gc -mabi=lp64d -mcmodel=medany -fno-stack-protector".to_string()) ); assert!(!env.contains_key("BINDGEN_EXTRA_CLANG_ARGS_riscv64gc_unknown_linux_musl")); } @@ -29,11 +29,11 @@ fn std_c_toolchain_env_exports_loongarch_softfloat_abi_flags() { assert_eq!( env.get("CFLAGS_loongarch64_unknown_linux_musl"), - Some(&"-mabi=lp64s -msoft-float".to_string()) + Some(&"-mabi=lp64s -msoft-float -fno-stack-protector".to_string()) ); assert_eq!( env.get("CXXFLAGS_loongarch64_unknown_linux_musl"), - Some(&"-mabi=lp64s -msoft-float".to_string()) + Some(&"-mabi=lp64s -msoft-float -fno-stack-protector".to_string()) ); if let Some(bindgen_args) = env.get("BINDGEN_EXTRA_CLANG_ARGS_loongarch64_unknown_linux_musl") { assert!(bindgen_args.contains("--target=loongarch64-linux-musl")); diff --git a/scripts/axbuild/src/starry/app/tests/qemu.rs b/scripts/axbuild/src/starry/app/tests/qemu.rs index 37da72d359..3d1a0f2769 100644 --- a/scripts/axbuild/src/starry/app/tests/qemu.rs +++ b/scripts/axbuild/src/starry/app/tests/qemu.rs @@ -195,6 +195,156 @@ fail_regex = [] assert!(!fields.snapshot); } +#[test] +fn selfhost_x86_app_preserves_the_persistent_build_contract() { + let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("axbuild manifest should live under scripts/axbuild") + .to_path_buf(); + let app_dir = repo.join("apps/starry/selfhost/selfhost-full-kernel"); + let config_path = app_dir.join("qemu-x86_64.toml"); + let config: toml::Value = toml::from_str(&fs::read_to_string(&config_path).unwrap()).unwrap(); + + assert_eq!( + config.get("snapshot").and_then(toml::Value::as_bool), + Some(false), + "{} must persist the guest-built kernel", + config_path.display() + ); + assert_eq!( + config.get("shell_init_cmd").and_then(toml::Value::as_str), + Some("/bin/sh /opt/starry-selfhost-run.sh"), + "{} must use the staged non-interactive guest runner", + config_path.display() + ); + let qemu_args = config + .get("args") + .and_then(toml::Value::as_array) + .expect("selfhost qemu args must be an array"); + let qemu_args = qemu_args + .iter() + .map(|arg| arg.as_str().expect("QEMU arguments must be strings")) + .collect::>(); + assert!( + qemu_args.contains(&"-no-shutdown") + && qemu_args.windows(2).any(|args| args == ["-smp", "4"]) + && qemu_args.windows(2).any(|args| args == ["-m", "16G"]) + && qemu_args + .windows(2) + .any(|args| args == ["-netdev", "user,id=net0"]) + && qemu_args + .windows(2) + .any(|args| args == ["-device", "virtio-net-pci,netdev=net0"]), + "{} must wait for an explicit success or failure marker before QEMU exits", + config_path.display() + ); + assert!( + fs::read_to_string(&config_path) + .unwrap() + .contains("rootfs-x86_64-selfhost.img"), + "{} must select a managed per-app rootfs", + config_path.display() + ); + + let prebuild_path = app_dir.join("prebuild.sh"); + let prebuild = fs::read_to_string(&prebuild_path).unwrap(); + assert!( + prebuild.contains("tgoskits-src.tar") + && prebuild.contains("starry-selfhost-run.sh") + && prebuild.contains("starry-selfhost-reboot-guard.sh") + && prebuild.contains("cargo xtask image resize") + && prebuild.contains("SELFHOST_ROOTFS_SIZE_MIB:-32768") + && prebuild.contains("stage_guest_resolver") + && prebuild.contains("/run/systemd/resolve/resolv.conf") + && prebuild.contains("sha256sum --check --status") + && prebuild.contains("--prefix=\"$toolchain_dir\"") + && prebuild.contains(".starry-selfhost-toolchain-version"), + "{} must stage source, the guest runner, the reboot guard, and a usable resolver into a \ + 32 GiB rootfs, and must install verified Rust components into a versioned toolchain \ + archive", + prebuild_path.display() + ); + + let guest_runner_path = app_dir.join("guest-selfbuild.sh"); + let guest_runner = fs::read_to_string(&guest_runner_path).unwrap(); + assert!( + guest_runner.contains("x86_64-unknown-linux-musl") + && guest_runner.contains("TOOLCHAIN=\"nightly-2026-05-28\"") + && guest_runner.contains("RUSTUP_TOOLCHAIN=\"starry-selfhost-") + && guest_runner.contains("--default-toolchain none") + && guest_runner.contains("export RUSTUP_TOOLCHAIN") + && guest_runner.contains("rustc -vV") + && guest_runner.contains("cargo-binutils --version 0.4.0 --locked") + && guest_runner.contains("ksym --version 0.6.0 --locked") + && guest_runner.contains("tg-xtask") + && guest_runner.contains("SELF_COMPILE_SUCCESS") + && !guest_runner.contains("SELFHOST_RUST_TOOLCHAIN") + && !guest_runner.contains("x86_64-unknown-linux-gnu") + && !guest_runner.contains("export CARGO_TARGET_DIR") + && guest_runner.contains("SELFHOST_TARGET_DIR:-/opt/starry-selfhost-target") + && guest_runner.contains("ln -s \"$TARGET_DIR\" \"$SOURCE_DIR/target\"") + && guest_runner.contains("SELFHOST_CARGO_BUILD_JOBS:-2") + && guest_runner + .contains("$SOURCE_DIR/target/x86_64-unknown-linux-musl/release/starryos"), + "{} must build the canonical x86_64 path with a native musl host toolchain", + guest_runner_path.display() + ); +} + +#[test] +fn selfhost_reboot_guard_reports_the_interrupted_phase() { + let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("axbuild manifest should live under scripts/axbuild") + .to_path_buf(); + let guard = + repo.join("apps/starry/selfhost/selfhost-full-kernel/guest-selfbuild-reboot-guard.sh"); + let root = tempdir().unwrap(); + let state = root.path().join("state"); + let bin_dir = root.path().join("bin"); + let poweroff = bin_dir.join("poweroff"); + let poweroff_marker = root.path().join("poweroff-called"); + fs::create_dir(&bin_dir).unwrap(); + fs::write( + &poweroff, + "#!/bin/sh\nprintf 'called\\n' >\"$POWER_OFF_MARKER\"\n", + ) + .unwrap(); + fs::set_permissions(&poweroff, fs::Permissions::from_mode(0o755)).unwrap(); + fs::write(&state, "running test-run kernel\n").unwrap(); + + let output = Command::new("/bin/sh") + .arg(&guard) + .env("SELFHOST_STATE_FILE", &state) + .env("POWER_OFF_MARKER", &poweroff_marker) + .env("PATH", &bin_dir) + .output() + .unwrap(); + + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stdout) + .contains("SELF_COMPILE_FAILED: unexpected guest reboot during kernel") + ); + assert_eq!(fs::read_to_string(&poweroff_marker).unwrap(), "called\n"); + + fs::write(&state, "ready test-run prebuild\n").unwrap(); + fs::remove_file(&poweroff_marker).unwrap(); + let output = Command::new("/bin/sh") + .arg(&guard) + .env("SELFHOST_STATE_FILE", &state) + .env("POWER_OFF_MARKER", &poweroff_marker) + .env("PATH", &bin_dir) + .output() + .unwrap(); + + assert!(output.status.success()); + assert!(!String::from_utf8_lossy(&output.stdout).contains("SELF_COMPILE_FAILED")); + assert!(!poweroff_marker.exists()); +} + #[test] fn app_qemu_test_case_preserves_host_symbolize_success_regex() { let case_dir = PathBuf::from("/tmp/apps/starry/memtrack-backtrace"); diff --git a/scripts/run-selfbuilt-kernel.sh b/scripts/run-selfbuilt-kernel.sh index b49dd54ccc..03c8df8256 100755 --- a/scripts/run-selfbuilt-kernel.sh +++ b/scripts/run-selfbuilt-kernel.sh @@ -4,15 +4,19 @@ # boot it in QEMU. # # Prerequisites: -# - scripts/self-compile.sh (must complete successfully first) +# - cargo starry app qemu -t selfhost/selfhost-full-kernel --arch x86_64 +# (for the x86_64 app rootfs) # - qemu-system-, debugfs # # Usage: # ./scripts/run-selfbuilt-kernel.sh [OPTIONS] [rootfs-image] # -# --arch Target architecture: riscv64 (default), x86_64, aarch64. -# Must match the arch used during self-compile.sh. +# --arch Target architecture: riscv64 (default), x86_64. Must match +# the self-built artifact. aarch64 is boot-capable here but +# requires an explicit --kernel or rootfs artifact. # --smp Number of QEMU CPUs (default: 4). +# --kernel Self-compiled kernel binary (default: tmp/starryos-selfbuilt-). +# Rootfs defaults to the arch-specific image unless specified. # rootfs-image Path to the selfhost rootfs (by arch default). # # x86_64 notes: KVM acceleration is enabled when /dev/kvm is available. @@ -28,13 +32,16 @@ cd "$REPO_ROOT" ARCH="riscv64" SMP=4 ROOTFS_IMG="" +KERNEL_PATH="" while [[ $# -gt 0 ]]; do case "$1" in - --arch) ARCH="$2"; shift 2 ;; - --smp) SMP="$2"; shift 2 ;; + --arch) ARCH="$2"; shift 2 ;; + --smp) SMP="$2"; shift 2 ;; + --kernel) KERNEL_PATH="$2"; shift 2 ;; --help|-h) - echo "Usage: $0 [--arch riscv64|x86_64|aarch64] [--smp N] [rootfs-image]" + echo "Usage: $0 [--arch riscv64|x86_64|aarch64] [--smp N] [--kernel ] [rootfs-image]" + echo " --kernel Self-compiled kernel binary (default: tmp/starryos-selfbuilt-)" exit 0 ;; *) ROOTFS_IMG="$1"; shift ;; @@ -86,7 +93,10 @@ esac if [ -z "$ROOTFS_IMG" ]; then case "$ARCH" in riscv64) ROOTFS_IMG="tmp/axbuild/rootfs/rootfs-riscv64-debian-selfhost-v2.img" ;; - x86_64) ROOTFS_IMG="tmp/axbuild/rootfs/rootfs-x86_64-debian-selfhost.img" ;; + x86_64) + ROOTFS_STORAGE="${TGOS_IMAGE_LOCAL_STORAGE:-$REPO_ROOT/tmp/axbuild/rootfs}" + ROOTFS_IMG="$ROOTFS_STORAGE/rootfs-x86_64-selfhost.img/rootfs-x86_64-selfhost.img" + ;; aarch64) ROOTFS_IMG="tmp/axbuild/rootfs/rootfs-aarch64-debian-selfhost.img" ;; esac fi @@ -106,20 +116,23 @@ done [ -f "$ROOTFS_IMG" ] || error "Rootfs image not found: $ROOTFS_IMG" -# ─── Step 1: Extract the self-compiled kernel ────────────────────────────────── +# ─── Step 1: Resolve the self-compiled kernel ────────────────────────────────── -if [ -f "$CACHED_KERNEL" ] && [ -s "$CACHED_KERNEL" ]; then - EXISTING_SIZE=$(stat -c%s "$CACHED_KERNEL" 2>/dev/null || echo "0") - info "Using cached kernel: $CACHED_KERNEL (${EXISTING_SIZE} bytes)" - info "Delete this file to force re-extraction from rootfs." +if [ -n "$KERNEL_PATH" ]; then + # Explicit kernel path — use directly, no extraction needed. + if [ ! -f "$KERNEL_PATH" ] || [ ! -s "$KERNEL_PATH" ]; then + error "Specified kernel not found or empty: $KERNEL_PATH" + fi + CACHED_KERNEL="$KERNEL_PATH" + info "Using specified kernel: $CACHED_KERNEL" else info "Extracting self-compiled kernel from rootfs..." mkdir -p "$(dirname "$CACHED_KERNEL")" - + rm -f "$CACHED_KERNEL" debugfs -R "dump /opt/starryos-selfbuilt $CACHED_KERNEL" "$ROOTFS_IMG" 2>/dev/null || true if [ ! -f "$CACHED_KERNEL" ] || [ ! -s "$CACHED_KERNEL" ]; then - error "Failed to extract kernel from rootfs. Did you run scripts/self-compile.sh first?" + error "Failed to extract kernel from rootfs. Run: cargo starry app qemu -t selfhost/selfhost-full-kernel --arch x86_64" fi KERNEL_SIZE=$(stat -c%s "$CACHED_KERNEL") @@ -128,24 +141,116 @@ fi # ─── Step 2: Show kernel info ───────────────────────────────────────────────── -info "Kernel path: $CACHED_KERNEL" -info "Kernel size: $(stat -c%s "$CACHED_KERNEL") bytes" -info "Kernel md5: $(md5sum "$CACHED_KERNEL" | cut -d' ' -f1)" +if [ -f "$CACHED_KERNEL" ] && [ -s "$CACHED_KERNEL" ]; then + info "Kernel path: $CACHED_KERNEL" + info "Kernel size: $(stat -c%s "$CACHED_KERNEL") bytes" + info "Kernel md5: $(md5sum "$CACHED_KERNEL" | cut -d' ' -f1)" +fi # ─── Step 3: Boot ───────────────────────────────────────────────────────────── +# +# x86_64: the self-compiled kernel IS an EFI binary (built with plat-dyn +# + axplat-dyn/efi). We convert it to raw binary (objcopy), place it in +# an EFI System Partition, and boot via OVMF UEFI firmware — the same +# boot path used by the seed kernel. +# +# riscv64 / aarch64: bare-metal ELF booted via QEMU's -kernel loader. + +case "$ARCH" in + x86_64) + info "Booting self-compiled kernel via UEFI / OVMF ..." + + [ -f "$CACHED_KERNEL" ] || error "Self-compiled kernel not found: $CACHED_KERNEL" -info "Booting self-compiled StarryOS kernel ($ARCH)..." -info "Press Ctrl+A then X to exit QEMU." - -exec "$QEMU_BIN" \ - -nographic \ - -machine "$QEMU_MACHINE" \ - -cpu "$QEMU_CPU" \ - $QEMU_EXTRA \ - -smp "$SMP" \ - -m 8G \ - -kernel "$CACHED_KERNEL" \ - -device "$QEMU_BLK_DEV" \ - -drive id=disk0,if=none,format=raw,file="$ROOTFS_IMG",file.locking=off \ - -device "$QEMU_NET_DEV" \ - -netdev user,id=net0 + # The self-compiled kernel is an x86_64-unknown-none ELF whose + # EFI stub (axplat-dyn/efi) makes it an EFI application. Strip + # the ELF headers (objcopy -O binary) to produce the raw PE/COFF + # payload that OVMF loads from the EFI System Partition. + command -v objcopy &>/dev/null || error "objcopy not found (install binutils)" + + BIN_KERNEL="$REPO_ROOT/tmp/starryos-selfbuilt-${ARCH}.bin" + info "Converting ELF to raw binary for UEFI boot..." + objcopy -O binary "$CACHED_KERNEL" "$BIN_KERNEL" || error "objcopy failed" + info "Raw binary: $(stat -c%s "$BIN_KERNEL") bytes" + + ESP_DIR="$REPO_ROOT/tmp/esp-${ARCH}" + rm -rf "$ESP_DIR" + mkdir -p "$ESP_DIR/EFI/BOOT" + cp "$BIN_KERNEL" "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" + info "ESP ready: $ESP_DIR/EFI/BOOT/BOOTX64.EFI" + + # OVMF firmware — search common distribution paths. + # Arch: /usr/share/edk2/x64/OVMF_CODE.4m.fd (edk2-ovmf) + # Debian: /usr/share/OVMF/OVMF_CODE_4M.fd, /usr/share/OVMF/OVMF_CODE.fd (ovmf) + # Fedora: /usr/share/edk2/ovmf/OVMF_CODE.fd (edk2-ovmf) + # Generic: /usr/share/ovmf/OVMF.fd, /usr/share/qemu/OVMF.fd + OVMF_CODE="" + for candidate in \ + /usr/share/edk2/x64/OVMF_CODE.4m.fd \ + /usr/share/OVMF/OVMF_CODE_4M.fd \ + /usr/share/OVMF/OVMF_CODE.fd \ + /usr/share/edk2/ovmf/OVMF_CODE.fd \ + /usr/share/ovmf/OVMF.fd \ + /usr/share/qemu/OVMF.fd; do + if [ -f "$candidate" ]; then + OVMF_CODE="$candidate" + break + fi + done + [ -n "$OVMF_CODE" ] || error "OVMF firmware not found; install edk2-ovmf or ovmf" + info "OVMF firmware: $OVMF_CODE" + + # Derive VARS template from same directory as CODE. + OVMF_DIR="$(dirname "$OVMF_CODE")" + OVMF_VARS_TEMPLATE="" + for candidate in \ + "${OVMF_DIR}/OVMF_VARS.4m.fd" \ + "${OVMF_DIR}/OVMF_VARS_4M.fd" \ + "${OVMF_DIR}/OVMF_VARS.fd"; do + if [ -f "$candidate" ]; then + OVMF_VARS_TEMPLATE="$candidate" + break + fi + done + [ -n "$OVMF_VARS_TEMPLATE" ] || error "OVMF_VARS not found alongside OVMF_CODE in $OVMF_DIR" + OVMF_VARS="$REPO_ROOT/tmp/OVMF_VARS.x86_64.fd" + if [ ! -f "$OVMF_VARS" ]; then + cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS" + fi + + info "Boot via OVMF UEFI: self-compiled kernel as EFI/BOOT/BOOTX64.EFI" + info "Press Ctrl+A then X to exit QEMU." + + exec "$QEMU_BIN" \ + -nographic \ + -machine "$QEMU_MACHINE" \ + -cpu "$QEMU_CPU" \ + $QEMU_EXTRA \ + -smp "$SMP" \ + -m 8G \ + -drive if=pflash,format=raw,unit=0,readonly=on,file="$OVMF_CODE" \ + -drive if=pflash,format=raw,unit=1,file="$OVMF_VARS" \ + -drive format=raw,file=fat:rw:"$ESP_DIR" \ + -device virtio-blk-pci,drive=disk0 \ + -drive id=disk0,if=none,format=raw,file="$ROOTFS_IMG",file.locking=off \ + -device virtio-net-pci,netdev=net0 \ + -netdev user,id=net0 + ;; + *) + info "Booting self-compiled StarryOS kernel ($ARCH)..." + info "Press Ctrl+A then X to exit QEMU." + + exec "$QEMU_BIN" \ + -nographic \ + -machine "$QEMU_MACHINE" \ + -cpu "$QEMU_CPU" \ + $QEMU_EXTRA \ + -smp "$SMP" \ + -m 8G \ + -kernel "$CACHED_KERNEL" \ + -device "$QEMU_BLK_DEV" \ + -drive id=disk0,if=none,format=raw,file="$ROOTFS_IMG",file.locking=off \ + -device "$QEMU_NET_DEV" \ + -netdev user,id=net0 + ;; +esac