diff --git a/data/templates/rust/Cargo.toml.tpl b/data/templates/rust/Cargo.toml.tpl new file mode 100644 index 0000000..8df4afe --- /dev/null +++ b/data/templates/rust/Cargo.toml.tpl @@ -0,0 +1,6 @@ +[package] +name = "{crate_name}" +version = "{upstream_version}" +edition = "2021" + +[dependencies] diff --git a/data/templates/rust/main.rs.tpl b/data/templates/rust/main.rs.tpl new file mode 100644 index 0000000..a5e4b5e --- /dev/null +++ b/data/templates/rust/main.rs.tpl @@ -0,0 +1,4 @@ +// Placeholder for {name}, generated by `pkh new`. +fn main() { + println!("Hello from {command}!"); +} diff --git a/data/templates/rust/manifest.yml b/data/templates/rust/manifest.yml index ebb7aaf..260687b 100644 --- a/data/templates/rust/manifest.yml +++ b/data/templates/rust/manifest.yml @@ -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 diff --git a/src/new/templates/mod.rs b/src/new/templates/mod.rs index d5d1360..48b3607 100644 --- a/src/new/templates/mod.rs +++ b/src/new/templates/mod.rs @@ -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: &[( - "rules.extra.tpl", - include_str!("../../../data/templates/rust/rules.extra.tpl"), - )], + 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, overrides: Vec) { 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). diff --git a/src/new/templates/rust.rs b/src/new/templates/rust.rs index 110b203..9d0752b 100644 --- a/src/new/templates/rust.rs +++ b/src/new/templates/rust.rs @@ -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 { - 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 - .iter() - .any(|f| f.path == "Cargo.toml" && f.contents.contains("name = \"mytool\"")) + assert_eq!(skeleton.len(), 2); + let cargo_toml = skeleton + .iter() + .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!(