#![expect(
clippy::disallowed_methods,
reason = "a build script is told what to build by cargo through its environment"
)]
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use sha2::{Digest, Sha256};
const EXECUTABLE: &str = "qemu-system-arm";
const CONFIGURE: [&str; 8] = [
"--target-list=arm-softmmu",
"--without-default-features",
"--without-default-devices",
"--with-devices-arm=rigless",
"--enable-tcg",
"--enable-fdt=internal",
"--disable-download",
"--disable-plugins",
];
const NOT_BUNDLED: [&str; 2] = ["system_main.c.o", "hw_misc_rigless-null-core.c.o"];
const NOT_PASSED: [&str; 5] = [
"-Wl,--as-needed",
"-Wl,--no-undefined",
"-pie",
"-Wl,-z,relro",
"-Wl,-z,now",
];
const ENTRY_POINTS: [&str; 4] = [
"rigless_core_new",
"rigless_core_access",
"rigless_core_stop",
"rigless_core_free",
];
const MUST_CARRY: [&str; 3] = [
"meson.build",
"subprojects/dtc/meson.build",
"subprojects/keycodemapdb/README",
];
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=emulator.pin");
println!("cargo:rerun-if-env-changed=DOCS_RS");
println!("cargo:rerun-if-env-changed=RIGLESS_EMULATOR_TARBALL");
println!("cargo::rustc-check-cfg=cfg(emulator_absent)");
if env::var_os("DOCS_RS").is_some() || env::var_os("CARGO_CFG_MIRI").is_some() {
println!("cargo::rustc-cfg=emulator_absent");
return;
}
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("cargo sets it"));
let out = PathBuf::from(env::var("OUT_DIR").expect("cargo sets it"));
let fork = source(&manifest, &out);
if fork.starts_with(&manifest) {
for watched in [
"hw/misc",
"hw/arm",
"include/hw/misc",
"accel/tcg",
"target/arm/tcg",
"configs/devices/arm-softmmu",
] {
println!("cargo:rerun-if-changed={}", fork.join(watched).display());
}
}
let build = out.join("qemu");
if !build.join("build.ninja").is_file() {
fs::create_dir_all(&build).expect("the build directory");
run(
Command::new(fork.join("configure"))
.current_dir(&build)
.args(CONFIGURE),
"configuring the emulator; it needs a C compiler, python3 with its venv and wheel packages, meson, ninja, pkg-config and the glib-2.0 headers",
);
}
let mut ninja = Command::new("ninja");
ninja.current_dir(&build).arg(EXECUTABLE);
if let Ok(jobs) = env::var("NUM_JOBS") {
ninja.arg("-j").arg(jobs);
}
run(&mut ninja, "building the emulator");
let (objects, arguments) = link_line(&build);
let bundle = out.join("libqemu_bundle.a");
fs::remove_file(&bundle).ok();
run(
Command::new("ar").arg("qc").arg(&bundle).args(&objects),
"bundling the emulator's objects",
);
run(
Command::new("ar").arg("s").arg(&bundle),
"indexing the bundle",
);
println!("cargo:rustc-link-search=native={}", out.display());
println!("cargo:rustc-link-lib=static:+whole-archive=qemu_bundle");
for symbol in ENTRY_POINTS {
println!("cargo:rustc-link-arg=-Wl,-u,{symbol}");
}
for argument in arguments {
link(&argument);
}
}
fn link(argument: &str) {
let path = Path::new(argument);
if let Some(stem) = library(path, "a") {
if let Some(directory) = path.parent() {
println!("cargo:rustc-link-search=native={}", directory.display());
}
println!("cargo:rustc-link-lib=static:-bundle={stem}");
} else if let Some(stem) = library(path, "so") {
if let Some(directory) = path.parent() {
println!("cargo:rustc-link-search=native={}", directory.display());
}
println!("cargo:rustc-link-lib=dylib={stem}");
} else if let Some(name) = argument.strip_prefix("-l") {
println!("cargo:rustc-link-lib=dylib={name}");
} else if argument == "-pthread" {
println!("cargo:rustc-link-lib=dylib=pthread");
} else if argument == "-Wl,--start-group" || argument == "-Wl,--end-group" {
} else {
println!("cargo:rustc-link-arg={argument}");
}
}
fn library<'a>(path: &'a Path, extension: &str) -> Option<&'a str> {
(path.extension()? == extension)
.then(|| path.file_stem()?.to_str()?.strip_prefix("lib"))
.flatten()
}
fn source(manifest: &Path, out: &Path) -> PathBuf {
let submodule = manifest.join("qemu");
if submodule.join("meson.build").is_file() {
carries(
&submodule,
"the emulator fork and its subprojects are the submodules under `qemu/`; fetch them with `git submodule update --init --depth 1 qemu` then `git -C qemu submodule update --init --depth 1 subprojects/dtc subprojects/keycodemapdb`",
);
return submodule;
}
let pin = Pin::read(manifest);
let tree = out.join(format!("qemu-{}", pin.commit));
if tree.join("meson.build").is_file() {
return tree;
}
let tarball = out.join(format!("qemu-{}.tar.gz", pin.commit));
match env::var_os("RIGLESS_EMULATOR_TARBALL") {
Some(local) => {
fs::copy(&local, &tarball).unwrap_or_else(|error| {
panic!(
"reading the emulator tarball at {}: {error}",
Path::new(&local).display()
)
});
}
None => run(
Command::new("curl")
.args(["--fail", "--location", "--silent", "--show-error"])
.arg("--output")
.arg(&tarball)
.arg(&pin.url),
"fetching the emulator; it needs curl and a network, or a local copy named by RIGLESS_EMULATOR_TARBALL",
),
}
let got = digest(&tarball);
assert_eq!(
got, pin.sha256,
"the emulator tarball at {} hashes to {got}, and `emulator.pin` expects {}; refusing to build something nobody pinned",
pin.url, pin.sha256
);
fs::create_dir_all(&tree).expect("the emulator directory");
run(
Command::new("tar")
.arg("--extract")
.arg("--gzip")
.arg("--strip-components=1")
.arg("--file")
.arg(&tarball)
.arg("--directory")
.arg(&tree),
"unpacking the emulator",
);
carries(
&tree,
"the emulator tarball is missing what a build needs; it is packed from a checkout with the dtc and keycodemapdb subprojects initialised",
);
tree
}
struct Pin {
commit: String,
url: String,
sha256: String,
}
impl Pin {
fn read(manifest: &Path) -> Self {
let path = manifest.join("emulator.pin");
let text = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("reading {}: {error}", path.display()));
let field = |name: &str| -> String {
text.lines()
.map(str::trim)
.filter(|line| !line.starts_with('#'))
.find_map(|line| {
let (key, value) = line.split_once('=')?;
(key.trim() == name).then(|| value.trim().to_owned())
})
.unwrap_or_else(|| panic!("{} names no {name}", path.display()))
};
Self {
commit: field("commit"),
url: field("url"),
sha256: field("sha256"),
}
}
}
fn digest(path: &Path) -> String {
use std::io::Read;
let mut file =
fs::File::open(path).unwrap_or_else(|error| panic!("{}: {error}", path.display()));
let mut hasher = Sha256::new();
let mut buffer = vec![0u8; 64 * 1024];
loop {
let read = file
.read(&mut buffer)
.unwrap_or_else(|error| panic!("reading {}: {error}", path.display()));
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
format!("{:x}", hasher.finalize())
}
fn carries(tree: &Path, missing: &str) {
for wanted in MUST_CARRY {
assert!(tree.join(wanted).is_file(), "{missing} (no {wanted})");
}
}
fn run(command: &mut Command, doing: &str) {
let status = command
.status()
.unwrap_or_else(|error| panic!("{doing}: {error}"));
assert!(status.success(), "{doing}: {status}");
}
fn link_line(build: &Path) -> (Vec<PathBuf>, Vec<String>) {
let ninja = fs::read_to_string(build.join("build.ninja")).expect("the fork's build.ninja");
let mut lines = ninja.lines();
let mut build_line = None;
for line in lines.by_ref() {
if line.starts_with(&format!("build {EXECUTABLE}:")) {
build_line = Some(line.to_owned());
break;
}
}
let build_line = build_line.expect("build.ninja links the executable");
let inputs = build_line
.split_once(':')
.expect("a build line")
.1
.split(" | ")
.next()
.expect("the inputs");
let objects: Vec<PathBuf> = inputs
.split_whitespace()
.skip(1)
.filter(|token| {
Path::new(token)
.extension()
.is_some_and(|extension| extension == "o")
})
.filter(|token| !NOT_BUNDLED.iter().any(|left_out| token.ends_with(left_out)))
.map(|token| build.join(token))
.collect();
assert!(objects.len() > 100, "the fork's link lists its objects");
let link_args = lines
.find_map(|line| line.trim_start().strip_prefix("LINK_ARGS = "))
.expect("the link arguments follow the build line");
let mut arguments = Vec::new();
let mut tokens = link_args.split_whitespace();
while let Some(token) = tokens.next() {
if NOT_PASSED.contains(&token) {
continue;
}
if token == "-Xlinker" {
tokens.next();
continue;
}
let relative = build.join(token);
if !token.starts_with('-') && relative.is_file() {
arguments.push(relative.display().to_string());
} else {
arguments.push(token.to_owned());
}
}
(objects, arguments)
}