465 lines
17 KiB
Rust
465 lines
17 KiB
Rust
//! 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, 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, PathBuf};
|
|
|
|
use super::options::{NewOptions, SourceDir, TemplateId};
|
|
|
|
/// One generated file, rendered in memory before anything touches the disk.
|
|
#[derive(Debug, Clone)]
|
|
pub struct OutputFile {
|
|
/// Path relative to the package tree root (e.g. `debian/control`).
|
|
pub path: String,
|
|
/// Full file contents.
|
|
pub contents: String,
|
|
/// Whether the file carries the executable bit (mode 0755).
|
|
pub executable: bool,
|
|
}
|
|
|
|
impl OutputFile {
|
|
/// A regular (non-executable) file.
|
|
pub fn new(path: impl Into<String>, contents: impl Into<String>) -> OutputFile {
|
|
OutputFile {
|
|
path: path.into(),
|
|
contents: contents.into(),
|
|
executable: false,
|
|
}
|
|
}
|
|
|
|
/// An executable file (mode 0755).
|
|
pub fn executable(path: impl Into<String>, contents: impl Into<String>) -> OutputFile {
|
|
OutputFile {
|
|
executable: true,
|
|
..OutputFile::new(path, contents)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What the template post-write hook did to the freshly written tree,
|
|
/// threaded through [`super::scaffold`] so the flow can react (e.g. word
|
|
/// the post-scaffold verification offer differently when vendoring failed).
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ScaffoldOutcome {
|
|
/// The vendoring step did not complete (host `cargo` missing, `cargo
|
|
/// vendor` failed, or the offline config could not be written): the
|
|
/// package will not build until the dependencies are vendored manually.
|
|
pub vendoring_failed: bool,
|
|
/// How the orig tarball was actually created (quilt only; `None` with
|
|
/// the native format, which has no orig tarball). Filled in by the
|
|
/// scaffold flow, not by the template hook.
|
|
pub orig_origin: Option<String>,
|
|
}
|
|
|
|
/// Metadata extracted from an existing project by [`Template::probe`], used
|
|
/// by the interactive wizard to pre-fill its answers (explicit flags always
|
|
/// win). Every field is optional; probe failures are silent and the generic
|
|
/// defaults apply.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ProbeResult {
|
|
/// Project name (e.g. the `name` key of `Cargo.toml`).
|
|
pub name: Option<String>,
|
|
/// Project version.
|
|
pub version: Option<String>,
|
|
/// Project description.
|
|
pub description: Option<String>,
|
|
/// Project homepage.
|
|
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>,
|
|
/// Rust toolchain channel pinned by the project's `rust-toolchain.toml`
|
|
/// (or legacy `rust-toolchain`), e.g. `1.98.0`. Only set by the rust
|
|
/// template; the chroot build uses the distribution's rustc and ignores
|
|
/// the pin, so the wizard surfaces it as a heads-up instead.
|
|
pub toolchain_pin: Option<String>,
|
|
}
|
|
|
|
/// A package template: one supported ecosystem / build system.
|
|
///
|
|
/// `Sync` is required so templates can live in the static registry.
|
|
pub trait Template: Sync {
|
|
/// Identifier of this template.
|
|
fn id(&self) -> TemplateId;
|
|
|
|
/// Upstream-side files for the skeleton mode (e.g. `Cargo.toml`,
|
|
/// `src/main.rs`). Only called when packaging a fresh skeleton.
|
|
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile>;
|
|
|
|
/// Extra `debian/` files beyond the common set rendered by
|
|
/// [`super::debian`] (e.g. `debian/install`).
|
|
fn debian(&self, opts: &NewOptions) -> Vec<OutputFile>;
|
|
|
|
/// Build-Depends beyond `debhelper-compat (= 13)`.
|
|
fn build_depends(&self, _opts: &NewOptions) -> Vec<String> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Architecture of the binary package (`all` or `any`).
|
|
fn architecture(&self, _opts: &NewOptions) -> &'static str {
|
|
"all"
|
|
}
|
|
|
|
/// The `dh` invocation (without leading tab) used by the `%:` target of
|
|
/// `debian/rules`. Templates needing more than the plain `dh $@` spell
|
|
/// their buildsystem/sequencer options here so the generated rules stay
|
|
/// valid make.
|
|
fn rules_dh_line(&self) -> String {
|
|
"dh $@".to_string()
|
|
}
|
|
|
|
/// Lines appended to `debian/rules` after the default `dh $@` stanza
|
|
/// (e.g. `override_dh_*` targets). Must use tabs for recipe lines.
|
|
fn rules_extra(&self, _opts: &NewOptions) -> String {
|
|
String::new()
|
|
}
|
|
|
|
/// Extra `debian/control` source-stanza fields beyond the common set
|
|
/// (e.g. `XS-Go-Import-Path`).
|
|
fn source_fields(&self, _opts: &NewOptions) -> Vec<(String, String)> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Root `.gitignore` entries the template contributes in every mode
|
|
/// (the skeleton-mode build-artifact entries of
|
|
/// [`crate::new::debian::ROOT_GITIGNORE_ENTRIES`] are separate), merged
|
|
/// into the root `.gitignore` after the files are written — missing
|
|
/// ones appended, an existing file never overwritten. Default: none.
|
|
fn gitignore_entries(&self, _opts: &NewOptions) -> Vec<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). Returns the outcome
|
|
/// the flow should know about ([`ScaffoldOutcome`]); failures that leave
|
|
/// the tree in place but not buildable are reported through it instead
|
|
/// of failing the scaffold.
|
|
fn post_write(
|
|
&self,
|
|
_opts: &NewOptions,
|
|
_tree: &Path,
|
|
) -> Result<ScaffoldOutcome, Box<dyn std::error::Error>> {
|
|
Ok(ScaffoldOutcome::default())
|
|
}
|
|
}
|
|
|
|
/// Static instance of the shell template.
|
|
pub static SHELL: shell::Shell = shell::Shell;
|
|
/// Static instance of the empty/metapackage template.
|
|
pub static EMPTY: empty::Empty = empty::Empty;
|
|
/// Static instance of the makefile template.
|
|
pub static MAKEFILE: makefile::Makefile = makefile::Makefile;
|
|
/// Static instance of the python template.
|
|
pub static PYTHON: python::Python = python::Python;
|
|
/// Static instance of the meson template.
|
|
pub static MESON: meson::Meson = meson::Meson;
|
|
/// Static instance of the cmake template.
|
|
pub static CMAKE: cmake::Cmake = cmake::Cmake;
|
|
/// Static instance of the autotools template.
|
|
pub static AUTOTOOLS: autotools::Autotools = autotools::Autotools;
|
|
/// Static instance of the go template.
|
|
pub static GO: go::Go = go::Go;
|
|
/// Static instance of the rust template.
|
|
pub static RUST: rust::Rust = rust::Rust;
|
|
|
|
/// Every implemented template.
|
|
static TEMPLATES: &[&dyn Template] = &[
|
|
&SHELL, &EMPTY, &MAKEFILE, &PYTHON, &MESON, &CMAKE, &AUTOTOOLS, &GO, &RUST,
|
|
];
|
|
|
|
/// Look up the template implementation for `id`; `None` only if a
|
|
/// [`TemplateId`] ever grows without a registered template (callers turn
|
|
/// this into a friendly error instead of panicking).
|
|
pub fn get(id: TemplateId) -> Option<&'static dyn Template> {
|
|
match id {
|
|
TemplateId::Shell => Some(&SHELL),
|
|
TemplateId::Empty => Some(&EMPTY),
|
|
TemplateId::Makefile => Some(&MAKEFILE),
|
|
TemplateId::Python => Some(&PYTHON),
|
|
TemplateId::Meson => Some(&MESON),
|
|
TemplateId::Cmake => Some(&CMAKE),
|
|
TemplateId::Autotools => Some(&AUTOTOOLS),
|
|
TemplateId::Go => Some(&GO),
|
|
TemplateId::Rust => Some(&RUST),
|
|
}
|
|
}
|
|
|
|
/// Every implemented template.
|
|
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_every_template() {
|
|
for id in TemplateId::all() {
|
|
assert!(get(id).is_some(), "{id} must be registered");
|
|
assert_eq!(get(id).unwrap().id(), id);
|
|
}
|
|
assert_eq!(all().len(), TemplateId::all().len());
|
|
}
|
|
|
|
#[test]
|
|
fn probe_defaults_to_none() {
|
|
assert!(
|
|
get(TemplateId::Shell)
|
|
.unwrap()
|
|
.probe(Path::new("/"))
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
/// Only the rust template contributes root `.gitignore` entries of its
|
|
/// own — exactly the vendoring pair, and unconditionally (its vendoring
|
|
/// hook runs in every mode); every other template contributes nothing.
|
|
#[test]
|
|
fn gitignore_entries_are_rust_only() {
|
|
let o = NewOptions {
|
|
name: "mytool".into(),
|
|
template: TemplateId::Rust,
|
|
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(),
|
|
source_format: crate::new::options::SourceFormat::Quilt,
|
|
orig: None,
|
|
git: false,
|
|
autopkgtest: false,
|
|
pkg_config: false,
|
|
watch: None,
|
|
};
|
|
|
|
for id in TemplateId::all() {
|
|
let entries = get(id).unwrap().gitignore_entries(&o);
|
|
if id == TemplateId::Rust {
|
|
assert_eq!(
|
|
entries,
|
|
vec!["vendor/".to_string(), ".cargo/config.toml".to_string()]
|
|
);
|
|
} else {
|
|
assert!(entries.is_empty(), "{id} contributes {entries:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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(),
|
|
source_format: crate::new::options::SourceFormat::Quilt,
|
|
orig: None,
|
|
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(),
|
|
source_format: crate::new::options::SourceFormat::Quilt,
|
|
orig: None,
|
|
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 $@");
|
|
}
|
|
}
|