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:
+33
-32
@@ -139,7 +139,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
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<NewOptions, Box<dyn Error>> {
|
||||
// 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<NewOptions, Box<dyn Error>> {
|
||||
// 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<NewOptions, Box<dyn Error>> {
|
||||
}
|
||||
|
||||
// 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<NewOptions, Box<dyn Error>> {
|
||||
// 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<NewOptions, Box<dyn Error>> {
|
||||
}
|
||||
|
||||
// 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<String> {
|
||||
.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"));
|
||||
|
||||
Reference in New Issue
Block a user