hermit 0.11.0

The Hermit unikernel for Rust.
Documentation
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, str};

use flate2::read::GzDecoder;
use tar::Archive;

fn main() {
	let targets_hermit =
		env::var_os("CARGO_CFG_TARGET_OS").is_some_and(|target_os| target_os == "hermit");
	let runs_clippy =
		env::var_os("CARGO_CFG_FEATURE").is_some_and(|feature| feature == "cargo-clippy");
	let is_docs_rs = env::var_os("DOCS_RS").is_some();
	let is_common_os = has_feature("common-os");
	if !targets_hermit || runs_clippy || is_docs_rs || is_common_os {
		return;
	}

	let kernel_src = KernelSrc::local().unwrap_or_else(KernelSrc::download);

	kernel_src.build();

	if has_feature("libc") {
		let libc = cc::Build::new()
			.get_compiler()
			.to_command()
			.arg("-print-file-name=libc.a")
			.output()
			.unwrap()
			.stdout;
		let libc = str::from_utf8(&libc).unwrap().trim_ascii_end();
		let libc_dir = Path::new(libc).parent().unwrap();
		println!("cargo:rustc-link-search={}", libc_dir.display());
		println!("cargo:rustc-link-lib=static:+whole-archive=c");
	}
}

struct KernelSrc {
	src_dir: PathBuf,
}

impl KernelSrc {
	fn local() -> Option<Self> {
		let mut src_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
		src_dir.set_file_name("kernel");
		src_dir.exists().then_some(Self { src_dir })
	}

	fn download() -> Self {
		let version = "0.10.0";
		let out_dir = out_dir();
		let src_dir = out_dir.join(format!("kernel-{version}"));

		if !src_dir.exists() {
			let url =
				format!("https://github.com/hermit-os/kernel/archive/refs/tags/v{version}.tar.gz");
			let response = ureq::get(url.as_str())
				.call()
				.unwrap()
				.into_body()
				.into_reader();
			let tar = GzDecoder::new(response);
			let mut archive = Archive::new(tar);
			archive.unpack(src_dir.parent().unwrap()).unwrap();
		}

		Self { src_dir }
	}

	fn build(self) {
		let target_dir = target_dir();
		let manifest_path = self.src_dir.join("Cargo.toml");
		assert!(
			manifest_path.exists(),
			"kernel manifest path `{}` does not exist",
			manifest_path.display()
		);
		let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();
		let profile = env::var("PROFILE").expect("PROFILE was not set");

		let mut cargo = cargo();

		cargo
			.current_dir(&self.src_dir)
			.arg("run")
			.arg("--package=xtask")
			.arg("--target-dir")
			.arg(&target_dir)
			.arg("--")
			.arg("build")
			.arg("--arch")
			.arg(&arch)
			.args([
				"--profile",
				match profile.as_str() {
					"debug" => "dev",
					profile => profile,
				},
			])
			.arg("--target-dir")
			.arg(&target_dir);

		if has_feature("instrument") {
			cargo.arg("--instrument-mcount");
		}

		if has_feature("randomize-layout") {
			cargo.arg("--randomize-layout");
		}

		// Control enabled features via this crate's features
		cargo.arg("--no-default-features");
		forward_features(
			&mut cargo,
			[
				"acpi",
				"dhcpv4",
				"dns",
				"fs",
				"fsgsbase",
				"idle-poll",
				"mmap",
				"pci",
				"pci-ids",
				"rtl8139",
				"shell",
				"smp",
				"strace",
				"tcp",
				"trace",
				"udp",
				"vga",
				"vsock",
			]
			.into_iter(),
		);

		println!("cargo:warning=$ {cargo:?}");
		let status = cargo.status().expect("failed to start kernel build");
		assert!(status.success());

		let lib_location = target_dir
			.join(&arch)
			.join(&profile)
			.canonicalize()
			.unwrap();

		println!("cargo:rustc-link-search=native={}", lib_location.display());
		println!("cargo:rustc-link-lib=static=hermit");

		self.rerun_if_changed_cargo(&self.src_dir.join("Cargo.toml"));
		self.rerun_if_changed_cargo(&self.src_dir.join("hermit-builtins/Cargo.toml"));
		self.rerun_if_changed_cargo(&self.src_dir.join("hermit-macro/Cargo.toml"));

		println!(
			"cargo:rerun-if-changed={}",
			self.src_dir.join("rust-toolchain.toml").display()
		);

		// HERMIT_LOG_LEVEL_FILTER sets the log level filter at compile time
		println!("cargo:rerun-if-env-changed=HERMIT_LOG_LEVEL_FILTER");
		println!("cargo:rerun-if-env-changed=HERMIT_CAREFUL");
		println!("cargo:rerun-if-env-changed=NO_COLOR");
	}

	fn rerun_if_changed_cargo(&self, cargo_toml: &Path) {
		let mut cargo = cargo();

		let output = cargo
			.arg("tree")
			.arg(format!("--manifest-path={}", cargo_toml.display()))
			.arg("--prefix=none")
			.arg("--workspace")
			.output()
			.unwrap();

		let output = str::from_utf8(&output.stdout).unwrap();

		let path_deps = output.lines().filter_map(|dep| {
			let mut split = dep.split(&['(', ')']);
			split.next();
			let path = split.next()?;
			path.starts_with('/').then_some(path)
		});

		for path_dep in path_deps {
			println!("cargo:rerun-if-changed={path_dep}/src");
			println!("cargo:rerun-if-changed={path_dep}/Cargo.toml");
			if Path::new(path_dep).join("Cargo.lock").exists() {
				println!("cargo:rerun-if-changed={path_dep}/Cargo.lock");
			}
			if Path::new(path_dep).join("build.rs").exists() {
				println!("cargo:rerun-if-changed={path_dep}/build.rs");
			}
		}
	}
}

fn cargo() -> Command {
	let cargo = {
		let exe = format!("cargo{}", env::consts::EXE_SUFFIX);
		// On windows, the userspace toolchain ends up in front of the rustup proxy in $PATH.
		// To reach the rustup proxy nonetheless, we explicitly query $CARGO_HOME.
		let mut cargo_home = home::cargo_home().unwrap();
		cargo_home.push("bin");
		cargo_home.push(&exe);
		if cargo_home.exists() {
			cargo_home
		} else {
			PathBuf::from(exe)
		}
	};

	let mut cargo = Command::new(cargo);

	// Remove rust-toolchain-specific environment variables from kernel cargo
	cargo.env_remove("LD_LIBRARY_PATH");
	env::vars()
		.filter(|(key, _value)| key.starts_with("CARGO") || key.starts_with("RUST"))
		.for_each(|(key, _value)| {
			cargo.env_remove(&key);
		});

	cargo
}

fn out_dir() -> PathBuf {
	env::var_os("OUT_DIR").unwrap().into()
}

fn target_dir() -> PathBuf {
	let mut target_dir = out_dir();
	target_dir.push("target");
	target_dir
}

fn has_feature(feature: &str) -> bool {
	let mut var = "CARGO_FEATURE_".to_string();

	var.extend(feature.chars().map(|c| match c {
		'-' => '_',
		c => c.to_ascii_uppercase(),
	}));

	env::var_os(&var).is_some()
}

fn forward_features<'a>(cmd: &mut Command, features: impl Iterator<Item = &'a str>) {
	let features = features.filter(|f| has_feature(f)).collect::<Vec<_>>();
	if !features.is_empty() {
		cmd.args(["--features", &features.join(" ")]);
	}
}