use std::{
collections::BTreeMap,
path::{Path, PathBuf},
process::Command,
};
use crate::{config::StaticFile, error::PersistError};
pub struct Vfs {
fs: BTreeMap<PathBuf, String>,
}
impl Vfs {
pub fn empty() -> Self {
Self {
fs: BTreeMap::new(),
}
}
pub(crate) fn rustfmt(path: impl AsRef<Path>) -> bool {
let path = path.as_ref();
if Command::new("rustfmt").arg("--version").output().is_err() {
return true;
}
Command::new("rustfmt")
.args([
"--edition",
"2024",
path.join("src/lib.rs").to_str().unwrap(),
])
.status()
.unwrap()
.success()
}
pub fn add(&mut self, path: impl Into<PathBuf>, content: proc_macro2::TokenStream) {
let path_buf = path.into();
let warning = "// This file was generated with `cornucopia`. Do not modify.\n\n";
let syntax_tree = syn::parse2(content).unwrap_or_else(|_| {
panic!(
"Failed to parse generated code. Trying to generate '{}'",
path_buf.display()
)
});
let formatted = prettyplease::unparse(&syntax_tree);
let file_content = format!("{warning}{formatted}");
assert!(self.fs.insert(path_buf, file_content).is_none())
}
pub fn add_string(&mut self, path: impl Into<PathBuf>, content: impl Into<String>) {
assert!(self.fs.insert(path.into(), content.into()).is_none())
}
pub fn persist(
self,
destination: impl AsRef<Path>,
static_files: Vec<StaticFile>,
) -> Result<(), PersistError> {
let destination = destination.as_ref();
let tmp = tempfile::tempdir().map_err(PersistError::wrap("tempfile"))?;
for (path, content) in self.fs {
let path = tmp.path().join(path);
let parent = path
.parent()
.expect("Must at least has 'destination' as parent");
std::fs::create_dir_all(parent).ok(); std::fs::write(&path, content).map_err(PersistError::wrap(path))?;
}
if !static_files.is_empty() {
for file in static_files {
let (source_path, destination_path, hard_link) = match file {
StaticFile::Simple(path) => (path, None, false),
StaticFile::Detailed {
path,
destination,
hard_link,
} => (path, destination, hard_link),
};
if !source_path.exists() {
return Err(PersistError::wrap(source_path)(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Static file not found",
)));
}
let destination_path = if let Some(dest) = destination_path {
dest
} else {
let target = source_path.file_name().ok_or_else(|| {
PersistError::wrap(&source_path)(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid file name",
))
})?;
PathBuf::from(target)
};
let dst = tmp.path().join(destination_path);
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent).map_err(PersistError::wrap(parent))?;
}
if hard_link {
std::fs::hard_link(&source_path, dst)
.map_err(PersistError::wrap(&source_path))?;
} else {
std::fs::copy(&source_path, dst).map_err(PersistError::wrap(&source_path))?;
}
}
}
Vfs::rustfmt(tmp.path());
fn copy_dir_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
let src = src.as_ref();
let dst = dst.as_ref();
if src.is_dir() {
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let path = entry.path();
let dst_path = dst.join(path.file_name().expect("file name exists"));
if ty.is_dir() {
std::fs::create_dir_all(&dst_path)?;
copy_dir_recursive(&path, &dst_path)?;
} else if ty.is_file() {
std::fs::copy(&path, &dst_path)?;
}
}
}
Ok(())
}
let backup_dir = if destination.exists() {
let backup = tempfile::tempdir().map_err(PersistError::wrap("backup tempfile"))?;
let backup_path = backup.path();
copy_dir_recursive(destination, backup_path)
.map_err(PersistError::wrap("backing up existing destination"))?;
std::fs::remove_dir_all(destination).map_err(PersistError::wrap(destination))?;
Some(backup)
} else {
None
};
if cfg!(not(target_os = "windows")) {
std::fs::create_dir_all(destination).map_err(PersistError::wrap(destination))?;
}
let result = match std::fs::rename(tmp.path(), destination) {
Ok(_) => Ok(()), Err(e) if e.raw_os_error() == Some(18) => {
copy_dir_recursive(tmp.path(), destination).map_err(PersistError::wrap(destination))
}
Err(e) => Err(PersistError::wrap(destination)(e)),
};
if result.is_err()
&& let Some(backup_dir) = backup_dir
{
if destination.exists() {
let _ = std::fs::remove_dir_all(destination);
}
let _ = std::fs::create_dir_all(destination);
if let Err(restore_err) = copy_dir_recursive(backup_dir.path(), destination) {
return Err(PersistError::wrap(
"failed to restore backup after generation error",
)(restore_err));
}
}
result
}
}