Skip to content
Open
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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,24 @@ jobs:
exit 1
fi

library-toolchain-selection:
name: Select older guest toolchain on ${{ matrix.os }}
strategy:
matrix:
os: ["ubuntu-latest", "windows-latest"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: "1.99.1"
rustflags: ""
- uses: Swatinem/rust-cache@v2
- name: Install guest toolchain separately
run: rustup toolchain install 1.94.1 --profile minimal --component rust-src
- name: Build guest through the library with a newer host
run: cargo +1.99.1 test --lib --test toolchain_file -- --include-ignored

spelling:
name: Spell check with typos
runs-on: ubuntu-latest
Expand Down
55 changes: 55 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
version = "0.1.14"
edition = "2024"
license = "Apache-2.0"
homepage = "https://github.com/hyperlight-dev/cargo-hyperlight"

Check warning on line 6 in Cargo.toml

View workflow job for this annotation

GitHub Actions / Detect target spec support on Rust nightly

`package.homepage` is redundant with `package.repository`
repository = "https://github.com/hyperlight-dev/cargo-hyperlight"
readme = "README.md"
description = "cargo subcommand to build hyperlight guest binaries"
Expand All @@ -23,6 +23,7 @@
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tempfile = "3"
toml = "1.0"
which = { version = "8", features = ["regex"] }
quote = "1.0"
proc-macro2 = "1.0"
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,45 @@ Your binary will be built for the `x86_64-hyperlight-none` target (or `aarch64-h

There's no need for any extra configuration, the command will take care of everything.

## Selecting a guest toolchain from Rust

When calling cargo-hyperlight as a library (for example from a host `build.rs`),
select the guest's installed toolchain explicitly:

```rust,no_run
fn main() -> anyhow::Result<()> {
cargo_hyperlight::cargo()?
.toolchain_file("guest/rust-toolchain.toml")?
.env_clear_cargo()
.current_dir("guest")
.arg("build")
.status()
}
```

For example, a newer host compiler can build a guest pinned to Rust 1.94.1:

```toml
[toolchain]
channel = "1.94.1"
components = ["rust-src"]
```

Install the toolchain and `rust-src` separately before building:

```sh
rustup toolchain install 1.94.1 --profile minimal --component rust-src
```

`toolchain_file` reads the TOML file immediately and returns an error if it is
invalid, unreadable, or names an unavailable toolchain. Relative file paths resolve
against the calling process's current directory, independently of `current_dir`.
The selection overrides `CARGO`, `RUSTC`, `RUSTDOC`, and `RUSTUP_TOOLCHAIN` for
preparation and the final guest command, including after environment clearing.
It does not change the parent process environment or install anything. The file's
`components`, `targets`, and `profile` settings do not trigger downloads; missing
`rust-src` is reported as an error. Path-based toolchains are not supported.

## Building C Guests

cargo-hyperlight also supports building C guest binaries. To get the compiler and linker flags needed to build a C guest, use:
Expand Down
193 changes: 180 additions & 13 deletions src/cargo_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use anyhow::{Result, bail};
use anyhow::{Context, Result, bail, ensure};

pub trait CargoCmd {
fn manifest_path(&mut self, path: &Option<impl AsRef<Path>>) -> &mut Self;
Expand Down Expand Up @@ -32,16 +32,6 @@ pub struct CargoBinary {
pub rustup_toolchain: Option<OsString>,
}

impl CargoBinary {
pub fn command(&self) -> Command {
let mut cmd = Command::new(&self.path);
if let Some(rustup_toolchain) = &self.rustup_toolchain {
cmd.env("RUSTUP_TOOLCHAIN", rustup_toolchain);
}
cmd
}
}

pub fn find_cargo() -> Result<CargoBinary> {
let cargo = match env::var_os("CARGO") {
Some(cargo) => PathBuf::from(cargo),
Expand All @@ -54,8 +44,108 @@ pub fn find_cargo() -> Result<CargoBinary> {
})
}

pub fn cargo_cmd() -> Result<Command> {
Ok(find_cargo()?.command())
pub fn cargo_cmd(env: &HashMap<OsString, OsString>) -> Result<Command> {
let cargo = match env.get(OsStr::new("CARGO")) {
Some(cargo) => PathBuf::from(cargo),
None => find_cargo()?.path,
Comment thread
simongdavies marked this conversation as resolved.
};
let mut cmd = Command::new(cargo);
cmd.env_clear().envs(env);
Ok(cmd)
}

#[derive(Clone)]
pub(crate) struct GuestToolchain {
pub cargo: PathBuf,
rustc: PathBuf,
rustdoc: PathBuf,
channel: String,
}

impl GuestToolchain {
pub fn from_file(path: &Path) -> Result<Self> {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read toolchain file {}", path.display()))?;
let channel = parse_toolchain(&contents)
.with_context(|| format!("Invalid toolchain file {}", path.display()))?;
let rustup = which::which("rustup").context("Toolchain selection requires rustup")?;
let binary = |name: &str| -> Result<PathBuf> {
let output = Command::new(&rustup)
.env("RUSTUP_AUTO_INSTALL", "0")
.env("RUSTUP_TOOLCHAIN", &channel)
.args(["which", "--toolchain", &channel, name])
.checked_output()
.with_context(|| {
format!("Toolchain {channel:?} must already be installed with {name}")
})?;
let path = PathBuf::from(String::from_utf8(output.stdout)?.trim());
ensure!(
path.is_file(),
"Missing {name} executable: {}",
path.display()
);
Ok(path)
};
Ok(Self {
cargo: binary("cargo")?,
rustc: binary("rustc")?,
rustdoc: binary("rustdoc")?,
channel,
})
}

pub fn env_overrides(&self) -> [(&str, &OsStr); 5] {
[
("CARGO", self.cargo.as_os_str()),
("RUSTC", self.rustc.as_os_str()),
("RUSTDOC", self.rustdoc.as_os_str()),
("RUSTUP_TOOLCHAIN", OsStr::new(&self.channel)),
("RUSTUP_AUTO_INSTALL", OsStr::new("0")),
]
}

pub fn apply_env(&self, env: &mut HashMap<OsString, OsString>) {
for (key, value) in self.env_overrides() {
// Windows environment names are case-insensitive.
env.retain(|k, _| {
if cfg!(windows) {
!k.eq_ignore_ascii_case(OsStr::new(key))
} else {
k != key
}
});
env.insert(key.into(), value.into());
}
}
}

fn parse_toolchain(contents: &str) -> Result<String> {
#[derive(serde::Deserialize)]
struct File {
toolchain: Toolchain,
}
#[derive(serde::Deserialize)]
struct Toolchain {
channel: String,
path: Option<PathBuf>,
#[serde(rename = "components")]
_components: Option<Vec<String>>,
#[serde(rename = "targets")]
_targets: Option<Vec<String>>,
#[serde(rename = "profile")]
_profile: Option<String>,
}
let file: File = toml::from_str(contents).context("Expected [toolchain] with a channel")?;
ensure!(
file.toolchain.path.is_none(),
"Path toolchains are not supported"
);
let channel = file.toolchain.channel;
ensure!(
!channel.is_empty() && channel.trim() == channel && !channel.starts_with('-'),
"Toolchain channel must be a nonempty rustup toolchain name"
);
Ok(channel)
}

pub struct CheckedOutput {
Expand Down Expand Up @@ -269,3 +359,80 @@ pub fn merge_env(

base
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_toolchain_channel() {
assert_eq!(
parse_toolchain(
"# guest compiler\n[toolchain]\nchannel = '1.94.1'\n\
components = ['rust-src']\nprofile = 'minimal'\n"
)
.unwrap(),
"1.94.1"
);
}

#[test]
fn reject_invalid_toolchain_files() {
for contents in [
"",
"1.94.1",
"[toolchain",
"[toolchain]\ncomponents = ['rust-src']",
"[toolchain]\nchannel = 1941",
"[toolchain]\nchannel = ''",
"[toolchain]\nchannel = ' 1.94.1 '",
"[toolchain]\nchannel = '--help'",
"[toolchain]\nchannel = '1.94.1'\npath = '/tmp/toolchain'",
"[toolchain]\nchannel = '1.94.1'\ncomponents = 123",
] {
assert!(parse_toolchain(contents).is_err(), "{contents}");
}
}

#[test]
fn effective_cargo_overrides_process_cargo() {
let env = HashMap::from([
("CARGO".into(), "guest-cargo".into()),
("RUSTC".into(), "guest-rustc".into()),
("RUSTUP_TOOLCHAIN".into(), "guest".into()),
]);
let command = cargo_cmd(&env).unwrap();
assert_eq!(command.get_program(), "guest-cargo");
assert_eq!(
merge_env(std::iter::empty::<(&str, &str)>(), command.get_envs()),
env
);
}

#[test]
fn selected_toolchain_replaces_host_environment() {
let toolchain = GuestToolchain {
cargo: "guest-cargo".into(),
rustc: "guest-rustc".into(),
rustdoc: "guest-rustdoc".into(),
channel: "1.94.1".into(),
};
let mut env = HashMap::from([
("CARGO".into(), "host-cargo".into()),
("RUSTC".into(), "host-rustc".into()),
("RUSTDOC".into(), "host-rustdoc".into()),
("RUSTUP_TOOLCHAIN".into(), "host".into()),
]);
toolchain.apply_env(&mut env);
assert_eq!(
env,
HashMap::from([
("CARGO".into(), "guest-cargo".into()),
("RUSTC".into(), "guest-rustc".into()),
("RUSTDOC".into(), "guest-rustdoc".into()),
("RUSTUP_TOOLCHAIN".into(), "1.94.1".into()),
("RUSTUP_AUTO_INSTALL".into(), "0".into()),
])
);
}
}
Loading
Loading