use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Symbols {
Stripped,
Kept,
Split,
}
const KEPT_WORD: &str = "debug";
const SPLIT_WORD: &str = "release";
pub(super) const SYMBOLS_DIR: &str = yog_ids::symbols::DIR;
const DEBUG_EXT: &str = yog_ids::symbols::EXT;
const STRIPPED_DIR: &str = "stripped";
const OBJCOPY: &str = "rust-objcopy";
impl Symbols {
pub(crate) fn of(word: Option<&str>) -> Symbols {
match word {
Some(KEPT_WORD) => Symbols::Kept,
Some(SPLIT_WORD) => Symbols::Split,
_ => Symbols::Stripped,
}
}
pub(crate) fn word(self) -> Option<&'static str> {
match self {
Symbols::Stripped => None,
Symbols::Kept => Some(KEPT_WORD),
Symbols::Split => Some(SPLIT_WORD),
}
}
pub(super) fn profile(self) -> &'static [(&'static str, &'static str)] {
match self {
Symbols::Stripped => &[],
Symbols::Kept => &[
("CARGO_PROFILE_RELEASE_DEBUG", "true"),
("CARGO_PROFILE_RELEASE_STRIP", "none"),
("CARGO_PROFILE_RELEASE_OPT_LEVEL", "0"),
("CARGO_PROFILE_RELEASE_LTO", "false"),
("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "256"),
("CARGO_PROFILE_RELEASE_INCREMENTAL", "true"),
],
Symbols::Split => &[
("CARGO_PROFILE_RELEASE_DEBUG", "true"),
("CARGO_PROFILE_RELEASE_STRIP", "none"),
],
}
}
}
pub(super) fn split(
bundled: Vec<(String, PathBuf)>,
build_dir: &Path,
artifacts: &Path,
) -> Result<Vec<(String, PathBuf)>, String> {
let objcopy = objcopy()?;
bundled
.into_iter()
.map(|(tag, native)| {
let name = native.file_name().ok_or("a native without a name")?;
let symbols = artifacts.join(SYMBOLS_DIR).join(&tag);
let stripped = build_dir.join(STRIPPED_DIR).join(&tag);
for dir in [&symbols, &stripped] {
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
}
let debug = symbols.join(name).with_added_extension(DEBUG_EXT);
let out = stripped.join(name);
run(
&objcopy,
&[
"--only-keep-debug".as_ref(),
native.as_os_str(),
debug.as_os_str(),
],
)?;
run(
&objcopy,
&[
"--strip-debug".as_ref(),
native.as_os_str(),
out.as_os_str(),
],
)?;
Ok((tag, out))
})
.collect()
}
fn run(program: &Path, args: &[&std::ffi::OsStr]) -> Result<(), String> {
let ok = Command::new(program)
.args(args)
.status()
.is_ok_and(|s| s.success());
ok.then_some(())
.ok_or_else(|| format!("{} {:?} failed", program.display(), args))
}
fn objcopy() -> Result<PathBuf, String> {
let out = Command::new("rustc")
.args(["--print", "sysroot"])
.output()
.map_err(|e| e.to_string())?;
let sysroot = PathBuf::from(String::from_utf8_lossy(&out.stdout).trim());
let rustlib = sysroot.join("lib").join("rustlib");
let file = format!("{OBJCOPY}{}", std::env::consts::EXE_SUFFIX);
std::fs::read_dir(&rustlib)
.map_err(|e| format!("{}: {e}", rustlib.display()))?
.flatten()
.map(|host| host.path().join("bin").join(&file))
.find(|p| p.is_file())
.ok_or_else(|| {
format!(
"no {OBJCOPY} in {} — the Rust toolchain ships it",
rustlib.display()
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_release_native_is_split_from_its_debug_info() {
let exe = std::env::current_exe().unwrap();
let dir = std::env::temp_dir().join(format!("yog-symbols-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let tag = "test-platform".to_string();
let out = split(
vec![(tag.clone(), exe.clone())],
&dir.join("build"),
&dir.join("artifacts"),
)
.unwrap();
let size = |p: &Path| std::fs::metadata(p).unwrap().len();
let name = exe.file_name().unwrap();
let debug = dir
.join("artifacts")
.join(SYMBOLS_DIR)
.join(&tag)
.join(name)
.with_added_extension(DEBUG_EXT);
assert!(size(&debug) > 0);
assert!(
size(&out[0].1) < size(&exe),
"the packaged copy lost its debug info"
);
assert!(exe.is_file(), "the built file stays");
let _ = std::fs::remove_dir_all(&dir);
}
}