mod build_files;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use anyhow::{Context, Result, bail};
use bindgen::Formatter::Prettyplease;
use bindgen::RustEdition::Edition2021;
use build_files::{LIBC_FILES, LIBC_FILES_X86, LIBM_FILES, LIBM_FILES_X86};
fn copy_includes<P: AsRef<Path>, Q: AsRef<Path> + std::fmt::Debug>(
include_dir: P,
base: Q,
) -> Result<()> {
let entries =
fs::read_dir(&base).with_context(|| format!("could not open include dir {:?}", base))?;
for entry in entries {
let entry = entry.with_context(|| format!("could not read include dir {:?}", base))?;
let src = entry.path();
let dst = include_dir.as_ref().join(entry.file_name());
let kind = entry
.file_type()
.with_context(|| format!("could not find type of {:?}", src))?;
if kind.is_dir() {
fs::create_dir_all(&dst)
.with_context(|| format!("could not create include dir {:?}", &dst))?;
copy_includes(&dst, src)?;
} else if Some(std::ffi::OsStr::new("h")) == src.extension() {
fs::copy(&src, &dst).with_context(|| format!("could not copy header {:?}", &src))?;
}
}
Ok(())
}
fn cc_build(picolibc_dir: &PathBuf, target: &str) -> Result<cc::Build> {
let mut build = cc::Build::new();
let compiler = env::var("HYPERLIGHT_GUEST_clang").unwrap_or("clang".to_string());
build.compiler(compiler).std("c18").opt_level(3);
build
.flag("-fPIC")
.flag("-nostdlib")
.flag("-nostdlibinc")
.flag("-ffreestanding")
.flag("-fno-common")
.flag("-fno-builtin")
.flag("-fdiagnostics-color=always")
.flag("-Wall")
.flag("-Winvalid-pch")
.flag("-Wno-unused-command-line-argument")
.flag("-Wno-unsupported-floating-point-opt")
.flag("-Wextra")
.flag("-Werror=vla")
.flag("-Warray-bounds")
.flag("-Werror=double-promotion")
.flag("-Werror=implicit-function-declaration")
.flag("-Werror=unreachable-code-fallthrough")
.flag("-Wmissing-declarations")
.flag("-Wold-style-definition")
.flag("-Wno-implicit-int")
.flag("-Wno-missing-braces")
.flag("-Wno-return-type")
.flag("-fno-stack-protector")
.flag("-fstack-clash-protection")
.flag("-mstack-probe-size=4096")
.flag("-mno-red-zone")
.flag("--target=x86_64-unknown-linux-none");
build
.flag_if_supported("-fdirect-access-external-data")
.flag_if_supported("-frounding-math")
.flag_if_supported("-fsignaling-nans")
.flag_if_supported("-fno-builtin-copysignl")
.flag_if_supported("-mstack-protector-guard=global")
.flag_if_supported("-fstrict-flex-arrays=3");
build
.flag("-U_FORTIFY_SOURCE")
.define("ABORT_PROVIDED", "1")
.define("DEFINE_MEMALIGN", "1")
.define("DEFINE_POSIX_MEMALIGN", "1")
.define("_LIBC", None)
.define("_FILE_OFFSET_BITS", "64");
match target {
"x86" | "x86_64" => {
build.include(picolibc_dir.join("libm/machine/x86"));
build.include(picolibc_dir.join("libc/machine/x86"));
}
arch => {
bail!("Unsupported target architecture: {arch}");
}
}
build
.include(picolibc_dir)
.include(picolibc_dir.join("libc/stdio"))
.include(picolibc_dir.join("libc/locale"))
.include(picolibc_dir.join("libc/include"));
Ok(build)
}
fn add_libc(build: &mut cc::Build, picolibc_dir: &Path, target: &str) -> Result<()> {
let base = LIBC_FILES.iter();
let files = match target {
"x86" | "x86_64" => base.chain(LIBC_FILES_X86.iter()),
arch => bail!("Unsupported target architecture: {arch}"),
};
for file in files {
let source_path = picolibc_dir.join("libc").join(file);
build.file(&source_path);
}
Ok(())
}
fn add_libm(build: &mut cc::Build, picolibc_dir: &Path, target: &str) -> Result<()> {
build.include(picolibc_dir.join("libm/common"));
let base = LIBM_FILES.iter();
let files = match target {
"x86" | "x86_64" => base.chain(LIBM_FILES_X86.iter()),
arch => bail!("Unsupported target architecture: {arch}"),
};
for file in files {
let source_path = picolibc_dir.join("libm").join(file);
build.file(&source_path);
}
Ok(())
}
fn init_submodule() -> Result<()> {
let status = Command::new("git")
.args(["submodule", "update", "--init"])
.status()?;
if !status.success() {
bail!("git submodule update --init failed");
}
Ok(())
}
fn generate_bindings(include_dir: &Path, out_dir: &Path) -> Result<()> {
bindgen::Builder::default()
.header(include_dir.join("stdlib.h").to_string_lossy())
.header(include_dir.join("stdio.h").to_string_lossy())
.header(include_dir.join("string.h").to_string_lossy())
.header(include_dir.join("math.h").to_string_lossy())
.header(include_dir.join("stdint.h").to_string_lossy())
.header(include_dir.join("ctype.h").to_string_lossy())
.header(include_dir.join("errno.h").to_string_lossy())
.header(include_dir.join("time.h").to_string_lossy())
.header(include_dir.join("limits.h").to_string_lossy())
.header(include_dir.join("signal.h").to_string_lossy())
.header(include_dir.join("setjmp.h").to_string_lossy())
.header(include_dir.join("locale.h").to_string_lossy())
.header(include_dir.join("wchar.h").to_string_lossy())
.header(include_dir.join("wctype.h").to_string_lossy())
.header(include_dir.join("fenv.h").to_string_lossy())
.header(include_dir.join("inttypes.h").to_string_lossy())
.header(include_dir.join("sys/time.h").to_string_lossy())
.clang_arg(format!("-I{}", include_dir.display()))
.clang_arg("-nostdlibinc")
.clang_arg("--target=x86_64-unknown-linux-none")
.clang_arg("-fno-stack-protector")
.clang_arg("-D_POSIX_MONOTONIC_CLOCK=1")
.use_core()
.wrap_unsafe_ops(true)
.rust_edition(Edition2021)
.formatter(Prettyplease)
.ctypes_prefix("core::ffi")
.derive_copy(true)
.derive_debug(true)
.derive_default(true)
.derive_eq(true)
.derive_hash(true)
.derive_ord(true)
.generate_comments(true)
.generate_cstr(true)
.layout_tests(false)
.generate()
.context("Unable to generate bindings")?
.write_to_file(out_dir.join("bindings.rs"))
.context("Couldn't write bindings")?;
Ok(())
}
fn cargo_main() -> Result<()> {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=third_party/picolibc");
println!("cargo:rerun-if-changed=include/picolibc.h");
println!("cargo:rerun-if-env-changed=HYPERLIGHT_GUEST_TOOLCHAIN_ROOT");
let out_dir = env::var("OUT_DIR").expect("cargo OUT_DIR not set");
let target = env::var("CARGO_CFG_TARGET_ARCH").expect("cargo TARGET_ARCH not set");
let include_dir = PathBuf::from(&out_dir).join("include");
fs::create_dir_all(&include_dir)
.with_context(|| format!("Could not create include dir {include_dir:?}"))?;
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("cargo MANIFEST_DIR not set");
let manifest_dir = PathBuf::from(manifest_dir);
let picolibc_dir = manifest_dir.join("third_party/picolibc");
if !picolibc_dir.join("COPYING.picolibc").exists() {
eprintln!("Setting up submodules");
init_submodule().with_context(|| "failed to init picolibc submodule")?;
}
let mut build = cc_build(&picolibc_dir, &target)?;
build.include(manifest_dir.join("include"));
add_libc(&mut build, &picolibc_dir, &target)?;
add_libm(&mut build, &picolibc_dir, &target)?;
if cfg!(windows) {
unsafe { env::set_var("AR_x86_64_unknown_none", "llvm-ar") };
}
build.compile("hyperlight_libc");
copy_includes(&include_dir, picolibc_dir.join("libc/include"))?;
copy_includes(&include_dir, manifest_dir.join("include"))?;
generate_bindings(&include_dir, &PathBuf::from(&out_dir))?;
let include_str = include_dir
.to_str()
.with_context(|| "out dir include dir was not valid utf-8")?;
println!("cargo::metadata=include={}", include_str);
if let Ok(binroot) = env::var("HYPERLIGHT_GUEST_TOOLCHAIN_ROOT") {
let binroot = PathBuf::from(binroot);
let binpath = env::current_exe().expect("couldn't get build script path");
fs::create_dir_all(&binroot)
.unwrap_or_else(|e| panic!("Could not create binary root {:?}: {}", &binroot, e));
fs::write(binroot.join(".out_dir"), out_dir).expect("Could not write out_dir");
fs::copy(&binpath, binroot.join("clang")).expect("Could not copy to clang");
fs::copy(&binpath, binroot.join("clang.exe")).expect("Could not copy to clang.exe");
}
Ok(())
}
#[derive(PartialEq)]
enum Tool {
CargoBuildScript,
Clang,
}
impl From<&std::ffi::OsStr> for Tool {
fn from(x: &std::ffi::OsStr) -> Tool {
if x == "clang" || x == "clang.exe" {
Tool::Clang
} else {
Tool::CargoBuildScript
}
}
}
fn find_next(root_dir: &Path, tool_name: &str) -> PathBuf {
if let Some(path) = env::var_os(format!("HYPERLIGHT_GUEST_{tool_name}")) {
return path.into();
}
let path = env::var_os("PATH").expect("$PATH should exist");
let paths: Vec<_> = env::split_paths(&path).collect();
for path in &paths {
let abs_path = fs::canonicalize(path);
let abs_path = abs_path.as_ref().unwrap_or(path);
if abs_path == root_dir {
continue;
}
let base_path = path.join(tool_name);
if base_path.exists() {
return base_path;
}
let exe_path = base_path.with_extension("exe");
if exe_path.exists() {
return exe_path;
}
}
panic!("Could not find another implementation of {}", tool_name);
}
fn main() -> std::process::ExitCode {
let exe = env::current_exe().expect("expected program name");
let name = Path::file_name(exe.as_ref()).expect("program name should not be directory");
let tool: Tool = name.into();
if tool == Tool::CargoBuildScript {
if let Err(err) = cargo_main() {
eprintln!("{:#}", err);
return std::process::ExitCode::FAILURE;
}
return std::process::ExitCode::SUCCESS;
}
let exe_abs = fs::canonicalize(&exe).expect("program name should be possible to canonicalize");
let root_dir = exe_abs
.parent()
.expect("program name should be in a directory");
let out_dir = std::fs::read_to_string(root_dir.join(".out_dir"))
.expect(".out_dir should have a valid path in it");
let mut args = env::args();
args.next(); let include_dir = <String as AsRef<Path>>::as_ref(&out_dir).join("include");
match tool {
Tool::CargoBuildScript => unreachable!("cargo build script should not be called directly"),
Tool::Clang => {
std::process::Command::new(find_next(root_dir, "clang"))
.arg("--target=x86_64-unknown-linux-none")
.args([
"-fno-stack-protector",
"-fstack-clash-protection",
"-mstack-probe-size=4096",
"-mno-red-zone",
])
.arg("-nostdinc")
.arg("-isystem")
.arg(include_dir)
.args(args)
.status()
.ok()
.and_then(|x| x.code())
.map(|x| (x as u8).into())
.unwrap_or(std::process::ExitCode::FAILURE)
}
}
}