new: port the rust template bodies to manifests

This commit is contained in:
2026-09-18 15:05:44 +02:00
parent ffac4d6b57
commit e6f2012835
5 changed files with 130 additions and 65 deletions
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "{crate_name}"
version = "{upstream_version}"
edition = "2021"
[dependencies]
+4
View File
@@ -0,0 +1,4 @@
// Placeholder for {name}, generated by `pkh new`.
fn main() {
println!("Hello from {command}!");
}
+12 -5
View File
@@ -1,8 +1,11 @@
## 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.
## src/new/templates/rust.rs for the vendoring strategy). The skeleton
## bodies and the vendored-build rules overrides below are static data; the
## logic half — the cargo vendor post-write hook, the project probe, and
## the `{crate_name}` / `{locked}` / `{artifact}` values of the bodies —
## lives in that module (dpkg package names may carry `+`/`.`, which cargo
## rejects in crate names, so the skeleton crate name is a derived
## placeholder, not the raw `{name}`).
##
## Schema: see src/new/templates/mod.rs.
@@ -23,4 +26,8 @@ rules_extra_file: rules.extra.tpl
gitignore_entries:
- vendor/
- .cargo/config.toml
files: []
files:
- path: Cargo.toml
template: Cargo.toml.tpl
- path: src/main.rs
template: main.rs.tpl
+57 -10
View File
@@ -141,10 +141,11 @@ pub struct ProbeResult {
/// The manifest-driven files are the base of both file lists; a hook's
/// [`skeleton_files`](TemplateHooks::skeleton_files) /
/// [`debian_files`](TemplateHooks::debian_files) entries are merged over
/// them by [`merge_files`]: an entry whose path a manifest file already
/// uses replaces it, anything else is appended. That is how a body needing
/// conditionals stays in Rust without giving up the data half — the hook
/// file shadows the manifest body it supersedes.
/// them by [`merge_files`]. The convention is *manifest unless it needs
/// branching*: a conditional body lives in the hook alone, and the
/// registry tests reject a hook file whose path a manifest entry also
/// carries — a path with two homes would be one dead body silently
/// shadowing the other.
pub trait TemplateHooks: Sync {
/// Extra defaults derived from the project metadata in `dir`
/// (detect.rs pre-fills the wizard answers); `None` when the project
@@ -336,10 +337,20 @@ static TEMPLATE_SOURCES: &[TemplateSources] = &[
TemplateSources {
id: TemplateId::RUST,
manifest: include_str!("../../../data/templates/rust/manifest.yml"),
tpls: &[(
tpls: &[
(
"rules.extra.tpl",
include_str!("../../../data/templates/rust/rules.extra.tpl"),
)],
),
(
"Cargo.toml.tpl",
include_str!("../../../data/templates/rust/Cargo.toml.tpl"),
),
(
"main.rs.tpl",
include_str!("../../../data/templates/rust/main.rs.tpl"),
),
],
hooks: Some(&rust::HOOKS),
},
TemplateSources {
@@ -727,10 +738,11 @@ impl Template {
}
/// Merge `overrides` into `files`: an entry whose path is already there
/// replaces the earlier entry in place, anything else is appended. Hook
/// files thereby both shadow manifest bodies needing logic and add extra
/// ones — and the merged list never carries a path twice (the scaffold's
/// collision check would reject that as an internal error).
/// replaces the earlier entry in place, anything else is appended, and
/// the merged list never carries a path twice (the scaffold's collision
/// check would reject that as an internal error). The registry tests pin
/// the built-in templates to appending only — a hook file shadowing a
/// manifest body would leave one of the two dead (the no-drift invariant).
fn merge_files(files: &mut Vec<OutputFile>, overrides: Vec<OutputFile>) {
for file in overrides {
match files.iter().position(|existing| existing.path == file.path) {
@@ -1022,6 +1034,41 @@ mod tests {
assert_eq!(files[2].contents, "extra");
}
/// The no-drift invariant of the "manifest unless it needs branching"
/// convention: no hook renders a file whose path a manifest entry also
/// carries. A path with two homes would be one dead body silently
/// shadowing the other — a body needing conditionals belongs to the
/// hook alone. (Since every template's bodies are manifest data, the
/// hooks render no files at all; this test locks that in.)
#[test]
fn hook_files_never_shadow_manifest_bodies() {
let o = opts(TemplateId::EMPTY);
for template in all() {
let Some(hooks) = template.hooks else {
continue;
};
let manifest_paths: Vec<&str> = template
.manifest
.files
.iter()
.map(|file| file.path.as_str())
.collect();
let hook_files = hooks
.skeleton_files(&o)
.into_iter()
.chain(hooks.debian_files(&o));
for file in hook_files {
assert!(
!manifest_paths.contains(&file.path.as_str()),
"{}: hook body '{}' is also a manifest path — a body has \
one home (manifest unless it needs branching)",
template.id(),
file.path
);
}
}
}
/// The empty template is pure data: a stub `README` skeleton, no extra
/// debian/ files (the metapackage `Depends` payload travels in the
/// options, not in the template).
+48 -47
View File
@@ -10,16 +10,17 @@
//! [`ScaffoldOutcome::vendoring_failed`] to the flow — the package will not
//! 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.
//! The metadata and bodies are manifest data
//! (`data/templates/rust/manifest.yml`: the skeleton `Cargo.toml` /
//! `src/main.rs` and the `debian/rules` vendored-build overrides), rendered
//! through the placeholders this module supplies; the logic half here is
//! the project probe and the vendoring hook.
use std::path::Path;
use serde_json::Value;
use super::{OutputFile, ProbeResult, ScaffoldOutcome, TemplateHooks, find_on_path, source_dir_of};
use super::{ProbeResult, ScaffoldOutcome, TemplateHooks, find_on_path, source_dir_of};
use crate::new::options::{NewOptions, SourceDir};
/// The logic half of the rust template.
@@ -39,42 +40,14 @@ fn crate_name(opts: &NewOptions) -> String {
}
impl TemplateHooks for Hooks {
/// A zero-dependency `Cargo.toml` and the matching `src/main.rs`.
fn skeleton_files(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![
OutputFile::new(
"Cargo.toml",
format!(
"[package]\n\
name = \"{name}\"\n\
version = \"{version}\"\n\
edition = \"2021\"\n\
\n\
[dependencies]\n",
name = crate_name(opts),
version = opts.upstream_version,
),
),
OutputFile::new(
"src/main.rs",
format!(
"// Placeholder for {name}, generated by `pkh new`.\n\
fn main() {{\n\
\tprintln!(\"Hello from {command}!\");\n\
}}\n",
name = opts.name,
command = opts.command,
),
),
]
}
/// The `{locked}`/`{artifact}` values of `rules.extra.tpl`: `--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); 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.
/// The placeholder values of the manifest bodies: `{crate_name}` names
/// the skeleton crate (a sanitized package name — see [`crate_name`]);
/// `{locked}` is ` --locked` 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);
/// `{artifact}` is the built binary of the rules install override —
/// a skeleton's is named after its crate, an existing project's under
/// the (probed or answered) command.
fn context(&self, opts: &NewOptions) -> Vec<(String, String)> {
let locked = if lockfile_present(opts) {
" --locked"
@@ -86,6 +59,7 @@ impl TemplateHooks for Hooks {
_ => opts.command.clone(),
};
vec![
("crate_name".to_string(), crate_name(opts)),
("locked".to_string(), locked.to_string()),
("artifact".to_string(), artifact),
]
@@ -492,14 +466,33 @@ mod tests {
assert_eq!(template.rules_dh_line(), "dh $@");
assert!(template.debian(&o).is_empty());
// Skeleton: Cargo.toml + src/main.rs.
// Skeleton (manifest data): Cargo.toml + src/main.rs, byte-exact.
let skeleton = template.skeleton(&o);
assert!(
skeleton
assert_eq!(skeleton.len(), 2);
let cargo_toml = skeleton
.iter()
.any(|f| f.path == "Cargo.toml" && f.contents.contains("name = \"mytool\""))
.find(|f| f.path == "Cargo.toml")
.expect("Cargo.toml skeleton");
assert_eq!(
cargo_toml.contents,
"[package]\n\
name = \"mytool\"\n\
version = \"0.1.0\"\n\
edition = \"2021\"\n\
\n\
[dependencies]\n"
);
let main_rs = skeleton
.iter()
.find(|f| f.path == "src/main.rs")
.expect("src/main.rs skeleton");
assert_eq!(
main_rs.contents,
"// Placeholder for mytool, generated by `pkh new`.\n\
fn main() {\n\
\tprintln!(\"Hello from mytool!\");\n\
}\n"
);
assert!(skeleton.iter().any(|f| f.path == "src/main.rs"));
// Fresh skeleton: no Cargo.lock, so no --locked flag anywhere.
let extra = template.rules_extra(&o);
@@ -553,7 +546,15 @@ mod tests {
.iter()
.find(|f| f.path == "Cargo.toml")
.expect("Cargo.toml skeleton");
assert!(cargo_toml.contents.contains("name = \"my_tool_\""));
assert_eq!(
cargo_toml.contents,
"[package]\n\
name = \"my_tool_\"\n\
version = \"0.1.0\"\n\
edition = \"2021\"\n\
\n\
[dependencies]\n"
);
let extra = template.rules_extra(&o);
assert!(