use std::collections::BTreeMap;
use std::path::Path;
use sha2::{Digest, Sha256};
use crate::artifact::{NativeArtifacts, NativeLib, OutDirFile};
use crate::error::Context;
pub fn capture_native_artifacts(
crate_name: &str,
build_script_out_dir: Option<&Path>,
) -> crate::error::Result<Option<NativeArtifacts>> {
let Some(out_dir) = build_script_out_dir else {
return Ok(None);
};
let build_dir = out_dir.parent().ok_or_else(|| {
crate::stow_error!(
"build script OUT_DIR {} for {crate_name} has no parent build dir",
out_dir.display()
)
})?;
let output_path = build_dir.join("output");
if !output_path.exists() {
return Err(crate::stow_error!(
"build script output file {} for {crate_name} is missing",
output_path.display()
));
}
let raw = std::fs::read_to_string(&output_path)
.wrap_err_with(|| format!("read build script output {}", output_path.display()))?;
let cargo_directives = parse_build_script_output(&raw);
let (static_libs, out_dir_files) = if out_dir.exists() {
collect_out_dir(out_dir)?
} else {
(Vec::new(), Vec::new())
};
Ok(Some(NativeArtifacts {
static_libs,
cargo_directives,
dep_env_vars: BTreeMap::new(),
out_dir_files,
}))
}
#[must_use]
pub fn parse_build_script_output(raw: &str) -> Vec<String> {
raw.lines()
.filter_map(|line| {
let line = line.trim();
if !line.starts_with("cargo:")
|| line.starts_with("cargo:rerun-if-")
|| line.starts_with("cargo:warning=")
{
return None;
}
Some(line.to_owned())
})
.collect()
}
fn collect_out_dir(out_dir: &Path) -> crate::error::Result<(Vec<NativeLib>, Vec<OutDirFile>)> {
let mut stack = vec![out_dir.to_path_buf()];
let mut static_libs = Vec::new();
let mut out_dir_files = Vec::new();
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir)
.wrap_err_with(|| format!("read native out dir {}", dir.display()))?
{
let entry = entry.wrap_err_with(|| format!("read entry in {}", dir.display()))?;
let path = entry.path();
let file_type = entry
.file_type()
.wrap_err_with(|| format!("stat native out entry {}", path.display()))?;
if file_type.is_dir() {
stack.push(path);
continue;
}
if !file_type.is_file() {
continue;
}
let bytes = std::fs::read(&path)
.wrap_err_with(|| format!("read native artifact file {}", path.display()))?;
let sha256 = hex::encode(Sha256::digest(&bytes));
let relative_path = relative_path(out_dir, &path)?;
if path
.extension()
.and_then(|value| value.to_str())
.is_some_and(|ext| ext == "a" || ext == "lib")
{
let name = path
.file_name()
.and_then(|value| value.to_str())
.map(str::to_owned)
.ok_or_else(|| {
crate::stow_error!("native library path {} is not UTF-8", path.display())
})?;
static_libs.push(NativeLib {
name,
bytes_sha256: sha256.clone(),
});
}
out_dir_files.push(OutDirFile {
relative_path,
sha256,
});
}
}
out_dir_files.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
static_libs.sort_by(|left, right| left.name.cmp(&right.name));
Ok((static_libs, out_dir_files))
}
fn relative_path(root: &Path, path: &Path) -> crate::error::Result<String> {
let relative = path
.strip_prefix(root)
.map_err(|error| crate::stow_error!("strip native out dir prefix: {error}"))?;
let mut components = Vec::new();
for component in relative.components() {
let std::path::Component::Normal(name) = component else {
return Err(crate::stow_error!(
"native relative path {} has a non-normal component",
relative.display()
));
};
components.push(name.to_str().ok_or_else(|| {
crate::stow_error!("native relative path {} is not UTF-8", path.display())
})?);
}
Ok(components.join("/"))
}
#[cfg(test)]
mod tests {
use super::capture_native_artifacts;
#[test]
fn nested_out_dir_files_use_slash_separators_on_every_host() {
let build_dir = tempfile::tempdir().expect("tempdir");
let out_dir = build_dir.path().join("out");
std::fs::create_dir_all(out_dir.join("gen")).expect("create out dir");
std::fs::write(build_dir.path().join("output"), "cargo:rustc-cfg=demo\n")
.expect("write output");
std::fs::write(out_dir.join("gen").join("bindings.rs"), "pub fn f() {}\n")
.expect("write nested file");
let native = capture_native_artifacts("demo", Some(&out_dir))
.expect("capture")
.expect("out dir present");
assert_eq!(
native
.out_dir_files
.iter()
.map(|file| file.relative_path.as_str())
.collect::<Vec<_>>(),
vec!["gen/bindings.rs"]
);
}
}