use std::fs;
use std::io;
use std::path::{Path, PathBuf};
enum Entry {
File(PathBuf, Vec<u8>),
Dir(PathBuf),
#[cfg(unix)]
Symlink {
link: PathBuf,
target: PathBuf,
},
}
pub struct FileTree {
root: PathBuf,
entries: Vec<Entry>,
}
impl FileTree {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
entries: Vec::new(),
}
}
pub fn file(mut self, relative_path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
self.entries.push(Entry::File(
relative_path.into(),
contents.into().into_bytes(),
));
self
}
pub fn bytes(
mut self,
relative_path: impl Into<PathBuf>,
contents: impl Into<Vec<u8>>,
) -> Self {
self.entries
.push(Entry::File(relative_path.into(), contents.into()));
self
}
pub fn dir(mut self, relative_path: impl Into<PathBuf>) -> Self {
self.entries.push(Entry::Dir(relative_path.into()));
self
}
#[cfg_attr(not(unix), allow(unused_mut, unused_variables))]
pub fn symlink(mut self, link: impl Into<PathBuf>, target: impl Into<PathBuf>) -> Self {
#[cfg(unix)]
{
self.entries.push(Entry::Symlink {
link: link.into(),
target: target.into(),
});
}
self
}
pub fn build(self) -> io::Result<()> {
for e in &self.entries {
match e {
Entry::File(rel, bytes) => {
let target = self.root.join(rel);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&target, bytes)?;
}
Entry::Dir(rel) => {
fs::create_dir_all(self.root.join(rel))?;
}
#[cfg(unix)]
Entry::Symlink { link, target } => {
let link_path = self.root.join(link);
if let Some(parent) = link_path.parent() {
fs::create_dir_all(parent)?;
}
std::os::unix::fs::symlink(target, &link_path)?;
}
}
}
Ok(())
}
}
pub fn rust_crate(root: &Path, name: &str, version: &str) -> io::Result<()> {
let cargo = format!(
"[package]\nname = \"{}\"\nversion = \"{}\"\nedition = \"2021\"\n\n[lib]\npath = \"src/lib.rs\"\n",
name, version
);
FileTree::new(root)
.file("Cargo.toml", cargo)
.file("src/lib.rs", "//! Sample crate.\n")
.build()
}
pub fn rust_workspace(root: &Path, members: &[&str]) -> io::Result<()> {
let members_lines: String = members
.iter()
.map(|m| format!(" \"{}\",\n", m))
.collect();
let workspace_toml = format!(
"[workspace]\nresolver = \"2\"\nmembers = [\n{}]\n",
members_lines
);
FileTree::new(root)
.file("Cargo.toml", workspace_toml)
.build()?;
for m in members {
rust_crate(&root.join(m), m, "0.0.0")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_basic_tree() {
let dir = tempfile::tempdir().unwrap();
FileTree::new(dir.path())
.file("a.txt", "hello")
.dir("empty")
.file("nested/b.txt", "world")
.build()
.unwrap();
assert!(dir.path().join("a.txt").exists());
assert!(dir.path().join("empty").is_dir());
assert_eq!(
fs::read_to_string(dir.path().join("nested/b.txt")).unwrap(),
"world"
);
}
#[test]
fn rust_crate_layout() {
let dir = tempfile::tempdir().unwrap();
rust_crate(dir.path(), "sample", "0.1.0").unwrap();
let cargo = fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
assert!(cargo.contains("name = \"sample\""));
assert!(cargo.contains("version = \"0.1.0\""));
assert!(dir.path().join("src/lib.rs").exists());
}
#[test]
fn rust_workspace_layout() {
let dir = tempfile::tempdir().unwrap();
rust_workspace(dir.path(), &["alpha", "beta"]).unwrap();
let ws = fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
assert!(ws.contains("\"alpha\""));
assert!(ws.contains("\"beta\""));
assert!(dir.path().join("alpha/Cargo.toml").exists());
assert!(dir.path().join("beta/src/lib.rs").exists());
}
#[cfg(unix)]
#[test]
fn symlink_unix() {
let dir = tempfile::tempdir().unwrap();
FileTree::new(dir.path())
.file("real.txt", "data")
.symlink("link.txt", "real.txt")
.build()
.unwrap();
assert!(dir.path().join("link.txt").exists());
}
#[cfg(windows)]
#[test]
fn symlink_no_op_on_windows() {
let dir = tempfile::tempdir().unwrap();
FileTree::new(dir.path())
.symlink("link.txt", "real.txt")
.build()
.unwrap();
assert!(!dir.path().join("link.txt").exists());
}
}