fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo::rustc-check-cfg=cfg(typelisp_bundled_runtime)");
let deployment = deployment_target();
bundled_runtime(deployment.as_deref());
profile_dir();
}
fn profile_dir() {
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("cargo sets OUT_DIR"));
let build = out_dir.parent().and_then(|p| p.parent());
let dir = match build {
Some(build) if out_dir.file_name() == Some("out".as_ref()) && build.file_name() == Some("build".as_ref()) => {
build.parent().expect("`build` has a parent")
}
_ => panic!("OUT_DIR `{}` is not `<target dir>/<profile>/build/<pkg>/out`", out_dir.display()),
};
println!("cargo:rustc-env=TYPELISP_PROFILE_DIR={}", dir.display());
}
fn bundled_runtime(deployment: Option<&str>) {
if std::env::var("PROFILE").as_deref() != Ok("release") {
return;
}
let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR"));
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("cargo sets OUT_DIR"));
let target = std::env::var("TARGET").expect("cargo sets TARGET");
let cargo = std::env::var("CARGO").expect("cargo sets CARGO");
for input in ["crates", "Cargo.toml", "Cargo.lock"] {
println!("cargo:rerun-if-changed={}", manifest_dir.join(input).display());
}
let target_dir = out_dir.join("runtime-target");
let mut build = std::process::Command::new(&cargo);
if let Some(version) = deployment {
build.env("MACOSX_DEPLOYMENT_TARGET", version);
build.arg(format!("--config=env.MACOSX_DEPLOYMENT_TARGET.value=\"{}\"", version));
build.arg("--config=env.MACOSX_DEPLOYMENT_TARGET.force=true");
}
let status = build
.arg("build")
.arg("--release")
.arg("--locked")
.args(["-p", "typelisp-front"])
.arg("--manifest-path")
.arg(manifest_dir.join("Cargo.toml"))
.arg("--target")
.arg(&target)
.arg("--target-dir")
.arg(&target_dir)
.status()
.unwrap_or_else(|e| panic!("build.rs: failed to run {} to build typelisp-front's static library: {}", cargo, e));
assert!(status.success(), "build.rs: building typelisp-front's static library failed ({})", status);
let archive = target_dir.join(&target).join("release").join("libtypelisp_front.a");
assert!(archive.is_file(), "build.rs: {} was not built", archive.display());
println!("cargo:rustc-env=TYPELISP_BUNDLED_STATICLIB={}", archive.display());
let bytes = std::fs::read(&archive).unwrap_or_else(|e| panic!("build.rs: failed to read {}: {}", archive.display(), e));
let mut hasher = std::hash::DefaultHasher::new();
std::hash::Hasher::write(&mut hasher, &bytes);
println!("cargo:rustc-env=TYPELISP_BUNDLED_STATICLIB_ID={:016x}", std::hash::Hasher::finish(&hasher));
println!("cargo:rustc-cfg=typelisp_bundled_runtime");
}
fn deployment_target() -> Option<String> {
println!("cargo:rerun-if-env-changed=MACOSX_DEPLOYMENT_TARGET");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") {
return None;
}
let version = match std::env::var("MACOSX_DEPLOYMENT_TARGET") {
Ok(version) => {
assert!(is_version(&version), "build.rs: MACOSX_DEPLOYMENT_TARGET `{}` is not a version like 10.12", version);
version
}
Err(_) => match std_deployment_target() {
Ok(version) => version,
Err(why) => {
println!(
"cargo:warning=MACOSX_DEPLOYMENT_TARGET is not set and std's minimum macOS version \
could not be read ({}); executables `typl -c` builds may be linked with warnings. \
Set MACOSX_DEPLOYMENT_TARGET to choose one",
why
);
return None;
}
},
};
println!("cargo:rustc-env=TYPELISP_MACOSX_DEPLOYMENT_TARGET={}", version);
Some(version)
}
fn is_version(version: &str) -> bool {
!version.is_empty() && version.split('.').all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
}
fn std_deployment_target() -> Result<String, String> {
let rustc = std::env::var("RUSTC").map_err(|_| "cargo did not set RUSTC".to_string())?;
let target = std::env::var("TARGET").map_err(|_| "cargo did not set TARGET".to_string())?;
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").map_err(|_| "cargo did not set OUT_DIR".to_string())?);
let sysroot = run(std::process::Command::new(&rustc).args(["--print", "sysroot"]))?;
let libdir = std::path::Path::new(sysroot.trim()).join("lib").join("rustlib").join(&target).join("lib");
let rlibs: Vec<_> = std::fs::read_dir(&libdir)
.map_err(|e| format!("cannot read {}: {}", libdir.display(), e))?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.file_name().and_then(|n| n.to_str()).is_some_and(|n| n.starts_with("libstd-") && n.ends_with(".rlib")))
.collect();
let [rlib] = rlibs.as_slice() else {
return Err(format!("expected one libstd rlib in {}, found {}", libdir.display(), rlibs.len()));
};
let work = out_dir.join("std-objects");
let _ = std::fs::remove_dir_all(&work);
std::fs::create_dir_all(&work).map_err(|e| format!("cannot create {}: {}", work.display(), e))?;
run(std::process::Command::new("ar").arg("x").arg(rlib).current_dir(&work))?;
let mut versions = std::collections::BTreeSet::new();
for entry in std::fs::read_dir(&work).map_err(|e| format!("cannot read {}: {}", work.display(), e))? {
let path = entry.map_err(|e| format!("cannot read {}: {}", work.display(), e))?.path();
if path.extension().and_then(|e| e.to_str()) != Some("o") {
continue;
}
let load_commands = run(std::process::Command::new("otool").arg("-l").arg(&path))?;
versions.insert(minimum_version(&load_commands).ok_or_else(|| format!("{} names no minimum macOS version", path.display()))?);
}
let _ = std::fs::remove_dir_all(&work);
match versions.len() {
1 => Ok(versions.into_iter().next().expect("one version")),
0 => Err(format!("{} holds no object files", rlib.display())),
_ => Err(format!("the objects of {} name more than one version: {:?}", rlib.display(), versions)),
}
}
fn minimum_version(load_commands: &str) -> Option<String> {
let mut prev = "";
for line in load_commands.lines() {
let mut words = line.split_whitespace();
let (Some(key), Some(value)) = (words.next(), words.next()) else { continue };
if key == "minos" || (key == "version" && prev == "cmdsize") {
return Some(value.to_string()).filter(|v| is_version(v));
}
prev = key;
}
None
}
fn run(command: &mut std::process::Command) -> Result<String, String> {
let output = command.output().map_err(|e| format!("cannot run {:?}: {}", command.get_program(), e))?;
if !output.status.success() {
return Err(format!("{:?} failed ({})", command.get_program(), output.status));
}
String::from_utf8(output.stdout).map_err(|_| format!("{:?} printed something that is not UTF-8", command.get_program()))
}