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:
+51
-76
@@ -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/<id>/` (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, String> {
|
||||
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::<Vec<_>>()
|
||||
@@ -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> {
|
||||
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<NewOptions, String> {
|
||||
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<NewOptions, String> {
|
||||
the 'empty' template",
|
||||
dir.display()
|
||||
);
|
||||
Some(TemplateId::Empty)
|
||||
Some(TemplateId::EMPTY)
|
||||
} else {
|
||||
missing.push(
|
||||
"--lang <id> (could not detect a build system; \
|
||||
@@ -845,7 +820,7 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
|
||||
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user