feat(scaffolding): add Rust package generator
This commit is contained in:
1 parent
d342370b89
commit
a5bc1d5303
12 files changed
+387
No files matched your search
@@ -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;
|
||||
Reference in new issue
Block a user