use std::env; use std::fs; use std::io; use std::path::{Path, PathBuf}; const BUILD_TEMPLATE: &str = include_str!("templates/BUILD.template"); const CARGO_TEMPLATE: &str = include_str!("templates/Cargo.toml.template"); const LIBRARY_TEMPLATE: &str = include_str!("templates/library.rs.template"); const BINARY_TEMPLATE: &str = include_str!("templates/binary.rs.template"); const TEST_TEMPLATE: &str = include_str!("templates/test.rs.template"); const CRATE_NAME_PLACEHOLDER: &str = "{CRATE_NAME}"; /// Creates a Rust package at `destination`. pub fn run(destination: &Path) -> io::Result { scaffold(&workspace_root()?, destination) } fn scaffold(workspace_root: &Path, destination: &Path) -> io::Result { let workspace_root = fs::canonicalize(workspace_root)?; let destination = resolve_destination(&workspace_root, destination)?; let crate_name = crate_name(&destination)?; let workspace_member = workspace_member(&workspace_root, &destination)?; let files = render_files(&crate_name); let cargo_path = workspace_root.join("Cargo.toml"); let cargo_manifest = fs::read_to_string(&cargo_path)?; let cargo_manifest = update_workspace_members(&cargo_manifest, &workspace_member)?; fs::create_dir(&destination)?; for file in files { fs::write(destination.join(file.name), file.contents)?; } fs::write(cargo_path, cargo_manifest)?; Ok(destination) } fn workspace_root() -> io::Result { match env::var_os("BUILD_WORKSPACE_DIRECTORY") { Some(path) => Ok(PathBuf::from(path)), None => env::current_dir(), } } fn resolve_destination(workspace_root: &Path, destination: &Path) -> io::Result { let destination = if destination.is_absolute() { destination.to_path_buf() } else { workspace_root.join(destination) }; let parent = destination.parent().ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, "the destination must have a parent directory", ) })?; let parent = fs::canonicalize(parent)?; if !parent.starts_with(workspace_root) { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!( "the destination is outside the workspace: {}", destination.display() ), )); } let destination = parent.join(destination.file_name().ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, "the destination must name a directory", ) })?); if destination.try_exists()? { return Err(io::Error::new( io::ErrorKind::AlreadyExists, format!("the destination already exists: {}", destination.display()), )); } Ok(destination) } fn crate_name(destination: &Path) -> io::Result { let crate_name = destination .file_name() .and_then(|name| name.to_str()) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, "the destination name must be valid UTF-8", ) })?; if crate_name.is_empty() || !crate_name.bytes().all(|character| { character.is_ascii_lowercase() || character.is_ascii_digit() || character == b'_' }) { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("invalid crate name: {crate_name}"), )); } Ok(crate_name.to_owned()) } fn workspace_member(workspace_root: &Path, destination: &Path) -> io::Result { let member = destination.strip_prefix(workspace_root).map_err(|_| { io::Error::new( io::ErrorKind::InvalidInput, "the destination is outside the workspace", ) })?; let member = member.to_str().ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, "the destination must be valid UTF-8", ) })?; Ok(member.replace(std::path::MAIN_SEPARATOR, "/")) } fn render_files(crate_name: &str) -> Vec { vec![ render_file("BUILD", BUILD_TEMPLATE, crate_name), render_file("Cargo.toml", CARGO_TEMPLATE, crate_name), render_file(&format!("{crate_name}.rs"), LIBRARY_TEMPLATE, crate_name), render_file( &format!("{crate_name}_main.rs"), BINARY_TEMPLATE, crate_name, ), render_file(&format!("{crate_name}_test.rs"), TEST_TEMPLATE, crate_name), ] } fn render_file(name: &str, template: &str, crate_name: &str) -> RenderedFile { RenderedFile { name: name.to_owned(), contents: template.replace(CRATE_NAME_PLACEHOLDER, crate_name), } } fn update_workspace_members(manifest: &str, member: &str) -> io::Result { let workspace_start = manifest.find("[workspace]").ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, "the root Cargo.toml has no [workspace] section", ) })?; let members_start = manifest[workspace_start..] .find("members = [") .map(|offset| workspace_start + offset) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, "the root Cargo.toml has no workspace members array", ) })?; let members_end = manifest[members_start..] .find(']') .map(|offset| members_start + offset) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, "the root Cargo.toml has an unclosed workspace members array", ) })?; if manifest[members_start..members_end].contains(&format!("\"{member}\"")) { return Err(io::Error::new( io::ErrorKind::AlreadyExists, format!("the workspace already has member: {member}"), )); } let mut manifest = manifest.to_owned(); manifest.insert_str(members_end, &format!(" \"{member}\",\n")); Ok(manifest) } struct RenderedFile { name: String, contents: String, } #[cfg(test)] #[path = "rust_test.rs"] mod rust_test;