diff --git a/apps/starry/README.md b/apps/starry/README.md index a75e80213f..8d8830cbfc 100644 --- a/apps/starry/README.md +++ b/apps/starry/README.md @@ -71,6 +71,23 @@ PATH="$PWD/target/qemu-k230-docker-build:$PATH" \ See `k230-kpu-nncase/README.md` and `docs/k230-kpu-nncase-runtime.md` for the asset preparation flow. +## macOS HVF Self-Build + +The `macos-selfbuild` case is an Apple Silicon macOS workflow that boots an +AArch64 StarryOS SMP kernel with QEMU/HVF, enters the StarryOS guest userland, +and runs guest `cargo build` to build StarryOS again. + +```bash +KERNEL=target/aarch64-unknown-none-softfloat/release/starryos.bin \ +ROOTFS=tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img \ +SMP=8 JOBS=8 RAYON_NUM_THREADS=1 RUSTC_THREADS=2 SOURCE_TMPFS=1 \ +apps/starry/macos-selfbuild/run_selfbuild.sh +``` + +See `macos-selfbuild/README.md` for the no-Docker reviewer path, native rootfs +build/check flow, QEMU/HVF details, PASS markers, and representative self-build +timings. + ## Redis The `redis` case is a QEMU app workflow that installs Redis into a temporary diff --git a/apps/starry/macos-selfbuild/README.md b/apps/starry/macos-selfbuild/README.md new file mode 100644 index 0000000000..ef5c28c134 --- /dev/null +++ b/apps/starry/macos-selfbuild/README.md @@ -0,0 +1,270 @@ +# StarryOS macOS HVF Self-Build + +This case is an Apple Silicon macOS reproduction workflow. The host runs QEMU +with HVF, boots an AArch64 StarryOS SMP guest, and runs guest `cargo build` to +build StarryOS again inside StarryOS. + +The normal reproduction path builds the rootfs locally on macOS and does not use +Docker. `build_rootfs.sh` creates the AArch64 Alpine guest rootfs, installs the +pinned guest Rust/Cargo toolchain, prefetches the offline Cargo cache, and +injects the current TGOSKits source tree. + +## What This Demonstrates + +- host: Apple Silicon macOS; +- accelerator: QEMU HVF; +- guest kernel: StarryOS AArch64 SMP kernel; +- guest workload: StarryOS guest runs Cargo and builds the `starryos` binary; +- stable build shape: 8-vCPU guest with eight Cargo jobs (`SMP=8 JOBS=8`); +- pass marker: `===STARRY-MACOS-SELFBUILD-PASS jobs= elapsed====`. + +The default reproduction proves that the StarryOS SMP guest can complete a +self-build while booted with 8 vCPUs and running Cargo with eight jobs. + +Using AArch64/HVF keeps the guest ISA aligned with the Mac host CPU. This avoids +the cross-ISA TCG cost of RISC-V-on-macOS experiments and makes the self-build +result practical to reproduce on a laptop. + +## Prerequisites + +```bash +brew install qemu e2fsprogs zig llvm +``` + +The scripts use: + +- `qemu-system-aarch64`; +- `debugfs` from Homebrew `e2fsprogs`; +- `zig` or an `aarch64-linux-musl-gcc` cross compiler for the seed kernel; +- `llvm-nm` and `llvm-objdump`, with Homebrew `llvm` used as fallback. + +The generated self-build rootfs contains the guest Rust/Cargo toolchain, +offline Cargo dependencies, and a TGOSKits source tarball. `check_rootfs.sh` +verifies the minimum paths. + +## Reproduce From a Fresh Clone + +Clone and enter the branch under test: + +```bash +git clone https://github.com/yks23/tgoskits.git +cd tgoskits +git checkout app/starry-macos-selfbuild-only +``` + +Run the complete reproduction on macOS: + +```bash +RUST_DIST_SERVER=https://rsproxy.cn \ +STARRY_CARGO_REGISTRY_INDEX=sparse+https://rsproxy.cn/index/ \ +apps/starry/macos-selfbuild/reproduce.sh +``` + +The mirror variables are optional. Omit them to use the official Rust and +crates.io endpoints, or set them to closer mirrors when `static.rust-lang.org` +or `index.crates.io` is slow. + +`reproduce.sh` runs the three required steps: build or refresh the rootfs, build +the seed kernel, and launch the QEMU HVF self-build. On a base M1 with 8 GB +memory, close other heavy applications before running the 8-vCPU case; if it is +memory pressured, first verify the setup with: + +```bash +SMP=4 JOBS=4 MEM=3072M \ +RUST_DIST_SERVER=https://rsproxy.cn \ +STARRY_CARGO_REGISTRY_INDEX=sparse+https://rsproxy.cn/index/ \ +apps/starry/macos-selfbuild/reproduce.sh +``` + +The rootfs build downloads Alpine AArch64 APKs and Rust nightly components, then +runs `cargo fetch` on the host to populate the guest offline cache. The +`cargo fetch` phase may print only `Updating crates.io index` for several +minutes while the sparse registry cache is being populated. + +For reproducibility, the default native rootfs payload is pinned to Alpine +`v3.23` and Rust `nightly-2026-05-28`. Do not switch `ALPINE_BRANCH=edge` for +the graded reproduction: Alpine edge currently provides a newer Cargo/Rust +package set and can change the build-std schedule substantially. + +The expected output ends with: + +```text +STARRY_MACOS_SELFBUILD_ROOTFS_OK +``` + +After every `git pull`, branch switch, or source edit, refresh the source +payload embedded in the rootfs before running QEMU: + +```bash +apps/starry/macos-selfbuild/prepare_rootfs.sh +``` + +`run_selfbuild.sh` checks `/opt/tgoskits-src.meta` by default and exits early if +the rootfs was built from a different commit. This avoids accidentally running +an old rootfs for an hour. Set `REQUIRE_FRESH_ROOTFS=0` only for deliberate +stale-rootfs experiments. + +If a prebuilt rootfs artifact is supplied, it can still be placed at the +standard path instead of rebuilding locally. Use either a local downloaded file: + +```bash +apps/starry/macos-selfbuild/fetch_rootfs.sh \ + --input /path/to/rootfs-aarch64-hvf-selfbuild.img +``` + +or a direct artifact URL: + +```bash +ROOTFS_URL='https://.../rootfs-aarch64-hvf-selfbuild.img' \ +apps/starry/macos-selfbuild/fetch_rootfs.sh +``` + +Build the seed StarryOS kernel on macOS: + +```bash +apps/starry/macos-selfbuild/build_kernel.sh +``` + +Run the complete 8-vCPU, eight-Cargo-job self-build: + +```bash +KERNEL=target/aarch64-unknown-none-softfloat/release/starryos.bin \ +ROOTFS=tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img \ +SMP=8 \ +JOBS=8 \ +RAYON_NUM_THREADS=1 \ +RUSTC_THREADS=2 \ +SOURCE_TMPFS=1 \ +QEMU_TIMEOUT_SEC=10800 \ +apps/starry/macos-selfbuild/run_selfbuild.sh +``` + +A successful run prints: + +```text +===STARRY-MACOS-SELFBUILD-QEMU-AARCH64-PROFILE expected_crates~420=== +===STARRY-MACOS-SELFBUILD-PASS jobs=8 elapsed==== +===STARRY-MACOS-SELFBUILD-RUN-END rc=0=== +``` + +The qemu-aarch64 reproducible profile should show: + +```text +rustc_threads=2 +features=plat-dyn,ax-driver/virtio-blk,ax-driver/virtio-net,ax-driver/virtio-gpu,ax-driver/virtio-input,ax-driver/virtio-socket,starry-kernel/input,starry-kernel/vsock +``` + +The guest refuses a run whose `RUSTC_THREADS` differs from `2` unless +`ALLOW_SLOW_SELFBUILD=1` is explicitly set. This keeps the graded run on the +same qemu-aarch64 profile that was debugged for reproducibility. + +The host runner also refuses unexpectedly large Cargo totals by default. The +current qemu-aarch64 profile is expected to report about `420` Cargo units. A much larger +total usually means a stale rootfs or slow feature set is being used; refresh the +rootfs from the current checkout and rerun the command above. + +Logs are written under: + +```text +target/starry-macos-selfbuild/logs/ +``` + +The runner copies the input rootfs into +`target/starry-macos-selfbuild/rootfs/`, injects the guest runner scripts, and +runs QEMU against that working copy, so the input artifact is not modified. Set +`QEMU_SNAPSHOT=1` only when an explicit throw-away QEMU disk overlay is desired. + +## Quick Boot Check + +To verify the rootfs and kernel reach the StarryOS guest shell without starting +the Cargo build: + +```bash +KERNEL=target/aarch64-unknown-none-softfloat/release/starryos.bin \ +ROOTFS=tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img \ +SMP=8 \ +BOOT_ONLY=1 \ +QEMU_TIMEOUT_SEC=180 \ +apps/starry/macos-selfbuild/run_selfbuild.sh +``` + +`BOOT_ONLY=1` is only a smoke test. Leave it unset for the full self-build. + +## Rootfs Contract + +The generated rootfs is intentionally kept outside git because it is large. It +must contain: + +```text +/usr/bin/cargo +/opt/rust-nightly/bin/rustc +/opt/rust-nightly/lib/rustlib/src/rust/library/Cargo.lock +/usr/bin/aarch64-linux-musl-gcc +/opt/rustc-nightly-sysroot +/opt/rustdoc-nightly-sysroot +/opt/tgoskits/Cargo.toml or /opt/tgoskits-src.tar +/root/.cargo/registry/index +/root/.cargo/registry/cache +``` + +Check an already placed rootfs with: + +```bash +apps/starry/macos-selfbuild/check_rootfs.sh \ + tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img +``` + +If a prebuilt toolchain rootfs is available and only the source tree needs to be +refreshed, inject the current checkout without rebuilding the toolchain: + +```bash +apps/starry/macos-selfbuild/prepare_rootfs.sh \ + --base-rootfs tmp/axbuild/rootfs/rootfs-aarch64-hvf-toolchain.img \ + --output-rootfs tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img +``` + +`prepare_rootfs.sh` writes `/opt/tgoskits-src.tar` and +`/opt/tgoskits-src.meta`. The host runner checks this metadata before booting +QEMU, and the guest checks it again when `TGOSKITS_COMMIT` is supplied. + +## Important Knobs + +| Variable | Default | Meaning | +| --- | --- | --- | +| `SMP` | `8` | QEMU vCPU count, passed to `-smp`. | +| `JOBS` | `SMP` | Guest Cargo job count. | +| `RAYON_NUM_THREADS` | `1` | Rayon worker limit for guest build scripts. | +| `RUSTC_THREADS` | `2` | Guest `-Zthreads=` value for the reproducible qemu-aarch64 profile. | +| `SOURCE_TMPFS` | `1` | Copy source into `/tmp` before building. | +| `QEMU_TIMEOUT_SEC` | `7200` | Host timeout; use `0` to disable. | +| `QEMU_ACCEL` | `hvf` | QEMU accelerator string. | +| `QEMU_MACHINE` | `virt,gic-version=3` | QEMU machine string. | +| `QEMU_CPU` | `host` | QEMU CPU model for HVF. | +| `BOOT_ONLY` | `0` | Stop after the shell prompt instead of starting Cargo. | +| `BUILD_TARGET` | `aarch64-unknown-none-softfloat` | Guest Cargo target. | +| `BUILD_PACKAGE` | `starryos` | Cargo package to build. | +| `BUILD_BIN` | `starryos` | Cargo binary to build; set `none` for library package diagnostics. | +| `FEATURES` | `plat-dyn,ax-driver/virtio-blk,ax-driver/virtio-net,ax-driver/virtio-gpu,ax-driver/virtio-input,ax-driver/virtio-socket,starry-kernel/input,starry-kernel/vsock` | QEMU AArch64 feature profile used by the reproducible self-build; set it to an empty string for single-crate diagnostics. | +| `REQUIRE_FRESH_ROOTFS` | `1` | Refuse a rootfs whose embedded source commit does not match the checkout. | +| `ALLOW_SLOW_SELFBUILD` | `0` | Permit the slow full-device feature profile only for explicit experiments. | +| `GUEST_MONITOR_INTERVAL_SEC` | `60` | Print guest `ps` snapshots while Cargo runs; set `0` to disable. | +| `EXTRA_RUSTFLAGS` | empty | Extra guest Rust flags for local experiments. | + +## Rootfs Rebuild Details + +```bash +apps/starry/macos-selfbuild/build_rootfs.sh +``` + +That helper creates an AArch64 Alpine payload natively on macOS, installs the +pinned guest Rust toolchain and offline Cargo cache, and injects it with +`debugfs`. It writes: + +```text +tmp/axbuild/rootfs/rootfs-aarch64-hvf-toolchain.img +tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img +``` + +The script still accepts `--payload` or `ROOTFS_PAYLOAD_URL` for an externally +prepared payload tarball. `--build-payload-with-docker` is kept only as an +explicit maintainer fallback and is not used by the macOS reproduction path. diff --git a/apps/starry/macos-selfbuild/RESULTS.md b/apps/starry/macos-selfbuild/RESULTS.md new file mode 100644 index 0000000000..4e0b0d8689 --- /dev/null +++ b/apps/starry/macos-selfbuild/RESULTS.md @@ -0,0 +1,92 @@ +# macOS HVF Self-Build Result Notes + +This file records the expected result shape for the macOS/HVF self-build +workflow. The authoritative reproduction command is in `README.md`. + +## Control Variables + +```text +host OS: macOS on Apple Silicon +guest ISA: AArch64 +accelerator: QEMU HVF +kernel: StarryOS AArch64 SMP +rootfs: macOS-native generated self-build ext4 image +QEMU disk mode: writable working-copy image; optional QEMU_SNAPSHOT=1 overlay +validated reproduction: SMP=8, JOBS=8 +success marker: STARRY-MACOS-SELFBUILD-PASS +``` + +The validated path boots an 8-vCPU guest and runs Cargo with eight jobs. + +The git repository contains the runner, checks, source-level fixes, and the +macOS-native rootfs construction script. The generated rootfs is still kept out +of git because it is large, but reviewers should not need Docker or a prebuilt +artifact to reproduce the macOS/HVF run. A prebuilt rootfs can be supplied only +as a faster optional path. + +## Guest Build Shape + +Inside the StarryOS guest, the runner executes: + +```bash +cargo build \ + -p starryos \ + --bin starryos \ + --target aarch64-unknown-none-softfloat \ + -Z build-std=core,alloc \ + --target-dir /tmp/starryos-selfbuild-target \ + --features plat-dyn,ax-driver/virtio-blk,ax-driver/virtio-net,ax-driver/virtio-gpu,ax-driver/virtio-input,ax-driver/virtio-socket,starry-kernel/input,starry-kernel/vsock \ + --release +``` + +with: + +```text +CARGO_BUILD_JOBS=8 +RAYON_NUM_THREADS=1 +RUSTC_THREADS=2 +CARGO_INCREMENTAL=0 +CARGO_NET_OFFLINE=true +RUSTC_BOOTSTRAP=1 +RUSTC=/opt/rustc-nightly-sysroot +RUSTDOC=/opt/rustdoc-nightly-sysroot +ALLOW_SLOW_SELFBUILD=0 +``` + +The qemu-aarch64 reproducible profile is guarded by the guest script. Unless +`ALLOW_SLOW_SELFBUILD=1` is set for experiments, it refuses runs that do not use +`RUSTC_THREADS=2`. + +## Reference Numbers + +These are local Apple Silicon reference measurements from the macOS/HVF +self-build experiments. Re-run locally for authoritative numbers on another +machine. + +| Case | Time | Notes | +| --- | --- | --- | +| `SMP=8`, `JOBS=8`, qemu-aarch64 profile, tmp source/target, `RUSTC_THREADS=2` | `331s` | latest validated default reproduction | +| `SMP=8`, `JOBS=1`, `SOURCE_TMPFS=0`, tuned feature set | `657s` | historical single-job fallback | +| `SMP=1`, `JOBS=1`, `SOURCE_TMPFS=0`, tuned feature set | `642s` | latest single-vCPU validation | +| `SMP=8`, `JOBS=1`, ext4 source/target | `951s` | slow guest baseline | +| `SMP=8`, `JOBS=8`, ext4 source/target | `917s` | first complete SMP self-build | +| `SMP=8`, `JOBS=8`, tmp target only | `660s` | moves Cargo target output to `/tmp` | +| `SMP=8`, `JOBS=8`, tmp source and target | `642s` | copies source and target output to `/tmp` | +| `SMP=8`, `JOBS=8`, tmp source/target, no LTO | `515s` | `CARGO_PROFILE_RELEASE_LTO=false` | +| `SMP=8`, `JOBS=8`, tmp source/target, no LTO, opt0, CGU256 | `427s` | reduces serial optimized codegen cost | +| host macOS reference | `134s` | host-side lower bound, not inside StarryOS | + +Useful ratios: + +```text +951s / 331s = 2.87x slow guest baseline to latest stable reproduction +642s / 331s = 1.94x tmp source/target baseline to latest stable reproduction +657s / 331s = 1.99x single-job fallback to eight-job default reproduction +``` + +## Interpretation + +The macOS/HVF app demonstrates a real StarryOS guest self-build, not a host-side +cross build. The remaining gap to the macOS host reference is from filesystem +writeback, process/wait/pipe overhead, SMP scheduling and wakeups, lock +contention, and Cargo's serial critical path. diff --git a/apps/starry/macos-selfbuild/build-aarch64-unknown-none-softfloat.toml b/apps/starry/macos-selfbuild/build-aarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..d870c1b1d5 --- /dev/null +++ b/apps/starry/macos-selfbuild/build-aarch64-unknown-none-softfloat.toml @@ -0,0 +1,17 @@ +target = "aarch64-unknown-none-softfloat" +env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2" } +log = "Warn" +features = [ + "ax-feat/display", + "ax-feat/rtc", + "ax-driver/virtio-blk", + "ax-driver/virtio-net", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", + "starry-kernel/input", + "starry-kernel/vsock", + "smp", +] +max_cpu_num = 8 +plat_dyn = true diff --git a/apps/starry/macos-selfbuild/build_kernel.sh b/apps/starry/macos-selfbuild/build_kernel.sh new file mode 100755 index 0000000000..6eb87c61f0 --- /dev/null +++ b/apps/starry/macos-selfbuild/build_kernel.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../../.." && pwd)" + +config="${CONFIG:-$script_dir/build-aarch64-unknown-none-softfloat.toml}" +host_tools_dir="${HOST_TOOLS_DIR:-$repo_root/target/starry-macos-selfbuild/host-tools}" +zig_cache_dir="${ZIG_CACHE_DIR:-$repo_root/target/starry-macos-selfbuild/zig-cache}" + +usage() { + cat <<'USAGE' +Usage: + apps/starry/macos-selfbuild/build_kernel.sh [extra cargo xtask starry build args] + +Builds the AArch64 StarryOS seed kernel used by the macOS HVF self-build run. +On macOS, lwprintf-rs expects an aarch64-linux-musl-gcc binary while compiling +bare-metal C helpers. If that binary is missing and zig is available, this +script creates a local wrapper under target/starry-macos-selfbuild/host-tools. + +Environment: + CONFIG Build config path + HOST_TOOLS_DIR Directory for generated host tool wrappers + ZIG_CACHE_DIR Directory for zig local/global caches +USAGE +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +zig_lib_dir() { + zig env 2>/dev/null | sed -n -E ' + s/^[[:space:]]*"lib_dir"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p + s/^[[:space:]]*\.lib_dir = "([^"]+)".*/\1/p + ' | head -1 +} + +ensure_aarch64_musl_gcc() { + if command -v aarch64-linux-musl-gcc >/dev/null 2>&1; then + return + fi + + if ! command -v zig >/dev/null 2>&1; then + echo "aarch64-linux-musl-gcc not found; install a musl cross gcc or brew install zig" >&2 + exit 1 + fi + + local zig_path lib_dir musl_include_root musl_generic musl_target wrapper cc_wrapper ar_wrapper sysroot_view llvm_ar + zig_path="$(command -v zig)" + lib_dir="$(zig_lib_dir)" + musl_include_root="$lib_dir/libc/include" + musl_generic="$musl_include_root/generic-musl" + musl_target="$musl_include_root/aarch64-linux-musl" + wrapper="$host_tools_dir/aarch64-linux-musl-gcc" + cc_wrapper="$host_tools_dir/aarch64-linux-musl-cc" + ar_wrapper="$host_tools_dir/aarch64-linux-musl-ar" + sysroot_view="$host_tools_dir/zig-aarch64-musl-sysroot" + llvm_ar="$(find_llvm_tool llvm-ar)" + + if [[ -z "$lib_dir" || ! -d "$musl_generic" || ! -d "$musl_target/bits" ]]; then + echo "could not locate zig musl headers under zig lib_dir=$lib_dir" >&2 + exit 1 + fi + if [[ -z "$llvm_ar" ]]; then + echo "llvm-ar is unavailable; install llvm-tools-preview or brew install llvm" >&2 + exit 1 + fi + + mkdir -p "$host_tools_dir" + rm -rf "$sysroot_view" + mkdir -p "$sysroot_view/include" + for entry in "$musl_generic"/*; do + ln -s "$entry" "$sysroot_view/include/$(basename "$entry")" + done + rm -f "$sysroot_view/include/bits" + mkdir -p "$sysroot_view/include/bits" + for entry in "$musl_generic/bits"/*; do + ln -s "$entry" "$sysroot_view/include/bits/$(basename "$entry")" + done + for entry in "$musl_target/bits"/*; do + rm -f "$sysroot_view/include/bits/$(basename "$entry")" + ln -s "$entry" "$sysroot_view/include/bits/$(basename "$entry")" + done + + cat >"$wrapper" <"$ar_wrapper" </dev/null 2>&1; then + command -v "$name" + return + fi + + for candidate in \ + "/opt/homebrew/opt/llvm/bin/$name" \ + "/opt/homebrew/opt/llvm@21/bin/$name" \ + "/opt/homebrew/opt/llvm@20/bin/$name"; do + if [[ -x "$candidate" ]]; then + printf '%s\n' "$candidate" + return + fi + done +} + +quote_toml_string() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + printf '"%s"' "$value" +} + +config_with_extra_features() { + local source_config="$1" + local extra_csv="$2" + local output="$host_tools_dir/$(basename "$source_config" .toml)-extra-features.toml" + local -a extra_features=() + local feature line in_features=0 inserted=0 + + IFS=',' read -r -a extra_features <<<"$extra_csv" + mkdir -p "$host_tools_dir" + : >"$output" + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$in_features" = "1" && "$line" =~ ^[[:space:]]*] ]]; then + if [[ "$inserted" = "0" ]]; then + for feature in "${extra_features[@]}"; do + feature="${feature#"${feature%%[![:space:]]*}"}" + feature="${feature%"${feature##*[![:space:]]}"}" + if [[ -n "$feature" ]]; then + printf ' %s,\n' "$(quote_toml_string "$feature")" >>"$output" + fi + done + inserted=1 + fi + in_features=0 + fi + + printf '%s\n' "$line" >>"$output" + + if [[ "$line" =~ ^[[:space:]]*features[[:space:]]*=.*\[[[:space:]]*$ ]]; then + in_features=1 + fi + done <"$source_config" + + if [[ "$inserted" = "0" ]]; then + printf '\nfeatures = [\n' >>"$output" + for feature in "${extra_features[@]}"; do + feature="${feature#"${feature%%[![:space:]]*}"}" + feature="${feature%"${feature##*[![:space:]]}"}" + if [[ -n "$feature" ]]; then + printf ' %s,\n' "$(quote_toml_string "$feature")" >>"$output" + fi + done + printf ']\n' >>"$output" + fi + + printf '%s\n' "$output" +} + +ensure_rust_binutils_wrappers() { + local rust_tool llvm_tool llvm_path wrapper + mkdir -p "$host_tools_dir" + + for rust_tool in rust-ar rust-nm rust-objcopy rust-objdump; do + llvm_tool="${rust_tool#rust-}" + llvm_tool="llvm-$llvm_tool" + llvm_path="$(find_llvm_tool "$llvm_tool")" + wrapper="$host_tools_dir/$rust_tool" + + if [[ -z "$llvm_path" ]]; then + if command -v "$rust_tool" >/dev/null 2>&1 && "$rust_tool" --version >/dev/null 2>&1; then + continue + fi + echo "$rust_tool is unavailable; install llvm-tools-preview or brew install llvm" >&2 + exit 1 + fi + + cat >"$wrapper" <&2 + exit 1 + fi + if ! ln -sf "$source" "$expected" 2>/dev/null; then + cat >&2 < $source +ostool expects llvm-objcopy at that exact Rust toolchain path. +Install/repair the component manually: + + rustup component add llvm-tools-preview + +or make the toolchain bin directory writable and rerun this script. +EOF + exit 1 + fi + echo "using llvm-objcopy compatibility link: $expected -> $source" +} + +ensure_aarch64_musl_gcc +prepend_rust_objcopy_dir +ensure_rust_binutils_wrappers +ensure_ostool_llvm_objcopy +mkdir -p "$zig_cache_dir/local" "$zig_cache_dir/global" +export ZIG_LOCAL_CACHE_DIR="$zig_cache_dir/local" +export ZIG_GLOBAL_CACHE_DIR="$zig_cache_dir/global" + +cd "$repo_root" +if [[ -n "${STARRY_KERNEL_EXTRA_FEATURES:-}" ]]; then + config="$(config_with_extra_features "$config" "$STARRY_KERNEL_EXTRA_FEATURES")" + echo "using extra Starry kernel features from config: $config" +fi +cargo_bin="$(rustc --print sysroot)/bin/cargo" +if [[ ! -x "$cargo_bin" ]]; then + cargo_bin="$(command -v cargo)" +fi +export PATH="$host_tools_dir:$PATH" +"$cargo_bin" xtask starry build -c "$config" "$@" + +actual_bin="$repo_root/target/aarch64-unknown-linux-musl/release/starryos.bin" +default_bin="$repo_root/target/aarch64-unknown-none-softfloat/release/starryos.bin" +if [[ -f "$actual_bin" && "$actual_bin" != "$default_bin" ]]; then + mkdir -p "$(dirname "$default_bin")" + cp "$actual_bin" "$default_bin" + echo "kernel=$default_bin" +fi diff --git a/apps/starry/macos-selfbuild/build_rootfs.sh b/apps/starry/macos-selfbuild/build_rootfs.sh new file mode 100755 index 0000000000..db08375281 --- /dev/null +++ b/apps/starry/macos-selfbuild/build_rootfs.sh @@ -0,0 +1,966 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../../.." && pwd)" + +usage() { + cat <<'USAGE' +Usage: + apps/starry/macos-selfbuild/build_rootfs.sh \ + [--base-rootfs tmp/axbuild/rootfs/rootfs-aarch64-alpine.img] \ + [--toolchain-rootfs tmp/axbuild/rootfs/rootfs-aarch64-hvf-toolchain.img] \ + [--selfbuild-rootfs tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img] \ + [--source .] \ + [--payload /path/to/starry-macos-selfbuild-payload.tar.zst] + + ROOTFS_PAYLOAD_URL=https://.../starry-macos-selfbuild-payload.tar.zst \ + apps/starry/macos-selfbuild/build_rootfs.sh + +Maintainer-only payload rebuild: + + apps/starry/macos-selfbuild/build_rootfs.sh --build-payload-with-docker + +Builds the rootfs set used by the macOS HVF self-build app: + + 1. ensure/copy the managed AArch64 Alpine rootfs; + 2. resize the copy so it can hold Rust/Cargo and offline Cargo cache; + 3. build or extract an AArch64 guest toolchain payload on macOS; + 4. inject that payload with debugfs to create the toolchain rootfs; + 5. inject the current TGOSKits source tree to create the self-build rootfs. + +The default path is macOS-native and does not use Docker. It downloads Alpine +aarch64 APKs and official Rust aarch64-musl toolchain tarballs, prepares the +guest payload under target/, and injects it into the rootfs. + +Environment: + ROOTFS_PAYLOAD Local payload tarball to inject + ROOTFS_PAYLOAD_URL Payload URL to download and inject + ALPINE_BRANCH Alpine branch for APK payloads (default: v3.23) + ALPINE_MIRROR Alpine mirror URL + RUST_DIST_SERVER Rust dist server URL (default: https://static.rust-lang.org) + STARRY_CARGO_REGISTRY_INDEX + Optional Cargo registry index URL, e.g. sparse+https://rsproxy.cn/index/ + RUST_TOOLCHAIN Rust toolchain date/name (default: nightly-2026-05-28) + BUILD_TARGET Guest Cargo target to prefetch (default: aarch64-unknown-none-softfloat) + DOCKER_IMAGE Maintainer-only Alpine image for --build-payload-with-docker + (default: alpine:v3.23) + ROOTFS_SIZE_MB Size of the toolchain image after resize (default: 16384) + DEBUGFS Path to debugfs + E2FSCK Path to e2fsck + RESIZE2FS Path to resize2fs +USAGE +} + +base_rootfs="$repo_root/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img" +toolchain_rootfs="$repo_root/tmp/axbuild/rootfs/rootfs-aarch64-hvf-toolchain.img" +selfbuild_rootfs="$repo_root/tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img" +source_dir="$repo_root" +rust_nightly_dir="${RUST_NIGHTLY_DIR:-}" +rust_toolchain="${RUST_TOOLCHAIN:-nightly-2026-05-28}" +rootfs_payload="${ROOTFS_PAYLOAD:-}" +rootfs_payload_url="${ROOTFS_PAYLOAD_URL:-}" +alpine_branch="${ALPINE_BRANCH:-v3.23}" +alpine_arch="${ALPINE_ARCH:-aarch64}" +alpine_mirror="${ALPINE_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}" +rust_dist_server="${RUST_DIST_SERVER:-https://static.rust-lang.org}" +cargo_registry_index="${STARRY_CARGO_REGISTRY_INDEX:-${CARGO_REGISTRY_INDEX:-}}" +guest_target="${BUILD_TARGET:-aarch64-unknown-none-softfloat}" +docker_image="${DOCKER_IMAGE:-alpine:v3.23}" +rootfs_size_mb="${ROOTFS_SIZE_MB:-16384}" +build_payload_with_docker=0 + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --base-rootfs) + base_rootfs="$2" + shift 2 + ;; + --toolchain-rootfs) + toolchain_rootfs="$2" + shift 2 + ;; + --selfbuild-rootfs) + selfbuild_rootfs="$2" + shift 2 + ;; + --source) + source_dir="$2" + shift 2 + ;; + --rust-nightly-dir) + rust_nightly_dir="$2" + shift 2 + ;; + --payload) + rootfs_payload="$2" + shift 2 + ;; + --payload-url) + rootfs_payload_url="$2" + shift 2 + ;; + --build-payload-with-docker) + build_payload_with_docker=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +find_tool() { + local env_name="$1" + local tool_name="$2" + local homebrew_path="$3" + local configured="${!env_name:-}" + + if [[ -n "$configured" ]]; then + printf '%s\n' "$configured" + elif command -v "$tool_name" >/dev/null 2>&1; then + command -v "$tool_name" + elif [[ -x "$homebrew_path" ]]; then + printf '%s\n' "$homebrew_path" + else + echo "$tool_name not found; install e2fsprogs or set $env_name=/path/to/$tool_name" >&2 + exit 1 + fi +} + +copy_image() { + local src="$1" + local dst="$2" + + rm -f "$dst" + if cp -c "$src" "$dst" 2>/dev/null; then + return + fi + if cp --reflink=auto "$src" "$dst" 2>/dev/null; then + return + fi + cp "$src" "$dst" +} + +resolve_image_path() { + local path="$1" + local nested="$path/$(basename "$path")" + + if [[ -f "$path" ]]; then + printf '%s\n' "$path" + return + fi + if [[ -f "$nested" ]]; then + printf '%s\n' "$nested" + return + fi + printf '%s\n' "$path" +} + +debugfs="$(find_tool DEBUGFS debugfs /opt/homebrew/opt/e2fsprogs/sbin/debugfs)" +e2fsck="$(find_tool E2FSCK e2fsck /opt/homebrew/opt/e2fsprogs/sbin/e2fsck)" +resize2fs="$(find_tool RESIZE2FS resize2fs /opt/homebrew/opt/e2fsprogs/sbin/resize2fs)" +e2fsprogs_bin="$(dirname "$debugfs")" +export PATH="$e2fsprogs_bin:$PATH" + +for cmd in awk cargo curl tar find; do + command -v "$cmd" >/dev/null 2>&1 || { + echo "$cmd not found" >&2 + exit 1 + } +done + +if [[ ! -f "$source_dir/Cargo.toml" ]]; then + echo "source dir does not look like TGOSKits: $source_dir" >&2 + exit 1 +fi +source_dir="$(cd "$source_dir" && pwd)" + +if [[ ! -f "$base_rootfs" ]]; then + echo "base rootfs not found: $base_rootfs" + echo "running: cargo xtask starry rootfs --arch aarch64" + (cd "$repo_root" && cargo xtask starry rootfs --arch aarch64) +fi +base_rootfs="$(resolve_image_path "$base_rootfs")" + +if [[ ! -f "$base_rootfs" ]]; then + echo "base rootfs still missing after xtask: $base_rootfs" >&2 + exit 1 +fi + +if [[ -n "$rust_nightly_dir" && ! -d "$rust_nightly_dir" ]]; then + echo "rust nightly sysroot dir not found: $rust_nightly_dir" >&2 + exit 1 +fi + +work_dir="$repo_root/target/starry-macos-selfbuild/rootfs-build" +overlay_dir="$work_dir/overlay" +debugfs_cmd="$work_dir/debugfs-build-rootfs.cmd" +debugfs_log="$work_dir/debugfs-build-rootfs.log" +mkdir -p "$work_dir" "$(dirname "$toolchain_rootfs")" "$(dirname "$selfbuild_rootfs")" +rm -rf "$overlay_dir" +mkdir -p "$overlay_dir" + +echo "base_rootfs=$base_rootfs" +echo "toolchain_rootfs=$toolchain_rootfs" +echo "selfbuild_rootfs=$selfbuild_rootfs" +echo "source_dir=$source_dir" +echo "rust_toolchain=$rust_toolchain" +echo "guest_target=$guest_target" +if [[ "$build_payload_with_docker" = "1" ]]; then + echo "payload_mode=docker" + echo "docker_image=$docker_image" +elif [[ -n "$rootfs_payload" || -n "$rootfs_payload_url" ]]; then + echo "payload_mode=artifact" + echo "rootfs_payload=${rootfs_payload:-}" + echo "rootfs_payload_url=${rootfs_payload_url:-}" +else + echo "payload_mode=native" + echo "alpine_branch=$alpine_branch" + echo "alpine_arch=$alpine_arch" + echo "alpine_mirror=$alpine_mirror" + echo "rust_dist_server=$rust_dist_server" + if [[ -n "$cargo_registry_index" ]]; then + echo "cargo_registry_index=$cargo_registry_index" + fi +fi + +echo "copying and resizing base rootfs..." +copy_image "$base_rootfs" "$toolchain_rootfs" +truncate -s "${rootfs_size_mb}M" "$toolchain_rootfs" +"$e2fsck" -fy "$toolchain_rootfs" >/dev/null 2>&1 || true +"$resize2fs" "$toolchain_rootfs" >/dev/null + +download_payload() { + local url="$1" + local out="$2" + + command -v curl >/dev/null 2>&1 || { + echo "curl not found; install curl or pass --payload /path/to/payload.tar.*" >&2 + exit 1 + } + + curl -fL --retry 5 --retry-delay 3 --retry-all-errors -o "$out" "$url" +} + +extract_payload() { + local archive="$1" + + if [[ ! -f "$archive" ]]; then + echo "payload not found: $archive" >&2 + exit 1 + fi + + case "$archive" in + *.tar.zst|*.tzst) + tar --zstd -xf "$archive" -C "$overlay_dir" + ;; + *.tar.xz|*.txz) + tar -xJf "$archive" -C "$overlay_dir" + ;; + *.tar.gz|*.tgz) + tar -xzf "$archive" -C "$overlay_dir" + ;; + *.tar) + tar -xf "$archive" -C "$overlay_dir" + ;; + *) + echo "unsupported payload extension: $archive" >&2 + echo "expected .tar, .tar.gz, .tgz, .tar.xz, .txz, .tar.zst, or .tzst" >&2 + exit 1 + ;; + esac + + if [[ -d "$overlay_dir/payload" ]]; then + shopt -s dotglob nullglob + mv "$overlay_dir"/payload/* "$overlay_dir"/ + rmdir "$overlay_dir/payload" + shopt -u dotglob nullglob + fi +} + +cache_alpine_branch="${alpine_branch//\//_}" +cache_rust_toolchain="${rust_toolchain//\//_}" +apk_cache_dir="$work_dir/apk-cache-${cache_alpine_branch}-${alpine_arch}" +rust_dist_dir="$work_dir/rust-dist-${cache_rust_toolchain}" +cargo_home_dir="$work_dir/cargo-home" +prefetch_source_dir="$work_dir/prefetch-source" +extra_fetch_dir="$work_dir/extra-fetch" +extra_fetch_aws_dir="$work_dir/extra-fetch-aws" +extra_fetch_workspace_dir="$work_dir/extra-fetch-workspace" + +download_apk_index() { + local repo="$1" + local out="$apk_cache_dir/APKINDEX.$repo" + local url="$alpine_mirror/$alpine_branch/$repo/$alpine_arch/APKINDEX.tar.gz" + + mkdir -p "$apk_cache_dir" + if [[ ! -f "$out" ]]; then + echo "downloading Alpine APKINDEX: $repo" + curl -fL --retry 5 --retry-delay 3 --retry-all-errors -o "$apk_cache_dir/APKINDEX.$repo.tar.gz" "$url" + tar -xzf "$apk_cache_dir/APKINDEX.$repo.tar.gz" -C "$apk_cache_dir" APKINDEX + mv "$apk_cache_dir/APKINDEX" "$out" + fi +} + +apk_field() { + local package="$1" + local field="$2" + awk -v pkg="$package" -v field="$field" ' + BEGIN { RS=""; FS="\n" } + { + name = "" + value = "" + for (i = 1; i <= NF; i++) { + if ($i ~ /^P:/) name = substr($i, 3) + if ($i ~ ("^" field ":")) value = substr($i, 3) + } + if (name == pkg) { + print value + exit + } + } + ' "$apk_cache_dir/APKINDEX.main" "$apk_cache_dir/APKINDEX.community" +} + +apk_provider() { + local dep="$1" + awk -v dep="$dep" ' + BEGIN { RS=""; FS="\n" } + { + name = "" + provides = "" + for (i = 1; i <= NF; i++) { + if ($i ~ /^P:/) name = substr($i, 3) + if ($i ~ /^p:/) provides = substr($i, 3) + } + split(provides, p, " ") + for (i in p) { + item = p[i] + sub(/[<>=~].*/, "", item) + if (item == dep) { + print name + exit + } + } + } + ' "$apk_cache_dir/APKINDEX.main" "$apk_cache_dir/APKINDEX.community" +} + +apk_repo_for() { + local package="$1" + for repo in main community; do + if awk -v pkg="$package" ' + BEGIN { RS=""; FS="\n"; found = 0 } + { for (i = 1; i <= NF; i++) if ($i == "P:" pkg) found = 1 } + END { exit found ? 0 : 1 } + ' \ + "$apk_cache_dir/APKINDEX.$repo"; then + printf '%s\n' "$repo" + return + fi + done +} + +normalize_dep() { + local dep="$1" + dep="${dep#!}" + dep="${dep%%[<>=~]*}" + printf '%s\n' "$dep" +} + +build_native_payload() { + local package queue seen_file pkg dep dep_pkg version repo apk_name apk_url apk_file + + echo "building AArch64 guest payload natively on macOS..." + rm -rf "$overlay_dir" + mkdir -p "$overlay_dir" "$apk_cache_dir" "$rust_dist_dir" + + download_apk_index main + download_apk_index community + + queue=( + alpine-baselayout + alpine-baselayout-data + alpine-keys + apk-tools + busybox + musl + libgcc + libstdc++ + zlib + zstd-libs + openssl + ca-certificates + ca-certificates-bundle + bash + coreutils + curl + findutils + grep + sed + gawk + tar + xz + git + make + cmake + pkgconf + build-base + clang + clang-libclang + lld + llvm + rust + cargo + rust-src + ) + + seen_file="$work_dir/apk-seen.txt" + : >"$seen_file" + + while ((${#queue[@]} > 0)); do + pkg="${queue[0]}" + queue=("${queue[@]:1}") + [[ -z "$pkg" ]] && continue + if grep -qx "$pkg" "$seen_file"; then + continue + fi + + version="$(apk_field "$pkg" V || true)" + if [[ -z "$version" ]]; then + dep_pkg="$(apk_provider "$pkg" || true)" + if [[ -n "$dep_pkg" ]]; then + queue+=("$dep_pkg") + continue + fi + echo "warning: Alpine package/provider not found: $pkg" >&2 + continue + fi + + printf '%s\n' "$pkg" >>"$seen_file" + repo="$(apk_repo_for "$pkg")" + apk_name="$pkg-$version.apk" + apk_url="$alpine_mirror/$alpine_branch/$repo/$alpine_arch/$apk_name" + apk_file="$apk_cache_dir/$apk_name" + + if [[ ! -f "$apk_file" ]]; then + echo "downloading APK: $pkg" + curl -fL --retry 5 --retry-delay 3 --retry-all-errors -o "$apk_file" "$apk_url" + fi + + tar -xzf "$apk_file" -C "$overlay_dir" --exclude='.SIGN.*' --exclude='.PKGINFO' --exclude='.INSTALL' || { + echo "failed to extract APK: $apk_file" >&2 + exit 1 + } + + for dep in $(apk_field "$pkg" D || true); do + dep="$(normalize_dep "$dep")" + [[ -z "$dep" ]] && continue + queue+=("$dep") + done + done + + install_rust_dist_component rustc "$rust_toolchain" aarch64-unknown-linux-musl + install_rust_dist_component cargo "$rust_toolchain" aarch64-unknown-linux-musl + install_rust_dist_component rust-std "$rust_toolchain" aarch64-unknown-linux-musl + install_rust_src_component "$rust_toolchain" + + finish_payload_overlay +} + +rust_dist_date() { + printf '%s\n' "${rust_toolchain#nightly-}" +} + +rust_dist_base() { + printf '%s/dist/%s\n' "${rust_dist_server%/}" "$(rust_dist_date)" +} + +install_rust_dist_component() { + local component="$1" + local toolchain="$2" + local target="$3" + local channel="${toolchain%%-*}" + local archive="$component-$channel-$target.tar.xz" + local archive_dir="${archive%.tar.xz}" + local url + + url="$(rust_dist_base)/$archive" + mkdir -p "$rust_dist_dir" + if [[ ! -f "$rust_dist_dir/$archive" ]]; then + echo "downloading Rust component: $archive" + rm -f "$rust_dist_dir/$archive.part" + curl -fL --retry 5 --retry-delay 3 --retry-all-errors -o "$rust_dist_dir/$archive.part" "$url" + mv "$rust_dist_dir/$archive.part" "$rust_dist_dir/$archive" + fi + + rm -rf "$rust_dist_dir/$archive_dir" + tar -xJf "$rust_dist_dir/$archive" -C "$rust_dist_dir" + (cd "$rust_dist_dir/$archive_dir" && ./install.sh --prefix="$overlay_dir/opt/rust-nightly" --disable-ldconfig) +} + +install_rust_src_component() { + local toolchain="$1" + local channel="${toolchain%%-*}" + local archive="rust-src-$channel.tar.xz" + local archive_dir="${archive%.tar.xz}" + local url + + url="$(rust_dist_base)/$archive" + mkdir -p "$rust_dist_dir" + if [[ ! -f "$rust_dist_dir/$archive" ]]; then + echo "downloading Rust component: $archive" + rm -f "$rust_dist_dir/$archive.part" + curl -fL --retry 5 --retry-delay 3 --retry-all-errors -o "$rust_dist_dir/$archive.part" "$url" + mv "$rust_dist_dir/$archive.part" "$rust_dist_dir/$archive" + fi + + rm -rf "$rust_dist_dir/$archive_dir" + tar -xJf "$rust_dist_dir/$archive" -C "$rust_dist_dir" + (cd "$rust_dist_dir/$archive_dir" && ./install.sh --prefix="$overlay_dir/opt/rust-nightly" --disable-ldconfig) +} + +finish_payload_overlay() { + local libclang_path cargo_env path prefetch_manifest + + mkdir -p "$cargo_home_dir" "$overlay_dir/root/.cargo" "$overlay_dir/opt" "$overlay_dir/usr/bin" + + libclang_path="$(find "$overlay_dir/usr/lib" -name "libclang.so*" | head -1 || true)" + if [[ -n "$libclang_path" && ! -e "$overlay_dir/usr/lib/libclang.so" ]]; then + ln -s "${libclang_path#$overlay_dir/usr/lib/}" "$overlay_dir/usr/lib/libclang.so" + fi + + cat >"$overlay_dir/opt/rustc-nightly-sysroot" <<'WRAP' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/rustc --sysroot /opt/rust-nightly "$@" +WRAP + cat >"$overlay_dir/opt/rustdoc-nightly-sysroot" <<'WRAP' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/rustdoc --sysroot /opt/rust-nightly "$@" +WRAP + cat >"$overlay_dir/opt/cargo-nightly-sysroot" <<'WRAP' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/cargo "$@" +WRAP + chmod +x "$overlay_dir/opt/rustc-nightly-sysroot" "$overlay_dir/opt/rustdoc-nightly-sysroot" "$overlay_dir/opt/cargo-nightly-sysroot" + + cat >"$overlay_dir/usr/bin/aarch64-linux-musl-gcc" <<'WRAP' +#!/bin/sh +if [ "${1:-}" = "-print-sysroot" ]; then + printf '%s\n' /usr + exit 0 +fi + +args="" +while [ "$#" -gt 0 ]; do + case "$1" in + --target=aarch64-unknown-none|--target=aarch64-unknown-none-softfloat) + shift + ;; + --target) + if [ "${2:-}" = "aarch64-unknown-none" ] || [ "${2:-}" = "aarch64-unknown-none-softfloat" ]; then + shift 2 + else + args="$args $1" + shift + fi + ;; + *) + args="$args $1" + shift + ;; + esac +done + +exec /usr/bin/gcc $args +WRAP + chmod +x "$overlay_dir/usr/bin/aarch64-linux-musl-gcc" + + cat >"$cargo_home_dir/config.toml" <<'CARGO_CFG' +[net] +git-fetch-with-cli = true +CARGO_CFG + if [[ -n "$cargo_registry_index" ]]; then + cat >>"$cargo_home_dir/config.toml" <"$extra_fetch_workspace_dir/Cargo.toml" <<'EXTRA_CARGO' +[package] +name = "starry-selfbuild-extra-fetch-workspace" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +EXTRA_CARGO + awk ' + function emit() { + if (name != "" && version != "" && source ~ /^registry/) { + alias = name + gsub(/[^A-Za-z0-9_]/, "_", alias) + sub(/\+.*/, "", version) + printf "extra_%04d_%s = { package = \"%s\", version = \"=%s\" }\n", count, alias, name, version + count++ + } + } + /^\[\[package\]\]/ { emit(); name = ""; version = ""; source = ""; next } + /^name = / { name = $0; sub(/^name = "/, "", name); sub(/"$/, "", name); next } + /^version = / { version = $0; sub(/^version = "/, "", version); sub(/"$/, "", version); next } + /^source = / { source = $0; sub(/^source = "/, "", source); sub(/"$/, "", source); next } + END { emit() } + ' "$source_dir/Cargo.lock" >>"$extra_fetch_workspace_dir/Cargo.toml" + cat >>"$extra_fetch_workspace_dir/Cargo.toml" <<'EXTRA_CARGO' + +[workspace] +EXTRA_CARGO + : >"$extra_fetch_workspace_dir/src/lib.rs" + env -u CARGO_REGISTRY_INDEX "${cargo_env[@]}" \ + cargo fetch --manifest-path "$extra_fetch_workspace_dir/Cargo.toml" + + if [[ -f "$overlay_dir/opt/rust-nightly/lib/rustlib/src/rust/library/sysroot/Cargo.toml" ]]; then + env -u CARGO_REGISTRY_INDEX "${cargo_env[@]}" \ + cargo fetch --locked --manifest-path "$overlay_dir/opt/rust-nightly/lib/rustlib/src/rust/library/sysroot/Cargo.toml" + fi + + rm -rf "$extra_fetch_dir" + mkdir -p "$extra_fetch_dir/src" + cat >"$extra_fetch_dir/Cargo.toml" <<'EXTRA_CARGO' +[package] +name = "starry-selfbuild-extra-fetch" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +extra_atomic_waker = { package = "atomic-waker", version = "=1.1.2" } +EXTRA_CARGO + awk ' + function emit() { + if (name != "" && version != "" && source ~ /^registry/) { + alias = name + gsub(/[^A-Za-z0-9_]/, "_", alias) + sub(/\+.*/, "", version) + printf "extra_%04d_%s = { package = \"%s\", version = \"=%s\" }\n", count, alias, name, version + count++ + } + } + /^\[\[package\]\]/ { emit(); name = ""; version = ""; source = ""; next } + /^name = / { name = $0; sub(/^name = "/, "", name); sub(/"$/, "", name); next } + /^version = / { version = $0; sub(/^version = "/, "", version); sub(/"$/, "", version); next } + /^source = / { source = $0; sub(/^source = "/, "", source); sub(/"$/, "", source); next } + END { emit() } + ' "$overlay_dir/opt/rust-nightly/lib/rustlib/src/rust/library/Cargo.lock" >>"$extra_fetch_dir/Cargo.toml" + cat >>"$extra_fetch_dir/Cargo.toml" <<'EXTRA_CARGO' + +[workspace] +EXTRA_CARGO + : >"$extra_fetch_dir/src/lib.rs" + env -u CARGO_REGISTRY_INDEX "${cargo_env[@]}" \ + cargo fetch --manifest-path "$extra_fetch_dir/Cargo.toml" + + rm -rf "$extra_fetch_aws_dir" + mkdir -p "$extra_fetch_aws_dir/src" + cat >"$extra_fetch_aws_dir/Cargo.toml" <<'EXTRA_CARGO' +[package] +name = "starry-selfbuild-extra-fetch-aws" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +aws-lc-rs = "=1.17.0" + +[workspace] +EXTRA_CARGO + : >"$extra_fetch_aws_dir/src/lib.rs" + env -u CARGO_REGISTRY_INDEX "${cargo_env[@]}" \ + cargo fetch --manifest-path "$extra_fetch_aws_dir/Cargo.toml" + + rm -rf "$overlay_dir/root/.cargo" + mkdir -p "$overlay_dir/root/.cargo" + cp -a "$cargo_home_dir"/. "$overlay_dir/root/.cargo"/ + + cat >"$overlay_dir/root/.cargo/config.toml" <<'CARGO_CFG' +[net] +git-fetch-with-cli = true +offline = true +CARGO_CFG + if [[ -n "$cargo_registry_index" ]]; then + cat >>"$overlay_dir/root/.cargo/config.toml" <"$overlay_dir/opt/toolchain-info.txt" </dev/null 2>&1 || { + echo "docker not found; omit --build-payload-with-docker and pass --payload or ROOTFS_PAYLOAD_URL" >&2 + exit 1 + } + + echo "building AArch64 Alpine Rust/Cargo payload in Docker..." + docker run --rm --platform linux/arm64 \ + -e "RUST_TOOLCHAIN=$rust_toolchain" \ + -v "$overlay_dir:/payload" \ + -v "$source_dir:/source:ro" \ + "$docker_image" /bin/sh -euxc ' + apk_add() { + apk_try=1 + while ! apk add --no-cache "$@"; do + if [ "$apk_try" -ge 5 ]; then + exit 1 + fi + sleep "$((apk_try * 5))" + apk_try="$((apk_try + 1))" + done + } + + apk_add \ + bash ca-certificates coreutils curl findutils grep sed gawk tar xz git make cmake pkgconf \ + build-base clang clang-libclang lld llvm rust cargo rust-src + update-ca-certificates + + mkdir -p /payload/usr /payload/lib /payload/root/.cargo /payload/opt + libclang_path="$(find /usr/lib -name "libclang.so*" | head -1 || true)" + if [ -n "$libclang_path" ] && [ ! -e /usr/lib/libclang.so ]; then + ln -s "$libclang_path" /usr/lib/libclang.so + fi + cp -a /usr/bin /payload/usr/ + cp -a /usr/lib /payload/usr/ + if [ -d /usr/libexec ]; then cp -a /usr/libexec /payload/usr/; fi + if [ -d /usr/src ]; then cp -a /usr/src /payload/usr/; fi + cp -a /lib/. /payload/lib/ + + rustup_home=/tmp/rustup + cargo_home=/tmp/cargo + export RUSTUP_HOME="$rustup_home" + export CARGO_HOME="$cargo_home" + mkdir -p "$rustup_home" "$cargo_home" + curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain "$RUST_TOOLCHAIN" + "$cargo_home/bin/rustup" component add rust-src --toolchain "$RUST_TOOLCHAIN" + mkdir -p /payload/opt/rust-nightly + cp -a "$rustup_home/toolchains/$RUST_TOOLCHAIN-aarch64-unknown-linux-musl"/. /payload/opt/rust-nightly/ + + cat >/payload/opt/rustc-nightly-sysroot <<'"'"'WRAP'"'"' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/rustc --sysroot /opt/rust-nightly "$@" +WRAP + cat >/payload/opt/rustdoc-nightly-sysroot <<'"'"'WRAP'"'"' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/rustdoc --sysroot /opt/rust-nightly "$@" +WRAP + cat >/payload/opt/cargo-nightly-sysroot <<'"'"'WRAP'"'"' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/cargo "$@" +WRAP + chmod +x /payload/opt/rustc-nightly-sysroot /payload/opt/rustdoc-nightly-sysroot /payload/opt/cargo-nightly-sysroot + + cat >/payload/usr/bin/aarch64-linux-musl-gcc <<'"'"'WRAP'"'"' +#!/bin/sh +if [ "${1:-}" = "-print-sysroot" ]; then + printf '%s\n' /usr + exit 0 +fi + +args="" +while [ "$#" -gt 0 ]; do + case "$1" in + --target=aarch64-unknown-none|--target=aarch64-unknown-none-softfloat) + shift + ;; + --target) + if [ "${2:-}" = "aarch64-unknown-none" ] || [ "${2:-}" = "aarch64-unknown-none-softfloat" ]; then + shift 2 + else + args="$args $1" + shift + fi + ;; + *) + args="$args $1" + shift + ;; + esac +done + +exec /usr/bin/gcc $args +WRAP + chmod +x /payload/usr/bin/aarch64-linux-musl-gcc + + cat >/payload/root/.cargo/config.toml <<'"'"'CARGO_CFG'"'"' +[net] +git-fetch-with-cli = true +CARGO_CFG + fetch_manifest() { + manifest="$1" + fetch_try=1 + while ! CARGO_HOME=/payload/root/.cargo \ + CARGO_HTTP_MULTIPLEXING=false \ + CARGO_HTTP_TIMEOUT=120 \ + CARGO_NET_RETRY=5 \ + RUSTC_BOOTSTRAP=1 \ + /payload/opt/rust-nightly/bin/cargo fetch --manifest-path "$manifest"; do + if [ "$fetch_try" -ge 5 ]; then + exit 1 + fi + sleep "$((fetch_try * 10))" + fetch_try="$((fetch_try + 1))" + done + } + + fetch_manifest /source/Cargo.toml + if [ -f /payload/opt/rust-nightly/lib/rustlib/src/rust/library/sysroot/Cargo.toml ]; then + fetch_manifest /payload/opt/rust-nightly/lib/rustlib/src/rust/library/sysroot/Cargo.toml + fi + cat >/payload/root/.cargo/config.toml <<'"'"'CARGO_CFG'"'"' +[net] +git-fetch-with-cli = true +offline = true +CARGO_CFG + + /payload/opt/rust-nightly/bin/rustc --version > /payload/opt/toolchain-info.txt + /payload/opt/rust-nightly/bin/cargo --version >> /payload/opt/toolchain-info.txt + /usr/bin/cargo --version >> /payload/opt/toolchain-info.txt + ' +elif [[ -n "$rootfs_payload" || -n "$rootfs_payload_url" ]]; then + if [[ -z "$rootfs_payload" ]]; then + if [[ -z "$rootfs_payload_url" ]]; then + cat >&2 <<'ERR' +No rootfs payload was provided. + +Use one of: + + ROOTFS_PAYLOAD_URL=https://.../starry-macos-selfbuild-payload.tar.zst \ + apps/starry/macos-selfbuild/build_rootfs.sh + + apps/starry/macos-selfbuild/build_rootfs.sh \ + --payload /path/to/starry-macos-selfbuild-payload.tar.zst + +Maintainers can explicitly rebuild the payload with: + + apps/starry/macos-selfbuild/build_rootfs.sh --build-payload-with-docker +ERR + exit 2 + fi + rootfs_payload="$work_dir/$(basename "${rootfs_payload_url%%\?*}")" + echo "downloading rootfs payload: $rootfs_payload_url" + download_payload "$rootfs_payload_url" "$rootfs_payload" + fi + + echo "extracting AArch64 guest toolchain payload..." + extract_payload "$rootfs_payload" +else + build_native_payload +fi + +if [[ -n "$rust_nightly_dir" ]]; then + echo "overlaying explicit Rust nightly sysroot: $rust_nightly_dir" + rm -rf "$overlay_dir/opt/rust-nightly" + mkdir -p "$overlay_dir/opt/rust-nightly" + (cd "$rust_nightly_dir" && tar cf - .) | (cd "$overlay_dir/opt/rust-nightly" && tar xf -) + cat >"$overlay_dir/opt/rustc-nightly-sysroot" <<'WRAP' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/rustc --sysroot /opt/rust-nightly "$@" +WRAP + cat >"$overlay_dir/opt/rustdoc-nightly-sysroot" <<'WRAP' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/rustdoc --sysroot /opt/rust-nightly "$@" +WRAP + cat >"$overlay_dir/opt/cargo-nightly-sysroot" <<'WRAP' +#!/bin/sh +exec /lib/ld-musl-aarch64.so.1 --library-path /opt/rust-nightly/lib:/usr/lib /opt/rust-nightly/bin/cargo "$@" +WRAP + chmod +x "$overlay_dir/opt/rustc-nightly-sysroot" "$overlay_dir/opt/rustdoc-nightly-sysroot" "$overlay_dir/opt/cargo-nightly-sysroot" +fi + +echo "injecting toolchain payload with debugfs..." +: >"$debugfs_cmd" +while IFS= read -r path; do + rel="${path#$overlay_dir/}" + [[ "$rel" = "$path" ]] && continue + printf 'mkdir /%s\n' "$rel" >>"$debugfs_cmd" +done < <(find "$overlay_dir" -type d | sort) + +while IFS= read -r path; do + rel="${path#$overlay_dir/}" + guest_path="/$rel" + printf 'rm %s\n' "$guest_path" >>"$debugfs_cmd" + printf 'write %s %s\n' "$path" "$guest_path" >>"$debugfs_cmd" +done < <(find "$overlay_dir" -type f | sort) + +while IFS= read -r path; do + rel="${path#$overlay_dir/}" + guest_path="/$rel" + target="$(readlink "$path")" + printf 'rm %s\n' "$guest_path" >>"$debugfs_cmd" + printf 'symlink %s %s\n' "$guest_path" "$target" >>"$debugfs_cmd" +done < <(find "$overlay_dir" -type l | sort) + +if ! "$debugfs" -w -f "$debugfs_cmd" "$toolchain_rootfs" >"$debugfs_log" 2>&1; then + cat "$debugfs_log" >&2 + exit 1 +fi + +echo "injecting TGOSKits source into self-build rootfs..." +"$script_dir/prepare_rootfs.sh" \ + --base-rootfs "$toolchain_rootfs" \ + --output-rootfs "$selfbuild_rootfs" \ + --source "$source_dir" + +echo "rootfs build complete" +echo "toolchain_rootfs=$toolchain_rootfs" +echo "selfbuild_rootfs=$selfbuild_rootfs" diff --git a/apps/starry/macos-selfbuild/check_rootfs.sh b/apps/starry/macos-selfbuild/check_rootfs.sh new file mode 100755 index 0000000000..11a660681b --- /dev/null +++ b/apps/starry/macos-selfbuild/check_rootfs.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + apps/starry/macos-selfbuild/check_rootfs.sh + +Checks whether a prepared StarryOS self-build rootfs contains the minimum guest +paths needed by run_selfbuild.sh. +USAGE +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +rootfs="${1:-}" +if [[ -z "$rootfs" ]]; then + usage >&2 + exit 2 +fi + +if [[ ! -f "$rootfs" ]]; then + echo "rootfs not found: $rootfs" >&2 + exit 1 +fi + +debugfs="${DEBUGFS:-}" +if [[ -z "$debugfs" ]]; then + if command -v debugfs >/dev/null 2>&1; then + debugfs="$(command -v debugfs)" + elif [[ -x /opt/homebrew/opt/e2fsprogs/sbin/debugfs ]]; then + debugfs="/opt/homebrew/opt/e2fsprogs/sbin/debugfs" + else + echo "debugfs not found; install e2fsprogs or set DEBUGFS=/path/to/debugfs" >&2 + exit 1 + fi +fi + +missing=0 +path_exists() { + local guest_path="$1" + local out + + out="$("$debugfs" -R "stat $guest_path" "$rootfs" 2>&1 || true)" + if printf '%s\n' "$out" | grep -E -q 'File not found|Ext2 inode is not a|ext2_lookup|No such file'; then + return 1 + fi + printf '%s\n' "$out" | grep -q 'Inode:' +} + +dir_contains() { + local guest_dir="$1" + local pattern="$2" + local out + + out="$("$debugfs" -R "ls $guest_dir" "$rootfs" 2>&1 || true)" + printf '%s\n' "$out" | grep -q "$pattern" +} + +for guest_path in \ + "/usr/bin/cargo" \ + "/opt/rust-nightly/bin/rustc" \ + "/opt/rust-nightly/lib/rustlib/src/rust/library/Cargo.lock" \ + "/usr/bin/aarch64-linux-musl-gcc" \ + "/opt/cargo-nightly-sysroot" \ + "/opt/rustc-nightly-sysroot" \ + "/opt/rustdoc-nightly-sysroot"; do + if path_exists "$guest_path"; then + echo "OK $guest_path" + else + echo "MISSING $guest_path" + missing=1 + fi +done + +if dir_contains "/opt/rust-nightly/lib" "librustc_driver-.*\\.so"; then + echo "OK /opt/rust-nightly/lib/librustc_driver-*.so" +else + echo "MISSING /opt/rust-nightly/lib/librustc_driver-*.so" + missing=1 +fi + +if path_exists "/usr/lib/libclang.so" || path_exists "/usr/lib/llvm22/lib/libclang.so"; then + echo "OK libclang.so" +else + echo "MISSING libclang.so" + missing=1 +fi + +source_ok=0 +for guest_path in "/opt/tgoskits/Cargo.toml" "/opt/tgoskits-src.tar"; do + if path_exists "$guest_path"; then + echo "OK $guest_path" + source_ok=1 + else + echo "MISSING $guest_path" + fi +done + +if [[ "$source_ok" = "0" ]]; then + missing=1 +fi + +deps_ok=0 +if path_exists "/opt/tgoskits/vendor"; then + echo "OK /opt/tgoskits/vendor" + deps_ok=1 +elif path_exists "/root/.cargo/registry/index" \ + && path_exists "/root/.cargo/registry/cache"; then + echo "OK /root/.cargo/registry/index" + echo "OK /root/.cargo/registry/cache" + deps_ok=1 +else + echo "MISSING offline Cargo dependencies (/opt/tgoskits/vendor or /root/.cargo/registry)" +fi + +if [[ "$deps_ok" = "0" ]]; then + missing=1 +fi + +if [[ "$missing" = "0" ]]; then + echo "STARRY_MACOS_SELFBUILD_ROOTFS_OK" +else + echo "STARRY_MACOS_SELFBUILD_ROOTFS_INCOMPLETE" +fi + +exit "$missing" diff --git a/apps/starry/macos-selfbuild/extract_kernel.sh b/apps/starry/macos-selfbuild/extract_kernel.sh new file mode 100755 index 0000000000..42efc71b28 --- /dev/null +++ b/apps/starry/macos-selfbuild/extract_kernel.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../../.." && pwd)" + +usage() { + cat <<'USAGE' +Usage: + eval "$(apps/starry/macos-selfbuild/extract_kernel.sh)" + + eval "$(apps/starry/macos-selfbuild/extract_kernel.sh \ + --rootfs-copy target/starry-macos-selfbuild/rootfs/rootfs-smp8-j8.img \ + --output-stem starryos-selfbuilt-smp8-j8)" + +Extracts the guest-built StarryOS kernel artifacts from a self-build rootfs copy. +By default it selects the newest image under: + + target/starry-macos-selfbuild/rootfs/rootfs-*.img + +It prints shell assignments to stdout: + + rootfs_copy=... + kernel_elf=... + kernel_bin=... + +All status and error messages are written to stderr so stdout can be used with +eval. +USAGE +} + +find_debugfs() { + if [[ -n "${DEBUGFS:-}" ]]; then + printf '%s\n' "$DEBUGFS" + elif command -v debugfs >/dev/null 2>&1; then + command -v debugfs + elif [[ -x /opt/homebrew/opt/e2fsprogs/sbin/debugfs ]]; then + printf '%s\n' /opt/homebrew/opt/e2fsprogs/sbin/debugfs + else + echo "debugfs not found; install e2fsprogs or set DEBUGFS=/path/to/debugfs" >&2 + exit 1 + fi +} + +latest_rootfs_copy() { + local rootfs_dir="$repo_root/target/starry-macos-selfbuild/rootfs" + local -a candidates=() + + if [[ -d "$rootfs_dir" ]]; then + shopt -s nullglob + candidates=("$rootfs_dir"/rootfs-*.img) + shopt -u nullglob + fi + if [[ "${#candidates[@]}" -eq 0 ]]; then + echo "no self-build rootfs copy found under $rootfs_dir" >&2 + echo "run apps/starry/macos-selfbuild/run_selfbuild.sh first, or pass --rootfs-copy" >&2 + exit 1 + fi + ls -t "${candidates[@]}" | head -1 +} + +shell_quote() { + local value="$1" + local i char + printf "'" + for ((i = 0; i < ${#value}; i++)); do + char="${value:i:1}" + if [[ "$char" == "'" ]]; then + printf '%s' "'\\''" + else + printf '%s' "$char" + fi + done + printf "'" +} + +emit_assignment() { + local name="$1" + local value="$2" + printf '%s=' "$name" + shell_quote "$value" + printf '\n' +} + +abs_existing_file() { + local path="$1" + local dir base + + if [[ ! -f "$path" ]]; then + echo "file not found: $path" >&2 + exit 1 + fi + dir="$(cd "$(dirname "$path")" && pwd)" + base="$(basename "$path")" + printf '%s/%s\n' "$dir" "$base" +} + +abs_dir() { + local path="$1" + mkdir -p "$path" + cd "$path" && pwd +} + +dump_guest_file() { + local debugfs="$1" + local rootfs="$2" + local guest_path="$3" + local host_path="$4" + local label="$5" + local err_file bytes + + err_file="$(mktemp "${TMPDIR:-/tmp}/starry-extract.XXXXXX")" + rm -f "$host_path" + if ! "$debugfs" -R "dump $guest_path $host_path" "$rootfs" >/dev/null 2>"$err_file"; then + cat "$err_file" >&2 + rm -f "$err_file" + echo "failed to extract $guest_path from $rootfs" >&2 + exit 1 + fi + rm -f "$err_file" + + if [[ ! -s "$host_path" ]]; then + echo "extracted $label is empty: $host_path" >&2 + exit 1 + fi + + bytes="$(wc -c <"$host_path" | tr -d '[:space:]')" + echo "extracted $label: $host_path ($bytes bytes)" >&2 +} + +rootfs_copy="${ROOTFS_COPY:-}" +output_dir="${OUTPUT_DIR:-$repo_root/target/starry-macos-selfbuild/extracted}" +output_stem="${OUTPUT_STEM:-}" +target="${BUILD_TARGET:-aarch64-unknown-none-softfloat}" +guest_elf_path="${GUEST_ELF_PATH:-/opt/starryos-selfbuild-artifacts/starryos-${target}}" +guest_bin_path="${GUEST_BIN_PATH:-${guest_elf_path}.bin}" +extract_bin="${EXTRACT_BIN:-1}" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --rootfs-copy|--rootfs) + rootfs_copy="$2" + shift 2 + ;; + --output-dir) + output_dir="$2" + shift 2 + ;; + --output-stem|--name) + output_stem="$2" + shift 2 + ;; + --target) + target="$2" + guest_elf_path="/opt/starryos-selfbuild-artifacts/starryos-${target}" + guest_bin_path="${guest_elf_path}.bin" + shift 2 + ;; + --guest-elf-path) + guest_elf_path="$2" + shift 2 + ;; + --guest-bin-path) + guest_bin_path="$2" + shift 2 + ;; + --no-bin) + extract_bin=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "$rootfs_copy" ]]; then + rootfs_copy="$(latest_rootfs_copy)" +fi +rootfs_copy="$(abs_existing_file "$rootfs_copy")" +output_dir="$(abs_dir "$output_dir")" + +if [[ -z "$output_stem" ]]; then + rootfs_base="$(basename "$rootfs_copy" .img)" + rootfs_base="${rootfs_base#rootfs-}" + output_stem="starryos-selfbuilt-${rootfs_base}" +fi + +debugfs="$(find_debugfs)" +kernel_elf="$output_dir/$output_stem" +kernel_bin="$kernel_elf.bin" + +dump_guest_file "$debugfs" "$rootfs_copy" "$guest_elf_path" "$kernel_elf" "kernel_elf" +if [[ "$extract_bin" = "1" ]]; then + dump_guest_file "$debugfs" "$rootfs_copy" "$guest_bin_path" "$kernel_bin" "kernel_bin" +else + kernel_bin="" +fi + +emit_assignment rootfs_copy "$rootfs_copy" +emit_assignment kernel_elf "$kernel_elf" +if [[ -n "$kernel_bin" ]]; then + emit_assignment kernel_bin "$kernel_bin" +fi diff --git a/apps/starry/macos-selfbuild/fetch_rootfs.sh b/apps/starry/macos-selfbuild/fetch_rootfs.sh new file mode 100755 index 0000000000..4a2721d8c8 --- /dev/null +++ b/apps/starry/macos-selfbuild/fetch_rootfs.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../../.." && pwd)" + +usage() { + cat <<'USAGE' +Usage: + ROOTFS_URL=https://.../rootfs-aarch64-hvf-selfbuild.img \ + apps/starry/macos-selfbuild/fetch_rootfs.sh + + apps/starry/macos-selfbuild/fetch_rootfs.sh \ + --input /path/to/rootfs-aarch64-hvf-selfbuild.img + + apps/starry/macos-selfbuild/fetch_rootfs.sh \ + --url https://.../rootfs-aarch64-hvf-selfbuild.img.tar.xz + +Places a prebuilt macOS/HVF self-build rootfs at: + + tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img + +This is the normal reproduction path. It does not build the rootfs and does not +use Docker. +USAGE +} + +url="${ROOTFS_URL:-}" +input="${ROOTFS_INPUT:-}" +output="$repo_root/tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --url) + url="$2" + shift 2 + ;; + --input) + input="$2" + shift 2 + ;; + --output) + output="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -n "$url" && -n "$input" ]]; then + echo "use either --url/ROOTFS_URL or --input/ROOTFS_INPUT, not both" >&2 + exit 2 +fi + +if [[ -z "$url" && -z "$input" ]]; then + usage >&2 + exit 2 +fi + +copy_image() { + local src="$1" + local dst="$2" + + rm -f "$dst" + if cp -c "$src" "$dst" 2>/dev/null; then + return + fi + if cp --reflink=auto "$src" "$dst" 2>/dev/null; then + return + fi + cp "$src" "$dst" +} + +mkdir -p "$(dirname "$output")" "$repo_root/target/starry-macos-selfbuild" + +if [[ -n "$input" ]]; then + if [[ ! -f "$input" ]]; then + echo "rootfs input not found: $input" >&2 + exit 1 + fi + copy_image "$input" "$output" +else + download="$repo_root/target/starry-macos-selfbuild/$(basename "${url%%\?*}")" + curl -fL --retry 5 --retry-delay 3 --retry-all-errors -o "$download" "$url" + case "$download" in + *.tar.xz|*.txz) + tar -xJf "$download" -C "$(dirname "$output")" + ;; + *.tar.gz|*.tgz) + tar -xzf "$download" -C "$(dirname "$output")" + ;; + *.img) + copy_image "$download" "$output" + ;; + *) + echo "unsupported rootfs artifact extension: $download" >&2 + echo "expected .img, .tar.xz, .txz, .tar.gz, or .tgz" >&2 + exit 1 + ;; + esac +fi + +if [[ ! -f "$output" ]]; then + found="$(find "$(dirname "$output")" -maxdepth 1 -type f -name '*selfbuild*.img' | head -1 || true)" + if [[ -n "$found" ]]; then + mv "$found" "$output" + fi +fi + +echo "rootfs=$output" +"$script_dir/check_rootfs.sh" "$output" diff --git a/apps/starry/macos-selfbuild/guest-selfbuild.sh b/apps/starry/macos-selfbuild/guest-selfbuild.sh new file mode 100755 index 0000000000..8b2834209a --- /dev/null +++ b/apps/starry/macos-selfbuild/guest-selfbuild.sh @@ -0,0 +1,641 @@ +#!/bin/sh +set -eu + +marker="${MARKER:-STARRY-MACOS-SELFBUILD}" +jobs="${JOBS:-8}" +rayon_threads="${RAYON_NUM_THREADS:-1}" +rustc_threads="${RUSTC_THREADS:-2}" +cargo_bin="${CARGO_BIN:-/opt/cargo-nightly-sysroot}" +source_dir="${SOURCE_DIR:-/opt/tgoskits}" +work_dir="${WORK_DIR:-/tmp/starryos-selfbuild-src}" +target_dir="${CARGO_TARGET_DIR:-/tmp/starryos-selfbuild-target}" +artifact_dir="${ARTIFACT_DIR:-/opt/starryos-selfbuild-artifacts}" +source_tmpfs="${SOURCE_TMPFS:-1}" +source_tar="${SOURCE_TAR:-/opt/tgoskits-src.tar}" +source_meta="${SOURCE_META:-/opt/tgoskits-src.meta}" +profile="${PROFILE:-release}" +cargo_subcommand="${CARGO_SUBCOMMAND:-build}" +build_target="${BUILD_TARGET:-aarch64-unknown-none-softfloat}" +build_package="${BUILD_PACKAGE:-starryos}" +build_bin="${BUILD_BIN:-starryos}" +build_std="${BUILD_STD:-core,alloc}" +features="${FEATURES:-plat-dyn,ax-driver/virtio-blk,ax-driver/virtio-net,ax-driver/virtio-gpu,ax-driver/virtio-input,ax-driver/virtio-socket,starry-kernel/input,starry-kernel/vsock}" +no_default_features="${NO_DEFAULT_FEATURES:-0}" +allow_slow_selfbuild="${ALLOW_SLOW_SELFBUILD:-0}" +guest_monitor_interval="${GUEST_MONITOR_INTERVAL_SEC:-60}" +target_heartbeat_sec="${TARGET_HEARTBEAT_SEC:-0}" +trace_rustc="${TRACE_RUSTC:-0}" +cargo_verbose="${CARGO_VERBOSE:-0}" +target_spec_mode="${TARGET_SPEC_MODE:-pie}" +target_spec_path="${TARGET_SPEC_PATH:-}" +artifact_to_bin="${ARTIFACT_TO_BIN:-1}" +kallsyms_reserved="${STARRY_KALLSYMS_RESERVED:-64M}" + +assert_fast_profile() { + if [ "$allow_slow_selfbuild" = "1" ]; then + echo "===${marker}-SLOW-PROFILE-ALLOWED===" + return + fi + + if [ "$rustc_threads" != "2" ]; then + echo "===${marker}-FAST-PROFILE-ERROR rustc_threads=${rustc_threads} expected=2===" + echo "Set RUSTC_THREADS=2 for the reproducible qemu-aarch64 profile, or ALLOW_SLOW_SELFBUILD=1 for experiments." + finish_guest 2 + fi + + echo "===${marker}-QEMU-AARCH64-PROFILE expected_crates~420===" +} + +resolve_target_spec() { + case "$target_spec_mode" in + none | "") + printf '%s\n' "$build_target" + ;; + pie) + printf 'scripts/targets/pie/%s.json\n' "$build_target" + ;; + no-pie) + printf 'scripts/targets/no-pie/%s.json\n' "$build_target" + ;; + path) + if [ -z "$target_spec_path" ]; then + echo "===${marker}-TARGET-SPEC-ERROR mode=path path=empty===" + finish_guest 2 + fi + printf '%s\n' "$target_spec_path" + ;; + *) + echo "===${marker}-TARGET-SPEC-ERROR mode=${target_spec_mode} expected=pie|no-pie|path|none===" + finish_guest 2 + ;; + esac +} + +finish_guest() { + rc="$1" + if [ -n "${target_heartbeat_pid:-}" ]; then + kill "$target_heartbeat_pid" 2>/dev/null || true + fi + if [ -n "${monitor_pid:-}" ]; then + kill "$monitor_pid" 2>/dev/null || true + fi + echo "===${marker}-RUN-END rc=${rc}===" + sync 2>/dev/null || true + if command -v poweroff >/dev/null 2>&1; then + poweroff -f 2>/dev/null || poweroff 2>/dev/null || true + elif command -v halt >/dev/null 2>&1; then + halt -f 2>/dev/null || halt 2>/dev/null || true + fi + exit "$rc" +} + +find_first_executable() { + for candidate do + if [ -x "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + return 1 +} + +ensure_tool_wrapper() { + wrapper_dir="$1" + rust_tool="$2" + llvm_tool="$3" + + if command -v "$rust_tool" >/dev/null 2>&1 && "$rust_tool" --version >/dev/null 2>&1; then + return + fi + + llvm_path="$(find_first_executable \ + "/usr/bin/${llvm_tool}" \ + "/usr/lib/llvm22/bin/${llvm_tool}" \ + "/usr/lib/llvm21/bin/${llvm_tool}" \ + "/usr/lib/llvm20/bin/${llvm_tool}" \ + "/usr/lib/llvm/bin/${llvm_tool}" || true)" + if [ -z "$llvm_path" ]; then + echo "===${marker}-KALLSYMS-TOOL-MISSING tool=${rust_tool} fallback=${llvm_tool}===" + finish_guest 2 + fi + + cat >"${wrapper_dir}/${rust_tool}" </dev/null 2>&1; then + return + fi + + ksym_manifest="" + for candidate in /root/.cargo/registry/src/*/ksym-0.6.0/Cargo.toml; do + if [ -f "$candidate" ]; then + ksym_manifest="$candidate" + break + fi + done + if [ -z "$ksym_manifest" ]; then + echo "===${marker}-KALLSYMS-TOOL-MISSING tool=gen_ksym crate=ksym-0.6.0===" + finish_guest 2 + fi + + echo "===${marker}-KALLSYMS-GEN-KSYM-BUILD manifest=${ksym_manifest}===" + "$cargo_bin" install \ + --offline \ + --locked \ + --path "$(dirname "$ksym_manifest")" \ + --root "$install_root" \ + --bin gen_ksym +} + +ensure_kallsyms_tools() { + tools_root="/tmp/starryos-selfbuild-tools" + wrapper_dir="${tools_root}/wrappers" + install_root="${tools_root}/cargo-install" + mkdir -p "$wrapper_dir" "$install_root" + + export PATH="${install_root}/bin:${wrapper_dir}:${PATH}" + ensure_tool_wrapper "$wrapper_dir" rust-nm llvm-nm + ensure_tool_wrapper "$wrapper_dir" rust-objdump llvm-objdump + ensure_tool_wrapper "$wrapper_dir" rust-objcopy llvm-objcopy + ensure_gen_ksym "$install_root" + + echo "===${marker}-KALLSYMS-TOOLS-READY===" +} + +run_starry_kallsyms() { + artifact="$1" + + if [ "$build_package" != "starryos" ] || [ "$build_bin" != "starryos" ]; then + return + fi + + ensure_kallsyms_tools + echo "===${marker}-KALLSYMS-BEGIN elf=${artifact}===" + + symbols="$(mktemp "${artifact}.symbols.XXXXXX")" + kallsyms="$(mktemp "${artifact}.kallsyms.XXXXXX")" + rm -f "$symbols" "$kallsyms" + set +e + ( + set -eu + rust-nm -n "$artifact" >"$symbols" + grep ' [TtDBR] ' "$symbols" \ + | awk '$3 !~ /^\.L/' \ + | awk '$3 != "$x"' \ + | gen_ksym >"$kallsyms" + + section_hex="$(rust-objdump -h "$artifact" | awk '$2 == ".kallsyms" { print $3; found = 1 } END { if (!found) exit 1 }')" + section_size="$(printf "%d\n" "0x${section_hex}")" + kallsyms_size="$(wc -c <"$kallsyms" | tr -d ' ')" + padding_size="$((section_size - kallsyms_size))" + + if [ "$kallsyms_size" -gt "$section_size" ]; then + echo "generated kallsyms (${kallsyms_size} bytes) exceed .kallsyms section (${section_size} bytes)" >&2 + exit 1 + fi + + if [ "$padding_size" -gt 0 ]; then + if command -v truncate >/dev/null 2>&1; then + truncate -s "$section_size" "$kallsyms" + else + full_blocks="$((padding_size / 1048576))" + tail_bytes="$((padding_size % 1048576))" + if [ "$full_blocks" -gt 0 ]; then + dd if=/dev/zero bs=1048576 count="$full_blocks" >>"$kallsyms" 2>/dev/null + fi + if [ "$tail_bytes" -gt 0 ]; then + dd if=/dev/zero bs="$tail_bytes" count=1 >>"$kallsyms" 2>/dev/null + fi + fi + fi + + rust-objcopy --update-section ".kallsyms=${kallsyms}" "$artifact" + ) + kallsyms_rc="$?" + rm -f "$symbols" "$kallsyms" + set -e + if [ "$kallsyms_rc" != "0" ]; then + echo "===${marker}-KALLSYMS-FAIL rc=${kallsyms_rc}===" + finish_guest "$kallsyms_rc" + fi + echo "===${marker}-KALLSYMS-END elf=${artifact}===" +} + +echo "===${marker}-BEGIN jobs=${jobs} source_tmpfs=${source_tmpfs}===" +echo "guest_cpu_count=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null || true)" + +if [ ! -f "${source_dir}/Cargo.toml" ] && [ -f "$source_tar" ]; then + echo "===${marker}-SOURCE-TAR-EXTRACT-BEGIN tar=${source_tar}===" + tar_work="/tmp/tgoskits-src-from-tar" + rm -rf "$tar_work" + mkdir -p "$tar_work" + tar -xf "$source_tar" -C "$tar_work" + source_dir="$tar_work" + echo "===${marker}-SOURCE-TAR-EXTRACT-END dir=${source_dir}===" +fi + +source_meta_path="" +for candidate in "$source_meta" "${source_dir}/.tgoskits-source-meta"; do + if [ -f "$candidate" ]; then + source_meta_path="$candidate" + break + fi +done + +if [ -n "$source_meta_path" ]; then + echo "===${marker}-SOURCE-META-BEGIN path=${source_meta_path}===" + cat "$source_meta_path" + echo "===${marker}-SOURCE-META-END===" + actual_commit="" + while IFS= read -r meta_line; do + case "$meta_line" in + commit=*) actual_commit="${meta_line#commit=}" ;; + esac + done < "$source_meta_path" + if [ -n "${TGOSKITS_COMMIT:-}" ] && [ "$actual_commit" != "$TGOSKITS_COMMIT" ]; then + echo "===${marker}-SOURCE-META-MISMATCH expected=${TGOSKITS_COMMIT} actual=${actual_commit}===" + finish_guest 2 + fi +else + echo "===${marker}-SOURCE-META-MISSING===" +fi + +export PATH="/opt/rust-nightly/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +ld_library_path="/opt/rust-nightly/lib:/usr/lib" +for candidate in /usr/lib/llvm*/lib; do + if [ -d "$candidate" ]; then + ld_library_path="${ld_library_path}:${candidate}" + fi +done +export LD_LIBRARY_PATH="${ld_library_path}:${LD_LIBRARY_PATH:-}" +export RUSTC="${RUSTC:-/opt/rustc-nightly-sysroot}" +export RUSTDOC="${RUSTDOC:-/opt/rustdoc-nightly-sysroot}" +export HOME="${HOME:-/root}" +export CARGO_HOME="${CARGO_HOME:-/root/.cargo}" +export RUSTC_BOOTSTRAP="${RUSTC_BOOTSTRAP:-1}" +export CARGO_INCREMENTAL="${CARGO_INCREMENTAL:-0}" +export CARGO_NET_OFFLINE="${CARGO_NET_OFFLINE:-true}" +export CARGO_BUILD_JOBS="$jobs" +export RAYON_NUM_THREADS="$rayon_threads" +export CARGO_TARGET_DIR="$target_dir" +export AX_ARCH="${AX_ARCH:-aarch64}" +export AX_TARGET="${AX_TARGET:-$build_target}" +export AX_LOG="${AX_LOG:-warn}" +export STARRY_KALLSYMS_RESERVED="$kallsyms_reserved" + +if [ "$trace_rustc" = "1" ]; then + real_rustc="$RUSTC" + trace_dir="/tmp/starry-rustc-trace" + rm -rf "$trace_dir" + mkdir -p "$trace_dir" + cat >"$trace_dir/rustc" <&2 +done +printf '===\n' >&2 +"\$real_rustc" "\$@" +rc="\$?" +end="\$(date +%s)" +elapsed="\$((end - begin))" +echo "===\${marker}-RUSTC-END pid=\$\$ rc=\${rc} elapsed=\${elapsed} crate=\${crate_name:-unknown} target=\${target_arg:-unknown}===" >&2 +exit "\$rc" +EOF + chmod +x "$trace_dir/rustc" + export RUSTC="$trace_dir/rustc" +fi + +if [ -z "${LIBCLANG_PATH:-}" ]; then + for candidate in /usr/lib /usr/lib/llvm*/lib; do + if [ -e "${candidate}/libclang.so" ]; then + export LIBCLANG_PATH="$candidate" + break + fi + done +fi + +export CARGO_PROFILE_RELEASE_LTO="${CARGO_PROFILE_RELEASE_LTO:-false}" +export CARGO_PROFILE_RELEASE_OPT_LEVEL="${CARGO_PROFILE_RELEASE_OPT_LEVEL:-0}" +export CARGO_PROFILE_RELEASE_CODEGEN_UNITS="${CARGO_PROFILE_RELEASE_CODEGEN_UNITS:-256}" +export CARGO_PROFILE_RELEASE_DEBUG="${CARGO_PROFILE_RELEASE_DEBUG:-0}" + +sanitize_cargo_config() { + config_dir="$1" + if [ -f "${config_dir}/.cargo/config.toml" ]; then + sed -i "s#${source_dir}/vendor#${config_dir}/vendor#g" "${config_dir}/.cargo/config.toml" || true + sed -i '/^include[[:space:]]*=/d' "${config_dir}/.cargo/config.toml" || true + fi +} + +if [ "$source_tmpfs" = "1" ]; then + echo "===${marker}-SOURCE-COPY-BEGIN from=${source_dir} to=${work_dir}===" + rm -rf "$work_dir" + mkdir -p "$work_dir" + for path in Cargo.toml Cargo.lock rust-toolchain.toml .tgoskits-source-meta .cargo apps components drivers memory os platforms scripts test-suit tools vendor virtualization xtask; do + if [ -e "${source_dir}/${path}" ]; then + cp -a "${source_dir}/${path}" "${work_dir}/" + fi + done + sanitize_cargo_config "$work_dir" + echo "===${marker}-SOURCE-COPY-END===" + cd "$work_dir" +else + sanitize_cargo_config "$source_dir" + cd "$source_dir" +fi + +if [ -n "${AX_CONFIG_PATH:-}" ]; then + export AX_CONFIG_PATH +elif [ -f "$(pwd)/os/StarryOS/.axconfig.toml" ]; then + export AX_CONFIG_PATH="$(pwd)/os/StarryOS/.axconfig.toml" +else + unset AX_CONFIG_PATH +fi + +rustflags="${EXTRA_RUSTFLAGS:-}" +if [ -n "$rustc_threads" ] && [ "$rustc_threads" != "auto" ]; then + rustflags="${rustflags} -Zthreads=${rustc_threads}" +fi +export RUSTFLAGS="$rustflags" +build_target_arg="$(resolve_target_spec)" +case "$target_spec_mode" in + pie | no-pie | path) + export CARGO_UNSTABLE_JSON_TARGET_SPEC="${CARGO_UNSTABLE_JSON_TARGET_SPEC:-true}" + if [ ! -f "$build_target_arg" ]; then + echo "===${marker}-TARGET-SPEC-MISSING path=${build_target_arg}===" + finish_guest 2 + fi + ;; +esac + +echo "===${marker}-ENV-BEGIN===" +echo "jobs=${jobs}" +echo "rayon_num_threads=${RAYON_NUM_THREADS}" +echo "rustc_threads=${rustc_threads}" +echo "profile=${profile}" +echo "cargo_subcommand=${cargo_subcommand}" +echo "build_package=${build_package}" +echo "build_bin=${build_bin}" +echo "build_target=${build_target}" +echo "build_target_arg=${build_target_arg}" +echo "target_spec_mode=${target_spec_mode}" +echo "build_std=${build_std}" +echo "features=${features}" +echo "artifact_to_bin=${artifact_to_bin}" +echo "starry_kallsyms_reserved=${STARRY_KALLSYMS_RESERVED}" +echo "allow_slow_selfbuild=${allow_slow_selfbuild}" +echo "guest_monitor_interval_sec=${guest_monitor_interval}" +echo "trace_rustc=${trace_rustc}" +echo "cargo_verbose=${cargo_verbose}" +echo "source_dir=${source_dir}" +echo "target_dir=${target_dir}" +echo "artifact_dir=${artifact_dir}" +echo "target_heartbeat_sec=${target_heartbeat_sec}" +echo "work_dir=$(pwd)" +echo "cargo_home=${CARGO_HOME}" +echo "ax_config_path=${AX_CONFIG_PATH:-}" +echo "libclang_path=${LIBCLANG_PATH:-}" +echo "rustflags=${RUSTFLAGS}" +"$RUSTC" --version || true +"$cargo_bin" --version || true +echo "===${marker}-ENV-END===" + +assert_fast_profile + +target_heartbeat_pid="" +if [ "$target_heartbeat_sec" != "0" ]; then + ( + target_heartbeat_mark="/tmp/${marker}-target-heartbeat.mark" + : >"$target_heartbeat_mark" 2>/dev/null || true + while :; do + sleep "$target_heartbeat_sec" + target_kib="$(du -sk "$target_dir" 2>/dev/null | awk '{ print $1 }' || true)" + target_files="$(find "$target_dir" -type f 2>/dev/null | wc -l | tr -d ' ' || true)" + target_changed_files="$(find "$target_dir" -type f -newer "$target_heartbeat_mark" 2>/dev/null | wc -l | tr -d ' ' || true)" + target_changed_sample="$(find "$target_dir" -type f -newer "$target_heartbeat_mark" 2>/dev/null | tail -n 3 | tr '\n' ',' | sed 's/,$//' || true)" + touch "$target_heartbeat_mark" 2>/dev/null || true + echo "===${marker}-TARGET-HEARTBEAT dir=${target_dir} kib=${target_kib:-unknown} files=${target_files:-unknown} changed=${target_changed_files:-unknown} sample=${target_changed_sample:-none}===" + done + ) & + target_heartbeat_pid="$!" +fi + +set -- "$cargo_bin" "$cargo_subcommand" \ + -p "$build_package" \ + --target "$build_target_arg" + +case "$target_spec_mode" in + pie | no-pie | path) + set -- "$@" -Z json-target-spec + ;; +esac + +case "$cargo_verbose" in + 0|"") + ;; + 1) + set -- "$@" -v + ;; + 2) + set -- "$@" -vv + ;; + *) + echo "===${marker}-CARGO-VERBOSE-ERROR value=${cargo_verbose} expected=0|1|2===" + finish_guest 2 + ;; +esac + +if [ -n "$build_bin" ] && [ "$build_bin" != "none" ]; then + set -- "$@" --bin "$build_bin" +fi + +if [ -n "$build_std" ] && [ "$build_std" != "none" ]; then + set -- "$@" -Z "build-std=${build_std}" +fi + +set -- "$@" --target-dir "$target_dir" + +if [ "$no_default_features" = "1" ]; then + set -- "$@" --no-default-features +fi + +if [ -n "$features" ] && [ "$features" != "none" ]; then + set -- "$@" --features "$features" +fi + +if [ "$profile" = "release" ]; then + set -- "$@" --release +fi + +echo "===${marker}-CARGO-COMMAND===" +printf '%s\n' "$*" +start="$(date +%s)" +echo "===${marker}-START jobs=${jobs} start=${start}===" + +set +e +cargo_pid="" +monitor_pid="" + +"$@" & +cargo_pid="$!" + +if [ "$guest_monitor_interval" != "0" ]; then + ( + while kill -0 "$cargo_pid" 2>/dev/null; do + echo "===${marker}-GUEST-PROCS timestamp=$(date +%s) cargo_pid=${cargo_pid}===" + ps -ef 2>/dev/null || ps 2>/dev/null || true + echo "===${marker}-GUEST-PROC-STAT-BEGIN===" + for stat in /proc/[0-9]*/stat; do + [ -r "$stat" ] || continue + read -r pid comm state ppid rest <"$stat" || continue + echo "pid=${pid} ppid=${ppid} state=${state} comm=${comm}" + case "$comm" in + "(cargo)"|"(rustc)"|"(ld-musl-aarch64.)") + if [ -r "/proc/${pid}/cmdline" ]; then + tr '\000' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true + echo + fi + if [ -r "/proc/${pid}/status" ]; then + sed -n '1,30p' "/proc/${pid}/status" 2>/dev/null || true + fi + if [ -r "/proc/${pid}/wchan" ]; then + printf 'wchan=' + cat "/proc/${pid}/wchan" 2>/dev/null || true + echo + fi + if [ -d "/proc/${pid}/task" ]; then + echo "===${marker}-GUEST-TASKS pid=${pid} comm=${comm}===" + for task_stat in /proc/"${pid}"/task/[0-9]*/stat; do + [ -r "$task_stat" ] || continue + read -r tid task_comm task_state task_ppid task_rest <"$task_stat" || continue + echo "tid=${tid} ppid=${task_ppid} state=${task_state} comm=${task_comm}" + done + fi + ;; + esac + done + echo "===${marker}-GUEST-PROC-STAT-END===" + if [ -d "/proc/${cargo_pid}/task" ]; then + echo "===${marker}-GUEST-CARGO-TASKS-BEGIN===" + for stat in /proc/"${cargo_pid}"/task/[0-9]*/stat; do + [ -r "$stat" ] || continue + read -r tid comm state ppid rest <"$stat" || continue + echo "tid=${tid} ppid=${ppid} state=${state} comm=${comm}" + done + echo "===${marker}-GUEST-CARGO-TASKS-END===" + fi + echo "===${marker}-GUEST-PROCS-END===" + sleep "$guest_monitor_interval" + done + ) & + monitor_pid="$!" +fi + +wait "$cargo_pid" +rc="$?" +if [ -n "$monitor_pid" ]; then + kill "$monitor_pid" 2>/dev/null || true + wait "$monitor_pid" 2>/dev/null || true +fi +set -e +if [ -n "$target_heartbeat_pid" ]; then + kill "$target_heartbeat_pid" 2>/dev/null || true + target_heartbeat_pid="" +fi + +end="$(date +%s)" +elapsed="$((end - start))" +echo "===${marker}-END jobs=${jobs} rc=${rc} elapsed=${elapsed}===" + +if [ "$rc" = "0" ]; then + if [ -n "$build_bin" ] && [ "$build_bin" != "none" ]; then + target_stem="$build_target" + case "$build_target_arg" in + */*.json) + target_base="${build_target_arg##*/}" + target_stem="${target_base%.json}" + ;; + esac + if [ "$profile" = "release" ]; then + artifact="${target_dir}/${target_stem}/release/${build_bin}" + else + artifact="${target_dir}/${target_stem}/debug/${build_bin}" + fi + + if [ -f "$artifact" ]; then + run_starry_kallsyms "$artifact" + bytes="$(wc -c <"$artifact" 2>/dev/null || echo unknown)" + echo "===${marker}-ARTIFACT path=${artifact} bytes=${bytes}===" + mkdir -p "$artifact_dir" + artifact_copy="${artifact_dir}/${build_bin}-${build_target}" + cp "$artifact" "$artifact_copy" + sync "$artifact_copy" 2>/dev/null || sync 2>/dev/null || true + copy_bytes="$(wc -c <"$artifact_copy" 2>/dev/null || echo unknown)" + echo "===${marker}-ARTIFACT-COPY path=${artifact_copy} bytes=${copy_bytes}===" + if [ "$artifact_to_bin" = "1" ]; then + artifact_bin="${artifact}.bin" + if command -v rust-objcopy >/dev/null 2>&1; then + rust-objcopy --strip-all -O binary "$artifact" "$artifact_bin" + elif command -v llvm-objcopy >/dev/null 2>&1; then + llvm-objcopy --strip-all -O binary "$artifact" "$artifact_bin" + else + echo "===${marker}-ARTIFACT-BIN-SKIP reason=objcopy-missing===" + artifact_bin="" + fi + if [ -n "$artifact_bin" ] && [ -f "$artifact_bin" ]; then + bin_bytes="$(wc -c <"$artifact_bin" 2>/dev/null || echo unknown)" + echo "===${marker}-ARTIFACT-BIN path=${artifact_bin} bytes=${bin_bytes}===" + artifact_bin_copy="${artifact_copy}.bin" + cp "$artifact_bin" "$artifact_bin_copy" + sync "$artifact_bin_copy" 2>/dev/null || sync 2>/dev/null || true + bin_copy_bytes="$(wc -c <"$artifact_bin_copy" 2>/dev/null || echo unknown)" + echo "===${marker}-ARTIFACT-BIN-COPY path=${artifact_bin_copy} bytes=${bin_copy_bytes}===" + fi + fi + fi + fi + echo "===${marker}-PASS jobs=${jobs} elapsed=${elapsed}===" +else + echo "===${marker}-FAIL jobs=${jobs} rc=${rc} elapsed=${elapsed}===" +fi + +finish_guest "$rc" diff --git a/apps/starry/macos-selfbuild/prebuild.sh b/apps/starry/macos-selfbuild/prebuild.sh new file mode 100755 index 0000000000..b04217cbf5 --- /dev/null +++ b/apps/starry/macos-selfbuild/prebuild.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -euo pipefail + +app_dir="${STARRY_APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +workspace="${STARRY_WORKSPACE:-$(cd "$app_dir/../../.." && pwd)}" +overlay_dir="${STARRY_OVERLAY_DIR:-}" +out_dir="$workspace/target/starry-macos-selfbuild" + +if [[ -z "$overlay_dir" ]]; then + echo "error: STARRY_OVERLAY_DIR is required" >&2 + exit 1 +fi + +if [[ ! -f "$workspace/Cargo.toml" ]]; then + echo "error: STARRY_WORKSPACE does not look like TGOSKits: $workspace" >&2 + exit 1 +fi + +shell_quote() { + local value="$1" + local i char + printf "'" + for ((i = 0; i < ${#value}; i++)); do + char="${value:i:1}" + if [[ "$char" == "'" ]]; then + printf '%s' "'\\''" + else + printf '%s' "$char" + fi + done + printf "'" +} + +git_value() { + local fallback="$1" + shift + git -C "$workspace" "$@" 2>/dev/null || printf '%s\n' "$fallback" +} + +actual_commit="$(git_value unknown rev-parse HEAD)" +if [[ -n "${TGOSKITS_COMMIT:-}" && "$actual_commit" != "unknown" && "$TGOSKITS_COMMIT" != "$actual_commit" ]]; then + echo "error: TGOSKITS_COMMIT=$TGOSKITS_COMMIT does not match workspace HEAD $actual_commit" >&2 + exit 1 +fi + +source_commit="${TGOSKITS_COMMIT:-$actual_commit}" +source_ref="${TGOSKITS_REF:-$(git_value detached symbolic-ref --quiet --short HEAD)}" +dirty="unknown" +if git -C "$workspace" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if [[ -n "$(git -C "$workspace" status --porcelain --untracked-files=all)" ]]; then + dirty="true" + else + dirty="false" + fi +fi + +mkdir -p "$out_dir" "$overlay_dir/opt" + +meta_file="$out_dir/tgoskits-src.meta" +cat >"$meta_file" <"$guest_runner" +chmod 0755 "$guest_runner" + +install -m 0755 "$app_dir/guest-selfbuild.sh" "$overlay_dir/opt/starry-macos-selfbuild.sh" +install -m 0755 "$guest_runner" "$overlay_dir/opt/starry-macos-run.sh" +install -m 0644 "$src_tar" "$overlay_dir/opt/tgoskits-src.tar" +install -m 0644 "$meta_file" "$overlay_dir/opt/tgoskits-src.meta" + +echo "macos-selfbuild overlay ready in $overlay_dir" +echo "source_commit=$source_commit" +echo "source_ref=$source_ref" +echo "source_dirty=$dirty" diff --git a/apps/starry/macos-selfbuild/prepare_rootfs.sh b/apps/starry/macos-selfbuild/prepare_rootfs.sh new file mode 100755 index 0000000000..af808eaebb --- /dev/null +++ b/apps/starry/macos-selfbuild/prepare_rootfs.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../../.." && pwd)" + +usage() { + cat <<'USAGE' +Usage: + apps/starry/macos-selfbuild/prepare_rootfs.sh \ + --base-rootfs tmp/axbuild/rootfs/rootfs-aarch64-hvf-toolchain.img \ + --output-rootfs tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img + +This script copies a prepared AArch64 StarryOS rootfs and injects the current +TGOSKits source tree as /opt/tgoskits-src.tar. It does not install Rust/Cargo; +the base rootfs must already contain the guest toolchain used by the self-build. +USAGE +} + +base_rootfs="$repo_root/tmp/axbuild/rootfs/rootfs-aarch64-hvf-toolchain.img" +output_rootfs="$repo_root/tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img" +source_dir="$repo_root" +debugfs="${DEBUGFS:-}" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --base-rootfs) + base_rootfs="$2" + shift 2 + ;; + --output-rootfs) + output_rootfs="$2" + shift 2 + ;; + --source) + source_dir="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "$debugfs" ]]; then + if command -v debugfs >/dev/null 2>&1; then + debugfs="$(command -v debugfs)" + elif [[ -x /opt/homebrew/opt/e2fsprogs/sbin/debugfs ]]; then + debugfs="/opt/homebrew/opt/e2fsprogs/sbin/debugfs" + else + echo "debugfs not found; install e2fsprogs or set DEBUGFS=/path/to/debugfs" >&2 + exit 1 + fi +fi + +copy_image() { + local src="$1" + local dst="$2" + + rm -f "$dst" + if cp -c "$src" "$dst" 2>/dev/null; then + return + fi + if cp --reflink=auto "$src" "$dst" 2>/dev/null; then + return + fi + cp "$src" "$dst" +} + +if [[ ! -f "$base_rootfs" ]]; then + echo "base rootfs not found: $base_rootfs" >&2 + echo "provide a rootfs that already contains guest Cargo/Rust toolchain files" >&2 + exit 1 +fi + +if [[ ! -f "$source_dir/Cargo.toml" ]]; then + echo "source dir does not look like TGOSKits: $source_dir" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$output_rootfs")" "$repo_root/target/starry-macos-selfbuild" +copy_image "$base_rootfs" "$output_rootfs" + +git_value() { + local fallback="$1" + shift + git -C "$source_dir" "$@" 2>/dev/null || printf '%s\n' "$fallback" +} + +actual_commit="$(git_value unknown rev-parse HEAD)" +if [[ -n "${TGOSKITS_COMMIT:-}" && "$actual_commit" != "unknown" && "$TGOSKITS_COMMIT" != "$actual_commit" ]]; then + echo "TGOSKITS_COMMIT=$TGOSKITS_COMMIT does not match source HEAD $actual_commit" >&2 + exit 1 +fi + +source_commit="${TGOSKITS_COMMIT:-$actual_commit}" +source_ref="${TGOSKITS_REF:-$(git_value detached symbolic-ref --quiet --short HEAD)}" +dirty="unknown" +if git -C "$source_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if [[ -n "$(git -C "$source_dir" status --porcelain --untracked-files=all)" ]]; then + dirty="true" + else + dirty="false" + fi +fi + +meta_file="$repo_root/target/starry-macos-selfbuild/tgoskits-src.meta" +cat >"$meta_file" <"$debugfs_cmd" <"$debugfs_log" 2>&1; then + cat "$debugfs_log" >&2 + exit 1 +fi + +echo "rootfs=$output_rootfs" +echo "source_tar=/opt/tgoskits-src.tar" +echo "source_commit=$source_commit" +echo "source_ref=$source_ref" +echo "source_dirty=$dirty" +"$script_dir/check_rootfs.sh" "$output_rootfs" || { + echo "warning: rootfs source was injected, but guest toolchain checks failed" >&2 + echo "install or inject Cargo/Rust wrappers before running self-build" >&2 + exit 1 +} diff --git a/apps/starry/macos-selfbuild/qemu-aarch64-hvf-boot.toml b/apps/starry/macos-selfbuild/qemu-aarch64-hvf-boot.toml new file mode 100644 index 0000000000..961aaf2982 --- /dev/null +++ b/apps/starry/macos-selfbuild/qemu-aarch64-hvf-boot.toml @@ -0,0 +1,33 @@ +# Apple Silicon macOS boot-only QEMU config for externally built StarryOS kernels. +args = [ + "-snapshot", + "-nographic", + "-accel", + "hvf", + "-machine", + "virt,gic-version=3", + "-cpu", + "host", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img,file.locking=off", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +success_regex = ["root@starry:"] +fail_regex = [ + "(?i)\\bpanic(?:ked)?\\b", + "(?i)\\bunhandled trap\\b", + "(?i)\\btrap frame\\b", + "(?i)fatal", + "(?i)segmentation fault", +] +timeout = 300 diff --git a/apps/starry/macos-selfbuild/qemu-aarch64-hvf.toml b/apps/starry/macos-selfbuild/qemu-aarch64-hvf.toml new file mode 100644 index 0000000000..825f30c28c --- /dev/null +++ b/apps/starry/macos-selfbuild/qemu-aarch64-hvf.toml @@ -0,0 +1,40 @@ +# Apple Silicon macOS only. When launched through `cargo xtask starry app qemu`, +# prebuild.sh injects the guest scripts and source tarball into the configured +# rootfs overlay before boot. For long operator runs, run_selfbuild.sh remains +# useful because it works on a temporary rootfs copy and stops QEMU on markers. +args = [ + "-snapshot", + "-nographic", + "-accel", + "hvf", + "-machine", + "virt,gic-version=3", + "-cpu", + "host", + "-m", + "4096M", + "-smp", + "8", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img,file.locking=off", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/bin/sh /opt/starry-macos-run.sh" +success_regex = ["(?m)^===STARRY-MACOS-SELFBUILD-PASS jobs=[0-9]+ elapsed=[0-9]+===$"] +fail_regex = [ + "(?i)\\bpanic(?:ked)?\\b", + "(?i)\\bunhandled trap\\b", + "(?i)\\btrap frame\\b", + "(?i)fatal", + "(?i)segmentation fault", + "(?m)^===STARRY-MACOS-SELFBUILD-FAIL", +] +timeout = 3600 diff --git a/apps/starry/macos-selfbuild/reproduce.sh b/apps/starry/macos-selfbuild/reproduce.sh new file mode 100755 index 0000000000..5ab8bc9f4c --- /dev/null +++ b/apps/starry/macos-selfbuild/reproduce.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../../.." && pwd)" + +usage() { + cat <<'USAGE' +Usage: + apps/starry/macos-selfbuild/reproduce.sh + +Runs the complete macOS HVF self-build reproduction: + 1. build or refresh the AArch64 self-build rootfs; + 2. build the seed StarryOS kernel; + 3. boot StarryOS with QEMU HVF and build StarryOS inside the guest. + +Common knobs: + SMP=8 JOBS=8 MEM=4096M QEMU_TIMEOUT_SEC=10800 + ROOTFS_MODE=build-rootfs|prepare-rootfs|skip + RUST_DIST_SERVER=https://rsproxy.cn + STARRY_CARGO_REGISTRY_INDEX=sparse+https://rsproxy.cn/index/ + +For memory-constrained base M1 machines, first verify the flow with: + SMP=4 JOBS=4 MEM=3072M apps/starry/macos-selfbuild/reproduce.sh +USAGE +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then + echo "warning: this workflow is intended for Apple Silicon macOS with QEMU HVF" >&2 +fi + +rootfs_mode="${ROOTFS_MODE:-build-rootfs}" +kernel="${KERNEL:-$repo_root/target/aarch64-unknown-none-softfloat/release/starryos.bin}" +rootfs="${ROOTFS:-$repo_root/tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img}" + +case "$rootfs_mode" in + build-rootfs) + "$script_dir/build_rootfs.sh" + ;; + prepare-rootfs) + "$script_dir/prepare_rootfs.sh" + ;; + skip) + ;; + *) + echo "unknown ROOTFS_MODE=$rootfs_mode; expected build-rootfs, prepare-rootfs, or skip" >&2 + exit 2 + ;; +esac + +"$script_dir/build_kernel.sh" + +exec env \ + KERNEL="$kernel" \ + ROOTFS="$rootfs" \ + SMP="${SMP:-8}" \ + JOBS="${JOBS:-${SMP:-8}}" \ + MEM="${MEM:-4096M}" \ + RAYON_NUM_THREADS="${RAYON_NUM_THREADS:-1}" \ + RUSTC_THREADS="${RUSTC_THREADS:-2}" \ + SOURCE_TMPFS="${SOURCE_TMPFS:-1}" \ + TARGET_SPEC_MODE="${TARGET_SPEC_MODE:-pie}" \ + ARTIFACT_TO_BIN="${ARTIFACT_TO_BIN:-1}" \ + TARGET_HEARTBEAT_SEC="${TARGET_HEARTBEAT_SEC:-0}" \ + TRACE_RUSTC="${TRACE_RUSTC:-0}" \ + CARGO_VERBOSE="${CARGO_VERBOSE:-0}" \ + EXPECTED_MAX_CRATES="${EXPECTED_MAX_CRATES:-420}" \ + QEMU_TIMEOUT_SEC="${QEMU_TIMEOUT_SEC:-10800}" \ + "$script_dir/run_selfbuild.sh" diff --git a/apps/starry/macos-selfbuild/run_selfbuild.sh b/apps/starry/macos-selfbuild/run_selfbuild.sh new file mode 100755 index 0000000000..4ad0ad207a --- /dev/null +++ b/apps/starry/macos-selfbuild/run_selfbuild.sh @@ -0,0 +1,440 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../../.." && pwd)" + +usage() { + cat <<'USAGE' +Usage: + apps/starry/macos-selfbuild/reproduce.sh + +or run the final QEMU step directly: + + KERNEL=target/aarch64-unknown-none-softfloat/release/starryos.bin \ + ROOTFS=tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img \ + apps/starry/macos-selfbuild/run_selfbuild.sh + +Common knobs: + SMP=8 JOBS=8 SOURCE_TMPFS=1 QEMU_TIMEOUT_SEC=7200 + EXPECTED_MAX_CRATES=420 + QEMU_ACCEL=hvf QEMU_MACHINE=virt,gic-version=3 QEMU_CPU=host + QEMU_SNAPSHOT=0 + BOOT_ONLY=1 + EXTRA_RUSTFLAGS='' +USAGE +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then + echo "warning: this workflow is intended for Apple Silicon macOS with QEMU HVF" >&2 +fi + +find_tool() { + local env_value="$1" + local name="$2" + local fallback="$3" + + if [[ -n "$env_value" ]]; then + printf '%s\n' "$env_value" + return + fi + if command -v "$name" >/dev/null 2>&1; then + command -v "$name" + return + fi + if [[ -n "$fallback" && -x "$fallback" ]]; then + printf '%s\n' "$fallback" + return + fi + echo "$name not found; install it or set the matching environment variable" >&2 + exit 1 +} + +shell_quote() { + local value="$1" + local i char + printf "'" + for ((i = 0; i < ${#value}; i++)); do + char="${value:i:1}" + if [[ "$char" == "'" ]]; then + printf '%s' "'\\''" + else + printf '%s' "$char" + fi + done + printf "'" +} + +emit_export() { + local name="$1" + local value="$2" + printf 'export %s=' "$name" + shell_quote "$value" + printf '\n' +} + +copy_image() { + local src="$1" + local dst="$2" + + rm -f "$dst" + if cp -c "$src" "$dst" 2>/dev/null; then + return + fi + if cp --reflink=auto "$src" "$dst" 2>/dev/null; then + return + fi + cp "$src" "$dst" +} + +qemu="$(find_tool "${QEMU:-}" qemu-system-aarch64 /opt/homebrew/bin/qemu-system-aarch64)" +debugfs="$(find_tool "${DEBUGFS:-}" debugfs /opt/homebrew/opt/e2fsprogs/sbin/debugfs)" + +git_value() { + local fallback="$1" + shift + git -C "$repo_root" "$@" 2>/dev/null || printf '%s\n' "$fallback" +} + +kernel="${KERNEL:-$repo_root/target/aarch64-unknown-none-softfloat/release/starryos.bin}" +rootfs="${ROOTFS:-$repo_root/tmp/axbuild/rootfs/rootfs-aarch64-hvf-selfbuild.img}" +smp="${SMP:-8}" +jobs="${JOBS:-$smp}" +mem="${MEM:-4096M}" +qemu_accel="${QEMU_ACCEL:-hvf}" +qemu_machine="${QEMU_MACHINE:-virt,gic-version=3}" +qemu_cpu="${QEMU_CPU:-host}" +boot_only="${BOOT_ONLY:-0}" +qemu_snapshot="${QEMU_SNAPSHOT:-0}" +source_tmpfs="${SOURCE_TMPFS:-1}" +qemu_timeout_sec="${QEMU_TIMEOUT_SEC:-7200}" +stamp="${STAMP:-$(date +%Y%m%dT%H%M%S)}" +case_name="${CASE_NAME:-smp${smp}-j${jobs}}" +out_root="${OUT_ROOT:-$repo_root/target/starry-macos-selfbuild}" +work_rootfs="${WORK_ROOTFS:-$out_root/rootfs/rootfs-${case_name}-${stamp}.img}" +log="${LOG:-$out_root/logs/${case_name}-${stamp}.log}" +guest_script="$script_dir/guest-selfbuild.sh" +work_dir="$out_root/work/${case_name}-${stamp}" +failure_pattern='(panicked at|kernel panic|panic:|unhandled trap|trap frame|fatal exception|segmentation fault)' +require_fresh_rootfs="${REQUIRE_FRESH_ROOTFS:-1}" + +if [[ ! -f "$kernel" ]]; then + echo "kernel not found: $kernel" >&2 + exit 1 +fi + +if [[ ! -f "$rootfs" ]]; then + echo "rootfs not found: $rootfs" >&2 + exit 1 +fi + +actual_commit="$(git_value unknown rev-parse HEAD)" +if [[ -n "${TGOSKITS_COMMIT:-}" && "$actual_commit" != "unknown" && "$TGOSKITS_COMMIT" != "$actual_commit" ]]; then + echo "TGOSKITS_COMMIT=$TGOSKITS_COMMIT does not match workspace HEAD $actual_commit" >&2 + exit 1 +fi +source_commit="${TGOSKITS_COMMIT:-$actual_commit}" +source_ref="${TGOSKITS_REF:-$(git_value detached symbolic-ref --quiet --short HEAD)}" + +mkdir -p "$(dirname "$work_rootfs")" "$(dirname "$log")" "$work_dir" +copy_image "$rootfs" "$work_rootfs" + +if [[ "$require_fresh_rootfs" = "1" ]]; then + rootfs_meta="$("$debugfs" -R "cat /opt/tgoskits-src.meta" "$work_rootfs" 2>/dev/null || true)" + rootfs_commit="$(printf '%s\n' "$rootfs_meta" | sed -n 's/^commit=//p' | tail -1)" + if [[ -z "$rootfs_commit" ]]; then + cat >&2 <&2 <"$guest_runner" +chmod +x "$guest_runner" + +debugfs_cmd="$work_dir/debugfs-inject.cmd" +cat >"$debugfs_cmd" </dev/null + +input_fifo="$work_dir/qemu-stdin.fifo" +mkfifo "$input_fifo" + +echo "log=$log" +echo "kernel=$kernel" +echo "rootfs_copy=$work_rootfs" +echo "qemu=$qemu" +echo "qemu_accel=$qemu_accel qemu_machine=$qemu_machine qemu_cpu=$qemu_cpu" +echo "smp=$smp jobs=$jobs mem=$mem source_tmpfs=$source_tmpfs boot_only=$boot_only qemu_snapshot=$qemu_snapshot qemu_timeout_sec=$qemu_timeout_sec" +echo "source_commit=$source_commit source_ref=$source_ref" +: >"$log" + +qemu_args=() +if [[ "$qemu_snapshot" = "1" ]]; then + qemu_args+=(-snapshot) +fi +qemu_args+=( + -nographic + -accel "$qemu_accel" + -machine "$qemu_machine" + -cpu "$qemu_cpu" + -m "$mem" + -smp "$smp" + -device virtio-blk-pci,drive=disk0 + -drive "id=disk0,if=none,format=raw,file=$work_rootfs,file.locking=off" + -device virtio-net-pci,netdev=net0 + -netdev user,id=net0 + -kernel "$kernel" + -monitor none + -serial mon:stdio +) + +"$qemu" \ + "${qemu_args[@]}" \ + <"$input_fifo" >"$log" 2>&1 & +qemu_pid="$!" + +exec 3>"$input_fifo" +sent_cmd=0 +host_rc=124 +start_seconds="$SECONDS" +heartbeat_sec="${HOST_HEARTBEAT_SEC:-30}" +next_heartbeat="$heartbeat_sec" +expected_max_crates="${EXPECTED_MAX_CRATES:-420}" +crate_count_guarded=0 + +check_crate_count_guard() { + local line total + + if [[ "${ALLOW_SLOW_SELFBUILD:-0}" = "1" || "$expected_max_crates" = "0" || "$crate_count_guarded" = "1" ]]; then + return 0 + fi + + line="$( + LC_ALL=C tr '\r' '\n' <"$log" \ + | grep -a -E 'Building \[[^]]*\][[:space:]]+[0-9]+/[0-9]+' \ + | tail -1 + )" + [[ -n "$line" ]] || return 0 + + total="$(printf '%s\n' "$line" | sed -n 's/.*Building \[[^]]*\][[:space:]]*[0-9][0-9]*\/\([0-9][0-9]*\).*/\1/p' | tail -1)" + [[ -n "$total" ]] || return 0 + + crate_count_guarded=1 + if (( total > expected_max_crates )); then + cat >>"$log" </dev/null || true + wait "$qemu_pid" 2>/dev/null + host_rc=2 + return 2 + fi +} + +set +e +while kill -0 "$qemu_pid" 2>/dev/null; do + elapsed=$((SECONDS - start_seconds)) + if [[ "$sent_cmd" = "0" ]] && LC_ALL=C grep -a -q "root@starry:" "$log"; then + if [[ "$boot_only" = "1" ]]; then + echo "===HOST-QEMU-STOP reason=boot-only-shell pid=$qemu_pid===" >>"$log" + kill "$qemu_pid" 2>/dev/null || true + wait "$qemu_pid" 2>/dev/null + host_rc=0 + break + else + printf '/bin/sh /opt/starry-macos-run.sh\n' >&3 + echo "===HOST-SENT-SELFBUILD-COMMAND===" >>"$log" + sent_cmd=1 + fi + fi + + if LC_ALL=C grep -a -q "===STARRY-MACOS-SELFBUILD-RUN-END rc=" "$log"; then + marker_rc="$( + LC_ALL=C sed -n 's/^===STARRY-MACOS-SELFBUILD-RUN-END rc=\([0-9][0-9]*\)===.*/\1/p' "$log" | tail -1 + )" + echo "===HOST-QEMU-STOP reason=guest-run-end pid=$qemu_pid rc=${marker_rc:-unknown}===" >>"$log" + kill "$qemu_pid" 2>/dev/null || true + wait "$qemu_pid" 2>/dev/null + host_rc="${marker_rc:-0}" + break + fi + + if LC_ALL=C grep -a -E -i -q "$failure_pattern" "$log"; then + echo "===HOST-QEMU-STOP reason=failure-pattern pid=$qemu_pid===" >>"$log" + kill "$qemu_pid" 2>/dev/null || true + wait "$qemu_pid" 2>/dev/null + host_rc=1 + break + fi + + if ! check_crate_count_guard; then + break + fi + + if (( heartbeat_sec > 0 && elapsed >= next_heartbeat )); then + heartbeat_line="$( + LC_ALL=C tr '\r' '\n' <"$log" \ + | grep -a -E '===STARRY-MACOS-SELFBUILD|Building \[|Compiling|Finished|error:' \ + | tail -1 \ + | cut -c 1-220 + )" + echo "host-heartbeat elapsed=${elapsed}s qemu_pid=$qemu_pid ${heartbeat_line:-waiting-for-guest-output}" + next_heartbeat=$((elapsed + heartbeat_sec)) + fi + + if [[ "$qemu_timeout_sec" != "0" ]]; then + if (( elapsed >= qemu_timeout_sec )); then + echo "===HOST-QEMU-STOP reason=timeout pid=$qemu_pid elapsed=$elapsed timeout=$qemu_timeout_sec===" >>"$log" + kill "$qemu_pid" 2>/dev/null || true + wait "$qemu_pid" 2>/dev/null + host_rc=124 + break + fi + fi + + sleep 2 +done + +if ! kill -0 "$qemu_pid" 2>/dev/null && ! LC_ALL=C grep -a -q "===HOST-QEMU-STOP" "$log"; then + wait "$qemu_pid" 2>/dev/null + qemu_rc="$?" + if [[ "$boot_only" != "1" && "$sent_cmd" = "1" ]] \ + && ! LC_ALL=C grep -a -q "===STARRY-MACOS-SELFBUILD-RUN-END rc=" "$log"; then + echo "===HOST-QEMU-STOP reason=qemu-exit-without-run-end pid=$qemu_pid rc=$qemu_rc===" >>"$log" + else + echo "===HOST-QEMU-STOP reason=qemu-exit pid=$qemu_pid rc=$qemu_rc===" >>"$log" + fi + host_rc="$qemu_rc" +fi + +if LC_ALL=C grep -a -E -i -q "$failure_pattern" "$log"; then + host_rc=1 +fi + +if LC_ALL=C grep -a -q "===STARRY-MACOS-SELFBUILD-RUN-END rc=" "$log"; then + marker_rc="$( + LC_ALL=C sed -n 's/^===STARRY-MACOS-SELFBUILD-RUN-END rc=\([0-9][0-9]*\)===.*/\1/p' "$log" | tail -1 + )" + host_rc="${marker_rc:-$host_rc}" +fi + +if [[ "$boot_only" != "1" && "$sent_cmd" = "1" ]] \ + && ! LC_ALL=C grep -a -q "===STARRY-MACOS-SELFBUILD-RUN-END rc=" "$log"; then + host_rc=1 +fi + +if [[ "$boot_only" = "1" ]] && LC_ALL=C grep -a -q "root@starry:" "$log"; then + if ! LC_ALL=C grep -a -q "===HOST-QEMU-STOP reason=boot-only-shell" "$log"; then + echo "===HOST-QEMU-STOP reason=boot-only-shell-observed pid=$qemu_pid===" >>"$log" + fi + host_rc=0 +fi + +if [[ "$boot_only" = "1" ]] \ + && ! LC_ALL=C grep -a -q "===HOST-QEMU-STOP reason=boot-only-shell" "$log"; then + if ! LC_ALL=C grep -a -q "===HOST-QEMU-STOP reason=boot-only-no-shell" "$log"; then + echo "===HOST-QEMU-STOP reason=boot-only-no-shell pid=$qemu_pid rc=$host_rc===" >>"$log" + fi + host_rc=1 +fi + +if kill -0 "$qemu_pid" 2>/dev/null; then + if ! LC_ALL=C grep -a -q "===HOST-QEMU-STOP" "$log"; then + echo "===HOST-QEMU-STOP reason=host-cleanup pid=$qemu_pid rc=$host_rc===" >>"$log" + fi + kill "$qemu_pid" 2>/dev/null || true + wait "$qemu_pid" 2>/dev/null +fi +set -e + +exec 3>&- + +if LC_ALL=C grep -a -q "===STARRY-MACOS-SELFBUILD-PASS" "$log"; then + LC_ALL=C grep -a "===STARRY-MACOS-SELFBUILD-PASS" "$log" | tail -1 +fi + +exit "$host_rc"