use std::path::{Path, PathBuf};
use std::process::Command;
use yog_cli::manifest::to_snake_name;
use yog_ids::platform::{self, Platform};
pub(super) enum Builder {
Zig,
Cargo,
}
impl Builder {
pub(super) fn detect() -> Self {
if has_zigbuild() {
Builder::Zig
} else {
Builder::Cargo
}
}
pub(super) fn subcmd(&self) -> &'static str {
match self {
Builder::Zig => "zigbuild",
Builder::Cargo => "build",
}
}
pub(super) fn build(
&self,
build_dir: &Path,
triple: &str,
root: &Path,
debugging_symbols: bool,
) -> Result<(), ()> {
let mut cmd = Command::new("cargo");
cmd.current_dir(build_dir)
.env("CARGO_TARGET_DIR", root.join("target"))
.args([self.subcmd(), "--release", "--target", triple]);
if debugging_symbols {
cmd.env("CARGO_PROFILE_RELEASE_DEBUG", "true");
cmd.env("CARGO_PROFILE_RELEASE_STRIP", "none");
cmd.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "0");
cmd.env("CARGO_PROFILE_RELEASE_LTO", "false");
cmd.env("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "256");
cmd.env("CARGO_PROFILE_RELEASE_INCREMENTAL", "true");
}
let output = cmd.output();
match output {
Ok(o) => {
eprint!(
"{}",
filter_benign_warnings(&String::from_utf8_lossy(&o.stderr))
);
if o.status.success() { Ok(()) } else { Err(()) }
}
_ => Err(()),
}
}
}
pub(super) fn is_benign_warning(t: &str) -> bool {
t.starts_with("warning:")
&& ((t.contains("xcrun") && t.contains("failed"))
|| (t.contains("linker stderr")
&& t.contains("deprecated linker optimization setting")))
}
pub(super) fn filter_benign_warnings(stderr: &str) -> String {
let mut out = String::new();
let mut in_block = false;
let mut suppressed = 0usize;
for line in stderr.lines() {
let t = line.trim_start();
if is_benign_warning(t) {
in_block = true;
suppressed += 1;
continue;
}
if in_block {
if t == "|" || t.starts_with("= note:") || t.starts_with("= help:") || t.is_empty() {
continue;
}
in_block = false;
}
let only_one =
t.ends_with("generated 1 warning") || t.ends_with("generated 1 warning (1 duplicate)");
if suppressed > 0 && t.starts_with("warning:") && only_one {
suppressed -= 1;
continue;
}
out.push_str(line);
out.push('\n');
}
out
}
pub(super) fn selected_targets() -> Option<Vec<String>> {
let raw = std::env::var(yog_ids::env::TARGETS).ok()?;
let list: Vec<String> = raw
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
(!list.is_empty()).then_some(list)
}
pub(super) const RUST_TOOLCHAIN: &str = env!("CARGO_PKG_RUST_VERSION");
pub(super) fn generate_rust_toolchain() -> String {
let targets: Vec<String> = platform::ALL
.iter()
.map(|t| format!(" \"{}\",\n", t.triple))
.collect();
format!(
"# Generated by `yog build` — do not edit.\n\
[toolchain]\n\
channel = \"{RUST_TOOLCHAIN}\"\n\
profile = \"minimal\"\n\
targets = [\n{}]\n",
targets.concat()
)
}
pub(super) fn install_targets(targets: &[&Platform]) {
if targets.is_empty() {
return;
}
let lock = std::env::temp_dir().join("yog-rustup.lock");
let _lock = std::fs::File::create(&lock)
.ok()
.filter(|f| f.lock().is_ok());
let rustup = |args: &[&str]| {
Command::new("rustup")
.args(args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
};
let ok = match toolchain_sysroot() {
Some(sysroot) => {
let missing: Vec<&str> = targets
.iter()
.map(|t| t.triple)
.filter(|t| !has_std(&sysroot, t))
.collect();
missing.is_empty() || {
rustup(
&[
&["target", "remove", "--toolchain", RUST_TOOLCHAIN][..],
&missing,
]
.concat(),
);
rustup(
&[
&["target", "add", "--toolchain", RUST_TOOLCHAIN][..],
&missing,
]
.concat(),
)
}
}
None => {
let list: Vec<&str> = targets.iter().map(|t| t.triple).collect();
rustup(&[
"toolchain",
"install",
RUST_TOOLCHAIN,
"--profile",
"minimal",
"--no-self-update",
"--target",
&list.join(","),
])
}
};
if !ok {
eprintln!(" warning: could not install Rust {RUST_TOOLCHAIN} targets via rustup");
}
}
pub(super) fn toolchain_sysroot() -> Option<PathBuf> {
let out = Command::new("rustc")
.args([&format!("+{RUST_TOOLCHAIN}"), "--print", "sysroot"])
.output()
.ok()?;
out.status
.success()
.then(|| PathBuf::from(String::from_utf8_lossy(&out.stdout).trim()))
}
pub(super) fn has_std(sysroot: &Path, triple: &str) -> bool {
std::fs::read_dir(sysroot.join("lib/rustlib").join(triple).join("lib")).is_ok_and(|d| {
d.flatten()
.any(|e| e.file_name().to_string_lossy().starts_with("libstd-"))
})
}
pub(super) fn installed_targets() -> Vec<String> {
Command::new("rustup")
.args([
"target",
"list",
"--installed",
"--toolchain",
RUST_TOOLCHAIN,
])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
})
.unwrap_or_default()
}
pub(super) fn lib_filename(name: &str, os: &str) -> String {
let stem = to_snake_name(name);
match os {
"windows" => format!("{stem}.dll"),
"macos" => format!("lib{stem}.dylib"),
_ => format!("lib{stem}.so"),
}
}
pub(super) fn has_zigbuild() -> bool {
Command::new("cargo")
.args(["zigbuild", "--help"])
.output()
.is_ok_and(|o| o.status.success())
}
pub(super) fn first_line_of(program: &str, args: &[&str]) -> Option<String> {
let out = Command::new(program).args(args).output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
Some(text.lines().next().unwrap_or("").trim().to_string())
}
#[cfg(test)]
mod tests;