commit - /dev/null
commit + 0649cc4916ba1ef924acd55eceba89df390dd605
blob - /dev/null
blob + 567609b1234a9b8806c5a05da6c866e480aa148d (mode 644)
--- /dev/null
+++ .gitignore
+build/
blob - /dev/null
blob + 32f3292e31bbd5acf75b7671b679e9017828078c (mode 644)
--- /dev/null
+++ LICENSE
+Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+
+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
+#
+# Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+#
+# 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
+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/<name>; 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/<name>
+ sr https://git.sr.ht/~ijanc/<name>
+ gh https://github.com/<name>.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
+.\"
+.\" Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+.\"
+.\" 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
+// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et :
+//
+// Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+//
+// 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<String> = 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<String> = 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]... <name> [path]");
+ eprintln!(" tpl new lib [-d description] [-D dep]... <name> [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' <murilo@ijanc.org>
+
+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' <murilo@ijanc.org>
+//
+// 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<String> = 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' <murilo@ijanc.org>
+#
+# 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' <murilo@ijanc.org>
+.\\\"
+.\\\" 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' <murilo@ijanc.org>
+//
+// 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' <murilo@ijanc.org>
+#
+# 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' <murilo@ijanc.org>
+.\\\"
+.\\\" 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
+";