193 lines
6.1 KiB
Rust
193 lines
6.1 KiB
Rust
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;
|