Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub struct Args {
pub current_dir: PathBuf,
pub clang: Option<PathBuf>,
pub ar: Option<PathBuf>,
/// Whether rustc needs `-Zunstable-options` to load the custom target
/// specification. Detected by [`Args::prepare_sysroot`].
pub unstable_target_spec: bool,
}

pub trait WarningLevel {
Expand Down Expand Up @@ -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,
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")?;
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)?;

Expand Down
29 changes: 15 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 </path/to/sysroot>"))?;
built_args.prepare_sysroot()?;
util::union_glob(
Expand All @@ -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"))?);
Expand All @@ -101,6 +101,10 @@ impl SpecialCommand {
}
}

fn run_cargo(args: impl Iterator<Item = OsString>) -> Result<()> {
cargo()?.args(args).status()
}

fn main() {
// Skip binary name; when invoked as `cargo hyperlight`, cargo passes
// "hyperlight" as argv[1] — skip that too.
Expand All @@ -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);
}
}
123 changes: 107 additions & 16 deletions src/sysroot.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<semver::Version> {
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<Output> {
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,
Expand All @@ -24,7 +121,9 @@ pub(crate) struct CargoBuildMessage {
pub(crate) filenames: Vec<PathBuf>,
}

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")?;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
Loading