new: add interactive wizard and remaining ecosystem templates
This commit is contained in:
+158
-31
@@ -2,17 +2,19 @@
|
||||
//! `plans/pkh-new.md`).
|
||||
//!
|
||||
//! This module orchestrates a scaffold run: target directory checks, project
|
||||
//! detection, in-memory rendering of every file (all-or-nothing write), orig
|
||||
//! tarball creation, git initialization, structural verification and the
|
||||
//! next-steps message. The interactive wizard and the remaining templates
|
||||
//! (python, meson, cmake, autotools, go, rust, makefile) are follow-up work
|
||||
//! built on top of the same [`options::NewOptions`] and
|
||||
//! [`templates::Template`] surface.
|
||||
//! detection, in-memory rendering of every file (all-or-nothing write), the
|
||||
//! template post-write hook (e.g. `cargo vendor`), orig tarball creation,
|
||||
//! git initialization, structural verification and the next-steps message.
|
||||
//! The interactive wizard ([`questions`]) fills a [`options::NewCli`] from
|
||||
//! its answers on a TTY and reuses [`options::resolve`] as the single source
|
||||
//! of truth for defaults and validation; without a TTY the same resolution
|
||||
//! runs flag-driven.
|
||||
|
||||
pub mod debian;
|
||||
pub mod detect;
|
||||
pub mod git;
|
||||
pub mod options;
|
||||
pub mod questions;
|
||||
pub mod templates;
|
||||
pub mod verify;
|
||||
|
||||
@@ -27,15 +29,17 @@ use templates::OutputFile;
|
||||
/// Scaffold a full Debian source tree from `opts`.
|
||||
///
|
||||
/// Steps, aborting early with a pointed error message:
|
||||
/// 1. resolve the template (unimplemented ids fail here, not at parse time),
|
||||
/// 1. resolve the template from the registry,
|
||||
/// 2. check the target directory (refuse an existing `debian/control`),
|
||||
/// 3. render every file in memory and check for collisions,
|
||||
/// 4. write the files all-or-nothing (plus the root `.gitignore` in skeleton
|
||||
/// mode, appending to an existing one),
|
||||
/// 5. create the orig tarball (quilt only, refusing overwrites),
|
||||
/// 6. `git init` unless `--no-git` or already inside a repository,
|
||||
/// 7. run the structural verification,
|
||||
/// 8. print the success message with the next steps.
|
||||
/// 5. run the template post-write hook (e.g. `cargo vendor`, so the vendored
|
||||
/// sources land inside the orig tarball created next),
|
||||
/// 6. create the orig tarball (quilt only, refusing overwrites),
|
||||
/// 7. `git init` unless `--no-git` or already inside a repository,
|
||||
/// 8. run the structural verification,
|
||||
/// 9. print the success message with the next steps.
|
||||
pub fn scaffold(opts: NewOptions, multi: &MultiProgress) -> Result<(), Box<dyn Error>> {
|
||||
let pb = multi.add(ProgressBar::new_spinner());
|
||||
pb.enable_steady_tick(Duration::from_millis(50));
|
||||
@@ -62,17 +66,13 @@ pub fn scaffold(opts: NewOptions, multi: &MultiProgress) -> Result<(), Box<dyn E
|
||||
/// The scaffold steps proper, reporting progress through `pb`. Nothing is
|
||||
/// written to the filesystem before every file rendered successfully.
|
||||
fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<(), Box<dyn Error>> {
|
||||
// 1. Template resolution: unknown-but-valid ids fail here with the
|
||||
// friendly message instead of a parse error.
|
||||
// 1. Template resolution: an id without a registered template fails
|
||||
// here with the friendly message instead of a parse error.
|
||||
let template = templates::get(opts.template).ok_or_else(|| {
|
||||
format!(
|
||||
"The '{}' template is not implemented yet. Implemented templates: {}.",
|
||||
"The '{}' template has no registered implementation. \
|
||||
This is a pkh bug; please report it.",
|
||||
opts.template,
|
||||
templates::all()
|
||||
.iter()
|
||||
.map(|t| t.id().as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -173,17 +173,23 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<(), Box<dyn Err
|
||||
std::fs::write(target.join(".gitignore"), contents)?;
|
||||
}
|
||||
|
||||
// 5. Orig tarball (quilt only).
|
||||
// 5. Template post-write hook: run before the orig tarball is created,
|
||||
// so files added here (rust: vendor/ + .cargo/config.toml) land
|
||||
// inside it.
|
||||
pb.set_message("Running template hooks");
|
||||
template.post_write(opts, &target)?;
|
||||
|
||||
// 6. Orig tarball (quilt only).
|
||||
if !opts.native {
|
||||
pb.set_message("Creating orig tarball");
|
||||
debian::create_orig_tarball(&target, &opts.name, &opts.upstream_version_no_epoch())?;
|
||||
}
|
||||
|
||||
// 6. Git.
|
||||
// 7. Git.
|
||||
pb.set_message("Initializing git");
|
||||
git::ensure_repository(&target, opts.git)?;
|
||||
|
||||
// 7. Structural verification.
|
||||
// 8. Structural verification.
|
||||
pb.set_message("Verifying");
|
||||
verify::verify(&target)?;
|
||||
|
||||
@@ -250,6 +256,9 @@ mod tests {
|
||||
depends: Vec::new(),
|
||||
native: false,
|
||||
git: false,
|
||||
autopkgtest: false,
|
||||
pkg_config: false,
|
||||
watch: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,15 +536,6 @@ mod tests {
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("does not exist"), "{err}");
|
||||
|
||||
// Unimplemented template.
|
||||
let dir = tempdir().unwrap();
|
||||
let err = scaffold_in(
|
||||
dir.path(),
|
||||
opts(TemplateId::Rust, "mytool", SourceDir::Skeleton),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("not implemented yet"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -555,6 +555,133 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// End-to-end rust skeleton: the vendoring hook runs before the orig
|
||||
/// tarball is created, so `.cargo/` (and `vendor/` when dependencies
|
||||
/// exist) travel inside it. The vendoring step needs host cargo; on a
|
||||
/// cargo-less host the scaffold still succeeds with a warning.
|
||||
#[test]
|
||||
#[serial]
|
||||
fn scaffold_rust_skeleton_vendors_before_tarball() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_in(
|
||||
dir.path(),
|
||||
opts(TemplateId::Rust, "mytool", SourceDir::Skeleton),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tree = dir.path().join("mytool");
|
||||
assert!(tree.join("Cargo.toml").exists());
|
||||
assert!(tree.join("src/main.rs").exists());
|
||||
|
||||
// rules: the vendored build overrides, no --locked on a fresh
|
||||
// skeleton without Cargo.lock.
|
||||
let rules = std::fs::read_to_string(tree.join("debian/rules")).unwrap();
|
||||
assert!(rules.contains("%:\n\tdh $@\n"));
|
||||
assert!(rules.contains("override_dh_auto_build:\n\tcargo build --release --offline\n"));
|
||||
assert!(rules.contains("override_dh_auto_install:\n\tinstall -Dm755 target/release/mytool debian/mytool/usr/bin/mytool"));
|
||||
assert!(!rules.contains("--locked"));
|
||||
|
||||
// control: Architecture any + the cargo/rustc build-deps.
|
||||
let control = crate::debian::ControlInfo::parse(&tree.join("debian/control")).unwrap();
|
||||
assert_eq!(control.binaries[0].get("Architecture"), Some("any"));
|
||||
assert_eq!(
|
||||
control.source.get("Build-Depends"),
|
||||
Some("debhelper-compat (= 13),\ncargo:native,\nrustc:native")
|
||||
);
|
||||
|
||||
// The offline config exists when host cargo vendored the skeleton,
|
||||
// and both it and the skeleton land inside the orig tarball.
|
||||
let has_cargo = crate::new::templates::find_on_path("cargo").is_some();
|
||||
if has_cargo {
|
||||
let config = std::fs::read_to_string(tree.join(".cargo/config.toml")).unwrap();
|
||||
assert!(config.contains("[source.crates-io]"), "{config}");
|
||||
assert!(config.contains("[net]\noffline = true"), "{config}");
|
||||
}
|
||||
|
||||
let tarball = dir.path().join("mytool_0.1.0.orig.tar.xz");
|
||||
let mut archive = tar::Archive::new(xz2::read::XzDecoder::new(
|
||||
std::fs::File::open(&tarball).unwrap(),
|
||||
));
|
||||
let names: Vec<String> = archive
|
||||
.entries()
|
||||
.unwrap()
|
||||
.map(|entry| {
|
||||
entry
|
||||
.unwrap()
|
||||
.path()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
names.iter().any(|n| n == "mytool-0.1.0/Cargo.toml"),
|
||||
"{names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.iter().any(|n| n == "mytool-0.1.0/src/main.rs"),
|
||||
"{names:?}"
|
||||
);
|
||||
if has_cargo {
|
||||
assert!(
|
||||
names.iter().any(|n| n == "mytool-0.1.0/.cargo/config.toml"),
|
||||
"{names:?}"
|
||||
);
|
||||
}
|
||||
assert!(!names.iter().any(|n| n.contains("debian")), "{names:?}");
|
||||
}
|
||||
|
||||
/// End-to-end python skeleton: pyproject-based Build-Depends and the
|
||||
/// module skeleton inside the orig tarball.
|
||||
#[test]
|
||||
#[serial]
|
||||
fn scaffold_python_skeleton_tree() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_in(
|
||||
dir.path(),
|
||||
opts(TemplateId::Python, "mytool", SourceDir::Skeleton),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tree = dir.path().join("mytool");
|
||||
let control = crate::debian::ControlInfo::parse(&tree.join("debian/control")).unwrap();
|
||||
assert_eq!(control.binaries[0].get("Architecture"), Some("all"));
|
||||
assert_eq!(
|
||||
control.source.get("Build-Depends"),
|
||||
Some(
|
||||
"debhelper-compat (= 13),\ndh-python,\npython3-all,\n\
|
||||
pybuild-plugin-pyproject,\npython3-setuptools"
|
||||
)
|
||||
);
|
||||
let rules = std::fs::read_to_string(tree.join("debian/rules")).unwrap();
|
||||
assert!(rules.contains("%:\n\tdh $@ --with python3 --buildsystem=pybuild\n"));
|
||||
|
||||
let tarball = dir.path().join("mytool_0.1.0.orig.tar.xz");
|
||||
let mut archive = tar::Archive::new(xz2::read::XzDecoder::new(
|
||||
std::fs::File::open(&tarball).unwrap(),
|
||||
));
|
||||
let names: Vec<String> = archive
|
||||
.entries()
|
||||
.unwrap()
|
||||
.map(|entry| {
|
||||
entry
|
||||
.unwrap()
|
||||
.path()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
names.iter().any(|n| n == "mytool-0.1.0/pyproject.toml"),
|
||||
"{names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.iter().any(|n| n == "mytool-0.1.0/mytool/__init__.py"),
|
||||
"{names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// End-to-end: the scaffolded shell tree passes the real source build
|
||||
/// (`dpkg-source` and friends, same prerequisites as the differential
|
||||
/// tests).
|
||||
|
||||
Reference in New Issue
Block a user