#![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};
#[path = "src/runtime/version/provenance.rs"]
mod provenance;
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)");
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"));
write_version(&manifest, &out);
if env::var_os("DOCS_RS").is_some() || env::var_os("CARGO_CFG_MIRI").is_some() {
println!("cargo::rustc-cfg=emulator_absent");
return;
}
let shared = emulator_dir(&manifest, &out);
fs::create_dir_all(&shared).expect("the emulator directory");
let _building = Building::take(&shared);
let fork = source(&manifest, &shared);
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 = shared.join("build");
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 = shared.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={}", shared.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 write_version(manifest: &Path, out: &Path) {
let version = env::var("CARGO_PKG_VERSION").expect("cargo sets it");
let source = source_of(manifest, &version);
let written = provenance::describe(&version, &source);
fs::write(out.join("version.rs"), format!("{written:?}")).expect("the version file");
}
fn source_of(manifest: &Path, version: &str) -> provenance::Provenance {
let record = manifest.join(".cargo_vcs_info.json");
if let Ok(json) = fs::read_to_string(&record) {
println!("cargo:rerun-if-changed={}", record.display());
return provenance::from_vcs_info(&json);
}
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(manifest)
.args(args)
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
};
let Some(commit) = git(&["rev-parse", "HEAD"]) else {
return provenance::Provenance::Unknown;
};
let mut watched: Vec<String> = ["HEAD", "index", "packed-refs", "refs/tags"]
.iter()
.map(|&path| path.to_owned())
.collect();
watched.extend(git(&["symbolic-ref", "-q", "HEAD"]));
for path in watched {
let Some(found) = git(&["rev-parse", "--git-path", &path]) else {
continue;
};
let found = manifest.join(found);
if found.exists() {
println!("cargo:rerun-if-changed={}", found.display());
}
}
for builds in ["src", "Cargo.toml", "Cargo.lock"] {
println!("cargo:rerun-if-changed={}", manifest.join(builds).display());
}
let changed = git(&[
"status",
"--porcelain",
"--untracked-files=no",
"--",
"src",
"build.rs",
"Cargo.toml",
"Cargo.lock",
"emulator.pin",
"qemu",
]);
let dirty = changed.is_none_or(|changes| !changes.is_empty());
let tags = git(&["tag", "--points-at", "HEAD"]).unwrap_or_default();
provenance::from_checkout(version, &commit, &tags, dirty)
}
fn emulator_dir(manifest: &Path, out: &Path) -> PathBuf {
let key = if manifest.join("qemu").join("meson.build").is_file() {
"checkout".to_owned()
} else {
Pin::read(manifest).commit
};
let target = out
.ancestors()
.nth(4)
.expect("cargo puts a build script's output four levels under the target directory");
target.join(format!("emulator-{key}"))
}
struct Building(PathBuf);
impl Building {
fn take(shared: &Path) -> Self {
let lock = shared.join(".building");
loop {
match fs::create_dir(&lock) {
Ok(()) => return Self(lock),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let stale = fs::metadata(&lock)
.and_then(|meta| meta.modified())
.is_ok_and(|since| since.elapsed().unwrap_or_default().as_secs() > 3_600);
if stale {
fs::remove_dir(&lock).ok();
continue;
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
Err(error) => panic!(
"taking the emulator's build lock at {}: {error}",
lock.display()
),
}
}
}
}
impl Drop for Building {
fn drop(&mut self) {
fs::remove_dir(&self.0).ok();
}
}
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)
}