diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2eabce3..54bf321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,68 @@ jobs: shell: bash run: just test + toolchain-support: + name: Detect target spec support on Rust ${{ matrix.toolchain }} + strategy: + fail-fast: false + matrix: + include: + # Custom target specifications, which describe the hyperlight guest + # targets, are stable up to Rust 1.94 ... + - toolchain: "1.94.0" + expected: supported + # ... were made unstable in 1.95, so stable releases from then on + # cannot build a guest ... + - toolchain: "1.95.0" + expected: unsupported + # ... but nightly can, behind `-Zunstable-options`. + - toolchain: "nightly" + expected: supported + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + # Builds cargo-hyperlight itself, so it uses the pinned toolchain from + # rust-toolchain.toml rather than the one under test. + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + # See the comment in the `run-tests` job. + rustflags: "" + - uses: Swatinem/rust-cache@v2 + - uses: extractions/setup-just@v4 + - name: Install cargo-hyperlight + shell: bash + run: just install + - name: Install Rust ${{ matrix.toolchain }} + shell: bash + # cargo-hyperlight installs the rust-src component itself when it needs it. + run: rustup toolchain install ${{ matrix.toolchain }} --profile minimal + - name: Build a guest with Rust ${{ matrix.toolchain }} + shell: bash + env: + RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }} + EXPECTED: ${{ matrix.expected }} + run: | + if just build-guest > build.log 2>&1; then + actual=supported + else + actual=unsupported + fi + cat build.log + + if [ "$actual" != "$EXPECTED" ]; then + echo "::error::expected this toolchain to be $EXPECTED, but the build reported it as $actual" + exit 1 + fi + + # A toolchain we reject must be rejected up front, with the + # diagnostic that tells the user how to fix it, rather than failing + # somewhere deeper in the build. + if [ "$EXPECTED" = unsupported ] && + ! grep -q "This toolchain cannot build for" build.log; then + echo "::error::the build failed, but not with the expected diagnostic" + exit 1 + fi + spelling: name: Spell check with typos runs-on: ubuntu-latest diff --git a/src/cli.rs b/src/cli.rs index 17e0df8..96200a7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,6 +24,9 @@ pub struct Args { pub current_dir: PathBuf, pub clang: Option, pub ar: Option, + /// Whether rustc needs `-Zunstable-options` to load the custom target + /// specification. Detected by [`Args::prepare_sysroot`]. + pub unstable_target_spec: bool, } pub trait WarningLevel { @@ -188,6 +191,7 @@ impl Args { current_dir: value.current_dir, clang: toolchain::find_cc().ok(), ar: toolchain::find_ar().ok(), + unstable_target_spec: false, }) } } diff --git a/src/command.rs b/src/command.rs index 86a1155..0ca73e4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -653,7 +653,7 @@ impl Command { /// - The cargo process could not be spawned /// - The cargo process returned a non-zero exit status pub fn status(&self) -> anyhow::Result<()> { - let args = self.build_args(); + let mut args = self.build_args(); args.prepare_sysroot() .context("Failed to prepare sysroot")?; diff --git a/src/lib.rs b/src/lib.rs index 9d624c4..440c33f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,9 @@ impl CargoCommandExt for std::process::Command { self.sysroot(args.sysroot_dir()); self.append_rustflags("--cfg=hyperlight"); self.append_rustflags("--check-cfg=cfg(hyperlight)"); + if args.unstable_target_spec { + self.append_rustflags(sysroot::UNSTABLE_TARGET_SPEC_FLAG); + } self.entrypoint("entrypoint"); if let Some(clang) = &args.clang { self.cc_env(&args.target, clang); @@ -117,7 +120,7 @@ impl CargoCommandExt for std::process::Command { } impl Args { - pub fn prepare_sysroot(&self) -> Result<()> { + pub fn prepare_sysroot(&mut self) -> Result<()> { // Build sysroot sysroot::build(self)?; diff --git a/src/main.rs b/src/main.rs index b8e152a..eacb9e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,14 +63,14 @@ impl SpecialCommand { Perf => perf::run(args)?, New => new::run(args)?, BuildCSysroot => { - let built_args = cargo()? + let mut built_args = cargo()? // a C sysroot needs the C API library .arg("--with-guest-capi") .args(args) .build_args(); let sysroot_dir = built_args .c_sysroot_dir - .as_ref() + .clone() .ok_or(anyhow!("Usage: cargo-hyperlight build-c-sysroot [opts] --c-sysroot-dir "))?; built_args.prepare_sysroot()?; util::union_glob( @@ -91,7 +91,7 @@ impl SpecialCommand { )?; } Flags(k) => { - let built_args = cargo()?.args(args).build_args(); + let mut built_args = cargo()?.args(args).build_args(); built_args.prepare_sysroot()?; let flags = k.get_flags(&built_args).joined(); println!("{}", flags.to_str().ok_or(anyhow!("flags were not UTF-8"))?); @@ -101,6 +101,10 @@ impl SpecialCommand { } } +fn run_cargo(args: impl Iterator) -> Result<()> { + cargo()?.args(args).status() +} + fn main() { // Skip binary name; when invoked as `cargo hyperlight`, cargo passes // "hyperlight" as argv[1] — skip that too. @@ -109,16 +113,13 @@ fn main() { args.next(); } - if let Some(sc) = args.peek().and_then(|x| SpecialCommand::parse(x)) { - if let Err(e) = sc.execute(args) { - eprintln!("{e:?}"); - std::process::exit(1); - } - } else { - cargo() - .expect("Failed to create cargo command") - .args(args) - .status() - .expect("Failed to execute cargo"); + let result = match args.peek().and_then(|x| SpecialCommand::parse(x)) { + Some(sc) => sc.execute(args), + None => run_cargo(args), + }; + + if let Err(e) = result { + eprintln!("{e:?}"); + std::process::exit(1); } } diff --git a/src/sysroot.rs b/src/sysroot.rs index 0e49de7..53bdd03 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -1,5 +1,7 @@ +use std::ffi::OsStr; use std::ops::Not as _; use std::path::PathBuf; +use std::process::{Output, Stdio}; use anyhow::{Context, Result, bail, ensure}; use serde_json::{Map, Value, json}; @@ -10,6 +12,101 @@ use crate::cli::Args; const CARGO_TOML: &str = include_str!("dummy/_Cargo.toml"); const LIB_RS: &str = include_str!("dummy/_lib.rs"); +/// Flag that gates loading a custom target specification. Custom targets +/// became unstable in Rust 1.95, so newer toolchains only accept them on the +/// nightly channel and only when this flag is passed. +pub(crate) const UNSTABLE_TARGET_SPEC_FLAG: &str = "-Zunstable-options"; + +/// Queries the version of the cargo release in use. +fn cargo_version(args: &Args) -> Result { + let output = cargo_cmd()? + .env_clear() + .envs(args.env.iter()) + .current_dir(&args.current_dir) + .arg("version") + .arg("--verbose") + .checked_output() + .context("Failed to get cargo version")?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let release = stdout + .lines() + .find_map(|l| l.trim().strip_prefix("release: ")) + .map(str::trim) + .context("Failed to parse cargo version")?; + + semver::Version::parse(release) + .with_context(|| format!("Failed to parse cargo version {release:?}")) +} + +/// Asks rustc to print the configuration of the hyperlight target, which forces +/// it to load the custom target specification from the sysroot. +fn probe_target_spec(args: &Args, extra_flags: &[&str]) -> Result { + let rustc = match args.env.get(OsStr::new("RUSTC")) { + Some(rustc) => PathBuf::from(rustc), + None => which::which("rustc").context("Failed to find rustc")?, + }; + + std::process::Command::new(rustc) + .env_clear() + .envs(args.env.iter()) + .current_dir(&args.current_dir) + .arg("--sysroot") + .arg(args.sysroot_dir()) + .arg("--target") + .arg(&args.target) + .args(extra_flags) + .arg("--print=cfg") + .stdin(Stdio::null()) + .output() + .context("Failed to run rustc") +} + +/// Checks that the toolchain in use can load the custom target specification +/// describing the hyperlight guest target, and records whether doing so +/// requires [`UNSTABLE_TARGET_SPEC_FLAG`]. +/// +/// The target specification must already have been written to the sysroot. +fn check_target_spec_support(args: &mut Args) -> Result<()> { + let output = probe_target_spec(args, &[])?; + if output.status.success() { + args.unstable_target_spec = false; + return Ok(()); + } + + if probe_target_spec(args, &[UNSTABLE_TARGET_SPEC_FLAG])? + .status + .success() + { + args.unstable_target_spec = true; + return Ok(()); + } + + let target = &args.target; + let stderr = String::from_utf8_lossy(&output.stderr); + bail!( + "This toolchain cannot build for {target}. + +The hyperlight guest targets are described by a custom target specification, \ +which rustc rejected: + +{} + +Custom target specifications were made unstable in Rust 1.95, so they are only \ +available on earlier releases or on nightly. For example, pin the toolchain for \ +your project by adding a `rust-toolchain.toml` file next to your `Cargo.toml`: + + [toolchain] + channel = \"1.94.0\" + +or select the toolchain for a single invocation with rustup: + + cargo +1.94.0 hyperlight build + cargo +nightly hyperlight build", + stderr.trim_end() + ); +} + #[derive(serde::Deserialize, Default, Debug)] pub(crate) struct CargoBuildMessageTarget { pub(crate) name: String, @@ -24,7 +121,9 @@ pub(crate) struct CargoBuildMessage { pub(crate) filenames: Vec, } -pub fn build(args: &Args) -> Result<()> { +pub fn build(args: &mut Args) -> Result<()> { + let cargo_version = cargo_version(args)?; + let target_spec = match args.target.as_str() { "x86_64-hyperlight-none" => { let mut spec = get_spec(args, "x86_64-unknown-none")?; @@ -86,22 +185,9 @@ Supported values are: ) .context("Failed to write target spec file")?; - let version = cargo_cmd()? - .env_clear() - .envs(args.env.iter()) - .current_dir(&args.current_dir) - .arg("version") - .arg("--verbose") - .checked_output() - .context("Failed to get cargo version")?; - - let version = String::from_utf8_lossy(&version.stdout); - let version = version - .lines() - .find_map(|l| l.trim().strip_prefix("release: ")) - .context("Failed to parse cargo version")?; + check_target_spec_support(args)?; - let cargo_toml = CARGO_TOML.replace("0.0.0", version); + let cargo_toml = CARGO_TOML.replace("0.0.0", &cargo_version.to_string()); std::fs::create_dir_all(&crate_dir).context("Failed to create target directory")?; std::fs::write(crate_dir.join("Cargo.toml"), cargo_toml) @@ -136,6 +222,11 @@ Supported values are: .arg("--message-format=json") // The core, alloc and compiler_builtins crates use unstable features .allow_unstable() + .append_rustflags(if args.unstable_target_spec { + UNSTABLE_TARGET_SPEC_FLAG + } else { + "" + }) .env_remove("RUSTC_WORKSPACE_WRAPPER") .sysroot(&sysroot_dir) .output()