new: template manifests and registry infrastructure

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

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

The static skeleton bodies of the shell/empty/makefile/go templates
stay in their Rust hooks for now; the next commit moves them into
their manifests.
This commit is contained in:
2026-09-18 14:46:28 +02:00
parent fc0d2f247e
commit 04a572cd77
26 changed files with 1477 additions and 716 deletions
+19
View File
@@ -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: []
+16
View File
@@ -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: []
+14
View File
@@ -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: []
+18
View File
@@ -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: []
+17
View File
@@ -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: []
+16
View File
@@ -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: []
+21
View File
@@ -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: []
+26
View File
@@ -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: []
+19
View File
@@ -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
+16
View File
@@ -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: []
+11 -11
View File
@@ -51,7 +51,7 @@ pub fn orig_tarball_path(
} }
/// Render every common `debian/` file of the package. /// Render every common `debian/` file of the package.
pub fn files(opts: &NewOptions, template: &dyn Template) -> Vec<OutputFile> { pub fn files(opts: &NewOptions, template: &Template) -> Vec<OutputFile> {
let mut files = vec![ let mut files = vec![
source_format(opts), source_format(opts),
changelog(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 /// comes from the template (`all` for shell/empty), and a non-empty
/// `opts.depends` (the empty/metapackage flavor) lands in the binary /// `opts.depends` (the empty/metapackage flavor) lands in the binary
/// stanza's `Depends` field. /// stanza's `Depends` field.
fn control(opts: &NewOptions, template: &dyn Template) -> OutputFile { fn control(opts: &NewOptions, template: &Template) -> OutputFile {
let mut control = String::new(); let mut control = String::new();
// Source stanza. // Source stanza.
@@ -238,7 +238,7 @@ fn control(opts: &NewOptions, template: &dyn Template) -> OutputFile {
/// `debian/rules`: the shebang and `%:` target whose recipe is the /// `debian/rules`: the shebang and `%:` target whose recipe is the
/// template's dh line (plus the template's extra overrides, when any), /// template's dh line (plus the template's extra overrides, when any),
/// written with the executable bit. /// 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 mut contents = format!("#!/usr/bin/make -f\n%:\n\t{}\n", template.rules_dh_line());
let extra = template.rules_extra(opts); let extra = template.rules_extra(opts);
if !extra.is_empty() { if !extra.is_empty() {
@@ -533,7 +533,7 @@ mod tests {
fn opts() -> NewOptions { fn opts() -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Shell, template: TemplateId::SHELL,
source_dir: SourceDir::Skeleton, source_dir: SourceDir::Skeleton,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -559,7 +559,7 @@ mod tests {
#[test] #[test]
fn source_format_and_local_options() { fn source_format_and_local_options() {
let o = opts(); 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| { let find = |path: &str| {
files files
.iter() .iter()
@@ -582,7 +582,7 @@ mod tests {
}; };
let files = super::files( let files = super::files(
&native, &native,
crate::new::templates::get(TemplateId::Shell).unwrap(), crate::new::templates::get(TemplateId::SHELL).unwrap(),
); );
assert!( assert!(
files files
@@ -642,7 +642,7 @@ mod tests {
#[test] #[test]
fn control_rendering_and_parse() { fn control_rendering_and_parse() {
let o = opts(); 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. // RFC822 continuation: first dep on the field line, the rest indented.
assert!( assert!(
@@ -679,7 +679,7 @@ mod tests {
homepage: None, homepage: None,
..opts() ..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:")); assert!(!control.contents.contains("Homepage:"));
let parsed = crate::debian::ControlInfo::parse_content(&control.contents).unwrap(); let parsed = crate::debian::ControlInfo::parse_content(&control.contents).unwrap();
assert!(parsed.source.get("Homepage").is_none()); assert!(parsed.source.get("Homepage").is_none());
@@ -688,7 +688,7 @@ mod tests {
#[test] #[test]
fn rules_is_executable_minimal_makefile() { fn rules_is_executable_minimal_makefile() {
let o = opts(); 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!(rules.executable);
assert_eq!(rules.contents, "#!/usr/bin/make -f\n%:\n\tdh $@\n"); 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" "version=4\nhttps://github.com/example/mytool/releases .*/v?@ANY_VERSION@\\.tar\\.gz\n"
.to_string(), .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| { let find = |path: &str| {
files files
.iter() .iter()
@@ -731,7 +731,7 @@ mod tests {
// Without the extras none of the files are rendered. // Without the extras none of the files are rendered.
let plain = super::files( let plain = super::files(
&opts(), &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.starts_with("debian/tests")));
assert!(!plain.iter().any(|f| f.path == "debian/watch")); assert!(!plain.iter().any(|f| f.path == "debian/watch"));
+35 -39
View File
@@ -4,13 +4,16 @@
//! The rule set is deliberately simple and table-driven (highest precedence //! The rule set is deliberately simple and table-driven (highest precedence
//! first): //! first):
//! //!
//! 1. well-known build-system marker files at the top level of the //! 1. the `detect.files` marker files declared by the template manifests
//! directory (`Cargo.toml`, `pyproject.toml`/`setup.py`/`setup.cfg`, //! (`data/templates/<id>/manifest.yml`, in registry order: `Cargo.toml`,
//! `meson.build`, `CMakeLists.txt`, `configure.ac`, `go.mod`, //! `pyproject.toml`/`setup.py`/`setup.cfg`, `meson.build`,
//! `Makefile`) — more than one distinct template matching is //! `CMakeLists.txt`, `configure.ac`, `go.mod`, `Makefile`) looked for at
//! [`Detection::Ambiguous`], //! 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 //! 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, //! several scripts or none → nothing,
//! 3. otherwise [`Detection::Empty`]. //! 3. otherwise [`Detection::Empty`].
//! //!
@@ -25,6 +28,7 @@ use regex::Regex;
use super::licenses; use super::licenses;
use super::options::TemplateId; use super::options::TemplateId;
use super::templates;
/// Outcome of the detection. /// Outcome of the detection.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -38,26 +42,18 @@ pub enum Detection {
Empty, Empty,
} }
/// Marker files per template, in precedence order (see the module docs). /// Detect the template matching the project in `dir`: the manifests'
const MARKERS: [(TemplateId, &[&str]); 7] = [ /// marker files in registry order (the detection priority), then the
(TemplateId::Rust, &["Cargo.toml"]), /// shell single-script heuristic.
(
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`.
pub fn detect(dir: &Path) -> Detection { pub fn detect(dir: &Path) -> Detection {
let mut hits: Vec<TemplateId> = Vec::new(); let mut hits: Vec<TemplateId> = Vec::new();
for (id, markers) in MARKERS { for template in templates::all() {
if markers.iter().any(|marker| dir.join(marker).exists()) && !hits.contains(&id) { let markers = template.detect_files();
hits.push(id); 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() { if single_script(dir).is_some() {
Detection::Single(TemplateId::Shell) Detection::Single(TemplateId::SHELL)
} else { } else {
Detection::Empty Detection::Empty
} }
@@ -176,15 +172,15 @@ mod tests {
#[test] #[test]
fn marker_files_map_to_templates() { fn marker_files_map_to_templates() {
let cases = [ let cases = [
("Cargo.toml", TemplateId::Rust), ("Cargo.toml", TemplateId::RUST),
("pyproject.toml", TemplateId::Python), ("pyproject.toml", TemplateId::PYTHON),
("setup.py", TemplateId::Python), ("setup.py", TemplateId::PYTHON),
("setup.cfg", TemplateId::Python), ("setup.cfg", TemplateId::PYTHON),
("meson.build", TemplateId::Meson), ("meson.build", TemplateId::MESON),
("CMakeLists.txt", TemplateId::Cmake), ("CMakeLists.txt", TemplateId::CMAKE),
("configure.ac", TemplateId::Autotools), ("configure.ac", TemplateId::AUTOTOOLS),
("go.mod", TemplateId::Go), ("go.mod", TemplateId::GO),
("Makefile", TemplateId::Makefile), ("Makefile", TemplateId::MAKEFILE),
]; ];
for (marker, expected) in cases { for (marker, expected) in cases {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
@@ -200,21 +196,21 @@ mod tests {
touch(dir.path(), "Makefile"); touch(dir.path(), "Makefile");
assert_eq!( assert_eq!(
detect(dir.path()), detect(dir.path()),
Detection::Ambiguous(vec![TemplateId::Rust, TemplateId::Makefile]) Detection::Ambiguous(vec![TemplateId::RUST, TemplateId::MAKEFILE])
); );
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
touch(dir.path(), "pyproject.toml"); touch(dir.path(), "pyproject.toml");
touch(dir.path(), "setup.py"); touch(dir.path(), "setup.py");
// Both markers map to the same template: one hit, not ambiguous. // 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(); let dir = tempdir().unwrap();
touch(dir.path(), "meson.build"); touch(dir.path(), "meson.build");
touch(dir.path(), "CMakeLists.txt"); touch(dir.path(), "CMakeLists.txt");
assert_eq!( assert_eq!(
detect(dir.path()), 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. // .sh extension.
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
touch(dir.path(), "run.sh"); 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. // Shebang without extension.
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
std::fs::write(dir.path().join("run"), "#!/usr/bin/env python3\n").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. // Two scripts: not exactly one, nothing recognized.
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
+19 -19
View File
@@ -181,7 +181,7 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<ScaffoldOutcome
// mode, plus the template's own entries (rust: the vendored layout) in // mode, plus the template's own entries (rust: the vendored layout) in
// every mode — missing ones appended, an existing file never // every mode — missing ones appended, an existing file never
// overwritten. // overwritten.
let template_gitignore = template.gitignore_entries(opts); let template_gitignore = template.gitignore_entries();
let mut entries: Vec<&str> = Vec::new(); let mut entries: Vec<&str> = Vec::new();
let mut header = None; let mut header = None;
if skeleton { if skeleton {
@@ -216,7 +216,7 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<ScaffoldOutcome
if opts.source_format == SourceFormat::Quilt { if opts.source_format == SourceFormat::Quilt {
pb.set_message("Creating orig tarball"); pb.set_message("Creating orig tarball");
let vendored_rust = let vendored_rust =
template.id() == options::TemplateId::Rust && orig::has_vendored_dir(&target); template.id() == options::TemplateId::RUST && orig::has_vendored_dir(&target);
let created = orig::create_orig( let created = orig::create_orig(
&target, &target,
&opts.name, &opts.name,
@@ -349,7 +349,7 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
scaffold_in( scaffold_in(
dir.path(), dir.path(),
opts(TemplateId::Shell, "mytool", SourceDir::Skeleton), opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton),
) )
.unwrap(); .unwrap();
@@ -423,7 +423,7 @@ mod tests {
#[serial] #[serial]
fn scaffold_skeleton_forced_quilt_snapshots_the_tree() { fn scaffold_skeleton_forced_quilt_snapshots_the_tree() {
let dir = tempdir().unwrap(); let dir = tempdir().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.source_format = SourceFormat::Quilt;
o.orig = Some(OrigOrigin::Snapshot); o.orig = Some(OrigOrigin::Snapshot);
scaffold_in(dir.path(), o).unwrap(); scaffold_in(dir.path(), o).unwrap();
@@ -471,7 +471,7 @@ mod tests {
fn scaffold_empty_base_and_metapackage_flavors() { fn scaffold_empty_base_and_metapackage_flavors() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
// Metapackage flavor: non-empty depends. // Metapackage flavor: non-empty depends.
let mut o = opts(TemplateId::Empty, "metapkg", SourceDir::Skeleton); let mut o = opts(TemplateId::EMPTY, "metapkg", SourceDir::Skeleton);
o.depends = vec!["hello".into(), "hello-data (>= 1.0)".into()]; o.depends = vec!["hello".into(), "hello-data (>= 1.0)".into()];
scaffold_in(dir.path(), o).unwrap(); scaffold_in(dir.path(), o).unwrap();
@@ -493,7 +493,7 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
scaffold_in( scaffold_in(
dir.path(), dir.path(),
opts(TemplateId::Empty, "basepkg", SourceDir::Skeleton), opts(TemplateId::EMPTY, "basepkg", SourceDir::Skeleton),
) )
.unwrap(); .unwrap();
let control = let control =
@@ -505,7 +505,7 @@ mod tests {
#[serial] #[serial]
fn scaffold_release_targets_series() { fn scaffold_release_targets_series() {
let dir = tempdir().unwrap(); 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; o.release = true;
scaffold_in(dir.path(), o).unwrap(); scaffold_in(dir.path(), o).unwrap();
@@ -525,7 +525,7 @@ mod tests {
let tree = dir.path().join("packdir"); let tree = dir.path().join("packdir");
std::fs::create_dir_all(&tree).unwrap(); std::fs::create_dir_all(&tree).unwrap();
std::fs::write(tree.join("run.sh"), "#!/bin/sh\necho hi\n").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 // debian/ lands directly in the directory; no skeleton file, no
// root .gitignore (the shell template contributes none and the // 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(); std::fs::write(tree.join("debian/control"), "Source: mytool\n").unwrap();
let err = scaffold_in( let err = scaffold_in(
dir.path(), dir.path(),
opts(TemplateId::Shell, "mytool", SourceDir::Skeleton), opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton),
) )
.unwrap_err(); .unwrap_err();
assert!(err.to_string().contains("debian/control"), "{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(); std::fs::write(dir.path().join("mytool/junk"), "x").unwrap();
let err = scaffold_in( let err = scaffold_in(
dir.path(), dir.path(),
opts(TemplateId::Shell, "mytool", SourceDir::Skeleton), opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton),
) )
.unwrap_err(); .unwrap_err();
assert!(err.to_string().contains("not empty"), "{err}"); assert!(err.to_string().contains("not empty"), "{err}");
@@ -593,7 +593,7 @@ mod tests {
// Existing orig tarball (quilt only): nothing gets written. // Existing orig tarball (quilt only): nothing gets written.
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
std::fs::write(dir.path().join("mytool_0.1.0.orig.tar.xz"), b"old").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.source_format = SourceFormat::Quilt;
o.orig = Some(OrigOrigin::Snapshot); o.orig = Some(OrigOrigin::Snapshot);
let err = scaffold_in(dir.path(), o).unwrap_err(); let err = scaffold_in(dir.path(), o).unwrap_err();
@@ -605,7 +605,7 @@ mod tests {
let err = scaffold_in( let err = scaffold_in(
dir.path(), dir.path(),
opts( opts(
TemplateId::Shell, TemplateId::SHELL,
"mytool", "mytool",
SourceDir::Path(dir.path().join("missing")), SourceDir::Path(dir.path().join("missing")),
), ),
@@ -622,7 +622,7 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
scaffold_in( scaffold_in(
dir.path(), dir.path(),
opts(TemplateId::Shell, "nativepkg", SourceDir::Skeleton), opts(TemplateId::SHELL, "nativepkg", SourceDir::Skeleton),
) )
.unwrap(); .unwrap();
@@ -649,7 +649,7 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let outcome = scaffold_in( let outcome = scaffold_in(
dir.path(), dir.path(),
opts(TemplateId::Rust, "mytool", SourceDir::Skeleton), opts(TemplateId::RUST, "mytool", SourceDir::Skeleton),
) )
.unwrap(); .unwrap();
@@ -711,7 +711,7 @@ mod tests {
let tree = dir.path().join("packdir"); let tree = dir.path().join("packdir");
std::fs::create_dir_all(&tree).unwrap(); std::fs::create_dir_all(&tree).unwrap();
std::fs::write(tree.join(".gitignore"), "# my project\n*.log\n").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(); let gitignore = std::fs::read_to_string(tree.join(".gitignore")).unwrap();
assert!( assert!(
@@ -753,7 +753,7 @@ mod tests {
.unwrap(); .unwrap();
std::fs::write(source.join("src/main.rs"), "fn main() {}\n").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.source_format = SourceFormat::Quilt;
o.orig = Some(OrigOrigin::Snapshot); o.orig = Some(OrigOrigin::Snapshot);
let outcome = scaffold_in(dir.path(), o).unwrap(); let outcome = scaffold_in(dir.path(), o).unwrap();
@@ -848,7 +848,7 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
scaffold_in( scaffold_in(
dir.path(), dir.path(),
opts(TemplateId::Python, "mytool", SourceDir::Skeleton), opts(TemplateId::PYTHON, "mytool", SourceDir::Skeleton),
) )
.unwrap(); .unwrap();
@@ -880,7 +880,7 @@ mod tests {
let tree = dir.path().join("mytool"); let tree = dir.path().join("mytool");
std::fs::create_dir_all(&tree).unwrap(); std::fs::create_dir_all(&tree).unwrap();
std::fs::write(tree.join("run.sh"), "#!/bin/sh\necho hi\n").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( let output = crate::build::run_source_build(
&dir.path().join("mytool"), &dir.path().join("mytool"),
@@ -909,7 +909,7 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
scaffold_in( scaffold_in(
dir.path(), dir.path(),
opts(TemplateId::Shell, "mytool", SourceDir::Skeleton), opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton),
) )
.unwrap(); .unwrap();
+51 -76
View File
@@ -19,74 +19,60 @@ use crate::new::licenses;
use crate::new::origin::{Forge, GitOrigin}; use crate::new::origin::{Forge, GitOrigin};
use crate::new::templates; 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, /// The templates themselves are defined by the per-template manifests
/// and every variant has a template implementation registered in /// under `data/templates/<id>/` (see [`crate::new::templates`]): label,
/// [`crate::new::templates`]. /// 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TemplateId { pub struct TemplateId(&'static str);
/// 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,
}
impl TemplateId { impl TemplateId {
/// Every template id, in the order offered by the wizard language menu. /// Rust project (`Cargo.toml`).
pub fn all() -> [TemplateId; 9] { pub const RUST: TemplateId = TemplateId("rust");
[ /// Python project (`pyproject.toml` / `setup.py` / `setup.cfg`).
TemplateId::Rust, pub const PYTHON: TemplateId = TemplateId("python");
TemplateId::Python, /// C/C++ with Meson (`meson.build`).
TemplateId::Meson, pub const MESON: TemplateId = TemplateId("meson");
TemplateId::Cmake, /// C/C++ with CMake (`CMakeLists.txt`).
TemplateId::Autotools, pub const CMAKE: TemplateId = TemplateId("cmake");
TemplateId::Go, /// C/C++ with Autotools (`configure.ac`).
TemplateId::Shell, pub const AUTOTOOLS: TemplateId = TemplateId("autotools");
TemplateId::Makefile, /// Go module (`go.mod`).
TemplateId::Empty, 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. /// Canonical CLI identifier of this template.
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
match self { self.0
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",
}
} }
/// 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> { pub fn parse(s: &str) -> Result<TemplateId, String> {
TemplateId::all() Self::all()
.into_iter() .iter()
.copied()
.find(|id| id.as_str() == s) .find(|id| id.as_str() == s)
.ok_or_else(|| { .ok_or_else(|| {
format!( format!(
"Unknown language/template '{}'. Supported values are: {}.", "Unknown language/template '{}'. Supported values are: {}.",
s, s,
TemplateId::all() Self::all()
.iter() .iter()
.map(|id| id.as_str()) .map(|id| id.as_str())
.collect::<Vec<_>>() .collect::<Vec<_>>()
@@ -96,26 +82,15 @@ impl TemplateId {
} }
/// Human-readable menu label of this template, as offered by the wizard /// 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 { pub fn display_name(&self) -> &'static str {
match self { templates::get(*self).map(|t| t.label()).unwrap_or(self.0)
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)",
}
} }
/// The template whose menu label (or CLI identifier) is `label`. /// The template whose menu label (or CLI identifier) is `label`.
pub fn from_label(label: &str) -> Option<TemplateId> { pub fn from_label(label: &str) -> Option<TemplateId> {
TemplateId::all() templates::from_label(label)
.into_iter()
.find(|id| id.display_name() == label || id.as_str() == label)
} }
} }
@@ -650,7 +625,7 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
SourceDir::Skeleton => { SourceDir::Skeleton => {
if cli.defaults { if cli.defaults {
log::info!("No language given, --defaults picks the 'empty' template"); log::info!("No language given, --defaults picks the 'empty' template");
Some(TemplateId::Empty) Some(TemplateId::EMPTY)
} else { } else {
missing.push(format!( missing.push(format!(
"--lang <{}|...> (no language given and there is nothing \ "--lang <{}|...> (no language given and there is nothing \
@@ -695,7 +670,7 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
the 'empty' template", the 'empty' template",
dir.display() dir.display()
); );
Some(TemplateId::Empty) Some(TemplateId::EMPTY)
} else { } else {
missing.push( missing.push(
"--lang <id> (could not detect a build system; \ "--lang <id> (could not detect a build system; \
@@ -845,7 +820,7 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
Ok(NewOptions { Ok(NewOptions {
name, name,
template: template.unwrap_or(TemplateId::Empty), template: template.unwrap_or(TemplateId::EMPTY),
source_dir, source_dir,
upstream_version, upstream_version,
revision, revision,
@@ -992,7 +967,7 @@ mod tests {
#[test] #[test]
fn template_ids_roundtrip() { 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); assert_eq!(TemplateId::parse(id.as_str()).unwrap(), id);
// Every id resolves from its menu label too, and labels are // Every id resolves from its menu label too, and labels are
// unique. // unique.
@@ -1001,9 +976,9 @@ mod tests {
assert!(TemplateId::parse("cobol").is_err()); assert!(TemplateId::parse("cobol").is_err());
assert_eq!( assert_eq!(
TemplateId::from_label("Rust (Cargo.toml)"), 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); assert_eq!(TemplateId::from_label("nope"), None);
} }
@@ -1099,7 +1074,7 @@ mod tests {
// The full version round-trips through DebianVersion. // The full version round-trips through DebianVersion.
let opts = NewOptions { let opts = NewOptions {
name: "t".into(), name: "t".into(),
template: TemplateId::Empty, template: TemplateId::EMPTY,
source_dir: SourceDir::Here, source_dir: SourceDir::Here,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -1301,7 +1276,7 @@ mod tests {
let opts = resolve(cli).await.unwrap(); let opts = resolve(cli).await.unwrap();
assert_eq!(opts.name, "my-tool"); 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!(matches!(opts.source_dir, SourceDir::Path(_)));
assert_eq!(opts.upstream_version, "0.1.0"); assert_eq!(opts.upstream_version, "0.1.0");
assert_eq!(opts.revision, 1); assert_eq!(opts.revision, 1);
@@ -1342,7 +1317,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
let opts = resolve(cli).await.unwrap(); let opts = resolve(cli).await.unwrap();
assert_eq!(opts.template, TemplateId::Go); assert_eq!(opts.template, TemplateId::GO);
} }
#[tokio::test] #[tokio::test]
+33 -32
View File
@@ -139,7 +139,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
LanguageChoice::Ask(preselected) => { LanguageChoice::Ask(preselected) => {
let menu = language_menu(&[]); let menu = language_menu(&[]);
let default = preselected let default = preselected
.unwrap_or(TemplateId::Empty) .unwrap_or(TemplateId::EMPTY)
.display_name() .display_name()
.to_string(); .to_string();
let id = select_template(&menu, &default)?; 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 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 // the chroot build: surface it now so a too-old pin is not a surprise
// when `pkh deb` compiles with the distribution's rustc. // 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()) probe.as_ref().and_then(|p| p.toolchain_pin.clone())
} else { } else {
None 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 // same `validate_command` bar as `resolve` applies (which also
// requires a non-empty answer), so an unusable probe is withheld and // requires a non-empty answer), so an unusable probe is withheld and
// invalid input re-asks here instead of failing late in `resolve`. // 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 let default = probe
.as_ref() .as_ref()
.and_then(|p| p.command.clone()) .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). // 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 validate = |answer: &str| options::validate_depends(answer).map(|_| ());
let answer = ask_text( let answer = ask_text(
"Depends (metapackage, comma-separated, blank for an empty base)", "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 // The meson/cmake opt-in question of the spec's template table: does the
// build resolve libraries through pkg-config? The project files prefill // build resolve libraries through pkg-config? The project files prefill
// the default (dependency() / pkg_check_modules calls found). // the default (dependency() / pkg_check_modules calls found).
if matches!(template, TemplateId::Meson | TemplateId::Cmake) if matches!(template, TemplateId::MESON | TemplateId::CMAKE)
&& prompt::confirm( && prompt::confirm(
"Does the build resolve libraries through pkg-config (add it to Build-Depends)?", "Does the build resolve libraries through pkg-config (add it to Build-Depends)?",
pkg_config_hint(&detect_dir, template), 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). // Wizard-only extras (default off).
if template != TemplateId::Empty if template != TemplateId::EMPTY
&& prompt::confirm( && prompt::confirm(
"Add an autopkgtest smoke test (debian/tests/control)?", "Add an autopkgtest smoke test (debian/tests/control)?",
false, false,
@@ -686,8 +686,8 @@ fn same_directory(a: &std::path::Path, b: &std::path::Path) -> bool {
/// CMakeLists.txt). /// CMakeLists.txt).
fn pkg_config_hint(dir: &std::path::Path, template: TemplateId) -> bool { fn pkg_config_hint(dir: &std::path::Path, template: TemplateId) -> bool {
let (file, needles): (&str, &[&str]) = match template { let (file, needles): (&str, &[&str]) = match template {
TemplateId::Meson => ("meson.build", &["dependency("]), TemplateId::MESON => ("meson.build", &["dependency("]),
TemplateId::Cmake => ( TemplateId::CMAKE => (
"CMakeLists.txt", "CMakeLists.txt",
&[ &[
"pkg_check_modules", "pkg_check_modules",
@@ -727,7 +727,8 @@ fn language_menu(candidates: &[TemplateId]) -> Vec<String> {
.copied() .copied()
.chain( .chain(
TemplateId::all() TemplateId::all()
.into_iter() .iter()
.copied()
.filter(|id| !candidates.contains(id)), .filter(|id| !candidates.contains(id)),
) )
.map(|id| id.display_name().to_string()) .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: {})", " debian/control Source + 1 binary (Architecture: {})",
template.architecture(opts) template.architecture(opts)
)); ));
if opts.template == TemplateId::Empty { if opts.template == TemplateId::EMPTY {
// The Depends list is the payload of the metapackage flavor. // The Depends list is the payload of the metapackage flavor.
if !opts.depends.is_empty() { if !opts.depends.is_empty() {
lines.push(format!(" Depends {}", opts.depends.join(", "))); 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 // Nothing is vendored yet at this point: only announce that the
// generation will attempt it. // generation will attempt it.
lines.push( lines.push(
@@ -1094,7 +1095,7 @@ pub fn summary_text(opts: &NewOptions, toolchain_pin: Option<&str>) -> String {
lines.push(format!(" + {} (new skeleton)", names.join(", "))); 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( lines.push(
" ! cargo not found on PATH: dependencies cannot be vendored at \ " ! cargo not found on PATH: dependencies cannot be vendored at \
scaffold time; the package will not build until you run \ scaffold time; the package will not build until you run \
@@ -1162,7 +1163,7 @@ mod tests {
#[test] #[test]
fn language_menu_lists_candidates_first() { 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[0], "Generic (Makefile)");
assert_eq!(menu[1], "Rust (Cargo.toml)"); assert_eq!(menu[1], "Rust (Cargo.toml)");
// The remaining seven follow in registry order, no duplicates. // The remaining seven follow in registry order, no duplicates.
@@ -1179,8 +1180,8 @@ mod tests {
#[test] #[test]
fn language_choice_flag_wins_over_detection() { fn language_choice_flag_wins_over_detection() {
let detections = [ let detections = [
Detection::Single(Tid::Rust), Detection::Single(Tid::RUST),
Detection::Ambiguous(vec![Tid::Rust, Tid::Python]), Detection::Ambiguous(vec![Tid::RUST, Tid::PYTHON]),
Detection::Empty, Detection::Empty,
]; ];
for detection in &detections { for detection in &detections {
@@ -1201,21 +1202,21 @@ mod tests {
#[test] #[test]
fn language_choice_without_flag_follows_detection() { fn language_choice_without_flag_follows_detection() {
assert_eq!( assert_eq!(
language_choice(None, &Detection::Single(Tid::Rust), true), language_choice(None, &Detection::Single(Tid::RUST), true),
LanguageChoice::Detected(Tid::Rust) LanguageChoice::Detected(Tid::RUST)
); );
// Skeleton run: ask, preselecting the detected ecosystem. // Skeleton run: ask, preselecting the detected ecosystem.
assert_eq!( assert_eq!(
language_choice(None, &Detection::Single(Tid::Rust), false), language_choice(None, &Detection::Single(Tid::RUST), false),
LanguageChoice::Ask(Some(Tid::Rust)) LanguageChoice::Ask(Some(Tid::RUST))
); );
assert_eq!( assert_eq!(
language_choice( language_choice(
None, None,
&Detection::Ambiguous(vec![Tid::Go, Tid::Python]), &Detection::Ambiguous(vec![Tid::GO, Tid::PYTHON]),
true true
), ),
LanguageChoice::Ambiguous(vec![Tid::Go, Tid::Python]) LanguageChoice::Ambiguous(vec![Tid::GO, Tid::PYTHON])
); );
// Nothing detected: plain menu, the empty template preselected. // Nothing detected: plain menu, the empty template preselected.
assert_eq!( assert_eq!(
@@ -1327,12 +1328,12 @@ mod tests {
#[test] #[test]
fn summary_screen_shows_format_and_orig_origin() { fn summary_screen_shows_format_and_orig_origin() {
// Native skeleton: the format row, no orig row. // 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("debian/source/format 3.0 (native)"), "{text}");
assert!(!text.contains("orig tarball"), "{text}"); assert!(!text.contains("orig tarball"), "{text}");
// Quilt over an existing project: both rows. // 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_dir = options::SourceDir::Here;
quilt.source_format = options::SourceFormat::Quilt; quilt.source_format = options::SourceFormat::Quilt;
quilt.orig = Some(options::OrigOrigin::GitArchive { quilt.orig = Some(options::OrigOrigin::GitArchive {
@@ -1360,7 +1361,7 @@ mod tests {
#[test] #[test]
fn summary_screen_skeleton() { fn summary_screen_skeleton() {
let text = summary_text(&opts(Tid::Makefile), None); let text = summary_text(&opts(Tid::MAKEFILE), None);
assert!( assert!(
text.contains("mytool 0.1.0-1 · builds for ubuntu/resolute"), text.contains("mytool 0.1.0-1 · builds for ubuntu/resolute"),
"{text}" "{text}"
@@ -1390,7 +1391,7 @@ mod tests {
#[test] #[test]
fn summary_screen_metapackage_shows_depends() { 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.depends = vec!["hello".to_string(), "hello-data (>= 1.0)".to_string()];
o.source_dir = options::SourceDir::Here; o.source_dir = options::SourceDir::Here;
let text = summary_text(&o, None); let text = summary_text(&o, None);
@@ -1406,7 +1407,7 @@ mod tests {
#[test] #[test]
fn summary_screen_release_and_extras() { fn summary_screen_release_and_extras() {
let mut o = opts(Tid::Shell); let mut o = opts(Tid::SHELL);
o.release = true; o.release = true;
o.autopkgtest = true; o.autopkgtest = true;
o.watch = Some("version=4\n".to_string()); o.watch = Some("version=4\n".to_string());
@@ -1424,7 +1425,7 @@ mod tests {
/// yet (regression: it claimed "(vendored)" before generating). /// yet (regression: it claimed "(vendored)" before generating).
#[test] #[test]
fn summary_screen_rust_does_not_presume_vendoring() { 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!( assert!(
text.contains("cargo build --release --offline (vendored at generation)"), text.contains("cargo build --release --offline (vendored at generation)"),
"{text}" "{text}"
@@ -1436,17 +1437,17 @@ mod tests {
/// (rust template only), flagged as ignored by the chroot build. /// (rust template only), flagged as ignored by the chroot build.
#[test] #[test]
fn summary_screen_shows_the_toolchain_pin() { 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!( assert!(
text.contains("rust-toolchain 1.98.0 (ignored by the chroot build)"), text.contains("rust-toolchain 1.98.0 (ignored by the chroot build)"),
"{text}" "{text}"
); );
// No pin, no row. // 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 // A pin under a template other than rust is not shown either (the
// pin only matters for a cargo build). // 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 /// 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. // The initial pass over dir A.
let (detection_a, probe_a) = detect_and_probe(dir_a.path()); 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"); 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.name.as_deref(), Some("alpha"));
assert_eq!(probe_a.version.as_deref(), Some("0.1.0")); 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, // The user chose dir B instead: the refreshed probe comes from B,
// never from A. // never from A.
let (detection_b, probe_b) = detect_and_probe(dir_b.path()); 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"); 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.name.as_deref(), Some("beta"));
assert_eq!(probe_b.version.as_deref(), Some("2.9.9")); assert_eq!(probe_b.version.as_deref(), Some("2.9.9"));
+25 -32
View File
@@ -1,27 +1,31 @@
//! The `autotools` template: a C project with a `configure.ac` built through //! The `autotools` template: a C project with a `configure.ac` built through
//! debhelper's auto-detection (dh runs `autoreconf` itself when it finds //! debhelper's auto-detection (dh runs `autoreconf` itself when it finds
//! `configure.ac`, debhelper ≥ 10 — no override needed). //! `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 std::path::Path;
use regex::Regex; use regex::Regex;
use super::meson::hello_c; use super::meson::hello_c;
use super::{OutputFile, ProbeResult, Template, source_dir_of}; use super::{OutputFile, ProbeResult, TemplateHooks, source_dir_of};
use crate::new::options::{NewOptions, TemplateId}; use crate::new::options::NewOptions;
/// C/C++ with Autotools (`configure.ac`). /// The logic half of the autotools template.
pub struct Autotools; pub struct Hooks;
impl Template for Autotools { /// The autotools template's hooks, registered in the template registry.
fn id(&self) -> TemplateId { pub static HOOKS: Hooks = Hooks;
TemplateId::Autotools
}
impl TemplateHooks for Hooks {
/// A minimal `configure.ac`, the matching `Makefile.am` and `hello.c`. /// A minimal `configure.ac`, the matching `Makefile.am` and `hello.c`.
/// The first source build runs `autoreconf` (integrated in the dh /// The first source build runs `autoreconf` (integrated in the dh
/// sequence), so no generated configure script is committed. /// sequence), so no generated configure script is committed.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![ vec![
OutputFile::new( OutputFile::new(
"configure.ac", "configure.ac",
@@ -47,25 +51,14 @@ impl Template for Autotools {
] ]
} }
/// No extra debian/ files: plain `dh $@` auto-detects `configure.ac`. /// A packaged `configure.ac` that sets up GNU gettext (the
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> { /// `AM_GNU_GETTEXT` macro, see [`uses_gettext`]) appends `gettext` to
Vec::new() /// the manifest's Build-Depends.
} fn build_depends(&self, opts: &NewOptions, mut base: Vec<String>) -> Vec<String> {
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
let mut deps = vec![
"autoconf".to_string(),
"automake".to_string(),
"libtool".to_string(),
];
if uses_gettext(opts) { if uses_gettext(opts) {
deps.push("gettext".to_string()); base.push("gettext".to_string());
} }
deps base
}
fn architecture(&self, _opts: &NewOptions) -> &'static str {
"any"
} }
/// Package name and version from the `AC_INIT` macro of `configure.ac`. /// Package name and version from the `AC_INIT` macro of `configure.ac`.
@@ -109,13 +102,13 @@ fn uses_gettext(opts: &NewOptions) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::new::options::{License, SourceDir}; use crate::new::options::{License, SourceDir, TemplateId};
use tempfile::tempdir; use tempfile::tempdir;
fn opts() -> NewOptions { fn opts() -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Autotools, template: TemplateId::AUTOTOOLS,
source_dir: SourceDir::Skeleton, source_dir: SourceDir::Skeleton,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -141,7 +134,7 @@ mod tests {
#[test] #[test]
fn autotools_template_shape() { fn autotools_template_shape() {
let o = opts(); 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!(template.architecture(&o), "any");
assert_eq!( assert_eq!(
@@ -177,7 +170,7 @@ mod tests {
#[test] #[test]
fn autotools_probe_reads_ac_init() { 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). // Bracketed form (the generated skeleton's own shape).
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
@@ -222,7 +215,7 @@ mod tests {
let o = opts(); let o = opts();
assert!(!uses_gettext(&o)); assert!(!uses_gettext(&o));
assert!( assert!(
!super::super::get(TemplateId::Autotools) !super::super::get(TemplateId::AUTOTOOLS)
.unwrap() .unwrap()
.build_depends(&o) .build_depends(&o)
.contains(&"gettext".to_string()) .contains(&"gettext".to_string())
@@ -241,7 +234,7 @@ mod tests {
}; };
assert!(uses_gettext(&o)); assert!(uses_gettext(&o));
assert!( assert!(
super::super::get(TemplateId::Autotools) super::super::get(TemplateId::AUTOTOOLS)
.unwrap() .unwrap()
.build_depends(&o) .build_depends(&o)
.contains(&"gettext".to_string()) .contains(&"gettext".to_string())
+24 -32
View File
@@ -1,27 +1,31 @@
//! The `cmake` template: a C/C++ project built with CMake through the //! The `cmake` template: a C/C++ project built with CMake through the
//! debhelper cmake buildsystem. //! 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 std::path::Path;
use regex::Regex; use regex::Regex;
use super::meson::hello_c; use super::meson::hello_c;
use super::{OutputFile, ProbeResult, Template}; use super::{ProbeResult, TemplateHooks};
use crate::new::options::{NewOptions, TemplateId}; use crate::new::options::NewOptions;
/// C/C++ with CMake (`CMakeLists.txt`). /// The logic half of the cmake template.
pub struct Cmake; pub struct Hooks;
impl Template for Cmake { /// The cmake template's hooks, registered in the template registry.
fn id(&self) -> TemplateId { pub static HOOKS: Hooks = Hooks;
TemplateId::Cmake
}
impl TemplateHooks for Hooks {
/// A minimal `CMakeLists.txt` (project declaration + one installed /// A minimal `CMakeLists.txt` (project declaration + one installed
/// executable) and the classic `hello.c`. /// executable) and the classic `hello.c`.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<super::OutputFile> {
vec![ vec![
OutputFile::new( super::OutputFile::new(
"CMakeLists.txt", "CMakeLists.txt",
format!( format!(
"cmake_minimum_required(VERSION 3.16)\n\ "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 /// The wizard's pkg-config opt-in ([`NewOptions::pkg_config`], offered
/// configure/build/install steps. /// when the project's build file hints at `pkg_check_modules` usage)
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> { /// adds `pkg-config` to the manifest's Build-Depends.
Vec::new() fn build_depends(&self, opts: &NewOptions, mut base: Vec<String>) -> Vec<String> {
}
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
let mut deps = vec!["cmake".to_string()];
if opts.pkg_config { if opts.pkg_config {
deps.push("pkg-config".to_string()); base.push("pkg-config".to_string());
} }
deps base
}
fn architecture(&self, _opts: &NewOptions) -> &'static str {
"any"
}
fn rules_dh_line(&self) -> String {
"dh $@ --buildsystem=cmake".to_string()
} }
/// Project name and version from the `project(<name> VERSION …)` /// Project name and version from the `project(<name> VERSION …)`
@@ -84,13 +76,13 @@ impl Template for Cmake {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::new::options::{License, SourceDir}; use crate::new::options::{License, SourceDir, TemplateId};
use tempfile::tempdir; use tempfile::tempdir;
fn opts() -> NewOptions { fn opts() -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Cmake, template: TemplateId::CMAKE,
source_dir: SourceDir::Skeleton, source_dir: SourceDir::Skeleton,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -116,7 +108,7 @@ mod tests {
#[test] #[test]
fn cmake_template_shape() { fn cmake_template_shape() {
let o = opts(); 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.architecture(&o), "any");
assert_eq!(template.build_depends(&o), vec!["cmake".to_string()]); assert_eq!(template.build_depends(&o), vec!["cmake".to_string()]);
@@ -144,7 +136,7 @@ mod tests {
#[test] #[test]
fn cmake_probe_reads_project_declaration() { 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(); let dir = tempdir().unwrap();
std::fs::write( std::fs::write(
+13 -18
View File
@@ -5,22 +5,23 @@
//! **metapackage** flavor (the canonical `Architecture: all`, nothing //! **metapackage** flavor (the canonical `Architecture: all`, nothing
//! compiled, the Depends list *is* the payload shape), while an empty list //! compiled, the Depends list *is* the payload shape), while an empty list
//! selects the **empty base** — pure `dh $@` plumbing as a starting point //! 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; use crate::new::options::NewOptions;
/// Metapackage / empty base (no build system). /// The logic half of the empty template.
pub struct Empty; pub struct Hooks;
impl Template for Empty { /// The empty template's hooks, registered in the template registry.
fn id(&self) -> crate::new::options::TemplateId { pub static HOOKS: Hooks = Hooks;
crate::new::options::TemplateId::Empty
}
/// 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. /// intentionally empty.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![OutputFile::new( vec![OutputFile::new(
"README", "README",
format!( 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<OutputFile> {
Vec::new()
}
} }
#[cfg(test)] #[cfg(test)]
@@ -46,7 +41,7 @@ mod tests {
fn opts(depends: Vec<String>) -> NewOptions { fn opts(depends: Vec<String>) -> NewOptions {
NewOptions { NewOptions {
name: "metapkg".into(), name: "metapkg".into(),
template: TemplateId::Empty, template: TemplateId::EMPTY,
source_dir: SourceDir::Skeleton, source_dir: SourceDir::Skeleton,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -71,7 +66,7 @@ mod tests {
#[test] #[test]
fn empty_template_shape() { 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. // Metapackage flavor: the depends list travels in the options.
let o = opts(vec!["hello".into(), "hello-data (>= 1.0)".into()]); let o = opts(vec!["hello".into(), "hello-data (>= 1.0)".into()]);
+25 -38
View File
@@ -1,26 +1,28 @@
//! The `go` template: a Go module built through dh-golang. //! The `go` template: a Go module built through dh-golang.
//! //!
//! The source stanza carries `XS-Go-Import-Path`, probed from the `module` //! The source stanza carries `XS-Go-Import-Path`, declared as the
//! line of `go.mod` when the packaged tree has one, defaulting to the //! `{go_import_path}` placeholder by the manifest and filled from the
//! package name (fresh skeletons embed the package name in their own //! `module` line of `go.mod` when the packaged tree has one, defaulting to
//! `go.mod`). //! 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 std::path::Path;
use super::{OutputFile, Template, source_dir_of}; use super::{OutputFile, ProbeResult, TemplateHooks, source_dir_of};
use crate::new::options::{NewOptions, TemplateId}; use crate::new::options::NewOptions;
/// Go module (`go.mod`). /// The logic half of the go template.
pub struct Go; pub struct Hooks;
impl Template for Go { /// The go template's hooks, registered in the template registry.
fn id(&self) -> TemplateId { pub static HOOKS: Hooks = Hooks;
TemplateId::Go
}
impl TemplateHooks for Hooks {
/// A stdlib-only `main.go` (no archive dependencies needed to build) and /// A stdlib-only `main.go` (no archive dependencies needed to build) and
/// the matching `go.mod` whose module path is the package name. /// the matching `go.mod` whose module path is the package name.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![ vec![
OutputFile::new( OutputFile::new(
"go.mod", "go.mod",
@@ -49,36 +51,21 @@ impl Template for Go {
] ]
} }
/// No extra debian/ files: dh-golang drives the build. /// The `{go_import_path}` value of the manifest's `XS-Go-Import-Path`
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> { /// source field (see [`import_path`]).
Vec::new() fn context(&self, opts: &NewOptions) -> Vec<(String, String)> {
} vec![("go_import_path".to_string(), import_path(opts))]
fn build_depends(&self, _opts: &NewOptions) -> Vec<String> {
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))]
} }
/// Name (and default command) from the `module` line of `go.mod`: the /// Name (and default command) from the `module` line of `go.mod`: the
/// last path segment is the conventional binary/package name. /// last path segment is the conventional binary/package name.
fn probe(&self, dir: &Path) -> Option<super::ProbeResult> { fn probe(&self, dir: &Path) -> Option<ProbeResult> {
let module = read_module_line(dir)?; let module = read_module_line(dir)?;
let name = module.rsplit('/').next()?.to_string(); let name = module.rsplit('/').next()?.to_string();
if name.is_empty() { if name.is_empty() {
return None; return None;
} }
Some(super::ProbeResult { Some(ProbeResult {
name: Some(name.clone()), name: Some(name.clone()),
command: Some(name), command: Some(name),
..Default::default() ..Default::default()
@@ -114,13 +101,13 @@ fn read_module_line(dir: &Path) -> Option<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::new::options::{License, SourceDir}; use crate::new::options::{License, SourceDir, TemplateId};
use tempfile::tempdir; use tempfile::tempdir;
fn opts(source_dir: SourceDir) -> NewOptions { fn opts(source_dir: SourceDir) -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Go, template: TemplateId::GO,
source_dir, source_dir,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -146,7 +133,7 @@ mod tests {
#[test] #[test]
fn go_template_shape() { fn go_template_shape() {
let o = opts(SourceDir::Skeleton); 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!(template.architecture(&o), "any");
assert_eq!( assert_eq!(
@@ -180,7 +167,7 @@ mod tests {
) )
.unwrap(); .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"); let probe = template.probe(dir.path()).expect("probe result");
assert_eq!(probe.name.as_deref(), Some("mytool")); assert_eq!(probe.name.as_deref(), Some("mytool"));
assert_eq!(probe.command.as_deref(), Some("mytool")); assert_eq!(probe.command.as_deref(), Some("mytool"));
+22 -26
View File
@@ -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 //! debhelper's makefile buildsystem runs `make` for the build and `make
//! `make install DESTDIR=...` when the Makefile carries an `install:` target //! install DESTDIR=...` when the Makefile carries an `install:` target
//! (missing targets are skipped gracefully), so plain `dh $@` plumbing is //! (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 std::path::Path;
use super::{OutputFile, Template, source_dir_of}; use super::meson::hello_c;
use crate::new::options::{NewOptions, SourceDir, TemplateId}; use super::{OutputFile, TemplateHooks, source_dir_of};
use crate::new::options::{NewOptions, SourceDir};
/// Generic Makefile-based project. /// The logic half of the makefile template.
pub struct Makefile; pub struct Hooks;
impl Template for Makefile { /// The makefile template's hooks, registered in the template registry.
fn id(&self) -> TemplateId { pub static HOOKS: Hooks = Hooks;
TemplateId::Makefile
}
impl TemplateHooks for Hooks {
/// A `hello.c` plus a `Makefile` with `all`/`install`/`clean` targets; /// A `hello.c` plus a `Makefile` with `all`/`install`/`clean` targets;
/// `install` honors `DESTDIR` and copies the binary to /// `install` honors `DESTDIR` and copies the binary to
/// `$(DESTDIR)/usr/bin/`. /// `$(DESTDIR)/usr/bin/`.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![ vec![
super::meson::hello_c(opts), hello_c(opts),
OutputFile::new( OutputFile::new(
"Makefile", "Makefile",
format!( format!(
@@ -55,7 +59,7 @@ impl Template for Makefile {
/// existing tree the Makefile is probed instead (no `debian/install` is /// existing tree the Makefile is probed instead (no `debian/install` is
/// emitted there — the source-relative mapping of an unknown artifact is /// emitted there — the source-relative mapping of an unknown artifact is
/// only the project's to write, and `make install` already ran). /// only the project's to write, and `make install` already ran).
fn debian(&self, opts: &NewOptions) -> Vec<OutputFile> { fn debian_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
match &opts.source_dir { match &opts.source_dir {
SourceDir::Skeleton => { SourceDir::Skeleton => {
vec![OutputFile::new( vec![OutputFile::new(
@@ -82,14 +86,6 @@ impl Template for Makefile {
} }
} }
} }
fn build_depends(&self, _opts: &NewOptions) -> Vec<String> {
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 /// 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<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::new::options::{License, SourceDir}; use crate::new::options::{License, SourceDir, TemplateId};
use tempfile::tempdir; use tempfile::tempdir;
fn opts(source_dir: SourceDir) -> NewOptions { fn opts(source_dir: SourceDir) -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Makefile, template: TemplateId::MAKEFILE,
source_dir, source_dir,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -151,7 +147,7 @@ mod tests {
#[test] #[test]
fn makefile_template_shape() { fn makefile_template_shape() {
let o = opts(SourceDir::Skeleton); 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!(template.architecture(&o), "any");
assert_eq!( assert_eq!(
+25 -32
View File
@@ -1,24 +1,29 @@
//! The `meson` template: a C/C++ project built with Meson through the //! The `meson` template: a C/C++ project built with Meson through the
//! debhelper meson buildsystem. //! 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 std::path::Path;
use regex::Regex; use regex::Regex;
use super::{OutputFile, ProbeResult, Template}; use super::{OutputFile, ProbeResult, TemplateHooks};
use crate::new::options::{NewOptions, TemplateId}; use crate::new::options::NewOptions;
/// C/C++ with Meson (`meson.build`). /// The logic half of the meson template.
pub struct Meson; pub struct Hooks;
impl Template for Meson { /// The meson template's hooks, registered in the template registry.
fn id(&self) -> TemplateId { pub static HOOKS: Hooks = Hooks;
TemplateId::Meson
}
impl TemplateHooks for Hooks {
/// A minimal `meson.build` (project declaration + one installed /// A minimal `meson.build` (project declaration + one installed
/// executable) and the classic `hello.c`. /// executable) and the classic `hello.c`.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![ vec![
OutputFile::new( OutputFile::new(
"meson.build", "meson.build",
@@ -37,26 +42,14 @@ impl Template for Meson {
] ]
} }
/// No extra debian/ files: debhelper's meson buildsystem handles the /// The wizard's pkg-config opt-in ([`NewOptions::pkg_config`], offered
/// configure/build/install steps. /// when the project's build file hints at `dependency(` usage) adds
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> { /// `pkg-config` to the manifest's Build-Depends.
Vec::new() fn build_depends(&self, opts: &NewOptions, mut base: Vec<String>) -> Vec<String> {
}
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
let mut deps = vec!["meson".to_string()];
if opts.pkg_config { if opts.pkg_config {
deps.push("pkg-config".to_string()); base.push("pkg-config".to_string());
} }
deps base
}
fn architecture(&self, _opts: &NewOptions) -> &'static str {
"any"
}
fn rules_dh_line(&self) -> String {
"dh $@ --buildsystem=meson".to_string()
} }
/// Project name and version from the `project('name', version: …)` /// Project name and version from the `project('name', version: …)`
@@ -98,13 +91,13 @@ pub(super) fn hello_c(opts: &NewOptions) -> OutputFile {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::new::options::{License, SourceDir}; use crate::new::options::{License, SourceDir, TemplateId};
use tempfile::tempdir; use tempfile::tempdir;
fn opts() -> NewOptions { fn opts() -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Meson, template: TemplateId::MESON,
source_dir: SourceDir::Skeleton, source_dir: SourceDir::Skeleton,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -130,7 +123,7 @@ mod tests {
#[test] #[test]
fn meson_template_shape() { fn meson_template_shape() {
let o = opts(); 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.architecture(&o), "any");
assert_eq!(template.build_depends(&o), vec!["meson".to_string()]); assert_eq!(template.build_depends(&o), vec!["meson".to_string()]);
@@ -155,7 +148,7 @@ mod tests {
#[test] #[test]
fn meson_probe_reads_project_declaration() { 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(); let dir = tempdir().unwrap();
std::fs::write( std::fs::write(
@@ -181,7 +174,7 @@ mod tests {
#[test] #[test]
fn pkg_config_opt_in_extends_build_depends() { 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(); let mut o = opts();
assert_eq!(template.build_depends(&o), vec!["meson".to_string()]); assert_eq!(template.build_depends(&o), vec!["meson".to_string()]);
o.pkg_config = true; o.pkg_config = true;
+908 -214
View File
File diff suppressed because it is too large Load Diff
+49 -46
View File
@@ -4,17 +4,25 @@
//! …`) with a deliberately minimal line-oriented reader (see //! …`) with a deliberately minimal line-oriented reader (see
//! [`read_pyproject`]) — pkh has no TOML dependency, and only a handful of //! [`read_pyproject`]) — pkh has no TOML dependency, and only a handful of
//! keys matter here. A bare `setup.py`/`setup.cfg` project falls back to //! keys matter here. A bare `setup.py`/`setup.cfg` project falls back to
//! setuptools without the `pybuild-plugin-pyproject` helper. //! setuptools without the `pybuild-plugin-pyproject` helper. The manifest's
//! Build-Depends/architecture are the fresh-skeleton baseline the hooks
//! here resolve for an existing project (backend package, pyproject
//! presence, C-extension hints); the skeleton bodies stay in Rust until
//! they move into the manifest's `files:` list (the module name of
//! `[project.scripts]` is derived, not substituted).
use std::path::Path; use std::path::Path;
use regex::Regex; use regex::Regex;
use super::{OutputFile, ProbeResult, Template, source_dir_of}; use super::{OutputFile, ProbeResult, TemplateHooks, source_dir_of};
use crate::new::options::{NewOptions, TemplateId}; use crate::new::options::NewOptions;
/// Python project (`pyproject.toml` / `setup.py` / `setup.cfg`). /// The logic half of the python template.
pub struct Python; pub struct Hooks;
/// The python template's hooks, registered in the template registry.
pub static HOOKS: Hooks = Hooks;
/// The PEP 517 backend of a project. /// The PEP 517 backend of a project.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -113,14 +121,10 @@ fn c_extension_hints(opts: &NewOptions) -> bool {
false false
} }
impl Template for Python { impl TemplateHooks for Hooks {
fn id(&self) -> TemplateId {
TemplateId::Python
}
/// A minimal setuptools-based `pyproject.toml` with one console script, /// A minimal setuptools-based `pyproject.toml` with one console script,
/// plus the one-module package providing it. /// plus the one-module package providing it.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
let module = module_name(opts); let module = module_name(opts);
vec![ vec![
OutputFile::new( OutputFile::new(
@@ -160,39 +164,36 @@ impl Template for Python {
] ]
} }
/// No extra debian/ files: pybuild installs the package and its console /// Amend the manifest's Build-Depends (the fresh-skeleton baseline)
/// entry points (under `/usr/bin`) automatically. /// with what only the packaged project decides: C-extension hints add
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> { /// `python3-all-dev` next to `python3-all`, a bare `setup.py` project
Vec::new() /// drops the pyproject plugin, and the actual backend package replaces
} /// the skeleton's setuptools entry.
fn build_depends(&self, opts: &NewOptions, mut base: Vec<String>) -> Vec<String> {
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
let mut deps = vec!["dh-python".to_string(), "python3-all".to_string()];
if c_extension_hints(opts) { if c_extension_hints(opts) {
deps.push("python3-all-dev".to_string()); let at = base
.iter()
.position(|dep| dep == "python3-all")
.map_or(base.len(), |at| at + 1);
base.insert(at, "python3-all-dev".to_string());
} }
if uses_pyproject(opts) { if !uses_pyproject(opts) {
deps.push("pybuild-plugin-pyproject".to_string()); base.retain(|dep| dep != "pybuild-plugin-pyproject");
} }
deps.push(backend(opts).package().to_string()); // The backend package: the skeleton baseline ends in the generated
deps // setuptools backend, which the packaged project's own replaces.
let backend_package = backend(opts).package().to_string();
match base.iter().position(|dep| dep == "python3-setuptools") {
Some(at) => base[at] = backend_package,
None => base.push(backend_package),
}
base
} }
/// `all` unless the project hints at compiled C extensions. /// `any` instead of the manifest's `all` when the project hints at
fn architecture(&self, opts: &NewOptions) -> &'static str { /// compiled C extensions.
if c_extension_hints(opts) { fn architecture(&self, opts: &NewOptions) -> Option<&'static str> {
"any" c_extension_hints(opts).then_some("any")
} else {
"all"
}
}
fn rules_dh_line(&self) -> String {
"dh $@ --with python3 --buildsystem=pybuild".to_string()
}
fn source_fields(&self, _opts: &NewOptions) -> Vec<(String, String)> {
Vec::new()
} }
/// Metadata from the `[project]` section of `pyproject.toml` (name, /// Metadata from the `[project]` section of `pyproject.toml` (name,
@@ -339,13 +340,13 @@ fn split_key_value(line: &str) -> Option<(&str, String)> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::new::options::{License, SourceDir}; use crate::new::options::{License, SourceDir, TemplateId};
use tempfile::tempdir; use tempfile::tempdir;
fn opts(source_dir: SourceDir) -> NewOptions { fn opts(source_dir: SourceDir) -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Python, template: TemplateId::PYTHON,
source_dir, source_dir,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -375,7 +376,7 @@ mod tests {
#[test] #[test]
fn python_skeleton_shape() { fn python_skeleton_shape() {
let o = opts(SourceDir::Skeleton); let o = opts(SourceDir::Skeleton);
let template = super::super::get(TemplateId::Python).unwrap(); let template = super::super::get(TemplateId::PYTHON).unwrap();
assert_eq!(template.architecture(&o), "all"); assert_eq!(template.architecture(&o), "all");
assert!(template.debian(&o).is_empty()); assert!(template.debian(&o).is_empty());
@@ -420,7 +421,7 @@ mod tests {
assert_eq!(module_name(&o), "my_tool"); assert_eq!(module_name(&o), "my_tool");
// The skeleton module path and the pyproject script agree. // The skeleton module path and the pyproject script agree.
let template = super::super::get(TemplateId::Python).unwrap(); let template = super::super::get(TemplateId::PYTHON).unwrap();
let skeleton = template.skeleton(&o); let skeleton = template.skeleton(&o);
assert!(skeleton.iter().any(|f| f.path == "my_tool/__init__.py")); assert!(skeleton.iter().any(|f| f.path == "my_tool/__init__.py"));
assert!(skeleton.iter().any(|f| { assert!(skeleton.iter().any(|f| {
@@ -447,7 +448,7 @@ mod tests {
// Unknown values fall back to setuptools. // Unknown values fall back to setuptools.
assert_eq!(backend_of_value("mystery.backend"), Backend::Setuptools); assert_eq!(backend_of_value("mystery.backend"), Backend::Setuptools);
let template = super::super::get(TemplateId::Python).unwrap(); let template = super::super::get(TemplateId::PYTHON).unwrap();
// No source dir (skeleton): setuptools + pyproject plugin. // No source dir (skeleton): setuptools + pyproject plugin.
assert_eq!(backend(&opts(SourceDir::Skeleton)), Backend::Setuptools); assert_eq!(backend(&opts(SourceDir::Skeleton)), Backend::Setuptools);
@@ -507,7 +508,7 @@ mod tests {
#[test] #[test]
fn c_extension_hints_flip_architecture_and_deps() { fn c_extension_hints_flip_architecture_and_deps() {
let template = super::super::get(TemplateId::Python).unwrap(); let template = super::super::get(TemplateId::PYTHON).unwrap();
// pyo3 in Cargo.toml. // pyo3 in Cargo.toml.
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
@@ -520,6 +521,7 @@ mod tests {
assert!(c_extension_hints(&o)); assert!(c_extension_hints(&o));
let deps = template.build_depends(&o); let deps = template.build_depends(&o);
assert!(deps.contains(&"python3-all-dev".to_string()), "{deps:?}"); assert!(deps.contains(&"python3-all-dev".to_string()), "{deps:?}");
assert_eq!(template.architecture(&o), "any");
// ext_modules in setup.py. // ext_modules in setup.py.
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
@@ -537,11 +539,12 @@ mod tests {
let o = source_opts(dir.path()); let o = source_opts(dir.path());
assert!(!c_extension_hints(&o)); assert!(!c_extension_hints(&o));
assert_eq!(template.build_depends(&o).len(), 4); // dh-python, python3-all, plugin, setuptools assert_eq!(template.build_depends(&o).len(), 4); // dh-python, python3-all, plugin, setuptools
assert_eq!(template.architecture(&o), "all");
} }
#[test] #[test]
fn probe_reads_project_and_scripts() { fn probe_reads_project_and_scripts() {
let template = super::super::get(TemplateId::Python).unwrap(); let template = super::super::get(TemplateId::PYTHON).unwrap();
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
std::fs::write( std::fs::write(
+34 -86
View File
@@ -9,16 +9,24 @@
//! scaffold continues with a loud warning and reports //! scaffold continues with a loud warning and reports
//! [`ScaffoldOutcome::vendoring_failed`] to the flow — the package will not //! [`ScaffoldOutcome::vendoring_failed`] to the flow — the package will not
//! build until the user vendors manually. //! build until the user vendors manually.
//!
//! The metadata is manifest data (`data/templates/rust/manifest.yml`),
//! including the `debian/rules` overrides of `rules.extra.tpl`; the logic
//! half here fills that template's `{locked}`/`{artifact}` placeholders,
//! renders the crate-name-sanitized skeleton and runs the vendoring.
use std::path::Path; use std::path::Path;
use serde_json::Value; use serde_json::Value;
use super::{OutputFile, ProbeResult, ScaffoldOutcome, Template, find_on_path, source_dir_of}; use super::{OutputFile, ProbeResult, ScaffoldOutcome, TemplateHooks, find_on_path, source_dir_of};
use crate::new::options::{NewOptions, SourceDir, TemplateId}; use crate::new::options::{NewOptions, SourceDir};
/// Rust project (`Cargo.toml`). /// The logic half of the rust template.
pub struct Rust; pub struct Hooks;
/// The rust template's hooks, registered in the template registry.
pub static HOOKS: Hooks = Hooks;
/// The source-replacement configuration, used when `cargo vendor` did not /// The source-replacement configuration, used when `cargo vendor` did not
/// print one itself (old cargo versions, empty output). /// print one itself (old cargo versions, empty output).
@@ -30,13 +38,9 @@ fn crate_name(opts: &NewOptions) -> String {
opts.name.replace(['+', '.'], "_") opts.name.replace(['+', '.'], "_")
} }
impl Template for Rust { impl TemplateHooks for Hooks {
fn id(&self) -> TemplateId {
TemplateId::Rust
}
/// A zero-dependency `Cargo.toml` and the matching `src/main.rs`. /// A zero-dependency `Cargo.toml` and the matching `src/main.rs`.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![ vec![
OutputFile::new( OutputFile::new(
"Cargo.toml", "Cargo.toml",
@@ -65,82 +69,26 @@ impl Template for Rust {
] ]
} }
/// No extra debian/ files: the vendored build lives entirely in the /// The `{locked}`/`{artifact}` values of `rules.extra.tpl`: `--locked`
/// rules overrides. /// is used only when the packaged tree already carries a `Cargo.lock`
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> { /// (fresh skeletons have none yet — the vendoring hook patches the flag
Vec::new() /// in once `cargo vendor` created it); the built artifact of a skeleton
} /// is named after its crate (a sanitized package name), an existing
/// project's under the (probed or answered) command.
fn build_depends(&self, _opts: &NewOptions) -> Vec<String> { fn context(&self, opts: &NewOptions) -> Vec<(String, String)> {
vec!["cargo:native".to_string(), "rustc:native".to_string()]
}
/// Root `.gitignore` entries contributed in every mode: the vendoring
/// hook runs unconditionally for this template, so `vendor/` and
/// `.cargo/config.toml` exist (or will exist) in every scaffolded rust
/// tree and must stay out of the repository. `.cargo/config.toml` is
/// the subtle one: it points cargo at `vendor/`, so committing it would
/// break plain `cargo build` for anyone cloning the repository without
/// the vendored sources.
fn gitignore_entries(&self, _opts: &NewOptions) -> Vec<String> {
vec!["vendor/".to_string(), ".cargo/config.toml".to_string()]
}
fn architecture(&self, _opts: &NewOptions) -> &'static str {
"any"
}
/// The vendored build overrides. `--locked` is used only when the
/// packaged tree already carries a `Cargo.lock` (fresh skeletons have
/// none yet — the vendoring hook patches the flag in once `cargo vendor`
/// created it, see [`patch_rules_locked`]); omitting it is always safe.
/// The built artifact of a skeleton is named after its crate (a
/// sanitized package name) and installed under the command name.
///
/// `dh_update_autotools_config` is overridden away: crates embedding C
/// sources (e.g. `-sys` crates shipping `config.sub`/`config.guess`)
/// carry per-file cargo checksums, and debhelper refreshing those files
/// with the system's newer copies would break `cargo build --offline`.
/// `dh_clean` gets `-X Cargo.toml.orig` for the same reason: it treats
/// every vendored `Cargo.toml.orig` as a patch backup and deletes it,
/// which breaks the checksums on any build without a warm cache.
fn rules_extra(&self, opts: &NewOptions) -> String {
let locked = if lockfile_present(opts) { let locked = if lockfile_present(opts) {
" --locked" " --locked"
} else { } else {
"" ""
}; };
// The skeleton's cargo artifact is the crate name; for an existing
// project the (probed or answered) command names the binary.
let artifact = match opts.source_dir { let artifact = match opts.source_dir {
SourceDir::Skeleton => crate_name(opts), SourceDir::Skeleton => crate_name(opts),
_ => opts.command.clone(), _ => opts.command.clone(),
}; };
format!( vec![
"override_dh_auto_build:\n\ ("locked".to_string(), locked.to_string()),
\tcargo build --release --offline{locked}\n\ ("artifact".to_string(), artifact),
\n\ ]
override_dh_auto_install:\n\
\tinstall -Dm755 target/release/{artifact} debian/{name}/usr/bin/{command}\n\
\n\
override_dh_auto_test:\n\
\tcargo test --release --offline{locked}\n\
\n\
override_dh_update_autotools_config:\n\
\n\
override_dh_clean:\n\
\t# dh_clean unlinks `*.orig` patch backups, but vendored crates\n\
\t# ship files like `Cargo.toml.orig` that cargo's per-file\n\
\t# checksums require on cold builds (chroots, Launchpad).\n\
\tdh_clean -X .orig\n\
\n\
override_dh_auto_clean:\n\
\tcargo clean\n",
locked = locked,
artifact = artifact,
command = opts.command,
name = opts.name,
)
} }
/// Name, version, description, homepage, license and first binary from /// Name, version, description, homepage, license and first binary from
@@ -501,14 +449,14 @@ fn parse_toolchain_channel(content: &str) -> Option<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::new::options::{License, SourceDir}; use crate::new::options::{License, SourceDir, TemplateId};
use serial_test::serial; use serial_test::serial;
use tempfile::tempdir; use tempfile::tempdir;
fn opts(source_dir: SourceDir) -> NewOptions { fn opts(source_dir: SourceDir) -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Rust, template: TemplateId::RUST,
source_dir, source_dir,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -534,7 +482,7 @@ mod tests {
#[test] #[test]
fn rust_template_shape() { fn rust_template_shape() {
let o = opts(SourceDir::Skeleton); let o = opts(SourceDir::Skeleton);
let template = super::super::get(TemplateId::Rust).unwrap(); let template = super::super::get(TemplateId::RUST).unwrap();
assert_eq!(template.architecture(&o), "any"); assert_eq!(template.architecture(&o), "any");
assert_eq!( assert_eq!(
@@ -568,7 +516,7 @@ mod tests {
#[test] #[test]
fn rules_use_locked_only_with_lockfile() { fn rules_use_locked_only_with_lockfile() {
let template = super::super::get(TemplateId::Rust).unwrap(); let template = super::super::get(TemplateId::RUST).unwrap();
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap(); std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
@@ -598,7 +546,7 @@ mod tests {
command: "mytool".into(), command: "mytool".into(),
..opts(SourceDir::Skeleton) ..opts(SourceDir::Skeleton)
}; };
let template = super::super::get(TemplateId::Rust).unwrap(); let template = super::super::get(TemplateId::RUST).unwrap();
let skeleton = template.skeleton(&o); let skeleton = template.skeleton(&o);
let cargo_toml = skeleton let cargo_toml = skeleton
@@ -618,7 +566,7 @@ mod tests {
#[test] #[test]
fn probe_reads_cargo_toml_lines() { fn probe_reads_cargo_toml_lines() {
let template = super::super::get(TemplateId::Rust).unwrap(); let template = super::super::get(TemplateId::RUST).unwrap();
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
std::fs::write( std::fs::write(
@@ -670,7 +618,7 @@ mod tests {
std::fs::create_dir_all(dir.path().join("src")).unwrap(); std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}\n").unwrap(); std::fs::write(dir.path().join("src/main.rs"), "fn main() {}\n").unwrap();
let template = super::super::get(TemplateId::Rust).unwrap(); let template = super::super::get(TemplateId::RUST).unwrap();
let probe = template.probe(dir.path()).expect("probe result"); let probe = template.probe(dir.path()).expect("probe result");
assert_eq!(probe.name.as_deref(), Some("metaprobe")); assert_eq!(probe.name.as_deref(), Some("metaprobe"));
assert_eq!(probe.version.as_deref(), Some("0.9.0")); assert_eq!(probe.version.as_deref(), Some("0.9.0"));
@@ -687,7 +635,7 @@ mod tests {
fn post_write_vendors_skeleton() { fn post_write_vendors_skeleton() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let o = opts(SourceDir::Skeleton); let o = opts(SourceDir::Skeleton);
let template = super::super::get(TemplateId::Rust).unwrap(); let template = super::super::get(TemplateId::RUST).unwrap();
for file in template.skeleton(&o) { for file in template.skeleton(&o) {
let path = dir.path().join(&file.path); let path = dir.path().join(&file.path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::create_dir_all(path.parent().unwrap()).unwrap();
@@ -881,7 +829,7 @@ mod tests {
/// no pin file leaves the field empty. /// no pin file leaves the field empty.
#[test] #[test]
fn probe_reports_the_toolchain_pin() { fn probe_reports_the_toolchain_pin() {
let template = super::super::get(TemplateId::Rust).unwrap(); let template = super::super::get(TemplateId::RUST).unwrap();
let cargo_toml = "[package]\nname = \"pinned\"\nversion = \"1.0.0\"\n"; let cargo_toml = "[package]\nname = \"pinned\"\nversion = \"1.0.0\"\n";
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
+18 -12
View File
@@ -1,22 +1,28 @@
//! The `shell` template: a single interpreted script installed to //! The `shell` template: a single interpreted script installed to
//! `/usr/bin` with plain `dh $@` plumbing. //! `/usr/bin` with plain `dh $@` plumbing.
//!
//! The data half lives in `data/templates/shell/manifest.yml`; the logic
//! half here is the single-script probe pre-filling the wizard answers from
//! the script file name (the same heuristic [`crate::new::detect`] bases
//! its shell detection on) plus, for now, the skeleton script and its
//! skeleton-only `debian/install` mapping (the manifest `files:` list takes
//! those over once their bodies are static data).
use std::path::Path; use std::path::Path;
use super::{OutputFile, ProbeResult, Template}; use super::{OutputFile, ProbeResult, TemplateHooks};
use crate::new::options::{self, NewOptions, SourceDir}; use crate::new::options::{self, NewOptions, SourceDir};
/// Shell script / single interpreted file. /// The logic half of the shell template.
pub struct Shell; pub struct Hooks;
impl Template for Shell { /// The shell template's hooks, registered in the template registry.
fn id(&self) -> crate::new::options::TemplateId { pub static HOOKS: Hooks = Hooks;
crate::new::options::TemplateId::Shell
}
impl TemplateHooks for Hooks {
/// A minimal executable script named after the command, with a `#!/bin/sh` /// A minimal executable script named after the command, with a `#!/bin/sh`
/// shebang and an `echo` placeholder. /// shebang and an `echo` placeholder.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> { fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![OutputFile::executable( vec![OutputFile::executable(
format!("{}.sh", opts.command), format!("{}.sh", opts.command),
format!( format!(
@@ -32,7 +38,7 @@ impl Template for Shell {
/// Only for the skeleton mode: when packaging an existing tree the /// Only for the skeleton mode: when packaging an existing tree the
/// generated mapping would reference the non-existent skeleton script, /// generated mapping would reference the non-existent skeleton script,
/// so the user writes their own install file instead. /// so the user writes their own install file instead.
fn debian(&self, opts: &NewOptions) -> Vec<OutputFile> { fn debian_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
if !matches!(opts.source_dir, SourceDir::Skeleton) { if !matches!(opts.source_dir, SourceDir::Skeleton) {
return Vec::new(); return Vec::new();
} }
@@ -65,7 +71,7 @@ mod tests {
fn opts() -> NewOptions { fn opts() -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Shell, template: TemplateId::SHELL,
source_dir: SourceDir::Skeleton, source_dir: SourceDir::Skeleton,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,
@@ -91,7 +97,7 @@ mod tests {
#[test] #[test]
fn shell_template_shape() { fn shell_template_shape() {
let o = opts(); let o = opts();
let template = super::super::get(TemplateId::Shell).unwrap(); let template = super::super::get(TemplateId::SHELL).unwrap();
assert_eq!(template.architecture(&o), "all"); assert_eq!(template.architecture(&o), "all");
assert!(template.build_depends(&o).is_empty()); assert!(template.build_depends(&o).is_empty());
@@ -111,7 +117,7 @@ mod tests {
#[test] #[test]
fn shell_probe_reads_script_file_name() { fn shell_probe_reads_script_file_name() {
let template = super::super::get(TemplateId::Shell).unwrap(); let template = super::super::get(TemplateId::SHELL).unwrap();
// The .sh extension is stripped, the stem sanitized into a package // The .sh extension is stripped, the stem sanitized into a package
// name and command. // name and command.
+1 -1
View File
@@ -126,7 +126,7 @@ mod tests {
fn opts() -> NewOptions { fn opts() -> NewOptions {
NewOptions { NewOptions {
name: "mytool".into(), name: "mytool".into(),
template: TemplateId::Shell, template: TemplateId::SHELL,
source_dir: SourceDir::Skeleton, source_dir: SourceDir::Skeleton,
upstream_version: "0.1.0".into(), upstream_version: "0.1.0".into(),
revision: 1, revision: 1,