new: template manifests and registry infrastructure

Split the Template trait into a data half and a logic half. Every
template is now declared by a manifest under data/templates/<id>/
(CLI id, wizard label, detection markers, Build-Depends, architecture,
rules dh line, rules-extra body, control source fields, gitignore
entries and static file bodies with {placeholder} substitution),
embedded through the TEMPLATE_SOURCES index and parsed once into the
registry; the order of the index is the wizard menu order and the
detection priority at once. The logic half is the slim TemplateHooks
trait (probe, post_write, file-body overrides merged over the manifest
bodies by path shadowing, Build-Depends/architecture amendments and
extra context values), registered per template as a HOOKS static: a
template without hooks needs zero Rust.

- TemplateId becomes a Copy wrapper of the stable CLI string; the
  enum, its all/as_str/display_name/from_label matches and the old
  statics array collapse into the registry accessors.
- rust's rules overrides move to data/templates/rust/rules.extra.tpl
  with {locked}/{artifact} hook context; python's backend table,
  meson/cmake's pkg-config opt-in, autotools' gettext and python's
  C-extension hints become hook amendments over the manifest baseline.
- detect.rs drops its hardcoded marker cascade: the manifests'
  detect.files drive detection in registry order, with the shell
  single-script heuristic and the never-detected empty template kept
  as the code special cases they are. License sniffing is untouched.
- The template tests port to manifest validation: registry coverage
  and stable order, placeholder presence in the rendering context,
  rules composition, the Build-Depends/architecture/dh-line table now
  asserted against the manifest data, and the hook shadowing merge.

The static skeleton bodies of the shell/empty/makefile/go templates
stay in their Rust hooks for now; the next commit moves them into
their manifests.
This commit is contained in:
2026-09-18 14:46:28 +02:00
parent fc0d2f247e
commit 04a572cd77
26 changed files with 1477 additions and 716 deletions
+35 -39
View File
@@ -4,13 +4,16 @@
//! The rule set is deliberately simple and table-driven (highest precedence
//! first):
//!
//! 1. well-known build-system marker files at the top level of the
//! directory (`Cargo.toml`, `pyproject.toml`/`setup.py`/`setup.cfg`,
//! `meson.build`, `CMakeLists.txt`, `configure.ac`, `go.mod`,
//! `Makefile`) — more than one distinct template matching is
//! [`Detection::Ambiguous`],
//! 1. the `detect.files` marker files declared by the template manifests
//! (`data/templates/<id>/manifest.yml`, in registry order: `Cargo.toml`,
//! `pyproject.toml`/`setup.py`/`setup.cfg`, `meson.build`,
//! `CMakeLists.txt`, `configure.ac`, `go.mod`, `Makefile`) looked for at
//! the top level of the directory — more than one distinct template
//! matching is [`Detection::Ambiguous`]; templates without markers
//! (shell: the single-script heuristic below; empty: never detected)
//! declare none,
//! 2. otherwise, exactly one top-level script (a `*.sh` file, or a file
//! whose first line is a `#!` shebang) → [`TemplateId::Shell`],
//! whose first line is a `#!` shebang) → [`TemplateId::SHELL`],
//! several scripts or none → nothing,
//! 3. otherwise [`Detection::Empty`].
//!
@@ -25,6 +28,7 @@ use regex::Regex;
use super::licenses;
use super::options::TemplateId;
use super::templates;
/// Outcome of the detection.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -38,26 +42,18 @@ pub enum Detection {
Empty,
}
/// Marker files per template, in precedence order (see the module docs).
const MARKERS: [(TemplateId, &[&str]); 7] = [
(TemplateId::Rust, &["Cargo.toml"]),
(
TemplateId::Python,
&["pyproject.toml", "setup.py", "setup.cfg"],
),
(TemplateId::Meson, &["meson.build"]),
(TemplateId::Cmake, &["CMakeLists.txt"]),
(TemplateId::Autotools, &["configure.ac"]),
(TemplateId::Go, &["go.mod"]),
(TemplateId::Makefile, &["Makefile"]),
];
/// Detect the template matching the project in `dir`.
/// Detect the template matching the project in `dir`: the manifests'
/// marker files in registry order (the detection priority), then the
/// shell single-script heuristic.
pub fn detect(dir: &Path) -> Detection {
let mut hits: Vec<TemplateId> = Vec::new();
for (id, markers) in MARKERS {
if markers.iter().any(|marker| dir.join(marker).exists()) && !hits.contains(&id) {
hits.push(id);
for template in templates::all() {
let markers = template.detect_files();
if !markers.is_empty()
&& markers.iter().any(|marker| dir.join(marker).exists())
&& !hits.contains(&template.id())
{
hits.push(template.id());
}
}
@@ -68,7 +64,7 @@ pub fn detect(dir: &Path) -> Detection {
}
if single_script(dir).is_some() {
Detection::Single(TemplateId::Shell)
Detection::Single(TemplateId::SHELL)
} else {
Detection::Empty
}
@@ -176,15 +172,15 @@ mod tests {
#[test]
fn marker_files_map_to_templates() {
let cases = [
("Cargo.toml", TemplateId::Rust),
("pyproject.toml", TemplateId::Python),
("setup.py", TemplateId::Python),
("setup.cfg", TemplateId::Python),
("meson.build", TemplateId::Meson),
("CMakeLists.txt", TemplateId::Cmake),
("configure.ac", TemplateId::Autotools),
("go.mod", TemplateId::Go),
("Makefile", TemplateId::Makefile),
("Cargo.toml", TemplateId::RUST),
("pyproject.toml", TemplateId::PYTHON),
("setup.py", TemplateId::PYTHON),
("setup.cfg", TemplateId::PYTHON),
("meson.build", TemplateId::MESON),
("CMakeLists.txt", TemplateId::CMAKE),
("configure.ac", TemplateId::AUTOTOOLS),
("go.mod", TemplateId::GO),
("Makefile", TemplateId::MAKEFILE),
];
for (marker, expected) in cases {
let dir = tempdir().unwrap();
@@ -200,21 +196,21 @@ mod tests {
touch(dir.path(), "Makefile");
assert_eq!(
detect(dir.path()),
Detection::Ambiguous(vec![TemplateId::Rust, TemplateId::Makefile])
Detection::Ambiguous(vec![TemplateId::RUST, TemplateId::MAKEFILE])
);
let dir = tempdir().unwrap();
touch(dir.path(), "pyproject.toml");
touch(dir.path(), "setup.py");
// Both markers map to the same template: one hit, not ambiguous.
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::Python));
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::PYTHON));
let dir = tempdir().unwrap();
touch(dir.path(), "meson.build");
touch(dir.path(), "CMakeLists.txt");
assert_eq!(
detect(dir.path()),
Detection::Ambiguous(vec![TemplateId::Meson, TemplateId::Cmake])
Detection::Ambiguous(vec![TemplateId::MESON, TemplateId::CMAKE])
);
}
@@ -223,12 +219,12 @@ mod tests {
// .sh extension.
let dir = tempdir().unwrap();
touch(dir.path(), "run.sh");
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::Shell));
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::SHELL));
// Shebang without extension.
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("run"), "#!/usr/bin/env python3\n").unwrap();
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::Shell));
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::SHELL));
// Two scripts: not exactly one, nothing recognized.
let dir = tempdir().unwrap();