commit 0649cc4916ba1ef924acd55eceba89df390dd605 from: Murilo Ijanc date: Sat Apr 25 19:34:52 2026 UTC Initial import of tpl(1). commit - /dev/null commit + 0649cc4916ba1ef924acd55eceba89df390dd605 blob - /dev/null blob + 567609b1234a9b8806c5a05da6c866e480aa148d (mode 644) --- /dev/null +++ .gitignore @@ -0,0 +1 @@ +build/ blob - /dev/null blob + 32f3292e31bbd5acf75b7671b679e9017828078c (mode 644) --- /dev/null +++ LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2026 Murilo Ijanc' + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. blob - /dev/null blob + aa24a519bdeb89af8dded0f39708872701525168 (mode 644) --- /dev/null +++ Makefile @@ -0,0 +1,59 @@ +# +# Copyright (c) 2026 Murilo Ijanc' +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +RUSTC ?= $(shell rustup which rustc 2>/dev/null || which rustc) +RUSTFLAGS ?= -C opt-level=2 -C strip=symbols +VERSION = 0.1.0 +PREFIX ?= /usr/local +MANDIR ?= $(PREFIX)/share/man + +BUILD = build +BIN = $(BUILD)/tpl +MAIN = tpl.rs + +CLIPPY ?= $(shell rustup which clippy-driver 2>/dev/null) +RUSTFMT ?= $(shell rustup which rustfmt 2>/dev/null) + +.PHONY: all clean install ci fmt-check clippy + +all: $(BIN) + +$(BIN): $(MAIN) + mkdir -p $(BUILD) + TPL_VERSION=$(VERSION) TMPDIR=/tmp $(RUSTC) --edition 2024 \ + --crate-type bin --crate-name tpl $(RUSTFLAGS) \ + -C link-arg=-lgit2 \ + -o $@ $< + +clean: + rm -rf $(BUILD) + +install: $(BIN) + install -d $(PREFIX)/bin $(MANDIR)/man1 + install -m 755 $(BIN) $(PREFIX)/bin/tpl + install -m 644 tpl.1 $(MANDIR)/man1/tpl.1 + +fmt-check: + $(RUSTFMT) --edition 2024 --check $(MAIN) + +clippy: + TPL_VERSION=$(VERSION) TMPDIR=/tmp $(CLIPPY) --edition 2024 \ + --crate-type bin --crate-name tpl \ + -C link-arg=-lgit2 \ + -W clippy::all -o /tmp/tpl.clippy $(MAIN) + @rm -f /tmp/tpl.clippy + +ci: fmt-check clippy $(BIN) blob - /dev/null blob + 44f3e3150d6626c5b05a6472c111904c6cf32dda (mode 644) --- /dev/null +++ README.md @@ -0,0 +1,110 @@ +tpl - minimal project template generator +======================================== +tpl is a minimal binary that scaffolds new Rust projects. +Each generated project is a single source file with an ISC +LICENSE, a rustc-based Makefile, a plain-text README and an +mdoc(7) manpage. + + +Requirements +------------ +In order to build tpl you need rustc and libgit2. + + +Installation +------------ +Edit Makefile to match your local setup (tpl is installed into +the /usr/local/bin namespace by default). + +Afterwards enter the following command to build and install +tpl: + + make clean install + + +Running tpl +----------- +Create a new binary project in ./foo/: + + tpl new bin foo + +Create a new library project in ./libfoo/: + + tpl new lib foo libfoo + +Set the project description (used as the manpage NAME line and +the README title): + + tpl new bin foo -d "say hi to the world" + +Vendor dependencies on creation by passing -D once per dep. +A bare name expands to ssh://ijanc@ijanc.org/; a full URL +is used as-is. Each -D triggers a marmita add inside the new +project, so marmita(1) must be on PATH: + + tpl new lib api -D http -D jackson + tpl new bin gst -D ssh://ijanc@ijanc.org/marmita + +Print the version: + + tpl -V + + +Generated files +--------------- +A binary project contains: + + foo.rs single-file Rust source with -V flag + Makefile rustc build with all/clean/install/ci targets + README.md plain-text overview + foo.1 mdoc(7) manpage in section 1 + LICENSE ISC license + .gitignore ignores build/ + +A library project contains the same set with foo.3 in place of +foo.1, a Makefile that builds an rlib, exposes test/doc/examples +targets, and a foo.rs that exposes a pub fn version(). + + +Git remotes +----------- +Every generated project is initialized as a git repository via +libgit2 with three default remotes: + + ijanc ssh://anon@ijanc.org/ + sr https://git.sr.ht/~ijanc/ + gh https://github.com/.git + + +Vendored dependencies +--------------------- +The generated bin Makefile auto-discovers vendored deps managed +by marmita(1). Adding vendor/jackson.rs is enough; the wildcard +rule compiles it into build/libjackson.rlib and passes +--extern jackson=... to the bin link. + +If the upstream repo ships a link.mk (e.g. for FFI crates that +need -lgit2), marmita copies it to vendor/jackson.mk and the +generated Makefile -includes it, appending to LINK_FLAGS and +BUILD_ENV automatically. + +marmita(1) is available at: + + https://got.ijanc.org/?action=summary&path=marmita.git + + +Philosophy +---------- +Generated scaffolds favour a single source file over a tree of +modules. No Cargo.toml, no proc-macros, no template engine. +Strings are rendered with std::str::replace. + + +Download +-------- + got clone ssh://ijanc@ijanc.org/tpl + + +License +------- +ISC -- see LICENSE. blob - /dev/null blob + 4e5a0421538fb9178c64bf29f61959d0fe1b29bc (mode 644) --- /dev/null +++ tpl.1 @@ -0,0 +1,181 @@ +.\" +.\" Copyright (c) 2026 Murilo Ijanc' +.\" +.\" Permission to use, copy, modify, and/or distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate: April 25 2026 $ +.Dt TPL 1 +.Os +.Sh NAME +.Nm tpl +.Nd minimal project template generator +.Sh SYNOPSIS +.Nm tpl +.Fl V +.Nm tpl +.Cm new +.Cm bin +.Op Fl d Ar description +.Op Fl D Ar dep +.Ar ... +.Ar name +.Op Ar path +.Nm tpl +.Cm new +.Cm lib +.Op Fl d Ar description +.Op Fl D Ar dep +.Ar ... +.Ar name +.Op Ar path +.Sh DESCRIPTION +.Nm +scaffolds new Rust projects from built-in templates. +Every generated project is a single source file with an ISC +.Pa LICENSE , +a +.Xr rustc 1 +based +.Pa Makefile , +a plain-text +.Pa README.md +and an +.Xr mdoc 7 +manpage. +.Pp +The new project directory is initialized as a git repository +via +.Xr libgit2 3 +and three default remotes are added: +.Bl -tag -width Ds +.It Li ijanc +.Li ssh://anon@ijanc.org/ Ns Ar name +.It Li sr +.Li https://git.sr.ht/~ijanc/ Ns Ar name +.It Li gh +.Li https://github.com/ Ns Ar name Ns Li .git +.El +.Pp +.Ar name +must start with a lowercase letter and contain only lowercase +letters, digits and underscores. +.Ar path +defaults to +.Ar name +in the current working directory and must point to a missing +or empty directory. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl V +Print the version and exit. +.It Fl d Ar description +Short description used as the +.Sx NAME +line of the manpage and the title of +.Pa README.md . +Defaults to +.Dq TODO: short description . +.It Fl D Ar dep +Vendor a dependency into the new project after the templates are +written by invoking +.Xr marmita 1 . +May be repeated. +.Ar dep +is either a full URL +.Pq Li ssh:// , Li https:// , Li git@ +used verbatim, or a short name without +.Li :// +which is expanded to +.Li ssh://ijanc@ijanc.org/ Ns Ar dep . +Requires +.Xr marmita 1 +on +.Ev PATH . +.El +.Pp +The commands are as follows: +.Bl -tag -width Ds +.It Cm new bin Ar name Op Ar path +Create a binary project in +.Ar path . +The project contains +.Ar name Ns Li .rs , +.Pa Makefile , +.Pa README.md , +.Ar name Ns Li .1 , +.Pa LICENSE +and +.Pa .gitignore . +The +.Pa Makefile +defines +.Cm all , clean , install , fmt-check , clippy +and +.Cm ci +targets and links a binary into +.Pa build/ Ns Ar name . +.It Cm new lib Ar name Op Ar path +Create a library project in +.Ar path . +The layout matches +.Cm new bin +with +.Ar name Ns Li .3 +in place of the section 1 manpage and a +.Pa Makefile +that builds +.Pa build/lib Ns Ar name Ns Li .rlib +and a +.Cm test +target driven by +.Xr rustc 1 No --test . +.El +.Sh EXIT STATUS +.Nm +exits 0 on success and non-zero on failure. +A diagnostic message is written to standard error. +.Sh EXAMPLES +Create a binary project at +.Pa ./hello : +.Bd -literal -offset indent +$ tpl new bin hello +.Ed +.Pp +Create a binary project with a description: +.Bd -literal -offset indent +$ tpl new bin hello -d "say hi to the world" +.Ed +.Pp +Create a library project at a custom path: +.Bd -literal -offset indent +$ tpl new lib parser /tmp/parser +.Ed +.Pp +Create a REST API library that vendors +.Li http +and +.Li json/jackson +from +.Li ssh://ijanc@ijanc.org : +.Bd -literal -offset indent +$ tpl new lib api -D http -D json/jackson +.Ed +.Sh SEE ALSO +.Xr make 1 , +.Xr marmita 1 , +.Xr rustc 1 , +.Xr libgit2 3 , +.Xr mdoc 7 +.Sh AUTHORS +.An Murilo Ijanc' Aq Mt murilo@ijanc.org blob - /dev/null blob + 7a2dce91f4225cd05dd712746295d83a9d6f6ffd (mode 644) --- /dev/null +++ tpl.rs @@ -0,0 +1,825 @@ +// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et : +// +// Copyright (c) 2026 Murilo Ijanc' +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +use std::env; +use std::ffi::CString; +use std::fs; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::process; +use std::process::Command; +use std::ptr; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Copy, Clone)] +enum Kind { + Bin, + Lib, +} + +fn main() { + let args: Vec = env::args().collect(); + let argv: Vec<&str> = args.iter().skip(1).map(String::as_str).collect(); + + match argv.as_slice() { + ["-V"] => { + println!("tpl {}", env!("TPL_VERSION")); + } + ["new", "bin", rest @ ..] => parse_new(Kind::Bin, rest), + ["new", "lib", rest @ ..] => parse_new(Kind::Lib, rest), + _ => usage(), + } +} + +fn parse_new(kind: Kind, args: &[&str]) { + let mut desc: Option<&str> = None; + let mut deps: Vec = Vec::new(); + let mut positional: Vec<&str> = Vec::new(); + let mut i = 0; + while i < args.len() { + match args[i] { + "-d" => { + let v = args.get(i + 1).copied().unwrap_or_else(|| usage()); + desc = Some(v); + i += 2; + } + "-D" => { + let v = args.get(i + 1).copied().unwrap_or_else(|| usage()); + deps.push(expand_dep(v)); + i += 2; + } + s => { + positional.push(s); + i += 1; + } + } + } + let name = match positional.first() { + Some(n) => *n, + None => usage(), + }; + let path = positional.get(1).copied(); + if positional.len() > 2 { + usage(); + } + generate(kind, name, path, desc, &deps); +} + +fn expand_dep(arg: &str) -> String { + if arg.contains("://") || arg.starts_with("git@") { + arg.to_string() + } else { + format!("ssh://ijanc@ijanc.org/{arg}") + } +} + +fn usage() -> ! { + eprintln!("usage: tpl [-V]"); + eprintln!(" tpl new bin [-d description] [-D dep]... [path]"); + eprintln!(" tpl new lib [-d description] [-D dep]... [path]"); + process::exit(1); +} + +fn die(msg: &str) -> ! { + eprintln!("tpl: {msg}"); + process::exit(1); +} + +////////////////////////////////////////////////////////////////////////////// +// Generator +////////////////////////////////////////////////////////////////////////////// + +struct Ctx<'a> { + name: &'a str, + name_upper: String, + year: u32, + date: String, + desc: &'a str, +} + +fn generate(kind: Kind, name: &str, path: Option<&str>, desc: Option<&str>, deps: &[String]) { + if let Err(e) = validate_name(name) { + die(&format!("invalid project name '{name}': {e}")); + } + + let dir = PathBuf::from(path.unwrap_or(name)); + if dir.exists() { + let count = fs::read_dir(&dir) + .unwrap_or_else(|e| die(&format!("{}: {e}", dir.display()))) + .count(); + if count > 0 { + die(&format!( + "{}: directory exists and is not empty", + dir.display() + )); + } + } else { + fs::create_dir_all(&dir).unwrap_or_else(|e| die(&format!("{}: {e}", dir.display()))); + } + + let (year, month, day) = current_ymd(); + let ctx = Ctx { + name, + name_upper: name.to_uppercase(), + year, + date: format!("{} {} {}", month_name(month), day, year), + desc: desc.unwrap_or("TODO: short description"), + }; + + let files: &[(&str, &str)] = match kind { + Kind::Bin => &[ + ("{NAME}.rs", BIN_SRC), + ("Makefile", BIN_MAKEFILE), + ("README.md", BIN_README), + ("{NAME}.1", BIN_MAN), + ("LICENSE", LICENSE), + (".gitignore", GITIGNORE), + ], + Kind::Lib => &[ + ("{NAME}.rs", LIB_SRC), + ("Makefile", LIB_MAKEFILE), + ("README.md", LIB_README), + ("{NAME}.3", LIB_MAN), + ("LICENSE", LICENSE), + (".gitignore", GITIGNORE), + ], + }; + + for (fname_tpl, body_tpl) in files { + let fname = render(fname_tpl, &ctx); + let body = render(body_tpl, &ctx); + let p = dir.join(&fname); + fs::write(&p, body).unwrap_or_else(|e| die(&format!("{}: {e}", p.display()))); + println!("created {}", p.display()); + } + + git_init_repo(&dir, name); + + for url in deps { + marmita_add(&dir, url); + } +} + +fn marmita_add(dir: &Path, url: &str) { + let status = Command::new("marmita") + .arg("add") + .arg(url) + .current_dir(dir) + .status(); + match status { + Ok(s) if s.success() => {} + Ok(s) => die(&format!( + "marmita add {url}: exit {}", + s.code().unwrap_or(-1) + )), + Err(e) => die(&format!("marmita add {url}: {e} (is marmita installed?)")), + } +} + +fn render(s: &str, c: &Ctx) -> String { + s.replace("{NAME}", c.name) + .replace("{NAME_UPPER}", &c.name_upper) + .replace("{YEAR}", &c.year.to_string()) + .replace("{DATE}", &c.date) + .replace("{DESC}", c.desc) +} + +fn validate_name(name: &str) -> Result<(), &'static str> { + if name.is_empty() { + return Err("empty"); + } + let mut chars = name.chars(); + let first = chars.next().unwrap(); + if !first.is_ascii_lowercase() { + return Err("must start with a lowercase letter"); + } + for c in chars { + if !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') { + return Err("only lowercase letters, digits and underscore are allowed"); + } + } + Ok(()) +} + +////////////////////////////////////////////////////////////////////////////// +// Calendar +////////////////////////////////////////////////////////////////////////////// + +fn current_ymd() -> (u32, u32, u32) { + let secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let mut days = (secs / 86400) as i64; + let mut year: i64 = 1970; + loop { + let dy = if is_leap(year) { 366 } else { 365 }; + if days < dy { + break; + } + days -= dy; + year += 1; + } + let dim: [i64; 12] = [ + 31, + if is_leap(year) { 29 } else { 28 }, + 31, + 30, + 31, + 30, + 31, + 31, + 30, + 31, + 30, + 31, + ]; + let mut month = 0usize; + while days >= dim[month] { + days -= dim[month]; + month += 1; + } + (year as u32, (month + 1) as u32, (days + 1) as u32) +} + +fn is_leap(y: i64) -> bool { + y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) +} + +fn month_name(m: u32) -> &'static str { + [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ][(m - 1) as usize] +} + +////////////////////////////////////////////////////////////////////////////// +// FFI (libgit2) -- repository init + remotes +////////////////////////////////////////////////////////////////////////////// + +#[allow(non_camel_case_types)] +mod ffi { + use std::os::raw::{c_char, c_int, c_uint}; + + pub enum git_repository {} + pub enum git_remote {} + + #[repr(C)] + pub struct git_error { + pub message: *const c_char, + pub klass: c_int, + } + + #[link(name = "git2")] + unsafe extern "C" { + pub fn git_libgit2_init() -> c_int; + pub fn git_libgit2_shutdown() -> c_int; + + pub fn git_repository_init( + out: *mut *mut git_repository, + path: *const c_char, + is_bare: c_uint, + ) -> c_int; + pub fn git_repository_free(repo: *mut git_repository); + + pub fn git_remote_create( + out: *mut *mut git_remote, + repo: *mut git_repository, + name: *const c_char, + url: *const c_char, + ) -> c_int; + pub fn git_remote_free(remote: *mut git_remote); + + pub fn git_error_last() -> *const git_error; + } +} + +fn last_git_err() -> String { + unsafe { + let e = ffi::git_error_last(); + if e.is_null() || (*e).message.is_null() { + return "unknown libgit2 error".to_string(); + } + std::ffi::CStr::from_ptr((*e).message) + .to_string_lossy() + .into_owned() + } +} + +fn git_init_repo(dir: &Path, name: &str) { + if unsafe { ffi::git_libgit2_init() } < 0 { + die(&format!("libgit2 init: {}", last_git_err())); + } + + let path_c = + CString::new(dir.as_os_str().as_bytes()).unwrap_or_else(|e| die(&format!("path: {e}"))); + + let mut repo: *mut ffi::git_repository = ptr::null_mut(); + let rc = unsafe { ffi::git_repository_init(&mut repo, path_c.as_ptr(), 0) }; + if rc != 0 { + die(&format!("git init {}: {}", dir.display(), last_git_err())); + } + println!("git init {}", dir.display()); + + let remotes: [(&str, String); 3] = [ + ("ijanc", format!("ssh://anon@ijanc.org/{name}")), + ("sr", format!("https://git.sr.ht/~ijanc/{name}")), + ("gh", format!("https://github.com/{name}.git")), + ]; + + for (rname, url) in &remotes { + let n = CString::new(*rname).unwrap(); + let u = CString::new(url.as_str()).unwrap(); + let mut remote: *mut ffi::git_remote = ptr::null_mut(); + let rc = unsafe { ffi::git_remote_create(&mut remote, repo, n.as_ptr(), u.as_ptr()) }; + if rc != 0 { + let err = last_git_err(); + unsafe { ffi::git_repository_free(repo) }; + die(&format!("git remote add {rname}: {err}")); + } + unsafe { ffi::git_remote_free(remote) }; + println!("git remote add {rname} {url}"); + } + + unsafe { + ffi::git_repository_free(repo); + ffi::git_libgit2_shutdown(); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Templates -- shared +////////////////////////////////////////////////////////////////////////////// + +const LICENSE: &str = "\ +Copyright (c) {YEAR} Murilo Ijanc' + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +"; + +const GITIGNORE: &str = "build/\n"; + +////////////////////////////////////////////////////////////////////////////// +// Templates -- bin +////////////////////////////////////////////////////////////////////////////// + +const BIN_SRC: &str = r##"// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et : +// +// Copyright (c) {YEAR} Murilo Ijanc' +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +use std::env; +use std::process; + +fn main() { + let args: Vec = env::args().collect(); + match args.get(1).map(String::as_str) { + Some("-V") => println!("{NAME} {}", env!("{NAME_UPPER}_VERSION")), + _ => { + eprintln!("usage: {NAME} [-V]"); + process::exit(1); + } + } +} +"##; + +const BIN_MAKEFILE: &str = r##"# +# Copyright (c) {YEAR} Murilo Ijanc' +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +RUSTC ?= $(shell rustup which rustc 2>/dev/null || which rustc) +RUSTFLAGS ?= -C opt-level=2 -C strip=symbols +VERSION = 0.1.0 +PREFIX ?= /usr/local +MANDIR ?= $(PREFIX)/share/man + +BUILD = build +BIN = $(BUILD)/{NAME} +MAIN = {NAME}.rs + +# Vendored deps managed by marmita(1). Adding vendor/foo.rs is enough +# for the pattern rule below to build it as an rlib; if the upstream +# ships link.mk, marmita copies it to vendor/foo.mk and the -include +# below appends to LINK_FLAGS / BUILD_ENV. +VENDOR_RS = $(wildcard vendor/*.rs) +VENDOR_NAMES = $(VENDOR_RS:vendor/%.rs=%) +VENDOR_LIBS = $(VENDOR_NAMES:%=$(BUILD)/lib%.rlib) +VENDOR_EXT = $(foreach n,$(VENDOR_NAMES),--extern $(n)=$(BUILD)/lib$(n).rlib) +LINK_FLAGS = +BUILD_ENV = +-include $(wildcard vendor/*.mk) +LINK_ARGS = $(addprefix -C link-arg=,$(LINK_FLAGS)) + +CLIPPY ?= $(shell rustup which clippy-driver 2>/dev/null) +RUSTFMT ?= $(shell rustup which rustfmt 2>/dev/null) + +.PHONY: all clean install ci fmt-check clippy + +all: $(BIN) + +$(BUILD)/lib%.rlib: vendor/%.rs + mkdir -p $(BUILD) + $(BUILD_ENV) $(RUSTC) --edition 2024 --crate-type rlib \ + --crate-name $* $(RUSTFLAGS) -o $@ $< + +$(BIN): $(MAIN) $(VENDOR_LIBS) + mkdir -p $(BUILD) + {NAME_UPPER}_VERSION=$(VERSION) TMPDIR=/tmp $(RUSTC) --edition 2024 \ + --crate-type bin --crate-name {NAME} $(RUSTFLAGS) \ + $(VENDOR_EXT) \ + $(LINK_ARGS) \ + -o $@ $< + +clean: + rm -rf $(BUILD) + +install: $(BIN) + install -d $(PREFIX)/bin $(MANDIR)/man1 + install -m 755 $(BIN) $(PREFIX)/bin/{NAME} + install -m 644 {NAME}.1 $(MANDIR)/man1/{NAME}.1 + +fmt-check: + $(RUSTFMT) --edition 2024 --check $(MAIN) + +clippy: $(VENDOR_LIBS) + {NAME_UPPER}_VERSION=$(VERSION) TMPDIR=/tmp $(CLIPPY) --edition 2024 \ + --crate-type bin --crate-name {NAME} \ + $(VENDOR_EXT) \ + $(LINK_ARGS) \ + -W clippy::all -o /tmp/{NAME}.clippy $(MAIN) + @rm -f /tmp/{NAME}.clippy + +ci: fmt-check clippy $(BIN) +"##; + +const BIN_README: &str = "\ +{NAME} - {DESC} +================================ +{NAME} is a minimal binary written in Rust. + + +Requirements +------------ +In order to build {NAME} you need rustc. + + +Installation +------------ +Edit Makefile to match your local setup ({NAME} is installed +into the /usr/local/bin namespace by default). + +Afterwards enter the following command to build and install +{NAME}: + + make clean install + + +Running {NAME} +-------------- +Print the version: + + {NAME} -V + + +Download +-------- + got clone ssh://ijanc@ijanc.org/{NAME} + + +License +------- +ISC -- see LICENSE. +"; + +const BIN_MAN: &str = "\ +.\\\" +.\\\" Copyright (c) {YEAR} Murilo Ijanc' +.\\\" +.\\\" Permission to use, copy, modify, and/or distribute this software for any +.\\\" purpose with or without fee is hereby granted, provided that the above +.\\\" copyright notice and this permission notice appear in all copies. +.\\\" +.\\\" THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\\\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\\\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\\\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\\\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\\\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\\\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\\\" +.Dd $Mdocdate: {DATE} $ +.Dt {NAME_UPPER} 1 +.Os +.Sh NAME +.Nm {NAME} +.Nd {DESC} +.Sh SYNOPSIS +.Nm {NAME} +.Fl V +.Sh DESCRIPTION +.Nm +is a minimal binary written in Rust. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl V +Print the version and exit. +.El +.Sh EXIT STATUS +.Nm +exits 0 on success and non-zero on failure. +A diagnostic message is written to standard error. +.Sh AUTHORS +.An Murilo Ijanc' Aq Mt murilo@ijanc.org +"; + +////////////////////////////////////////////////////////////////////////////// +// Templates -- lib +////////////////////////////////////////////////////////////////////////////// + +const LIB_SRC: &str = r##"// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et : +// +// Copyright (c) {YEAR} Murilo Ijanc' +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +pub fn version() -> &'static str { + env!("{NAME_UPPER}_VERSION") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn version_is_set() { + assert!(!version().is_empty()); + } +} +"##; + +const LIB_MAKEFILE: &str = r##"# +# Copyright (c) {YEAR} Murilo Ijanc' +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +RUSTC ?= $(shell rustup which rustc 2>/dev/null || which rustc) +RUSTFLAGS ?= -C opt-level=2 +VERSION = 0.1.0 +PREFIX ?= /usr/local +MANDIR ?= $(PREFIX)/share/man + +BUILD = build +SRC = {NAME}.rs +LIB = $(BUILD)/lib{NAME}.rlib +TEST = $(BUILD)/{NAME}-test + +EXAMPLE_SRCS = $(wildcard examples/*.rs) +EXAMPLES = $(EXAMPLE_SRCS:examples/%.rs=$(BUILD)/ex-%) + +# Vendored deps managed by marmita(1). Adding vendor/foo.rs is enough +# for the pattern rule below to build it as an rlib; if the upstream +# ships link.mk, marmita copies it to vendor/foo.mk and the -include +# below appends to LINK_FLAGS / BUILD_ENV. +VENDOR_RS = $(wildcard vendor/*.rs) +VENDOR_NAMES = $(VENDOR_RS:vendor/%.rs=%) +VENDOR_LIBS = $(VENDOR_NAMES:%=$(BUILD)/lib%.rlib) +VENDOR_EXT = $(foreach n,$(VENDOR_NAMES),--extern $(n)=$(BUILD)/lib$(n).rlib) +LINK_FLAGS = +BUILD_ENV = +-include $(wildcard vendor/*.mk) +LINK_ARGS = $(addprefix -C link-arg=,$(LINK_FLAGS)) + +CLIPPY ?= $(shell rustup which clippy-driver 2>/dev/null) +RUSTFMT ?= $(shell rustup which rustfmt 2>/dev/null) +RUSTDOC ?= $(shell rustup which rustdoc 2>/dev/null || which rustdoc) + +DOC = $(BUILD)/doc/{NAME}/index.html + +.PHONY: all clean test fmt-check clippy ci doc examples install + +all: $(LIB) + +$(BUILD)/lib%.rlib: vendor/%.rs + mkdir -p $(BUILD) + $(BUILD_ENV) $(RUSTC) --edition 2024 --crate-type rlib \ + --crate-name $* $(RUSTFLAGS) -o $@ $< + +$(LIB): $(SRC) $(VENDOR_LIBS) + mkdir -p $(BUILD) + {NAME_UPPER}_VERSION=$(VERSION) $(RUSTC) --edition 2024 \ + --crate-type rlib --crate-name {NAME} $(RUSTFLAGS) \ + $(VENDOR_EXT) \ + $(LINK_ARGS) \ + -o $@ $< + +$(TEST): $(SRC) $(VENDOR_LIBS) + mkdir -p $(BUILD) + {NAME_UPPER}_VERSION=$(VERSION) $(RUSTC) --edition 2024 \ + --test --crate-name {NAME} \ + $(VENDOR_EXT) \ + $(LINK_ARGS) \ + -o $@ $< + +test: $(TEST) + $(TEST) + +fmt-check: + $(RUSTFMT) --edition 2024 --check $(SRC) + +clippy: $(VENDOR_LIBS) + mkdir -p $(BUILD) + {NAME_UPPER}_VERSION=$(VERSION) $(CLIPPY) --edition 2024 \ + --crate-type rlib --crate-name {NAME} \ + $(VENDOR_EXT) \ + $(LINK_ARGS) \ + -W clippy::all -o $(BUILD)/{NAME}.clippy $(SRC) + @rm -f $(BUILD)/{NAME}.clippy + +$(DOC): $(SRC) + mkdir -p $(BUILD) + $(RUSTDOC) --edition 2024 --crate-name {NAME} \ + -o $(BUILD)/doc $(SRC) + +doc: $(DOC) + +$(BUILD)/ex-%: examples/%.rs $(LIB) + mkdir -p $(BUILD) + {NAME_UPPER}_VERSION=$(VERSION) $(RUSTC) --edition 2024 \ + --crate-name $* --extern {NAME}=$(LIB) -L $(BUILD) \ + $(RUSTFLAGS) -o $@ $< + +examples: $(EXAMPLES) + +install: $(SRC) + install -d $(MANDIR)/man3 + install -m 644 {NAME}.3 $(MANDIR)/man3/{NAME}.3 + +clean: + rm -rf $(BUILD) + +ci: fmt-check clippy $(LIB) test +"##; + +const LIB_README: &str = "\ +{NAME} - {DESC} +================================ +{NAME} is a minimal Rust library distributed as a single source file. + + +Requirements +------------ +In order to build {NAME} you need rustc. + + +Building +-------- +Build the rlib: + + make + +Run the test suite: + + make test + +Generate documentation: + + make doc + + +Using {NAME} +------------ +Vendor {NAME}.rs into your project's vendor/ directory and link +with rustc: + + rustc --edition 2024 --crate-type rlib --crate-name {NAME} \\ + -o build/lib{NAME}.rlib vendor/{NAME}.rs + +Then in your binary: + + rustc --edition 2024 --crate-type bin \\ + --extern {NAME}=build/lib{NAME}.rlib -L build \\ + -o build/myapp main.rs + + +Download +-------- + got clone ssh://ijanc@ijanc.org/{NAME} + git clone https://git.ijanc.org/{NAME}.git + git clone https://git.sr.ht/~ijanc/{NAME} + git clone https://github.com/{NAME}.git + + +License +------- +ISC -- see LICENSE. +"; + +const LIB_MAN: &str = "\ +.\\\" +.\\\" Copyright (c) {YEAR} Murilo Ijanc' +.\\\" +.\\\" Permission to use, copy, modify, and/or distribute this software for any +.\\\" purpose with or without fee is hereby granted, provided that the above +.\\\" copyright notice and this permission notice appear in all copies. +.\\\" +.\\\" THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\\\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\\\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\\\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\\\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\\\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\\\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\\\" +.Dd $Mdocdate: {DATE} $ +.Dt {NAME_UPPER} 3 +.Os +.Sh NAME +.Nm {NAME} +.Nd {DESC} +.Sh DESCRIPTION +.Nm +is a minimal Rust library distributed as a single source file. +.Sh AUTHORS +.An Murilo Ijanc' Aq Mt murilo@ijanc.org +";