diff --git a/.cargo/config.toml b/.cargo/config.toml index f9029a13..3bbcf928 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -19,6 +19,16 @@ rustflags = [ # which wraps it in an ISO and boots it under QEMU. runner = "target/release/xtask test-runner" +# Userspace target: vibix std programs cross-compiled with -Z build-std. +# The target spec JSON lives at the workspace root; rustflags here apply +# when --target=x86_64-unknown-vibix.json is passed explicitly by xtask. +[target.x86_64-unknown-vibix] +rustflags = [ + "-C", "relocation-model=static", + "-C", "no-redzone=yes", + "-C", "force-frame-pointers=yes", +] + # `build-std` is intentionally NOT in [unstable] here — it would apply # to host `cargo test --lib` too and clash with the sysroot's std. # xtask passes `-Z build-std=...` explicitly whenever it invokes cargo diff --git a/x86_64-unknown-vibix.json b/x86_64-unknown-vibix.json new file mode 100644 index 00000000..c75e95bd --- /dev/null +++ b/x86_64-unknown-vibix.json @@ -0,0 +1,27 @@ +{ + "llvm-target": "x86_64-unknown-none-elf", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "vibix", + "env": "", + "vendor": "unknown", + "linker-flavor": "gnu-lld", + "linker": "rust-lld", + "pre-link-args": { + "gnu-lld": ["-nostdlib", "-static"] + }, + "panic-strategy": "abort", + "disable-redzone": true, + "features": "+sse,+sse2", + "has-thread-local": true, + "tls-model": "initial-exec", + "position-independent-executables": false, + "static-position-independent-executables": false, + "relocation-model": "static", + "executables": true, + "max-atomic-width": 64, + "crt-objects-fallback": "false" +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index f742607c..32fb4524 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -11,3 +11,4 @@ path = "src/main.rs" object = { version = "0.36", default-features = false, features = ["read", "std"] } rustc-demangle = "0.1" gimli = { version = "0.31", default-features = false, features = ["read-all"] } +serde_json = "1" diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 43509b74..2052b64e 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -58,6 +58,10 @@ const KERNEL_BUILD_STD_ARGS: &[&str] = &[ "build-std-features=compiler-builtins-mem", ]; +/// Custom target spec for vibix userspace (std programs cross-compiled +/// with `-Z build-std`). The JSON file lives at the workspace root. +const VIBIX_USERSPACE_TARGET: &str = "x86_64-unknown-vibix.json"; + // QEMU process exit codes produced by `isa-debug-exit` writing our // QemuExitCode values. See kernel/src/test_harness.rs. const QEMU_EXIT_SUCCESS: i32 = 65; // (0x20 << 1) | 1 @@ -393,6 +397,9 @@ fn main() -> R<()> { } } "clean" => clean()?, + "validate-target" => { + validate_vibix_target()?; + } "bench" => { // `xtask bench `. Currently only `page-cache` is // wired (#743 — RFC 0007 host-side cold-mmap fault-latency @@ -427,7 +434,7 @@ fn main() -> R<()> { other => { eprintln!("unknown subcommand: {other}"); eprintln!( - "usage: cargo xtask [build|initrd|ext2-image|iso|run|test|test-unit|test-integration|smoke|pjdfstest|repro-fork|repro-fork-build|shell-pipeline|lint|isr-audit|nm-check|bench|fuzz|clean] [--release] [--fault-test] [--panic-test] [--bench] [--fork-trace] [--shard=I/N (test-integration only)]" + "usage: cargo xtask [build|initrd|ext2-image|iso|run|test|test-unit|test-integration|smoke|pjdfstest|repro-fork|repro-fork-build|shell-pipeline|lint|isr-audit|nm-check|validate-target|bench|fuzz|clean] [--release] [--fault-test] [--panic-test] [--bench] [--fork-trace] [--shard=I/N (test-integration only)]" ); std::process::exit(2); } @@ -3416,3 +3423,91 @@ repro: starting fork loop cycles=500 hb=50 } } } + +// ─── validate-target ─────────────────────────────────────────────────────── + +/// Validate that the `x86_64-unknown-vibix.json` target spec is well-formed +/// and contains the required fields for vibix userspace cross-compilation. +fn validate_vibix_target() -> R<()> { + let spec_path = workspace_root().join(VIBIX_USERSPACE_TARGET); + if !spec_path.exists() { + return Err(format!("target spec not found at {}", spec_path.display()).into()); + } + + let contents = fs::read_to_string(&spec_path)?; + let spec: serde_json::Value = serde_json::from_str(&contents) + .map_err(|e| format!("target spec {} is not valid JSON: {e}", spec_path.display()))?; + + let obj = spec + .as_object() + .ok_or("target spec root is not a JSON object")?; + + // Required fields and their expected values (where a specific value is + // mandated by the RFC). `None` means "must be present, any value ok". + let required: &[(&str, Option<&str>)] = &[ + ("llvm-target", Some("x86_64-unknown-none-elf")), + ("arch", Some("x86_64")), + ("os", Some("vibix")), + ("linker-flavor", Some("gnu-lld")), + ("linker", Some("rust-lld")), + ("panic-strategy", Some("abort")), + ("relocation-model", Some("static")), + ("tls-model", Some("initial-exec")), + ("features", Some("+sse,+sse2")), + ("data-layout", None), + ("target-endian", Some("little")), + ("target-pointer-width", Some("64")), + ]; + + let mut errors = Vec::new(); + for &(key, expected_val) in required { + match obj.get(key) { + None => errors.push(format!("missing required field: \"{key}\"")), + Some(val) => { + if let Some(expected) = expected_val { + let actual = val.as_str().unwrap_or(""); + if actual != expected { + errors.push(format!( + "field \"{key}\": expected \"{expected}\", got \"{actual}\"" + )); + } + } + } + } + } + + // Boolean fields that must be true. + for key in ["has-thread-local", "disable-redzone"] { + match obj.get(key) { + None => errors.push(format!("missing required field: \"{key}\"")), + Some(val) => { + if val.as_bool() != Some(true) { + errors.push(format!("field \"{key}\" must be true")); + } + } + } + } + + // Boolean fields that must be false. + for key in ["position-independent-executables"] { + match obj.get(key) { + None => errors.push(format!("missing required field: \"{key}\"")), + Some(val) => { + if val.as_bool() != Some(false) { + errors.push(format!("field \"{key}\" must be false")); + } + } + } + } + + if errors.is_empty() { + println!("✓ target spec {} is well-formed", spec_path.display()); + Ok(()) + } else { + Err(format!( + "target spec validation failed:\n • {}", + errors.join("\n • ") + ) + .into()) + } +}