228 lines
6.8 KiB
Rust
228 lines
6.8 KiB
Rust
use std::{
|
|
fs, io,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use crate::diag::Diagnostic;
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum ProjectTemplate {
|
|
Binary,
|
|
Library,
|
|
Workspace,
|
|
}
|
|
|
|
impl ProjectTemplate {
|
|
pub fn parse(value: &str) -> Option<Self> {
|
|
match value {
|
|
"binary" => Some(Self::Binary),
|
|
"library" => Some(Self::Library),
|
|
"workspace" => Some(Self::Workspace),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn create_project(
|
|
target: &str,
|
|
explicit_name: Option<&str>,
|
|
template: ProjectTemplate,
|
|
) -> Result<(), Diagnostic> {
|
|
if target.trim().is_empty() {
|
|
return Err(Diagnostic::new(
|
|
target,
|
|
"UsageError",
|
|
"project directory must not be empty",
|
|
));
|
|
}
|
|
|
|
let root = Path::new(target);
|
|
if root.is_file() {
|
|
return Err(Diagnostic::new(
|
|
target,
|
|
"ProjectScaffoldBlocked",
|
|
format!(
|
|
"cannot create project `{}` because a file exists there",
|
|
target
|
|
),
|
|
));
|
|
}
|
|
if root.is_dir() && has_entries(root)? {
|
|
return Err(Diagnostic::new(
|
|
target,
|
|
"ProjectScaffoldBlocked",
|
|
format!(
|
|
"cannot create project `{}` because the directory is not empty",
|
|
target
|
|
),
|
|
)
|
|
.hint("choose an empty directory or a new project path"));
|
|
}
|
|
|
|
let raw_name = explicit_name
|
|
.map(str::to_string)
|
|
.unwrap_or_else(|| basename(root).unwrap_or_else(|| "slovo-project".to_string()));
|
|
let name = sanitize_project_name(&raw_name);
|
|
if name.is_empty() {
|
|
return Err(Diagnostic::new(
|
|
target,
|
|
"InvalidProjectName",
|
|
"project name must contain at least one ASCII letter or digit",
|
|
));
|
|
}
|
|
|
|
match template {
|
|
ProjectTemplate::Binary => create_binary_project(root, target, &name),
|
|
ProjectTemplate::Library => create_library_project(root, target, &name),
|
|
ProjectTemplate::Workspace => create_workspace_project(root, target),
|
|
}
|
|
}
|
|
|
|
fn create_binary_project(root: &Path, target: &str, name: &str) -> Result<(), Diagnostic> {
|
|
let src = root.join("src");
|
|
create_dir_all_checked(&src, target)?;
|
|
write_checked(
|
|
&root.join("slovo.toml"),
|
|
&format!(
|
|
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
name
|
|
),
|
|
target,
|
|
)?;
|
|
write_checked(
|
|
&src.join("main.slo"),
|
|
"(module main)\n\n(fn main () -> i32\n 0)\n\n(test \"main returns zero\"\n (= (main) 0))\n",
|
|
target,
|
|
)
|
|
}
|
|
|
|
fn create_library_project(root: &Path, target: &str, name: &str) -> Result<(), Diagnostic> {
|
|
let src = root.join("src");
|
|
create_dir_all_checked(&src, target)?;
|
|
write_checked(
|
|
&root.join("slovo.toml"),
|
|
&format!(
|
|
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"lib\"\n",
|
|
name
|
|
),
|
|
target,
|
|
)?;
|
|
write_checked(
|
|
&src.join("lib.slo"),
|
|
"(module lib (export answer double))\n\n(fn answer () -> i32\n 42)\n\n(fn double ((value i32)) -> i32\n (+ value value))\n\n(test \"answer is stable\"\n (= (answer) 42))\n\n(test \"double works\"\n (= (double 21) 42))\n",
|
|
target,
|
|
)
|
|
}
|
|
|
|
fn create_workspace_project(root: &Path, target: &str) -> Result<(), Diagnostic> {
|
|
let app_src = root.join("packages/app/src");
|
|
let lib_src = root.join("packages/libutil/src");
|
|
create_dir_all_checked(&app_src, target)?;
|
|
create_dir_all_checked(&lib_src, target)?;
|
|
write_checked(
|
|
&root.join("slovo.toml"),
|
|
"[workspace]\nmembers = [\"packages/app\", \"packages/libutil\"]\ndefault_package = \"app\"\n",
|
|
target,
|
|
)?;
|
|
write_checked(
|
|
&root.join("packages/libutil/slovo.toml"),
|
|
"[package]\nname = \"libutil\"\nversion = \"0.1.0\"\n",
|
|
target,
|
|
)?;
|
|
write_checked(
|
|
&root.join("packages/libutil/src/libutil.slo"),
|
|
"(module libutil (export answer label))\n\n(fn answer () -> i32\n 42)\n\n(fn label () -> string\n \"libutil\")\n\n(test \"answer is stable\"\n (= (answer) 42))\n",
|
|
target,
|
|
)?;
|
|
write_checked(
|
|
&root.join("packages/app/slovo.toml"),
|
|
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nlibutil = { path = \"../libutil\" }\n",
|
|
target,
|
|
)?;
|
|
write_checked(
|
|
&root.join("packages/app/src/main.slo"),
|
|
"(module main)\n\n(import libutil.libutil (answer label))\n\n(fn main () -> i32\n (if (= (answer) 42)\n 0\n 1))\n\n(test \"app uses libutil\"\n (if (= (label) \"libutil\")\n (= (answer) 42)\n false))\n",
|
|
target,
|
|
)
|
|
}
|
|
|
|
fn has_entries(path: &Path) -> Result<bool, Diagnostic> {
|
|
let mut entries = fs::read_dir(path).map_err(|err| io_diagnostic(path, err))?;
|
|
match entries.next() {
|
|
Some(Ok(_)) => Ok(true),
|
|
Some(Err(err)) => Err(io_diagnostic(path, err)),
|
|
None => Ok(false),
|
|
}
|
|
}
|
|
|
|
fn basename(path: &Path) -> Option<String> {
|
|
path.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.map(str::to_string)
|
|
}
|
|
|
|
fn sanitize_project_name(value: &str) -> String {
|
|
let mut out = String::new();
|
|
let mut previous_dash = false;
|
|
for ch in value.chars().flat_map(char::to_lowercase) {
|
|
let mapped = if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
|
|
Some(ch)
|
|
} else if ch == '-' || ch == '_' || ch == '.' || ch.is_whitespace() {
|
|
Some('-')
|
|
} else {
|
|
None
|
|
};
|
|
let Some(ch) = mapped else {
|
|
continue;
|
|
};
|
|
if ch == '-' {
|
|
if !out.is_empty() && !previous_dash {
|
|
out.push('-');
|
|
previous_dash = true;
|
|
}
|
|
} else {
|
|
out.push(ch);
|
|
previous_dash = false;
|
|
}
|
|
}
|
|
while out.ends_with('-') {
|
|
out.pop();
|
|
}
|
|
if out
|
|
.chars()
|
|
.next()
|
|
.is_some_and(|first| first.is_ascii_digit())
|
|
{
|
|
out.insert_str(0, "project-");
|
|
}
|
|
out
|
|
}
|
|
|
|
fn create_dir_all_checked(path: &PathBuf, target: &str) -> Result<(), Diagnostic> {
|
|
fs::create_dir_all(path).map_err(|err| {
|
|
Diagnostic::new(
|
|
target,
|
|
"ProjectScaffoldBlocked",
|
|
format!("cannot create directory `{}`: {}", path.display(), err),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn write_checked(path: &Path, text: &str, target: &str) -> Result<(), Diagnostic> {
|
|
fs::write(path, text).map_err(|err| {
|
|
Diagnostic::new(
|
|
target,
|
|
"ProjectScaffoldBlocked",
|
|
format!("cannot write `{}`: {}", path.display(), err),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn io_diagnostic(path: &Path, err: io::Error) -> Diagnostic {
|
|
Diagnostic::new(
|
|
path.display().to_string(),
|
|
"ProjectScaffoldBlocked",
|
|
format!("cannot inspect `{}`: {}", path.display(), err),
|
|
)
|
|
}
|