Add misses init command with binary and library scaffold modes

`misses init [--lib] [dir]` scaffolds a new proc-macro crate that uses
misses as its build toolchain. Two modes are supported:

Binary mode (default):
  The .mrs source files live in src/macros/ and are compiled to
  $OUT_DIR at build time via a generated build.rs. The generated
  Rust code is never committed — users must have misses installed
  to build from source.

Library mode (--lib):
  The generated code is committed to generated/ so downstream users
  of the crate need no misses install at all. The .mrs source and
  build.rs are still present for the library author, who can
  regenerate via `cargo build --features misses-source`. The
  misses-source feature makes misses an optional build-dependency
  and gates the regeneration path in build.rs with #[cfg(feature)].

The command also teaches `main.rs` a proper subcommand dispatch so
that `misses <file.mrs>` (compile) and `misses init` are cleanly
separated.

https://claude.ai/code/session_01D2g6zGJsBeUEzxBb7AvFDt
This commit is contained in:
Claude
2026-03-08 09:30:14 +00:00
parent 0ac3a473d4
commit 3be88ec21f
2 changed files with 262 additions and 12 deletions
+222
View File
@@ -0,0 +1,222 @@
use std::path::Path;
/// Starter macro source written into every scaffolded project.
const HELLO_MRS: &str = r#"// hello.mrs — created by `misses init`
// Edit this file, then run `misses src/macros/hello.mrs src/generated/hello.rs`
// (or `cargo build --features misses-source`) to regenerate.
macro hello! {
$name:ident => {
T~{
pub fn hello() -> &'static str {
"hello from macro!"
}
}
}
}
"#;
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
pub fn run(dir: &str, is_lib: bool) {
let dir = Path::new(dir);
let crate_name = infer_crate_name(dir);
let src_dir = dir.join("src");
let src_macros = src_dir.join("macros");
let gen_dir = dir.join("generated");
create_dir(&src_macros);
if is_lib {
create_dir(&gen_dir);
}
write_file(&dir.join("Cargo.toml"), &cargo_toml(&crate_name, is_lib));
write_file(&dir.join("build.rs"), if is_lib { lib_build_rs() } else { bin_build_rs() });
write_file(&src_macros.join("hello.mrs"), HELLO_MRS);
if is_lib {
// Pre-generate hello.rs so users can build without the misses-source feature.
let ast = misses::parse(HELLO_MRS)
.unwrap_or_else(|e| die(&format!("internal: failed to parse starter macro: {e}")));
let generated = misses::codegen(&ast);
write_file(&gen_dir.join("hello.rs"), &generated);
write_file(&src_dir.join("lib.rs"), lib_rs_library_mode());
} else {
write_file(&src_dir.join("lib.rs"), bin_rs_binary_mode());
}
println!("Scaffolded misses project in `{}`", dir.display());
if is_lib {
println!();
println!(" Mode: library");
println!(" - generated/ is committed so downstream users need no misses install.");
println!(" - To rebuild from .mrs source:");
println!(" cargo build --features misses-source");
} else {
println!();
println!(" Mode: binary");
println!(" - .mrs sources live in src/macros/ and are compiled at build time.");
println!(" - Generated Rust code goes to $OUT_DIR (never committed).");
}
}
// ---------------------------------------------------------------------------
// Scaffold file contents
// ---------------------------------------------------------------------------
fn cargo_toml(crate_name: &str, is_lib: bool) -> String {
if is_lib {
format!(
r#"[package]
name = "{crate_name}"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
# Enable this feature to rebuild the generated code from .mrs source files.
# Downstream users of your crate do NOT need this feature; they use the
# pre-generated code in generated/ that you commit alongside your source.
[features]
misses-source = ["dep:misses"]
[build-dependencies]
misses = {{ version = "0.1", optional = true }}
"#,
crate_name = crate_name
)
} else {
format!(
r#"[package]
name = "{crate_name}"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[build-dependencies]
misses = "0.1"
"#,
crate_name = crate_name
)
}
}
/// build.rs for binary mode: always compile .mrs → OUT_DIR at build time.
fn bin_build_rs() -> &'static str {
r#"fn main() {
let src_dir = std::path::Path::new("src/macros");
let out_dir = std::env::var("OUT_DIR").unwrap();
let out_dir = std::path::Path::new(&out_dir);
for entry in std::fs::read_dir(src_dir).expect("src/macros not found") {
let path = entry.unwrap().path();
if path.extension().and_then(|e| e.to_str()) == Some("mrs") {
let stem = path.file_stem().unwrap().to_str().unwrap().to_owned();
let src = std::fs::read_to_string(&path).unwrap();
let ast = misses::parse(&src)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display()));
let generated = misses::codegen(&ast);
std::fs::write(out_dir.join(format!("{stem}.rs")), generated).unwrap();
println!("cargo:rerun-if-changed={}", path.display());
}
}
println!("cargo:rerun-if-changed=build.rs");
}
"#
}
/// build.rs for library mode: only regenerate when the `misses-source` feature is active.
fn lib_build_rs() -> &'static str {
r#"fn main() {
// Only re-generate when the author explicitly opts in with --features misses-source.
// Downstream users build against the pre-generated files in generated/.
#[cfg(feature = "misses-source")]
regen();
}
#[cfg(feature = "misses-source")]
fn regen() {
let src_dir = std::path::Path::new("src/macros");
let gen_dir = std::path::Path::new("generated");
std::fs::create_dir_all(gen_dir).unwrap();
for entry in std::fs::read_dir(src_dir).expect("src/macros not found") {
let path = entry.unwrap().path();
if path.extension().and_then(|e| e.to_str()) == Some("mrs") {
let stem = path.file_stem().unwrap().to_str().unwrap().to_owned();
let src = std::fs::read_to_string(&path).unwrap();
let ast = misses::parse(&src)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display()));
let generated = misses::codegen(&ast);
std::fs::write(gen_dir.join(format!("{stem}.rs")), &generated).unwrap();
println!("cargo:rerun-if-changed={}", path.display());
}
}
println!("cargo:rerun-if-changed=build.rs");
}
"#
}
/// lib.rs for library mode: include from the committed generated/ directory.
fn lib_rs_library_mode() -> &'static str {
r#"// This file is generated by misses. The generated/ directory is committed
// so that downstream users do not need misses installed.
// To regenerate: cargo build --features misses-source
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/generated/hello.rs"));
"#
}
/// lib.rs for binary mode: include from Cargo's OUT_DIR (never committed).
fn bin_rs_binary_mode() -> &'static str {
r#"// This file is generated by misses via build.rs.
// The generated code lives in $OUT_DIR and is never committed to git.
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
"#
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn infer_crate_name(dir: &Path) -> String {
let name = if dir == Path::new(".") {
std::env::current_dir()
.unwrap()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_owned()
} else {
dir.file_name()
.unwrap_or_else(|| die("cannot determine crate name from path"))
.to_str()
.unwrap()
.to_owned()
};
// Cargo convention: hyphens in package names, underscores in crate names.
name.replace('-', "_")
}
fn create_dir(path: &Path) {
std::fs::create_dir_all(path).unwrap_or_else(|e| die(&format!("create {}: {e}", path.display())));
}
fn write_file(path: &Path, contents: &str) {
std::fs::write(path, contents)
.unwrap_or_else(|e| die(&format!("write {}: {e}", path.display())));
println!(" wrote {}", path.display());
}
fn die(msg: &str) -> ! {
eprintln!("error: {msg}");
std::process::exit(1);
}
+40 -12
View File
@@ -1,17 +1,35 @@
mod init;
use std::path::PathBuf;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: misses <input.mrs> [output.rs]");
std::process::exit(1);
}
match args.get(1).map(String::as_str) {
Some("init") => {
let is_lib = args.iter().any(|a| a == "--lib");
let dir = args
.iter()
.skip(2)
.find(|a| !a.starts_with('-'))
.map(String::as_str)
.unwrap_or(".");
init::run(dir, is_lib);
}
Some(_) => compile(&args),
None => {
print_usage();
std::process::exit(1);
}
}
}
fn compile(args: &[String]) {
let input_path = PathBuf::from(&args[1]);
let src = match std::fs::read_to_string(&input_path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading {:?}: {}", input_path, e);
eprintln!("error reading {}: {}", input_path.display(), e);
std::process::exit(1);
}
};
@@ -19,7 +37,7 @@ fn main() {
let ast = match misses::parse(&src) {
Ok(a) => a,
Err(e) => {
eprintln!("Parse error: {}", e);
eprintln!("parse error: {}", e);
std::process::exit(1);
}
};
@@ -28,14 +46,24 @@ fn main() {
if args.len() >= 3 {
let output_path = PathBuf::from(&args[2]);
match std::fs::write(&output_path, &generated) {
Ok(_) => {}
Err(e) => {
eprintln!("Error writing {:?}: {}", output_path, e);
std::process::exit(1);
}
if let Err(e) = std::fs::write(&output_path, &generated) {
eprintln!("error writing {}: {}", output_path.display(), e);
std::process::exit(1);
}
} else {
print!("{}", generated);
}
}
fn print_usage() {
eprintln!("Usage:");
eprintln!(" misses <input.mrs> [output.rs] Compile a .mrs file to Rust");
eprintln!(" misses init [--lib] [dir] Scaffold a new misses project");
eprintln!();
eprintln!("Modes for `misses init`:");
eprintln!(" (default) Binary — .mrs sources are compiled at build time via build.rs;");
eprintln!(" generated code lives in \\$OUT_DIR and is never committed.");
eprintln!(" --lib Library — generated code is committed to generated/ so");
eprintln!(" downstream users need no misses install. Rebuild with");
eprintln!(" `cargo build --features misses-source`.");
}