use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use crate::platform::BinarySet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileContent {
Text(String),
Copy(Utf8PathBuf),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackagedFile {
pub path: Utf8PathBuf,
pub content: FileContent,
}
impl PackagedFile {
pub fn text(path: impl Into<Utf8PathBuf>, contents: impl Into<String>) -> Self {
Self {
path: normalize_separators(path.into()),
content: FileContent::Text(contents.into()),
}
}
pub fn copy(path: impl Into<Utf8PathBuf>, source: impl Into<Utf8PathBuf>) -> Self {
Self {
path: normalize_separators(path.into()),
content: FileContent::Copy(source.into()),
}
}
pub fn is_binary(&self) -> bool {
matches!(self.content, FileContent::Copy(_))
}
}
fn normalize_separators(path: Utf8PathBuf) -> Utf8PathBuf {
if cfg!(windows) {
Utf8PathBuf::from(path.into_string().replace('\\', "/"))
} else {
path
}
}
#[derive(Debug, Clone, Copy)]
pub struct PackageContext<'a> {
pub binaries: &'a BinarySet,
pub input_basename: Option<&'a str>,
}
pub fn write_package(files: &[PackagedFile]) -> Result<()> {
for file in files {
if let Some(parent) = file.path.parent() {
std::fs::create_dir_all(parent.as_std_path())
.with_context(|| format!("failed to create directory {parent}"))?;
}
match &file.content {
FileContent::Text(contents) => {
std::fs::write(file.path.as_std_path(), contents)
.with_context(|| format!("failed to write {}", file.path))?;
}
FileContent::Copy(source) => copy_binary(source, &file.path)?,
}
}
Ok(())
}
fn copy_binary(source: &Utf8Path, dest: &Utf8Path) -> Result<()> {
std::fs::copy(source.as_std_path(), dest.as_std_path())
.with_context(|| format!("failed to copy native library {source} -> {dest}"))?;
Ok(())
}
pub fn summarize(files: &[PackagedFile]) -> (usize, usize) {
let binaries = files.iter().filter(|f| f.is_binary()).count();
(files.len() - binaries, binaries)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_package_writes_text_and_copies_binaries() {
let dir = tempfile::tempdir().unwrap();
let root = Utf8Path::from_path(dir.path()).unwrap();
let src = root.join("src-lib.bin");
std::fs::write(src.as_std_path(), b"\x00native\x01").unwrap();
let files = vec![
PackagedFile::text(root.join("pkg/manifest.json"), "{\"name\":\"x\"}"),
PackagedFile::copy(root.join("pkg/native/lib.bin"), src.clone()),
];
write_package(&files).unwrap();
assert_eq!(
std::fs::read_to_string(root.join("pkg/manifest.json")).unwrap(),
"{\"name\":\"x\"}"
);
assert_eq!(
std::fs::read(root.join("pkg/native/lib.bin")).unwrap(),
b"\x00native\x01"
);
assert_eq!(summarize(&files), (1, 1));
}
#[test]
fn destination_paths_are_forward_slashed() {
let text = PackagedFile::text(Utf8Path::new("out").join("dotnet").join("x.cs"), "x");
assert_eq!(text.path.as_str(), "out/dotnet/x.cs");
let copied = PackagedFile::copy(
Utf8Path::new("out")
.join("runtimes")
.join("osx-arm64")
.join("native"),
"/src/libcalculator.dylib",
);
assert_eq!(copied.path.as_str(), "out/runtimes/osx-arm64/native");
}
#[test]
fn missing_binary_source_is_an_error() {
let dir = tempfile::tempdir().unwrap();
let root = Utf8Path::from_path(dir.path()).unwrap();
let files = vec![PackagedFile::copy(
root.join("pkg/native/lib.bin"),
root.join("does-not-exist.bin"),
)];
let err = write_package(&files).unwrap_err();
assert!(err.to_string().contains("failed to copy native library"));
}
}