feat(scaffolding): add Rust package generator

This commit is contained in:
alexion committed 2026-10-24 11:12:19 -04:00
1 parent d342370b89
commit a5bc1d5303
12 files changed
+387

No files matched your search

+192
View File
@@ -0,0 +1,192 @@
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<PathBuf> {
scaffold(&workspace_root()?, destination)
}
fn scaffold(workspace_root: &Path, destination: &Path) -> io::Result<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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<String> {
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<String> {
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<RenderedFile> {
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<String> {
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;