new: add interactive wizard and remaining ecosystem templates
This commit is contained in:
+261
-37
@@ -1,16 +1,25 @@
|
||||
//! Per-ecosystem template registry for `pkh new`.
|
||||
//!
|
||||
//! Every template implements [`Template`]: it renders the upstream-side
|
||||
//! skeleton files, the extra `debian/` files beyond the common set, and (in
|
||||
//! the future) probes an existing project for metadata. Rendering is plain
|
||||
//! `format!` composition — no template engine, matching the codebase style.
|
||||
//! skeleton files, the extra `debian/` files beyond the common set, probes an
|
||||
//! existing project for metadata used to pre-fill the wizard answers, and
|
||||
//! describes its Build-Depends / architecture / `debian/rules` shape.
|
||||
//! Rendering is plain `format!` composition — no template engine, matching
|
||||
//! the codebase style.
|
||||
|
||||
pub mod autotools;
|
||||
pub mod cmake;
|
||||
pub mod empty;
|
||||
pub mod go;
|
||||
pub mod makefile;
|
||||
pub mod meson;
|
||||
pub mod python;
|
||||
pub mod rust;
|
||||
pub mod shell;
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::options::{NewOptions, TemplateId};
|
||||
use super::options::{NewOptions, SourceDir, TemplateId};
|
||||
|
||||
/// One generated file, rendered in memory before anything touches the disk.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -44,8 +53,8 @@ impl OutputFile {
|
||||
|
||||
/// Metadata extracted from an existing project by [`Template::probe`], used
|
||||
/// by the interactive wizard to pre-fill its answers (explicit flags always
|
||||
/// win). The per-template extraction is follow-up work; the hook already
|
||||
/// exists so templates can grow it independently.
|
||||
/// win). Every field is optional; probe failures are silent and the generic
|
||||
/// defaults apply.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProbeResult {
|
||||
/// Project name (e.g. the `name` key of `Cargo.toml`).
|
||||
@@ -58,6 +67,9 @@ pub struct ProbeResult {
|
||||
pub homepage: Option<String>,
|
||||
/// Project license (SPDX identifier).
|
||||
pub license: Option<String>,
|
||||
/// Installed command / binary name (e.g. the first `[[bin]]` target or
|
||||
/// console script).
|
||||
pub command: Option<String>,
|
||||
}
|
||||
|
||||
/// A package template: one supported ecosystem / build system.
|
||||
@@ -81,45 +93,87 @@ pub trait Template: Sync {
|
||||
}
|
||||
|
||||
/// Architecture of the binary package (`all` or `any`).
|
||||
fn architecture(&self) -> &'static str {
|
||||
fn architecture(&self, _opts: &NewOptions) -> &'static str {
|
||||
"all"
|
||||
}
|
||||
|
||||
/// Lines appended to `debian/rules` after the default `dh $@` stanza.
|
||||
fn rules_extra(&self) -> String {
|
||||
/// The `dh` invocation (without leading tab) used by the `%:` target of
|
||||
/// `debian/rules`. Templates needing more than the plain `dh $@` spell
|
||||
/// their buildsystem/sequencer options here so the generated rules stay
|
||||
/// valid make.
|
||||
fn rules_dh_line(&self) -> String {
|
||||
"dh $@".to_string()
|
||||
}
|
||||
|
||||
/// Lines appended to `debian/rules` after the default `dh $@` stanza
|
||||
/// (e.g. `override_dh_*` targets). Must use tabs for recipe lines.
|
||||
fn rules_extra(&self, _opts: &NewOptions) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Extra `debian/control` source-stanza fields beyond the common set
|
||||
/// (e.g. `XS-Go-Import-Path`).
|
||||
fn source_fields(&self, _opts: &NewOptions) -> Vec<(String, String)> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Extra defaults derived from project metadata in `dir` (detect.rs);
|
||||
/// `None` when the project carries nothing this template can read.
|
||||
fn probe(&self, _dir: &Path) -> Option<ProbeResult> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Hook run after the generated files have been written to `tree` and
|
||||
/// before the orig tarball is created, for templates that need to run
|
||||
/// host tooling over the freshly written tree (e.g. `cargo vendor`, so
|
||||
/// the vendored sources land inside the tarball).
|
||||
fn post_write(
|
||||
&self,
|
||||
_opts: &NewOptions,
|
||||
_tree: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Static instance of the shell template.
|
||||
pub static SHELL: shell::Shell = shell::Shell;
|
||||
/// Static instance of the empty/metapackage template.
|
||||
pub static EMPTY: empty::Empty = empty::Empty;
|
||||
/// Static instance of the makefile template.
|
||||
pub static MAKEFILE: makefile::Makefile = makefile::Makefile;
|
||||
/// Static instance of the python template.
|
||||
pub static PYTHON: python::Python = python::Python;
|
||||
/// Static instance of the meson template.
|
||||
pub static MESON: meson::Meson = meson::Meson;
|
||||
/// Static instance of the cmake template.
|
||||
pub static CMAKE: cmake::Cmake = cmake::Cmake;
|
||||
/// Static instance of the autotools template.
|
||||
pub static AUTOTOOLS: autotools::Autotools = autotools::Autotools;
|
||||
/// Static instance of the go template.
|
||||
pub static GO: go::Go = go::Go;
|
||||
/// Static instance of the rust template.
|
||||
pub static RUST: rust::Rust = rust::Rust;
|
||||
|
||||
/// Every implemented template (the wizard language menu lists
|
||||
/// [`TemplateId::all()`] and greys the rest out).
|
||||
static TEMPLATES: &[&dyn Template] = &[&SHELL, &EMPTY];
|
||||
/// Every implemented template.
|
||||
static TEMPLATES: &[&dyn Template] = &[
|
||||
&SHELL, &EMPTY, &MAKEFILE, &PYTHON, &MESON, &CMAKE, &AUTOTOOLS, &GO, &RUST,
|
||||
];
|
||||
|
||||
/// Look up the template implementation for `id`; `None` for the ids whose
|
||||
/// template is not implemented yet (callers turn this into the friendly
|
||||
/// "not implemented yet" error).
|
||||
/// Look up the template implementation for `id`; `None` only if a
|
||||
/// [`TemplateId`] ever grows without a registered template (callers turn
|
||||
/// this into a friendly error instead of panicking).
|
||||
pub fn get(id: TemplateId) -> Option<&'static dyn Template> {
|
||||
match id {
|
||||
TemplateId::Shell => Some(&SHELL),
|
||||
TemplateId::Empty => Some(&EMPTY),
|
||||
TemplateId::Rust
|
||||
| TemplateId::Python
|
||||
| TemplateId::Meson
|
||||
| TemplateId::Cmake
|
||||
| TemplateId::Autotools
|
||||
| TemplateId::Go
|
||||
| TemplateId::Makefile => None,
|
||||
TemplateId::Makefile => Some(&MAKEFILE),
|
||||
TemplateId::Python => Some(&PYTHON),
|
||||
TemplateId::Meson => Some(&MESON),
|
||||
TemplateId::Cmake => Some(&CMAKE),
|
||||
TemplateId::Autotools => Some(&AUTOTOOLS),
|
||||
TemplateId::Go => Some(&GO),
|
||||
TemplateId::Rust => Some(&RUST),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,28 +182,37 @@ pub fn all() -> &'static [&'static dyn Template] {
|
||||
TEMPLATES
|
||||
}
|
||||
|
||||
/// Locate `name` on `$PATH` (a tiny `which`): `None` when `PATH` is unset or
|
||||
/// nothing executable-looking matches.
|
||||
pub(crate) fn find_on_path(name: &str) -> Option<PathBuf> {
|
||||
let path = std::env::var_os("PATH")?;
|
||||
std::env::split_paths(&path)
|
||||
.map(|dir| dir.join(name))
|
||||
.find(|candidate| candidate.is_file())
|
||||
}
|
||||
|
||||
/// The directory whose sources are being packaged, when there is one:
|
||||
/// `None` for the skeleton mode (the skeleton files are rendered in memory
|
||||
/// and do not exist on disk yet).
|
||||
pub(crate) fn source_dir_of(opts: &NewOptions) -> Option<PathBuf> {
|
||||
match &opts.source_dir {
|
||||
SourceDir::Skeleton => None,
|
||||
SourceDir::Here => std::env::current_dir().ok(),
|
||||
SourceDir::Path(path) => Some(path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn registry_covers_implemented_templates() {
|
||||
for id in [TemplateId::Shell, TemplateId::Empty] {
|
||||
fn registry_covers_every_template() {
|
||||
for id in TemplateId::all() {
|
||||
assert!(get(id).is_some(), "{id} must be registered");
|
||||
assert_eq!(get(id).unwrap().id(), id);
|
||||
}
|
||||
for id in [
|
||||
TemplateId::Rust,
|
||||
TemplateId::Python,
|
||||
TemplateId::Meson,
|
||||
TemplateId::Cmake,
|
||||
TemplateId::Autotools,
|
||||
TemplateId::Go,
|
||||
TemplateId::Makefile,
|
||||
] {
|
||||
assert!(get(id).is_none(), "{id} must not pretend to be implemented");
|
||||
}
|
||||
assert_eq!(all().len(), 2);
|
||||
assert_eq!(all().len(), TemplateId::all().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -161,4 +224,165 @@ mod tests {
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
/// The final `debian/rules` of every template must be valid-looking
|
||||
/// make: `#!/usr/bin/make -f` shebang, exactly one `%:` target whose
|
||||
/// recipe is the template's dh line, tab-indented recipes only, and no
|
||||
/// trailing blank lines.
|
||||
#[test]
|
||||
fn rules_composition_per_template() {
|
||||
let o = NewOptions {
|
||||
name: "mytool".into(),
|
||||
template: TemplateId::Empty,
|
||||
source_dir: SourceDir::Here,
|
||||
upstream_version: "0.1.0".into(),
|
||||
revision: 1,
|
||||
summary: "A tool".into(),
|
||||
long_description: "A tool".into(),
|
||||
homepage: None,
|
||||
license: crate::new::options::License::Mit,
|
||||
command: "mytool".into(),
|
||||
maintainer: ("Jane".into(), "jane@example.com".into()),
|
||||
dist: "ubuntu".into(),
|
||||
series: "resolute".into(),
|
||||
release: false,
|
||||
depends: Vec::new(),
|
||||
native: false,
|
||||
git: false,
|
||||
autopkgtest: false,
|
||||
pkg_config: false,
|
||||
watch: None,
|
||||
};
|
||||
|
||||
for id in TemplateId::all() {
|
||||
let template = get(id).unwrap();
|
||||
let files = super::super::debian::files(&o, template);
|
||||
let rules = files
|
||||
.iter()
|
||||
.find(|f| f.path == "debian/rules")
|
||||
.expect("every template renders debian/rules");
|
||||
|
||||
assert!(rules.executable, "{id}: rules must carry the exec bit");
|
||||
assert!(
|
||||
rules.contents.starts_with("#!/usr/bin/make -f\n%:\n\t"),
|
||||
"{id}: rules must start with the shebang and %: target"
|
||||
);
|
||||
assert!(
|
||||
rules.contents.matches("\n%:\n").count() == 1,
|
||||
"{id}: exactly one %: target expected"
|
||||
);
|
||||
// No recipe may be indented with spaces (make requires tabs).
|
||||
for line in rules.contents.lines() {
|
||||
assert!(
|
||||
!line.starts_with(' '),
|
||||
"{id}: space-indented line in rules: {line:?}"
|
||||
);
|
||||
}
|
||||
// The template's dh line is the %: recipe.
|
||||
assert!(
|
||||
rules
|
||||
.contents
|
||||
.contains(&format!("\n%:\n\t{}\n", template.rules_dh_line())),
|
||||
"{id}: %: recipe must be the dh line {:?} in {:?}",
|
||||
template.rules_dh_line(),
|
||||
rules.contents
|
||||
);
|
||||
// rule_extra targets must be declared at column 0 with tabbed
|
||||
// recipes.
|
||||
let extra = template.rules_extra(&o);
|
||||
if !extra.is_empty() {
|
||||
assert!(rules.contents.contains(&format!("\n{extra}")), "{id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-template Build-Depends / architecture / rules shape, locking the
|
||||
/// table from the spec.
|
||||
#[test]
|
||||
fn build_depends_architecture_and_rules_table() {
|
||||
let o = NewOptions {
|
||||
name: "mytool".into(),
|
||||
template: TemplateId::Empty,
|
||||
source_dir: SourceDir::Skeleton,
|
||||
upstream_version: "0.1.0".into(),
|
||||
revision: 1,
|
||||
summary: "A tool".into(),
|
||||
long_description: "A tool".into(),
|
||||
homepage: None,
|
||||
license: crate::new::options::License::Mit,
|
||||
command: "mytool".into(),
|
||||
maintainer: ("Jane".into(), "jane@example.com".into()),
|
||||
dist: "ubuntu".into(),
|
||||
series: "resolute".into(),
|
||||
release: false,
|
||||
depends: Vec::new(),
|
||||
native: false,
|
||||
git: false,
|
||||
autopkgtest: false,
|
||||
pkg_config: false,
|
||||
watch: None,
|
||||
};
|
||||
let deps = |id| {
|
||||
let mut all = vec!["debhelper-compat (= 13)".to_string()];
|
||||
all.extend(get(id).unwrap().build_depends(&o));
|
||||
all.join(", ")
|
||||
};
|
||||
let arch = |id| get(id).unwrap().architecture(&o);
|
||||
let dh = |id| get(id).unwrap().rules_dh_line();
|
||||
|
||||
assert_eq!(deps(TemplateId::Shell), "debhelper-compat (= 13)");
|
||||
assert_eq!(arch(TemplateId::Shell), "all");
|
||||
assert_eq!(dh(TemplateId::Shell), "dh $@");
|
||||
|
||||
assert_eq!(deps(TemplateId::Empty), "debhelper-compat (= 13)");
|
||||
assert_eq!(arch(TemplateId::Empty), "all");
|
||||
|
||||
assert_eq!(
|
||||
deps(TemplateId::Makefile),
|
||||
"debhelper-compat (= 13), build-essential"
|
||||
);
|
||||
assert_eq!(arch(TemplateId::Makefile), "any");
|
||||
assert_eq!(dh(TemplateId::Makefile), "dh $@");
|
||||
|
||||
assert_eq!(
|
||||
dh(TemplateId::Python),
|
||||
"dh $@ --with python3 --buildsystem=pybuild"
|
||||
);
|
||||
// Skeleton projects use the setuptools pyproject backend.
|
||||
assert_eq!(
|
||||
deps(TemplateId::Python),
|
||||
"debhelper-compat (= 13), dh-python, python3-all, \
|
||||
pybuild-plugin-pyproject, python3-setuptools"
|
||||
);
|
||||
assert_eq!(arch(TemplateId::Python), "all");
|
||||
|
||||
assert_eq!(deps(TemplateId::Meson), "debhelper-compat (= 13), meson");
|
||||
assert_eq!(arch(TemplateId::Meson), "any");
|
||||
assert_eq!(dh(TemplateId::Meson), "dh $@ --buildsystem=meson");
|
||||
|
||||
assert_eq!(deps(TemplateId::Cmake), "debhelper-compat (= 13), cmake");
|
||||
assert_eq!(arch(TemplateId::Cmake), "any");
|
||||
assert_eq!(dh(TemplateId::Cmake), "dh $@ --buildsystem=cmake");
|
||||
|
||||
assert_eq!(
|
||||
deps(TemplateId::Autotools),
|
||||
"debhelper-compat (= 13), autoconf, automake, libtool"
|
||||
);
|
||||
assert_eq!(arch(TemplateId::Autotools), "any");
|
||||
assert_eq!(dh(TemplateId::Autotools), "dh $@");
|
||||
|
||||
assert_eq!(
|
||||
deps(TemplateId::Go),
|
||||
"debhelper-compat (= 13), golang-any, dh-golang"
|
||||
);
|
||||
assert_eq!(arch(TemplateId::Go), "any");
|
||||
assert_eq!(dh(TemplateId::Go), "dh $@ --buildsystem=golang");
|
||||
|
||||
assert_eq!(
|
||||
deps(TemplateId::Rust),
|
||||
"debhelper-compat (= 13), cargo:native, rustc:native"
|
||||
);
|
||||
assert_eq!(arch(TemplateId::Rust), "any");
|
||||
assert_eq!(dh(TemplateId::Rust), "dh $@");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user