diff --git a/data/templates/autotools/manifest.yml b/data/templates/autotools/manifest.yml new file mode 100644 index 0000000..073302e --- /dev/null +++ b/data/templates/autotools/manifest.yml @@ -0,0 +1,19 @@ +## The `autotools` template: a C project with a configure.ac built through +## debhelper's auto-detection (dh runs autoreconf itself when it finds +## configure.ac, debhelper >= 10 — no override needed). The logic half — +## the AC_INIT probe, the skeleton bodies and the gettext detection +## (appended to Build-Depends) — lives in src/new/templates/autotools.rs. +## +## Schema: see src/new/templates/mod.rs. + +id: autotools +label: C/C++ (Autotools) +detect: + files: [configure.ac] +build_depends: + - autoconf + - automake + - libtool +architecture: any +rules_dh_line: "dh $@" +files: [] diff --git a/data/templates/cmake/manifest.yml b/data/templates/cmake/manifest.yml new file mode 100644 index 0000000..28bd032 --- /dev/null +++ b/data/templates/cmake/manifest.yml @@ -0,0 +1,16 @@ +## The `cmake` template: a C/C++ project built with CMake through the +## debhelper cmake buildsystem. The logic half — the project() probe, the +## skeleton bodies and the wizard's pkg-config opt-in (appended to +## Build-Depends) — lives in src/new/templates/cmake.rs. +## +## Schema: see src/new/templates/mod.rs. + +id: cmake +label: C/C++ (CMake) +detect: + files: [CMakeLists.txt] +build_depends: + - cmake +architecture: any +rules_dh_line: "dh $@ --buildsystem=cmake" +files: [] diff --git a/data/templates/empty/manifest.yml b/data/templates/empty/manifest.yml new file mode 100644 index 0000000..2938e01 --- /dev/null +++ b/data/templates/empty/manifest.yml @@ -0,0 +1,14 @@ +## The `empty` template: a metapackage (non-empty Depends list) or an +## empty base package with no build system at all — pure `dh $@` plumbing +## as a starting point for hand-written rules. +## +## Schema: see src/new/templates/mod.rs. + +id: empty +label: Metapackage / empty base (no build system) +detect: + files: [] +build_depends: [] +architecture: all +rules_dh_line: "dh $@" +files: [] diff --git a/data/templates/go/manifest.yml b/data/templates/go/manifest.yml new file mode 100644 index 0000000..dbb3aec --- /dev/null +++ b/data/templates/go/manifest.yml @@ -0,0 +1,18 @@ +## The `go` template: a Go module built through dh-golang. The logic half +## — the go.mod module-line probe and the `{go_import_path}` value below — +## lives in src/new/templates/go.rs. +## +## Schema: see src/new/templates/mod.rs. + +id: go +label: Go module +detect: + files: [go.mod] +build_depends: + - golang-any + - dh-golang +architecture: any +rules_dh_line: "dh $@ --buildsystem=golang" +source_fields: + XS-Go-Import-Path: "{go_import_path}" +files: [] diff --git a/data/templates/makefile/manifest.yml b/data/templates/makefile/manifest.yml new file mode 100644 index 0000000..5947115 --- /dev/null +++ b/data/templates/makefile/manifest.yml @@ -0,0 +1,17 @@ +## The `makefile` template: a generic project driven by a plain Makefile. +## debhelper's makefile buildsystem runs `make` for the build and +## `make install DESTDIR=...` when the Makefile carries an `install:` +## target (missing targets are skipped gracefully), so plain `dh $@` +## plumbing is enough here. +## +## Schema: see src/new/templates/mod.rs. + +id: makefile +label: Generic (Makefile) +detect: + files: [Makefile] +build_depends: + - build-essential +architecture: any +rules_dh_line: "dh $@" +files: [] diff --git a/data/templates/meson/manifest.yml b/data/templates/meson/manifest.yml new file mode 100644 index 0000000..190bdc5 --- /dev/null +++ b/data/templates/meson/manifest.yml @@ -0,0 +1,16 @@ +## The `meson` template: a C/C++ project built with Meson through the +## debhelper meson buildsystem. The logic half — the project() probe, the +## skeleton bodies and the wizard's pkg-config opt-in (appended to +## Build-Depends) — lives in src/new/templates/meson.rs. +## +## Schema: see src/new/templates/mod.rs. + +id: meson +label: C/C++ (Meson) +detect: + files: [meson.build] +build_depends: + - meson +architecture: any +rules_dh_line: "dh $@ --buildsystem=meson" +files: [] diff --git a/data/templates/python/manifest.yml b/data/templates/python/manifest.yml new file mode 100644 index 0000000..d1e1845 --- /dev/null +++ b/data/templates/python/manifest.yml @@ -0,0 +1,21 @@ +## The `python` template: a PEP 517 project built with pybuild. The logic +## half — the pyproject.toml/setup.py probe, the skeleton bodies and the +## Build-Depends/architecture resolution for existing projects (backend +## package, pyproject presence, C-extension hints) — lives in +## src/new/templates/python.rs; the lists below are the fresh-skeleton +## baseline it starts from. +## +## Schema: see src/new/templates/mod.rs. + +id: python +label: Python (pyproject.toml / setup.py) +detect: + files: [pyproject.toml, setup.py, setup.cfg] +build_depends: + - dh-python + - python3-all + - pybuild-plugin-pyproject + - python3-setuptools +architecture: all +rules_dh_line: "dh $@ --with python3 --buildsystem=pybuild" +files: [] diff --git a/data/templates/rust/manifest.yml b/data/templates/rust/manifest.yml new file mode 100644 index 0000000..ebb7aaf --- /dev/null +++ b/data/templates/rust/manifest.yml @@ -0,0 +1,26 @@ +## The `rust` template: a vendored Cargo build (see the module docs of +## src/new/templates/rust.rs for the vendoring strategy). The logic half — +## the cargo vendor post-write hook, the Cargo.toml/src skeletons with +## their crate-name sanitizing, and the `{locked}` / `{artifact}` values of +## rules.extra.tpl — lives in that module. +## +## Schema: see src/new/templates/mod.rs. + +id: rust +label: Rust (Cargo.toml) +detect: + files: [Cargo.toml] +build_depends: + - cargo:native + - rustc:native +architecture: any +rules_dh_line: "dh $@" +# The vendored-build overrides appended to debian/rules; `--locked` is only +# used when the packaged tree already carries a Cargo.lock (the vendoring +# hook patches it in once it creates the lockfile), and the built artifact +# of a fresh skeleton is named after its crate. +rules_extra_file: rules.extra.tpl +gitignore_entries: + - vendor/ + - .cargo/config.toml +files: [] diff --git a/data/templates/rust/rules.extra.tpl b/data/templates/rust/rules.extra.tpl new file mode 100644 index 0000000..991a7ae --- /dev/null +++ b/data/templates/rust/rules.extra.tpl @@ -0,0 +1,19 @@ +override_dh_auto_build: + cargo build --release --offline{locked} + +override_dh_auto_install: + install -Dm755 target/release/{artifact} debian/{name}/usr/bin/{command} + +override_dh_auto_test: + cargo test --release --offline{locked} + +override_dh_update_autotools_config: + +override_dh_clean: + # dh_clean unlinks `*.orig` patch backups, but vendored crates + # ship files like `Cargo.toml.orig` that cargo's per-file + # checksums require on cold builds (chroots, Launchpad). + dh_clean -X .orig + +override_dh_auto_clean: + cargo clean diff --git a/data/templates/shell/manifest.yml b/data/templates/shell/manifest.yml new file mode 100644 index 0000000..5bec072 --- /dev/null +++ b/data/templates/shell/manifest.yml @@ -0,0 +1,16 @@ +## The `shell` template: a single interpreted script installed to +## /usr/bin with plain `dh $@` plumbing. Detection is not marker-based: the +## single-script heuristic of src/new/detect.rs (a lone *.sh or shebang +## file) maps here. The probe pre-filling the wizard answers from the +## script file name lives in src/new/templates/shell.rs. +## +## Schema: see src/new/templates/mod.rs. + +id: shell +label: Shell script / single interpreted file +detect: + files: [] +build_depends: [] +architecture: all +rules_dh_line: "dh $@" +files: [] diff --git a/src/new/debian.rs b/src/new/debian.rs index df4ff40..26cfe05 100644 --- a/src/new/debian.rs +++ b/src/new/debian.rs @@ -51,7 +51,7 @@ pub fn orig_tarball_path( } /// Render every common `debian/` file of the package. -pub fn files(opts: &NewOptions, template: &dyn Template) -> Vec { +pub fn files(opts: &NewOptions, template: &Template) -> Vec { let mut files = vec![ source_format(opts), changelog(opts), @@ -195,7 +195,7 @@ fn render_continuation_text(text: &str) -> String { /// comes from the template (`all` for shell/empty), and a non-empty /// `opts.depends` (the empty/metapackage flavor) lands in the binary /// stanza's `Depends` field. -fn control(opts: &NewOptions, template: &dyn Template) -> OutputFile { +fn control(opts: &NewOptions, template: &Template) -> OutputFile { let mut control = String::new(); // Source stanza. @@ -238,7 +238,7 @@ fn control(opts: &NewOptions, template: &dyn Template) -> OutputFile { /// `debian/rules`: the shebang and `%:` target whose recipe is the /// template's dh line (plus the template's extra overrides, when any), /// written with the executable bit. -fn rules(opts: &NewOptions, template: &dyn Template) -> OutputFile { +fn rules(opts: &NewOptions, template: &Template) -> OutputFile { let mut contents = format!("#!/usr/bin/make -f\n%:\n\t{}\n", template.rules_dh_line()); let extra = template.rules_extra(opts); if !extra.is_empty() { @@ -533,7 +533,7 @@ mod tests { fn opts() -> NewOptions { NewOptions { name: "mytool".into(), - template: TemplateId::Shell, + template: TemplateId::SHELL, source_dir: SourceDir::Skeleton, upstream_version: "0.1.0".into(), revision: 1, @@ -559,7 +559,7 @@ mod tests { #[test] fn source_format_and_local_options() { let o = opts(); - let files = super::files(&o, crate::new::templates::get(TemplateId::Shell).unwrap()); + let files = super::files(&o, crate::new::templates::get(TemplateId::SHELL).unwrap()); let find = |path: &str| { files .iter() @@ -582,7 +582,7 @@ mod tests { }; let files = super::files( &native, - crate::new::templates::get(TemplateId::Shell).unwrap(), + crate::new::templates::get(TemplateId::SHELL).unwrap(), ); assert!( files @@ -642,7 +642,7 @@ mod tests { #[test] fn control_rendering_and_parse() { let o = opts(); - let control = super::control(&o, crate::new::templates::get(TemplateId::Shell).unwrap()); + let control = super::control(&o, crate::new::templates::get(TemplateId::SHELL).unwrap()); // RFC822 continuation: first dep on the field line, the rest indented. assert!( @@ -679,7 +679,7 @@ mod tests { homepage: None, ..opts() }; - let control = super::control(&o, crate::new::templates::get(TemplateId::Shell).unwrap()); + let control = super::control(&o, crate::new::templates::get(TemplateId::SHELL).unwrap()); assert!(!control.contents.contains("Homepage:")); let parsed = crate::debian::ControlInfo::parse_content(&control.contents).unwrap(); assert!(parsed.source.get("Homepage").is_none()); @@ -688,7 +688,7 @@ mod tests { #[test] fn rules_is_executable_minimal_makefile() { let o = opts(); - let rules = super::rules(&o, crate::new::templates::get(TemplateId::Shell).unwrap()); + let rules = super::rules(&o, crate::new::templates::get(TemplateId::SHELL).unwrap()); assert!(rules.executable); assert_eq!(rules.contents, "#!/usr/bin/make -f\n%:\n\tdh $@\n"); } @@ -701,7 +701,7 @@ mod tests { "version=4\nhttps://github.com/example/mytool/releases .*/v?@ANY_VERSION@\\.tar\\.gz\n" .to_string(), ); - let files = super::files(&o, crate::new::templates::get(TemplateId::Shell).unwrap()); + let files = super::files(&o, crate::new::templates::get(TemplateId::SHELL).unwrap()); let find = |path: &str| { files .iter() @@ -731,7 +731,7 @@ mod tests { // Without the extras none of the files are rendered. let plain = super::files( &opts(), - crate::new::templates::get(TemplateId::Shell).unwrap(), + crate::new::templates::get(TemplateId::SHELL).unwrap(), ); assert!(!plain.iter().any(|f| f.path.starts_with("debian/tests"))); assert!(!plain.iter().any(|f| f.path == "debian/watch")); diff --git a/src/new/detect.rs b/src/new/detect.rs index a2e2431..8d923db 100644 --- a/src/new/detect.rs +++ b/src/new/detect.rs @@ -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//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 = 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(); diff --git a/src/new/mod.rs b/src/new/mod.rs index a0c343b..55b09cb 100644 --- a/src/new/mod.rs +++ b/src/new/mod.rs @@ -181,7 +181,7 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result = Vec::new(); let mut header = None; if skeleton { @@ -216,7 +216,7 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result= 1.0)".into()]; scaffold_in(dir.path(), o).unwrap(); @@ -493,7 +493,7 @@ mod tests { let dir = tempdir().unwrap(); scaffold_in( dir.path(), - opts(TemplateId::Empty, "basepkg", SourceDir::Skeleton), + opts(TemplateId::EMPTY, "basepkg", SourceDir::Skeleton), ) .unwrap(); let control = @@ -505,7 +505,7 @@ mod tests { #[serial] fn scaffold_release_targets_series() { let dir = tempdir().unwrap(); - let mut o = opts(TemplateId::Empty, "released", SourceDir::Skeleton); + let mut o = opts(TemplateId::EMPTY, "released", SourceDir::Skeleton); o.release = true; scaffold_in(dir.path(), o).unwrap(); @@ -525,7 +525,7 @@ mod tests { let tree = dir.path().join("packdir"); std::fs::create_dir_all(&tree).unwrap(); std::fs::write(tree.join("run.sh"), "#!/bin/sh\necho hi\n").unwrap(); - scaffold_in(&tree, opts(TemplateId::Shell, "runtool", SourceDir::Here)).unwrap(); + scaffold_in(&tree, opts(TemplateId::SHELL, "runtool", SourceDir::Here)).unwrap(); // debian/ lands directly in the directory; no skeleton file, no // root .gitignore (the shell template contributes none and the @@ -574,7 +574,7 @@ mod tests { std::fs::write(tree.join("debian/control"), "Source: mytool\n").unwrap(); let err = scaffold_in( dir.path(), - opts(TemplateId::Shell, "mytool", SourceDir::Skeleton), + opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton), ) .unwrap_err(); assert!(err.to_string().contains("debian/control"), "{err}"); @@ -585,7 +585,7 @@ mod tests { std::fs::write(dir.path().join("mytool/junk"), "x").unwrap(); let err = scaffold_in( dir.path(), - opts(TemplateId::Shell, "mytool", SourceDir::Skeleton), + opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton), ) .unwrap_err(); assert!(err.to_string().contains("not empty"), "{err}"); @@ -593,7 +593,7 @@ mod tests { // Existing orig tarball (quilt only): nothing gets written. let dir = tempdir().unwrap(); std::fs::write(dir.path().join("mytool_0.1.0.orig.tar.xz"), b"old").unwrap(); - let mut o = opts(TemplateId::Shell, "mytool", SourceDir::Skeleton); + let mut o = opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton); o.source_format = SourceFormat::Quilt; o.orig = Some(OrigOrigin::Snapshot); let err = scaffold_in(dir.path(), o).unwrap_err(); @@ -605,7 +605,7 @@ mod tests { let err = scaffold_in( dir.path(), opts( - TemplateId::Shell, + TemplateId::SHELL, "mytool", SourceDir::Path(dir.path().join("missing")), ), @@ -622,7 +622,7 @@ mod tests { let dir = tempdir().unwrap(); scaffold_in( dir.path(), - opts(TemplateId::Shell, "nativepkg", SourceDir::Skeleton), + opts(TemplateId::SHELL, "nativepkg", SourceDir::Skeleton), ) .unwrap(); @@ -649,7 +649,7 @@ mod tests { let dir = tempdir().unwrap(); let outcome = scaffold_in( dir.path(), - opts(TemplateId::Rust, "mytool", SourceDir::Skeleton), + opts(TemplateId::RUST, "mytool", SourceDir::Skeleton), ) .unwrap(); @@ -711,7 +711,7 @@ mod tests { let tree = dir.path().join("packdir"); std::fs::create_dir_all(&tree).unwrap(); std::fs::write(tree.join(".gitignore"), "# my project\n*.log\n").unwrap(); - scaffold_in(&tree, opts(TemplateId::Rust, "mytool", SourceDir::Here)).unwrap(); + scaffold_in(&tree, opts(TemplateId::RUST, "mytool", SourceDir::Here)).unwrap(); let gitignore = std::fs::read_to_string(tree.join(".gitignore")).unwrap(); assert!( @@ -753,7 +753,7 @@ mod tests { .unwrap(); std::fs::write(source.join("src/main.rs"), "fn main() {}\n").unwrap(); - let mut o = opts(TemplateId::Rust, "mytool", SourceDir::Path(source.clone())); + let mut o = opts(TemplateId::RUST, "mytool", SourceDir::Path(source.clone())); o.source_format = SourceFormat::Quilt; o.orig = Some(OrigOrigin::Snapshot); let outcome = scaffold_in(dir.path(), o).unwrap(); @@ -848,7 +848,7 @@ mod tests { let dir = tempdir().unwrap(); scaffold_in( dir.path(), - opts(TemplateId::Python, "mytool", SourceDir::Skeleton), + opts(TemplateId::PYTHON, "mytool", SourceDir::Skeleton), ) .unwrap(); @@ -880,7 +880,7 @@ mod tests { let tree = dir.path().join("mytool"); std::fs::create_dir_all(&tree).unwrap(); std::fs::write(tree.join("run.sh"), "#!/bin/sh\necho hi\n").unwrap(); - scaffold_in(&tree, opts(TemplateId::Shell, "mytool", SourceDir::Here)).unwrap(); + scaffold_in(&tree, opts(TemplateId::SHELL, "mytool", SourceDir::Here)).unwrap(); let output = crate::build::run_source_build( &dir.path().join("mytool"), @@ -909,7 +909,7 @@ mod tests { let dir = tempdir().unwrap(); scaffold_in( dir.path(), - opts(TemplateId::Shell, "mytool", SourceDir::Skeleton), + opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton), ) .unwrap(); diff --git a/src/new/options.rs b/src/new/options.rs index a83812f..12c9917 100644 --- a/src/new/options.rs +++ b/src/new/options.rs @@ -19,74 +19,60 @@ use crate::new::licenses; use crate::new::origin::{Forge, GitOrigin}; use crate::new::templates; -/// Build systems / project kinds `pkh new` knows about. +/// Build systems / project kinds `pkh new` knows about: a lightweight id +/// wrapping the stable CLI string (the `--lang` value). /// -/// The identifiers are stable CLI surface: `--lang` accepts every variant, -/// and every variant has a template implementation registered in -/// [`crate::new::templates`]. +/// The templates themselves are defined by the per-template manifests +/// under `data/templates//` (see [`crate::new::templates`]): label, +/// detection markers, policy metadata and file bodies all live there, and +/// every id below must have a manifest registered — the registry is built +/// from exactly these constants and a consistency test keeps the two in +/// lockstep. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum TemplateId { - /// Rust project (`Cargo.toml`) - Rust, - /// Python project (`pyproject.toml` / `setup.py` / `setup.cfg`) - Python, - /// C/C++ with Meson (`meson.build`) - Meson, - /// C/C++ with CMake (`CMakeLists.txt`) - Cmake, - /// C/C++ with Autotools (`configure.ac`) - Autotools, - /// Go module (`go.mod`) - Go, - /// Shell script / single interpreted file - Shell, - /// Generic Makefile-based project - Makefile, - /// Metapackage / empty base (no build system) - Empty, -} +pub struct TemplateId(&'static str); impl TemplateId { - /// Every template id, in the order offered by the wizard language menu. - pub fn all() -> [TemplateId; 9] { - [ - TemplateId::Rust, - TemplateId::Python, - TemplateId::Meson, - TemplateId::Cmake, - TemplateId::Autotools, - TemplateId::Go, - TemplateId::Shell, - TemplateId::Makefile, - TemplateId::Empty, - ] + /// Rust project (`Cargo.toml`). + pub const RUST: TemplateId = TemplateId("rust"); + /// Python project (`pyproject.toml` / `setup.py` / `setup.cfg`). + pub const PYTHON: TemplateId = TemplateId("python"); + /// C/C++ with Meson (`meson.build`). + pub const MESON: TemplateId = TemplateId("meson"); + /// C/C++ with CMake (`CMakeLists.txt`). + pub const CMAKE: TemplateId = TemplateId("cmake"); + /// C/C++ with Autotools (`configure.ac`). + pub const AUTOTOOLS: TemplateId = TemplateId("autotools"); + /// Go module (`go.mod`). + pub const GO: TemplateId = TemplateId("go"); + /// Shell script / single interpreted file. + pub const SHELL: TemplateId = TemplateId("shell"); + /// Generic Makefile-based project. + pub const MAKEFILE: TemplateId = TemplateId("makefile"); + /// Metapackage / empty base (no build system). + pub const EMPTY: TemplateId = TemplateId("empty"); + + /// Every template id, in the order offered by the wizard language menu + /// (the registry order of the manifests). + pub fn all() -> &'static [TemplateId] { + templates::ids() } /// Canonical CLI identifier of this template. pub fn as_str(&self) -> &'static str { - match self { - TemplateId::Rust => "rust", - TemplateId::Python => "python", - TemplateId::Meson => "meson", - TemplateId::Cmake => "cmake", - TemplateId::Autotools => "autotools", - TemplateId::Go => "go", - TemplateId::Shell => "shell", - TemplateId::Makefile => "makefile", - TemplateId::Empty => "empty", - } + self.0 } - /// Parse a CLI identifier, accepting exactly the canonical spellings. + /// Parse a CLI identifier, accepting exactly the registered spellings. pub fn parse(s: &str) -> Result { - TemplateId::all() - .into_iter() + Self::all() + .iter() + .copied() .find(|id| id.as_str() == s) .ok_or_else(|| { format!( "Unknown language/template '{}'. Supported values are: {}.", s, - TemplateId::all() + Self::all() .iter() .map(|id| id.as_str()) .collect::>() @@ -96,26 +82,15 @@ impl TemplateId { } /// Human-readable menu label of this template, as offered by the wizard - /// language question (and reused in the summary screen). + /// language question (and reused in the summary screen): the + /// manifest's `label`. pub fn display_name(&self) -> &'static str { - match self { - TemplateId::Rust => "Rust (Cargo.toml)", - TemplateId::Python => "Python (pyproject.toml / setup.py)", - TemplateId::Meson => "C/C++ (Meson)", - TemplateId::Cmake => "C/C++ (CMake)", - TemplateId::Autotools => "C/C++ (Autotools)", - TemplateId::Go => "Go module", - TemplateId::Shell => "Shell script / single interpreted file", - TemplateId::Makefile => "Generic (Makefile)", - TemplateId::Empty => "Metapackage / empty base (no build system)", - } + templates::get(*self).map(|t| t.label()).unwrap_or(self.0) } /// The template whose menu label (or CLI identifier) is `label`. pub fn from_label(label: &str) -> Option { - TemplateId::all() - .into_iter() - .find(|id| id.display_name() == label || id.as_str() == label) + templates::from_label(label) } } @@ -650,7 +625,7 @@ pub async fn resolve(cli: NewCli) -> Result { SourceDir::Skeleton => { if cli.defaults { log::info!("No language given, --defaults picks the 'empty' template"); - Some(TemplateId::Empty) + Some(TemplateId::EMPTY) } else { missing.push(format!( "--lang <{}|...> (no language given and there is nothing \ @@ -695,7 +670,7 @@ pub async fn resolve(cli: NewCli) -> Result { the 'empty' template", dir.display() ); - Some(TemplateId::Empty) + Some(TemplateId::EMPTY) } else { missing.push( "--lang (could not detect a build system; \ @@ -845,7 +820,7 @@ pub async fn resolve(cli: NewCli) -> Result { Ok(NewOptions { name, - template: template.unwrap_or(TemplateId::Empty), + template: template.unwrap_or(TemplateId::EMPTY), source_dir, upstream_version, revision, @@ -992,7 +967,7 @@ mod tests { #[test] fn template_ids_roundtrip() { - for id in TemplateId::all() { + for id in TemplateId::all().iter().copied() { assert_eq!(TemplateId::parse(id.as_str()).unwrap(), id); // Every id resolves from its menu label too, and labels are // unique. @@ -1001,9 +976,9 @@ mod tests { assert!(TemplateId::parse("cobol").is_err()); assert_eq!( TemplateId::from_label("Rust (Cargo.toml)"), - Some(TemplateId::Rust) + Some(TemplateId::RUST) ); - assert_eq!(TemplateId::from_label("rust"), Some(TemplateId::Rust)); + assert_eq!(TemplateId::from_label("rust"), Some(TemplateId::RUST)); assert_eq!(TemplateId::from_label("nope"), None); } @@ -1099,7 +1074,7 @@ mod tests { // The full version round-trips through DebianVersion. let opts = NewOptions { name: "t".into(), - template: TemplateId::Empty, + template: TemplateId::EMPTY, source_dir: SourceDir::Here, upstream_version: "0.1.0".into(), revision: 1, @@ -1301,7 +1276,7 @@ mod tests { let opts = resolve(cli).await.unwrap(); assert_eq!(opts.name, "my-tool"); - assert_eq!(opts.template, TemplateId::Empty); + assert_eq!(opts.template, TemplateId::EMPTY); assert!(matches!(opts.source_dir, SourceDir::Path(_))); assert_eq!(opts.upstream_version, "0.1.0"); assert_eq!(opts.revision, 1); @@ -1342,7 +1317,7 @@ mod tests { ..Default::default() }; let opts = resolve(cli).await.unwrap(); - assert_eq!(opts.template, TemplateId::Go); + assert_eq!(opts.template, TemplateId::GO); } #[tokio::test] diff --git a/src/new/questions.rs b/src/new/questions.rs index 13a1302..2993fca 100644 --- a/src/new/questions.rs +++ b/src/new/questions.rs @@ -139,7 +139,7 @@ async fn run_wizard(mut cli: NewCli) -> Result> { LanguageChoice::Ask(preselected) => { let menu = language_menu(&[]); let default = preselected - .unwrap_or(TemplateId::Empty) + .unwrap_or(TemplateId::EMPTY) .display_name() .to_string(); let id = select_template(&menu, &default)?; @@ -166,7 +166,7 @@ async fn run_wizard(mut cli: NewCli) -> Result> { // The rust toolchain pin of the packaged project does not travel into // the chroot build: surface it now so a too-old pin is not a surprise // when `pkh deb` compiles with the distribution's rustc. - let toolchain_pin = if template == TemplateId::Rust { + let toolchain_pin = if template == TemplateId::RUST { probe.as_ref().and_then(|p| p.toolchain_pin.clone()) } else { None @@ -390,7 +390,7 @@ async fn run_wizard(mut cli: NewCli) -> Result> { // same `validate_command` bar as `resolve` applies (which also // requires a non-empty answer), so an unusable probe is withheld and // invalid input re-asks here instead of failing late in `resolve`. - if cli.command.is_none() && template != TemplateId::Empty { + if cli.command.is_none() && template != TemplateId::EMPTY { let default = probe .as_ref() .and_then(|p| p.command.clone()) @@ -469,7 +469,7 @@ async fn run_wizard(mut cli: NewCli) -> Result> { } // 13. Metapackage Depends (empty template only). - if template == TemplateId::Empty && cli.depends.is_empty() { + if template == TemplateId::EMPTY && cli.depends.is_empty() { let validate = |answer: &str| options::validate_depends(answer).map(|_| ()); let answer = ask_text( "Depends (metapackage, comma-separated, blank for an empty base)", @@ -504,7 +504,7 @@ async fn run_wizard(mut cli: NewCli) -> Result> { // The meson/cmake opt-in question of the spec's template table: does the // build resolve libraries through pkg-config? The project files prefill // the default (dependency() / pkg_check_modules calls found). - if matches!(template, TemplateId::Meson | TemplateId::Cmake) + if matches!(template, TemplateId::MESON | TemplateId::CMAKE) && prompt::confirm( "Does the build resolve libraries through pkg-config (add it to Build-Depends)?", pkg_config_hint(&detect_dir, template), @@ -514,7 +514,7 @@ async fn run_wizard(mut cli: NewCli) -> Result> { } // Wizard-only extras (default off). - if template != TemplateId::Empty + if template != TemplateId::EMPTY && prompt::confirm( "Add an autopkgtest smoke test (debian/tests/control)?", false, @@ -686,8 +686,8 @@ fn same_directory(a: &std::path::Path, b: &std::path::Path) -> bool { /// CMakeLists.txt). fn pkg_config_hint(dir: &std::path::Path, template: TemplateId) -> bool { let (file, needles): (&str, &[&str]) = match template { - TemplateId::Meson => ("meson.build", &["dependency("]), - TemplateId::Cmake => ( + TemplateId::MESON => ("meson.build", &["dependency("]), + TemplateId::CMAKE => ( "CMakeLists.txt", &[ "pkg_check_modules", @@ -727,7 +727,8 @@ fn language_menu(candidates: &[TemplateId]) -> Vec { .copied() .chain( TemplateId::all() - .into_iter() + .iter() + .copied() .filter(|id| !candidates.contains(id)), ) .map(|id| id.display_name().to_string()) @@ -1033,12 +1034,12 @@ pub fn summary_text(opts: &NewOptions, toolchain_pin: Option<&str>) -> String { " debian/control Source + 1 binary (Architecture: {})", template.architecture(opts) )); - if opts.template == TemplateId::Empty { + if opts.template == TemplateId::EMPTY { // The Depends list is the payload of the metapackage flavor. if !opts.depends.is_empty() { lines.push(format!(" Depends {}", opts.depends.join(", "))); } - } else if opts.template == TemplateId::Rust { + } else if opts.template == TemplateId::RUST { // Nothing is vendored yet at this point: only announce that the // generation will attempt it. lines.push( @@ -1094,7 +1095,7 @@ pub fn summary_text(opts: &NewOptions, toolchain_pin: Option<&str>) -> String { lines.push(format!(" + {} (new skeleton)", names.join(", "))); } } - if opts.template == TemplateId::Rust && templates::find_on_path("cargo").is_none() { + if opts.template == TemplateId::RUST && templates::find_on_path("cargo").is_none() { lines.push( " ! cargo not found on PATH: dependencies cannot be vendored at \ scaffold time; the package will not build until you run \ @@ -1162,7 +1163,7 @@ mod tests { #[test] fn language_menu_lists_candidates_first() { - let menu = language_menu(&[Tid::Makefile, Tid::Rust]); + let menu = language_menu(&[Tid::MAKEFILE, Tid::RUST]); assert_eq!(menu[0], "Generic (Makefile)"); assert_eq!(menu[1], "Rust (Cargo.toml)"); // The remaining seven follow in registry order, no duplicates. @@ -1179,8 +1180,8 @@ mod tests { #[test] fn language_choice_flag_wins_over_detection() { let detections = [ - Detection::Single(Tid::Rust), - Detection::Ambiguous(vec![Tid::Rust, Tid::Python]), + Detection::Single(Tid::RUST), + Detection::Ambiguous(vec![Tid::RUST, Tid::PYTHON]), Detection::Empty, ]; for detection in &detections { @@ -1201,21 +1202,21 @@ mod tests { #[test] fn language_choice_without_flag_follows_detection() { assert_eq!( - language_choice(None, &Detection::Single(Tid::Rust), true), - LanguageChoice::Detected(Tid::Rust) + language_choice(None, &Detection::Single(Tid::RUST), true), + LanguageChoice::Detected(Tid::RUST) ); // Skeleton run: ask, preselecting the detected ecosystem. assert_eq!( - language_choice(None, &Detection::Single(Tid::Rust), false), - LanguageChoice::Ask(Some(Tid::Rust)) + language_choice(None, &Detection::Single(Tid::RUST), false), + LanguageChoice::Ask(Some(Tid::RUST)) ); assert_eq!( language_choice( None, - &Detection::Ambiguous(vec![Tid::Go, Tid::Python]), + &Detection::Ambiguous(vec![Tid::GO, Tid::PYTHON]), true ), - LanguageChoice::Ambiguous(vec![Tid::Go, Tid::Python]) + LanguageChoice::Ambiguous(vec![Tid::GO, Tid::PYTHON]) ); // Nothing detected: plain menu, the empty template preselected. assert_eq!( @@ -1327,12 +1328,12 @@ mod tests { #[test] fn summary_screen_shows_format_and_orig_origin() { // Native skeleton: the format row, no orig row. - let text = summary_text(&opts(Tid::Shell), None); + let text = summary_text(&opts(Tid::SHELL), None); assert!(text.contains("debian/source/format 3.0 (native)"), "{text}"); assert!(!text.contains("orig tarball"), "{text}"); // Quilt over an existing project: both rows. - let mut quilt = opts(Tid::Shell); + let mut quilt = opts(Tid::SHELL); quilt.source_dir = options::SourceDir::Here; quilt.source_format = options::SourceFormat::Quilt; quilt.orig = Some(options::OrigOrigin::GitArchive { @@ -1360,7 +1361,7 @@ mod tests { #[test] fn summary_screen_skeleton() { - let text = summary_text(&opts(Tid::Makefile), None); + let text = summary_text(&opts(Tid::MAKEFILE), None); assert!( text.contains("mytool 0.1.0-1 · builds for ubuntu/resolute"), "{text}" @@ -1390,7 +1391,7 @@ mod tests { #[test] fn summary_screen_metapackage_shows_depends() { - let mut o = opts(Tid::Empty); + let mut o = opts(Tid::EMPTY); o.depends = vec!["hello".to_string(), "hello-data (>= 1.0)".to_string()]; o.source_dir = options::SourceDir::Here; let text = summary_text(&o, None); @@ -1406,7 +1407,7 @@ mod tests { #[test] fn summary_screen_release_and_extras() { - let mut o = opts(Tid::Shell); + let mut o = opts(Tid::SHELL); o.release = true; o.autopkgtest = true; o.watch = Some("version=4\n".to_string()); @@ -1424,7 +1425,7 @@ mod tests { /// yet (regression: it claimed "(vendored)" before generating). #[test] fn summary_screen_rust_does_not_presume_vendoring() { - let text = summary_text(&opts(Tid::Rust), None); + let text = summary_text(&opts(Tid::RUST), None); assert!( text.contains("cargo build --release --offline (vendored at generation)"), "{text}" @@ -1436,17 +1437,17 @@ mod tests { /// (rust template only), flagged as ignored by the chroot build. #[test] fn summary_screen_shows_the_toolchain_pin() { - let text = summary_text(&opts(Tid::Rust), Some("1.98.0")); + let text = summary_text(&opts(Tid::RUST), Some("1.98.0")); assert!( text.contains("rust-toolchain 1.98.0 (ignored by the chroot build)"), "{text}" ); // No pin, no row. - assert!(!summary_text(&opts(Tid::Rust), None).contains("rust-toolchain")); + assert!(!summary_text(&opts(Tid::RUST), None).contains("rust-toolchain")); // A pin under a template other than rust is not shown either (the // pin only matters for a cargo build). - assert!(!summary_text(&opts(Tid::Go), Some("1.98.0")).contains("rust-toolchain")); + assert!(!summary_text(&opts(Tid::GO), Some("1.98.0")).contains("rust-toolchain")); } /// The git-init question is only asked when a git init would actually @@ -1595,7 +1596,7 @@ mod tests { // The initial pass over dir A. let (detection_a, probe_a) = detect_and_probe(dir_a.path()); - assert_eq!(detection_a, Detection::Single(Tid::Rust)); + assert_eq!(detection_a, Detection::Single(Tid::RUST)); let probe_a = probe_a.expect("dir A is a rust project"); assert_eq!(probe_a.name.as_deref(), Some("alpha")); assert_eq!(probe_a.version.as_deref(), Some("0.1.0")); @@ -1604,7 +1605,7 @@ mod tests { // The user chose dir B instead: the refreshed probe comes from B, // never from A. let (detection_b, probe_b) = detect_and_probe(dir_b.path()); - assert_eq!(detection_b, Detection::Single(Tid::Rust)); + assert_eq!(detection_b, Detection::Single(Tid::RUST)); let probe_b = probe_b.expect("dir B is a rust project"); assert_eq!(probe_b.name.as_deref(), Some("beta")); assert_eq!(probe_b.version.as_deref(), Some("2.9.9")); diff --git a/src/new/templates/autotools.rs b/src/new/templates/autotools.rs index 4562dd8..8cfe0dc 100644 --- a/src/new/templates/autotools.rs +++ b/src/new/templates/autotools.rs @@ -1,27 +1,31 @@ //! The `autotools` template: a C project with a `configure.ac` built through //! debhelper's auto-detection (dh runs `autoreconf` itself when it finds //! `configure.ac`, debhelper ≥ 10 — no override needed). +//! +//! The metadata is manifest data; the logic half here is the `AC_INIT` +//! probe, the GNU-gettext detection (appending `gettext` to the manifest's +//! Build-Depends) and — until the bodies move into the manifest's `files:` +//! list — the `configure.ac`/`Makefile.am`/`hello.c` skeleton. use std::path::Path; use regex::Regex; use super::meson::hello_c; -use super::{OutputFile, ProbeResult, Template, source_dir_of}; -use crate::new::options::{NewOptions, TemplateId}; +use super::{OutputFile, ProbeResult, TemplateHooks, source_dir_of}; +use crate::new::options::NewOptions; -/// C/C++ with Autotools (`configure.ac`). -pub struct Autotools; +/// The logic half of the autotools template. +pub struct Hooks; -impl Template for Autotools { - fn id(&self) -> TemplateId { - TemplateId::Autotools - } +/// The autotools template's hooks, registered in the template registry. +pub static HOOKS: Hooks = Hooks; +impl TemplateHooks for Hooks { /// A minimal `configure.ac`, the matching `Makefile.am` and `hello.c`. /// The first source build runs `autoreconf` (integrated in the dh /// sequence), so no generated configure script is committed. - fn skeleton(&self, opts: &NewOptions) -> Vec { + fn skeleton_files(&self, opts: &NewOptions) -> Vec { vec![ OutputFile::new( "configure.ac", @@ -47,25 +51,14 @@ impl Template for Autotools { ] } - /// No extra debian/ files: plain `dh $@` auto-detects `configure.ac`. - fn debian(&self, _opts: &NewOptions) -> Vec { - Vec::new() - } - - fn build_depends(&self, opts: &NewOptions) -> Vec { - let mut deps = vec![ - "autoconf".to_string(), - "automake".to_string(), - "libtool".to_string(), - ]; + /// A packaged `configure.ac` that sets up GNU gettext (the + /// `AM_GNU_GETTEXT` macro, see [`uses_gettext`]) appends `gettext` to + /// the manifest's Build-Depends. + fn build_depends(&self, opts: &NewOptions, mut base: Vec) -> Vec { if uses_gettext(opts) { - deps.push("gettext".to_string()); + base.push("gettext".to_string()); } - deps - } - - fn architecture(&self, _opts: &NewOptions) -> &'static str { - "any" + base } /// Package name and version from the `AC_INIT` macro of `configure.ac`. @@ -109,13 +102,13 @@ fn uses_gettext(opts: &NewOptions) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::new::options::{License, SourceDir}; + use crate::new::options::{License, SourceDir, TemplateId}; use tempfile::tempdir; fn opts() -> NewOptions { NewOptions { name: "mytool".into(), - template: TemplateId::Autotools, + template: TemplateId::AUTOTOOLS, source_dir: SourceDir::Skeleton, upstream_version: "0.1.0".into(), revision: 1, @@ -141,7 +134,7 @@ mod tests { #[test] fn autotools_template_shape() { let o = opts(); - let template = super::super::get(TemplateId::Autotools).unwrap(); + let template = super::super::get(TemplateId::AUTOTOOLS).unwrap(); assert_eq!(template.architecture(&o), "any"); assert_eq!( @@ -177,7 +170,7 @@ mod tests { #[test] fn autotools_probe_reads_ac_init() { - let template = super::super::get(TemplateId::Autotools).unwrap(); + let template = super::super::get(TemplateId::AUTOTOOLS).unwrap(); // Bracketed form (the generated skeleton's own shape). let dir = tempdir().unwrap(); @@ -222,7 +215,7 @@ mod tests { let o = opts(); assert!(!uses_gettext(&o)); assert!( - !super::super::get(TemplateId::Autotools) + !super::super::get(TemplateId::AUTOTOOLS) .unwrap() .build_depends(&o) .contains(&"gettext".to_string()) @@ -241,7 +234,7 @@ mod tests { }; assert!(uses_gettext(&o)); assert!( - super::super::get(TemplateId::Autotools) + super::super::get(TemplateId::AUTOTOOLS) .unwrap() .build_depends(&o) .contains(&"gettext".to_string()) diff --git a/src/new/templates/cmake.rs b/src/new/templates/cmake.rs index fed4e8f..9b6c283 100644 --- a/src/new/templates/cmake.rs +++ b/src/new/templates/cmake.rs @@ -1,27 +1,31 @@ //! The `cmake` template: a C/C++ project built with CMake through the //! debhelper cmake buildsystem. +//! +//! The metadata is manifest data; the logic half here is the `project()` +//! probe, the wizard's pkg-config opt-in (appended to the manifest's +//! Build-Depends) and — until the bodies move into the manifest's `files:` +//! list — the `CMakeLists.txt`/`hello.c` skeleton. use std::path::Path; use regex::Regex; use super::meson::hello_c; -use super::{OutputFile, ProbeResult, Template}; -use crate::new::options::{NewOptions, TemplateId}; +use super::{ProbeResult, TemplateHooks}; +use crate::new::options::NewOptions; -/// C/C++ with CMake (`CMakeLists.txt`). -pub struct Cmake; +/// The logic half of the cmake template. +pub struct Hooks; -impl Template for Cmake { - fn id(&self) -> TemplateId { - TemplateId::Cmake - } +/// The cmake template's hooks, registered in the template registry. +pub static HOOKS: Hooks = Hooks; +impl TemplateHooks for Hooks { /// A minimal `CMakeLists.txt` (project declaration + one installed /// executable) and the classic `hello.c`. - fn skeleton(&self, opts: &NewOptions) -> Vec { + fn skeleton_files(&self, opts: &NewOptions) -> Vec { vec![ - OutputFile::new( + super::OutputFile::new( "CMakeLists.txt", format!( "cmake_minimum_required(VERSION 3.16)\n\ @@ -38,26 +42,14 @@ impl Template for Cmake { ] } - /// No extra debian/ files: debhelper's cmake buildsystem handles the - /// configure/build/install steps. - fn debian(&self, _opts: &NewOptions) -> Vec { - Vec::new() - } - - fn build_depends(&self, opts: &NewOptions) -> Vec { - let mut deps = vec!["cmake".to_string()]; + /// The wizard's pkg-config opt-in ([`NewOptions::pkg_config`], offered + /// when the project's build file hints at `pkg_check_modules` usage) + /// adds `pkg-config` to the manifest's Build-Depends. + fn build_depends(&self, opts: &NewOptions, mut base: Vec) -> Vec { if opts.pkg_config { - deps.push("pkg-config".to_string()); + base.push("pkg-config".to_string()); } - deps - } - - fn architecture(&self, _opts: &NewOptions) -> &'static str { - "any" - } - - fn rules_dh_line(&self) -> String { - "dh $@ --buildsystem=cmake".to_string() + base } /// Project name and version from the `project( VERSION …)` @@ -84,13 +76,13 @@ impl Template for Cmake { #[cfg(test)] mod tests { use super::*; - use crate::new::options::{License, SourceDir}; + use crate::new::options::{License, SourceDir, TemplateId}; use tempfile::tempdir; fn opts() -> NewOptions { NewOptions { name: "mytool".into(), - template: TemplateId::Cmake, + template: TemplateId::CMAKE, source_dir: SourceDir::Skeleton, upstream_version: "0.1.0".into(), revision: 1, @@ -116,7 +108,7 @@ mod tests { #[test] fn cmake_template_shape() { let o = opts(); - let template = super::super::get(TemplateId::Cmake).unwrap(); + let template = super::super::get(TemplateId::CMAKE).unwrap(); assert_eq!(template.architecture(&o), "any"); assert_eq!(template.build_depends(&o), vec!["cmake".to_string()]); @@ -144,7 +136,7 @@ mod tests { #[test] fn cmake_probe_reads_project_declaration() { - let template = super::super::get(TemplateId::Cmake).unwrap(); + let template = super::super::get(TemplateId::CMAKE).unwrap(); let dir = tempdir().unwrap(); std::fs::write( diff --git a/src/new/templates/empty.rs b/src/new/templates/empty.rs index 944d20c..d06ae13 100644 --- a/src/new/templates/empty.rs +++ b/src/new/templates/empty.rs @@ -5,22 +5,23 @@ //! **metapackage** flavor (the canonical `Architecture: all`, nothing //! compiled, the Depends list *is* the payload shape), while an empty list //! selects the **empty base** — pure `dh $@` plumbing as a starting point -//! for hand-written rules. +//! for hand-written rules. Both flavors are pure manifest data except for +//! the stub `README` of the skeleton, which lives here until its static +//! body moves into the manifest's `files:` list. -use super::{OutputFile, Template}; +use super::{OutputFile, TemplateHooks}; use crate::new::options::NewOptions; -/// Metapackage / empty base (no build system). -pub struct Empty; +/// The logic half of the empty template. +pub struct Hooks; -impl Template for Empty { - fn id(&self) -> crate::new::options::TemplateId { - crate::new::options::TemplateId::Empty - } +/// The empty template's hooks, registered in the template registry. +pub static HOOKS: Hooks = Hooks; - /// No upstream files; just a stub `README` marking the tree as +impl TemplateHooks for Hooks { + /// No upstream files beyond a stub `README` marking the tree as /// intentionally empty. - fn skeleton(&self, opts: &NewOptions) -> Vec { + fn skeleton_files(&self, opts: &NewOptions) -> Vec { vec![OutputFile::new( "README", format!( @@ -30,12 +31,6 @@ impl Template for Empty { ), )] } - - /// No extra debian/ files: the metapackage `Depends` list is carried by - /// [`NewOptions::depends`] into the common `debian/control` rendering. - fn debian(&self, _opts: &NewOptions) -> Vec { - Vec::new() - } } #[cfg(test)] @@ -46,7 +41,7 @@ mod tests { fn opts(depends: Vec) -> NewOptions { NewOptions { name: "metapkg".into(), - template: TemplateId::Empty, + template: TemplateId::EMPTY, source_dir: SourceDir::Skeleton, upstream_version: "0.1.0".into(), revision: 1, @@ -71,7 +66,7 @@ mod tests { #[test] fn empty_template_shape() { - let template = super::super::get(TemplateId::Empty).unwrap(); + let template = super::super::get(TemplateId::EMPTY).unwrap(); // Metapackage flavor: the depends list travels in the options. let o = opts(vec!["hello".into(), "hello-data (>= 1.0)".into()]); diff --git a/src/new/templates/go.rs b/src/new/templates/go.rs index 310e9c9..41839b8 100644 --- a/src/new/templates/go.rs +++ b/src/new/templates/go.rs @@ -1,26 +1,28 @@ //! The `go` template: a Go module built through dh-golang. //! -//! The source stanza carries `XS-Go-Import-Path`, probed from the `module` -//! line of `go.mod` when the packaged tree has one, defaulting to the -//! package name (fresh skeletons embed the package name in their own -//! `go.mod`). +//! The source stanza carries `XS-Go-Import-Path`, declared as the +//! `{go_import_path}` placeholder by the manifest and filled from the +//! `module` line of `go.mod` when the packaged tree has one, defaulting to +//! the package name (fresh skeletons embed the package name in their own +//! `go.mod`). The module line is also the probe of an existing project; the +//! skeleton bodies stay here until they move into the manifest's `files:` +//! list. use std::path::Path; -use super::{OutputFile, Template, source_dir_of}; -use crate::new::options::{NewOptions, TemplateId}; +use super::{OutputFile, ProbeResult, TemplateHooks, source_dir_of}; +use crate::new::options::NewOptions; -/// Go module (`go.mod`). -pub struct Go; +/// The logic half of the go template. +pub struct Hooks; -impl Template for Go { - fn id(&self) -> TemplateId { - TemplateId::Go - } +/// The go template's hooks, registered in the template registry. +pub static HOOKS: Hooks = Hooks; +impl TemplateHooks for Hooks { /// A stdlib-only `main.go` (no archive dependencies needed to build) and /// the matching `go.mod` whose module path is the package name. - fn skeleton(&self, opts: &NewOptions) -> Vec { + fn skeleton_files(&self, opts: &NewOptions) -> Vec { vec![ OutputFile::new( "go.mod", @@ -49,36 +51,21 @@ impl Template for Go { ] } - /// No extra debian/ files: dh-golang drives the build. - fn debian(&self, _opts: &NewOptions) -> Vec { - Vec::new() - } - - fn build_depends(&self, _opts: &NewOptions) -> Vec { - vec!["golang-any".to_string(), "dh-golang".to_string()] - } - - fn architecture(&self, _opts: &NewOptions) -> &'static str { - "any" - } - - fn rules_dh_line(&self) -> String { - "dh $@ --buildsystem=golang".to_string() - } - - fn source_fields(&self, opts: &NewOptions) -> Vec<(String, String)> { - vec![("XS-Go-Import-Path".to_string(), import_path(opts))] + /// The `{go_import_path}` value of the manifest's `XS-Go-Import-Path` + /// source field (see [`import_path`]). + fn context(&self, opts: &NewOptions) -> Vec<(String, String)> { + vec![("go_import_path".to_string(), import_path(opts))] } /// Name (and default command) from the `module` line of `go.mod`: the /// last path segment is the conventional binary/package name. - fn probe(&self, dir: &Path) -> Option { + fn probe(&self, dir: &Path) -> Option { let module = read_module_line(dir)?; let name = module.rsplit('/').next()?.to_string(); if name.is_empty() { return None; } - Some(super::ProbeResult { + Some(ProbeResult { name: Some(name.clone()), command: Some(name), ..Default::default() @@ -114,13 +101,13 @@ fn read_module_line(dir: &Path) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::new::options::{License, SourceDir}; + use crate::new::options::{License, SourceDir, TemplateId}; use tempfile::tempdir; fn opts(source_dir: SourceDir) -> NewOptions { NewOptions { name: "mytool".into(), - template: TemplateId::Go, + template: TemplateId::GO, source_dir, upstream_version: "0.1.0".into(), revision: 1, @@ -146,7 +133,7 @@ mod tests { #[test] fn go_template_shape() { let o = opts(SourceDir::Skeleton); - let template = super::super::get(TemplateId::Go).unwrap(); + let template = super::super::get(TemplateId::GO).unwrap(); assert_eq!(template.architecture(&o), "any"); assert_eq!( @@ -180,7 +167,7 @@ mod tests { ) .unwrap(); - let template = super::super::get(TemplateId::Go).unwrap(); + let template = super::super::get(TemplateId::GO).unwrap(); let probe = template.probe(dir.path()).expect("probe result"); assert_eq!(probe.name.as_deref(), Some("mytool")); assert_eq!(probe.command.as_deref(), Some("mytool")); diff --git a/src/new/templates/makefile.rs b/src/new/templates/makefile.rs index ca5147c..d255c66 100644 --- a/src/new/templates/makefile.rs +++ b/src/new/templates/makefile.rs @@ -1,29 +1,33 @@ -//! The `makefile` template: a generic project driven by a plain `Makefile`. +//! The `makefile` template: a generic project driven by a plain Makefile. //! -//! debhelper's makefile buildsystem runs `make` for the build and -//! `make install DESTDIR=...` when the Makefile carries an `install:` target +//! debhelper's makefile buildsystem runs `make` for the build and `make +//! install DESTDIR=...` when the Makefile carries an `install:` target //! (missing targets are skipped gracefully), so plain `dh $@` plumbing is -//! enough here. +//! enough here. The metadata is manifest data; the logic half here is the +//! phony-`install:` heuristic advising the user whether `dh_auto_install` +//! will run `make install`, plus (until their bodies move into the +//! manifest's `files:` list) the `hello.c`/`Makefile` skeleton and its +//! `debian/install` mapping. use std::path::Path; -use super::{OutputFile, Template, source_dir_of}; -use crate::new::options::{NewOptions, SourceDir, TemplateId}; +use super::meson::hello_c; +use super::{OutputFile, TemplateHooks, source_dir_of}; +use crate::new::options::{NewOptions, SourceDir}; -/// Generic Makefile-based project. -pub struct Makefile; +/// The logic half of the makefile template. +pub struct Hooks; -impl Template for Makefile { - fn id(&self) -> TemplateId { - TemplateId::Makefile - } +/// The makefile template's hooks, registered in the template registry. +pub static HOOKS: Hooks = Hooks; +impl TemplateHooks for Hooks { /// A `hello.c` plus a `Makefile` with `all`/`install`/`clean` targets; /// `install` honors `DESTDIR` and copies the binary to /// `$(DESTDIR)/usr/bin/`. - fn skeleton(&self, opts: &NewOptions) -> Vec { + fn skeleton_files(&self, opts: &NewOptions) -> Vec { vec![ - super::meson::hello_c(opts), + hello_c(opts), OutputFile::new( "Makefile", format!( @@ -55,7 +59,7 @@ impl Template for Makefile { /// existing tree the Makefile is probed instead (no `debian/install` is /// emitted there — the source-relative mapping of an unknown artifact is /// only the project's to write, and `make install` already ran). - fn debian(&self, opts: &NewOptions) -> Vec { + fn debian_files(&self, opts: &NewOptions) -> Vec { match &opts.source_dir { SourceDir::Skeleton => { vec![OutputFile::new( @@ -82,14 +86,6 @@ impl Template for Makefile { } } } - - fn build_depends(&self, _opts: &NewOptions) -> Vec { - vec!["build-essential".to_string()] - } - - fn architecture(&self, _opts: &NewOptions) -> &'static str { - "any" - } } /// The name of the phony `install:` target of the Makefile at `path`, when @@ -119,13 +115,13 @@ pub fn phony_install_target(path: &Path) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::new::options::{License, SourceDir}; + use crate::new::options::{License, SourceDir, TemplateId}; use tempfile::tempdir; fn opts(source_dir: SourceDir) -> NewOptions { NewOptions { name: "mytool".into(), - template: TemplateId::Makefile, + template: TemplateId::MAKEFILE, source_dir, upstream_version: "0.1.0".into(), revision: 1, @@ -151,7 +147,7 @@ mod tests { #[test] fn makefile_template_shape() { let o = opts(SourceDir::Skeleton); - let template = super::super::get(TemplateId::Makefile).unwrap(); + let template = super::super::get(TemplateId::MAKEFILE).unwrap(); assert_eq!(template.architecture(&o), "any"); assert_eq!( diff --git a/src/new/templates/meson.rs b/src/new/templates/meson.rs index 6ab43d0..9f9e4d4 100644 --- a/src/new/templates/meson.rs +++ b/src/new/templates/meson.rs @@ -1,24 +1,29 @@ //! The `meson` template: a C/C++ project built with Meson through the //! debhelper meson buildsystem. +//! +//! The metadata is manifest data; the logic half here is the `project()` +//! probe, the wizard's pkg-config opt-in (appended to the manifest's +//! Build-Depends) and — until the bodies move into the manifest's `files:` +//! list — the `meson.build`/`hello.c` skeleton. [`hello_c`] is the shared +//! placeholder of the C/C++ skeletons. use std::path::Path; use regex::Regex; -use super::{OutputFile, ProbeResult, Template}; -use crate::new::options::{NewOptions, TemplateId}; +use super::{OutputFile, ProbeResult, TemplateHooks}; +use crate::new::options::NewOptions; -/// C/C++ with Meson (`meson.build`). -pub struct Meson; +/// The logic half of the meson template. +pub struct Hooks; -impl Template for Meson { - fn id(&self) -> TemplateId { - TemplateId::Meson - } +/// The meson template's hooks, registered in the template registry. +pub static HOOKS: Hooks = Hooks; +impl TemplateHooks for Hooks { /// A minimal `meson.build` (project declaration + one installed /// executable) and the classic `hello.c`. - fn skeleton(&self, opts: &NewOptions) -> Vec { + fn skeleton_files(&self, opts: &NewOptions) -> Vec { vec![ OutputFile::new( "meson.build", @@ -37,26 +42,14 @@ impl Template for Meson { ] } - /// No extra debian/ files: debhelper's meson buildsystem handles the - /// configure/build/install steps. - fn debian(&self, _opts: &NewOptions) -> Vec { - Vec::new() - } - - fn build_depends(&self, opts: &NewOptions) -> Vec { - let mut deps = vec!["meson".to_string()]; + /// The wizard's pkg-config opt-in ([`NewOptions::pkg_config`], offered + /// when the project's build file hints at `dependency(` usage) adds + /// `pkg-config` to the manifest's Build-Depends. + fn build_depends(&self, opts: &NewOptions, mut base: Vec) -> Vec { if opts.pkg_config { - deps.push("pkg-config".to_string()); + base.push("pkg-config".to_string()); } - deps - } - - fn architecture(&self, _opts: &NewOptions) -> &'static str { - "any" - } - - fn rules_dh_line(&self) -> String { - "dh $@ --buildsystem=meson".to_string() + base } /// Project name and version from the `project('name', version: …)` @@ -98,13 +91,13 @@ pub(super) fn hello_c(opts: &NewOptions) -> OutputFile { #[cfg(test)] mod tests { use super::*; - use crate::new::options::{License, SourceDir}; + use crate::new::options::{License, SourceDir, TemplateId}; use tempfile::tempdir; fn opts() -> NewOptions { NewOptions { name: "mytool".into(), - template: TemplateId::Meson, + template: TemplateId::MESON, source_dir: SourceDir::Skeleton, upstream_version: "0.1.0".into(), revision: 1, @@ -130,7 +123,7 @@ mod tests { #[test] fn meson_template_shape() { let o = opts(); - let template = super::super::get(TemplateId::Meson).unwrap(); + let template = super::super::get(TemplateId::MESON).unwrap(); assert_eq!(template.architecture(&o), "any"); assert_eq!(template.build_depends(&o), vec!["meson".to_string()]); @@ -155,7 +148,7 @@ mod tests { #[test] fn meson_probe_reads_project_declaration() { - let template = super::super::get(TemplateId::Meson).unwrap(); + let template = super::super::get(TemplateId::MESON).unwrap(); let dir = tempdir().unwrap(); std::fs::write( @@ -181,7 +174,7 @@ mod tests { #[test] fn pkg_config_opt_in_extends_build_depends() { - let template = super::super::get(TemplateId::Meson).unwrap(); + let template = super::super::get(TemplateId::MESON).unwrap(); let mut o = opts(); assert_eq!(template.build_depends(&o), vec!["meson".to_string()]); o.pkg_config = true; diff --git a/src/new/templates/mod.rs b/src/new/templates/mod.rs index d5f9e68..f4fd722 100644 --- a/src/new/templates/mod.rs +++ b/src/new/templates/mod.rs @@ -1,11 +1,46 @@ -//! Per-ecosystem template registry for `pkh new`. +//! Per-ecosystem template registry for `pkh new`, driven by the +//! per-template manifests under `data/templates//`. //! -//! Every template implements [`Template`]: it renders the upstream-side -//! skeleton files, the extra `debian/` files beyond the common set, probes an -//! existing project for metadata used to pre-fill the wizard answers, and -//! describes its Build-Depends / architecture / `debian/rules` shape. -//! Rendering is plain `format!` composition — no template engine, matching -//! the codebase style. +//! Every template is split into a **data half** and a **logic half**. The +//! data half is the template's `manifest.yml` — CLI id, wizard label, +//! detection markers, Build-Depends, architecture, `debian/rules` shape, +//! `debian/control` source fields, `.gitignore` entries and the file +//! bodies — embedded together with the `*.tpl` bodies it references (the +//! [`TEMPLATE_SOURCES`] index is the single place to touch when adding a +//! template; `include_str!` cannot glob). Bodies render by `{placeholder}` +//! substitution from an answer-derived context — sequential +//! [`str::replace`], no template engine, matching the house data +//! convention. The logic half is [`TemplateHooks`]: a slim Rust trait for +//! everything the data cannot express — probing an existing project, +//! post-write host tooling (`cargo vendor`) and file bodies needing +//! conditionals. A template without hooks needs zero Rust. +//! +//! Manifest schema (unknown fields are rejected, so typos fail loudly at +//! registry load / test time instead of silently at runtime): +//! +//! ```yaml +//! id: rust # must match the registry entry's id +//! label: Rust (Cargo.toml) # wizard menu label (unique) +//! detect: +//! files: [Cargo.toml] # detection markers (detect.rs); any match +//! # identifies the template, empty = none +//! build_depends: [...] # beyond debhelper-compat (= 13) +//! architecture: any | all +//! rules_dh_line: "dh $@ ..." # the %: recipe of debian/rules +//! rules_extra_file: rules.extra.tpl # optional; appended after the dh +//! # stanza (override_dh_* targets) +//! source_fields: # optional; extra debian/control source +//! XS-Go-Import-Path: "{...}" # stanza fields, values may carry +//! # {placeholders} +//! gitignore_entries: [...] # optional; root .gitignore lines +//! files: # optional; static bodies, split by path: +//! - path: Cargo.toml # debian/-prefixed = debian/ extras +//! template: Cargo.toml.tpl # the .tpl body (name relative to the +//! # template directory) +//! executable: false # optional; mode 0755 when true +//! skeleton_only: true # optional; debian/ extras rendered in +//! # skeleton mode only +//! ``` pub mod autotools; pub mod cmake; @@ -19,8 +54,15 @@ pub mod shell; use std::path::{Path, PathBuf}; +use serde::Deserialize; + use super::options::{NewOptions, SourceDir, TemplateId}; +/// Prefix separating the extra `debian/` files of a manifest from its +/// upstream-side skeleton files (the scaffold renders the former in every +/// mode, the latter only when packaging a fresh skeleton). +const DEBIAN_PREFIX: &str = "debian/"; + /// One generated file, rendered in memory before anything touches the disk. #[derive(Debug, Clone)] pub struct OutputFile { @@ -66,10 +108,10 @@ pub struct ScaffoldOutcome { pub orig_origin: Option, } -/// Metadata extracted from an existing project by [`Template::probe`], used -/// by the interactive wizard to pre-fill its answers (explicit flags always -/// win). Every field is optional; probe failures are silent and the generic -/// defaults apply. +/// Metadata extracted from an existing project by +/// [`TemplateHooks::probe`], used by the interactive wizard to pre-fill +/// its answers (explicit flags always win). Every field is optional; probe +/// failures are silent and the generic defaults apply. #[derive(Debug, Clone, Default)] pub struct ProbeResult { /// Project name (e.g. the `name` key of `Cargo.toml`). @@ -92,62 +134,22 @@ pub struct ProbeResult { pub toolchain_pin: Option, } -/// A package template: one supported ecosystem / build system. +/// The logic half of a template: everything the manifest data cannot +/// express, registered by id in [`TEMPLATE_SOURCES`]. Every method has a +/// do-nothing default, so a template needing none of them ships as pure +/// data. /// -/// `Sync` is required so templates can live in the static registry. -pub trait Template: Sync { - /// Identifier of this template. - fn id(&self) -> TemplateId; - - /// Upstream-side files for the skeleton mode (e.g. `Cargo.toml`, - /// `src/main.rs`). Only called when packaging a fresh skeleton. - fn skeleton(&self, opts: &NewOptions) -> Vec; - - /// Extra `debian/` files beyond the common set rendered by - /// [`super::debian`] (e.g. `debian/install`). - fn debian(&self, opts: &NewOptions) -> Vec; - - /// Build-Depends beyond `debhelper-compat (= 13)`. - fn build_depends(&self, _opts: &NewOptions) -> Vec { - Vec::new() - } - - /// Architecture of the binary package (`all` or `any`). - fn architecture(&self, _opts: &NewOptions) -> &'static str { - "all" - } - - /// The `dh` invocation (without leading tab) used by the `%:` target of - /// `debian/rules`. Templates needing more than the plain `dh $@` spell - /// their buildsystem/sequencer options here so the generated rules stay - /// valid make. - fn rules_dh_line(&self) -> String { - "dh $@".to_string() - } - - /// Lines appended to `debian/rules` after the default `dh $@` stanza - /// (e.g. `override_dh_*` targets). Must use tabs for recipe lines. - fn rules_extra(&self, _opts: &NewOptions) -> String { - String::new() - } - - /// Extra `debian/control` source-stanza fields beyond the common set - /// (e.g. `XS-Go-Import-Path`). - fn source_fields(&self, _opts: &NewOptions) -> Vec<(String, String)> { - Vec::new() - } - - /// Root `.gitignore` entries the template contributes in every mode - /// (the skeleton-mode build-artifact entries of - /// [`crate::new::debian::ROOT_GITIGNORE_ENTRIES`] are separate), merged - /// into the root `.gitignore` after the files are written — missing - /// ones appended, an existing file never overwritten. Default: none. - fn gitignore_entries(&self, _opts: &NewOptions) -> Vec { - Vec::new() - } - - /// Extra defaults derived from project metadata in `dir` (detect.rs); - /// `None` when the project carries nothing this template can read. +/// The manifest-driven files are the base of both file lists; a hook's +/// [`skeleton_files`](TemplateHooks::skeleton_files) / +/// [`debian_files`](TemplateHooks::debian_files) entries are merged over +/// them by [`merge_files`]: an entry whose path a manifest file already +/// uses replaces it, anything else is appended. That is how a body needing +/// conditionals stays in Rust without giving up the data half — the hook +/// file shadows the manifest body it supersedes. +pub trait TemplateHooks: Sync { + /// Extra defaults derived from the project metadata in `dir` + /// (detect.rs pre-fills the wizard answers); `None` when the project + /// carries nothing this template can read. fn probe(&self, _dir: &Path) -> Option { None } @@ -156,9 +158,9 @@ pub trait Template: Sync { /// before the orig tarball is created, for templates that need to run /// host tooling over the freshly written tree (e.g. `cargo vendor`, so /// the vendored sources land inside the tarball). Returns the outcome - /// the flow should know about ([`ScaffoldOutcome`]); failures that leave - /// the tree in place but not buildable are reported through it instead - /// of failing the scaffold. + /// the flow should know about ([`ScaffoldOutcome`]); failures that + /// leave the tree in place but not buildable are reported through it + /// instead of failing the scaffold. fn post_write( &self, _opts: &NewOptions, @@ -166,52 +168,617 @@ pub trait Template: Sync { ) -> Result> { Ok(ScaffoldOutcome::default()) } -} -/// Static instance of the shell template. -pub static SHELL: shell::Shell = shell::Shell; -/// Static instance of the empty/metapackage template. -pub static EMPTY: empty::Empty = empty::Empty; -/// Static instance of the makefile template. -pub static MAKEFILE: makefile::Makefile = makefile::Makefile; -/// Static instance of the python template. -pub static PYTHON: python::Python = python::Python; -/// Static instance of the meson template. -pub static MESON: meson::Meson = meson::Meson; -/// Static instance of the cmake template. -pub static CMAKE: cmake::Cmake = cmake::Cmake; -/// Static instance of the autotools template. -pub static AUTOTOOLS: autotools::Autotools = autotools::Autotools; -/// Static instance of the go template. -pub static GO: go::Go = go::Go; -/// Static instance of the rust template. -pub static RUST: rust::Rust = rust::Rust; + /// Upstream-side files merged over the manifest's skeleton bodies + /// ([`Template::skeleton`]). + fn skeleton_files(&self, _opts: &NewOptions) -> Vec { + Vec::new() + } -/// Every implemented template. -static TEMPLATES: &[&dyn Template] = &[ - &SHELL, &EMPTY, &MAKEFILE, &PYTHON, &MESON, &CMAKE, &AUTOTOOLS, &GO, &RUST, -]; + /// Extra `debian/` files merged over the manifest's debian extras + /// ([`Template::debian`]). + fn debian_files(&self, _opts: &NewOptions) -> Vec { + Vec::new() + } -/// Look up the template implementation for `id`; `None` only if a -/// [`TemplateId`] ever grows without a registered template (callers turn -/// this into a friendly error instead of panicking). -pub fn get(id: TemplateId) -> Option<&'static dyn Template> { - match id { - TemplateId::Shell => Some(&SHELL), - TemplateId::Empty => Some(&EMPTY), - TemplateId::Makefile => Some(&MAKEFILE), - TemplateId::Python => Some(&PYTHON), - TemplateId::Meson => Some(&MESON), - TemplateId::Cmake => Some(&CMAKE), - TemplateId::Autotools => Some(&AUTOTOOLS), - TemplateId::Go => Some(&GO), - TemplateId::Rust => Some(&RUST), + /// The effective Build-Depends beyond `debhelper-compat (= 13)`: + /// `base` is the manifest's list, returned unchanged by default — + /// amend it when only the packaged project decides (meson/cmake's + /// pkg-config opt-in, autotools' gettext) or replace it wholesale when + /// the answers do (python's backend table). + fn build_depends(&self, _opts: &NewOptions, base: Vec) -> Vec { + base + } + + /// The binary architecture the packaged project needs instead of the + /// manifest's `architecture` (python's C-extension hints flip `all` to + /// `any`); `None` keeps the manifest value. + fn architecture(&self, _opts: &NewOptions) -> Option<&'static str> { + None + } + + /// Extra placeholder values this template's `*.tpl` bodies and manifest + /// values substitute beyond the generic answer context ([`context`]): + /// go's `{go_import_path}`, rust's `{locked}`/`{artifact}`. Keys must + /// not shadow the generic ones. + fn context(&self, _opts: &NewOptions) -> Vec<(String, String)> { + Vec::new() } } -/// Every implemented template. -pub fn all() -> &'static [&'static dyn Template] { - TEMPLATES +/// The binary package architecture of a template's manifest. +#[derive(Debug, Clone, Copy, Deserialize)] +enum Architecture { + /// Compiled code: architecture-dependent (`any`). + #[serde(rename = "any")] + Any, + /// Interpreted or payload-only content (`all`). + #[serde(rename = "all")] + All, +} + +impl Architecture { + /// The `debian/control` spelling. + fn as_str(self) -> &'static str { + match self { + Architecture::Any => "any", + Architecture::All => "all", + } + } +} + +/// The language-detection spec of a template: the top-level marker files +/// [`super::detect`] looks for (any match identifies the template). +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct DetectSpec { + /// Marker file names relative to the packaged directory; empty for + /// templates detected by other means (shell: the single-script + /// heuristic; empty: never detected). + #[serde(default)] + files: Vec, +} + +/// One static body file of a manifest. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ManifestFile { + /// Path relative to the package tree root, `{placeholder}`s included + /// (`debian/`-prefixed paths are the template's debian/ extras, the + /// rest upstream-side skeleton files). + path: String, + /// Name of the `.tpl` body, relative to the template's data directory. + template: String, + /// Whether the file carries the executable bit (mode 0755). + #[serde(default)] + executable: bool, + /// Render this debian/ extra in skeleton mode only (a no-op flag on + /// upstream-side paths, which are skeleton-only by nature). + #[serde(default)] + skeleton_only: bool, +} + +/// One template's `manifest.yml` (see the module docs for the schema). +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Manifest { + /// Stable CLI identifier (`--lang`); must agree with the registry + /// entry carrying the manifest. + id: String, + /// Wizard menu label (unique across the registry). + label: String, + /// Language-detection spec (see [`DetectSpec`]). + #[serde(default)] + detect: DetectSpec, + /// Build-Depends beyond `debhelper-compat (= 13)`. + build_depends: Vec, + /// Architecture of the binary package. + architecture: Architecture, + /// The `dh` invocation (without leading tab) used by the `%:` target + /// of `debian/rules`. + rules_dh_line: String, + /// Name of the `.tpl` body appended to `debian/rules` after the dh + /// stanza (e.g. `override_dh_*` targets). + #[serde(default)] + rules_extra_file: Option, + /// Extra `debian/control` source-stanza fields, in document order; + /// values may carry `{placeholder}`s. + #[serde(default, deserialize_with = "source_field_pairs")] + source_fields: Vec<(String, String)>, + /// Root `.gitignore` entries the template contributes in every mode. + #[serde(default)] + gitignore_entries: Vec, + /// Static file bodies (see [`ManifestFile`]). + #[serde(default)] + files: Vec, +} + +/// Deserialize a YAML mapping (`field: value`) into document-ordered +/// pairs, rejecting non-string keys and values — the strictness keeps +/// manifest typos loud. +fn source_field_pairs<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let mapping = serde_yaml::Mapping::deserialize(deserializer)?; + let mut pairs = Vec::with_capacity(mapping.len()); + for (field, value) in mapping { + let field = field + .as_str() + .ok_or_else(|| serde::de::Error::custom("source_fields keys must be strings"))?; + let value = value + .as_str() + .ok_or_else(|| serde::de::Error::custom("source_fields values must be strings"))?; + pairs.push((field.to_string(), value.to_string())); + } + Ok(pairs) +} + +/// One template's embedded sources: the raw `manifest.yml` plus the bodies +/// of the `*.tpl` files it references, by file name, and the hooks +/// implementing its logic half (`None` for a pure-data template). +/// +/// **Adding a template = one `data/templates//` directory plus one +/// entry here** — the only hand-maintained index (`include_str!` cannot +/// glob). The entry order is the registry order, which is the wizard menu +/// order and the detection priority order at once. +struct TemplateSources { + /// The template this entry belongs to (its manifest's `id` must agree). + id: TemplateId, + /// Raw contents of the template's `manifest.yml`. + manifest: &'static str, + /// `(file name, contents)` of every `.tpl` body the manifest references. + tpls: &'static [(&'static str, &'static str)], + /// The logic half, when the template has one. + hooks: Option<&'static dyn TemplateHooks>, +} + +static TEMPLATE_SOURCES: &[TemplateSources] = &[ + TemplateSources { + id: TemplateId::RUST, + manifest: include_str!("../../../data/templates/rust/manifest.yml"), + tpls: &[( + "rules.extra.tpl", + include_str!("../../../data/templates/rust/rules.extra.tpl"), + )], + hooks: Some(&rust::HOOKS), + }, + TemplateSources { + id: TemplateId::PYTHON, + manifest: include_str!("../../../data/templates/python/manifest.yml"), + tpls: &[], + hooks: Some(&python::HOOKS), + }, + TemplateSources { + id: TemplateId::MESON, + manifest: include_str!("../../../data/templates/meson/manifest.yml"), + tpls: &[], + hooks: Some(&meson::HOOKS), + }, + TemplateSources { + id: TemplateId::CMAKE, + manifest: include_str!("../../../data/templates/cmake/manifest.yml"), + tpls: &[], + hooks: Some(&cmake::HOOKS), + }, + TemplateSources { + id: TemplateId::AUTOTOOLS, + manifest: include_str!("../../../data/templates/autotools/manifest.yml"), + tpls: &[], + hooks: Some(&autotools::HOOKS), + }, + TemplateSources { + id: TemplateId::GO, + manifest: include_str!("../../../data/templates/go/manifest.yml"), + tpls: &[], + hooks: Some(&go::HOOKS), + }, + TemplateSources { + id: TemplateId::SHELL, + manifest: include_str!("../../../data/templates/shell/manifest.yml"), + tpls: &[], + hooks: Some(&shell::HOOKS), + }, + TemplateSources { + id: TemplateId::MAKEFILE, + manifest: include_str!("../../../data/templates/makefile/manifest.yml"), + tpls: &[], + hooks: Some(&makefile::HOOKS), + }, + TemplateSources { + id: TemplateId::EMPTY, + manifest: include_str!("../../../data/templates/empty/manifest.yml"), + tpls: &[], + hooks: Some(&empty::HOOKS), + }, +]; + +lazy_static::lazy_static! { + /// The parsed registry: manifest data plus resolved hooks, in the + /// order of [`TEMPLATE_SOURCES`] (wizard menu = detection priority). + static ref REGISTRY: Vec