new: surface cargo vendor failures and pin the vendoring toolchain
This commit is contained in:
+5
-3
@@ -287,11 +287,13 @@ fn main() {
|
|||||||
// resolves through the same pipeline; without a TTY the resolve
|
// resolves through the same pipeline; without a TTY the resolve
|
||||||
// error lists every missing answer. Afterwards the two
|
// error lists every missing answer. Afterwards the two
|
||||||
// verification builds are offered (`--no-verify` skips them;
|
// verification builds are offered (`--no-verify` skips them;
|
||||||
// the structural self-checks inside `scaffold` always run).
|
// the structural self-checks inside `scaffold` always run), with
|
||||||
|
// the scaffold outcome (e.g. a failed vendoring) shaping the
|
||||||
|
// offer.
|
||||||
if let Err(e) = rt.block_on(async {
|
if let Err(e) = rt.block_on(async {
|
||||||
let opts = pkh::new::questions::run(cli).await?;
|
let opts = pkh::new::questions::run(cli).await?;
|
||||||
pkh::new::scaffold(opts.clone(), &multi)?;
|
let outcome = pkh::new::scaffold(opts.clone(), &multi)?;
|
||||||
pkh::new::questions::offer_verification(&opts, &multi, no_verify).await;
|
pkh::new::questions::offer_verification(&opts, &outcome, &multi, no_verify).await;
|
||||||
Ok::<(), Box<dyn std::error::Error>>(())
|
Ok::<(), Box<dyn std::error::Error>>(())
|
||||||
}) {
|
}) {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
|
|||||||
+43
-20
@@ -24,7 +24,7 @@ use std::time::Duration;
|
|||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
|
|
||||||
use options::NewOptions;
|
use options::NewOptions;
|
||||||
use templates::OutputFile;
|
use templates::{OutputFile, ScaffoldOutcome};
|
||||||
|
|
||||||
/// Scaffold a full Debian source tree from `opts`.
|
/// Scaffold a full Debian source tree from `opts`.
|
||||||
///
|
///
|
||||||
@@ -40,7 +40,14 @@ use templates::OutputFile;
|
|||||||
/// 7. `git init` unless `--no-git` or already inside a repository,
|
/// 7. `git init` unless `--no-git` or already inside a repository,
|
||||||
/// 8. run the structural verification,
|
/// 8. run the structural verification,
|
||||||
/// 9. print the success message with the next steps.
|
/// 9. print the success message with the next steps.
|
||||||
pub fn scaffold(opts: NewOptions, multi: &MultiProgress) -> Result<(), Box<dyn Error>> {
|
///
|
||||||
|
/// On success the [`ScaffoldOutcome`] of the post-write hook is returned, so
|
||||||
|
/// the caller can adapt the end of the flow (e.g. the verification offer)
|
||||||
|
/// to what the templates managed to do.
|
||||||
|
pub fn scaffold(
|
||||||
|
opts: NewOptions,
|
||||||
|
multi: &MultiProgress,
|
||||||
|
) -> Result<ScaffoldOutcome, Box<dyn Error>> {
|
||||||
let pb = multi.add(ProgressBar::new_spinner());
|
let pb = multi.add(ProgressBar::new_spinner());
|
||||||
pb.enable_steady_tick(Duration::from_millis(50));
|
pb.enable_steady_tick(Duration::from_millis(50));
|
||||||
pb.set_style(
|
pb.set_style(
|
||||||
@@ -65,7 +72,7 @@ pub fn scaffold(opts: NewOptions, multi: &MultiProgress) -> Result<(), Box<dyn E
|
|||||||
|
|
||||||
/// The scaffold steps proper, reporting progress through `pb`. Nothing is
|
/// The scaffold steps proper, reporting progress through `pb`. Nothing is
|
||||||
/// written to the filesystem before every file rendered successfully.
|
/// written to the filesystem before every file rendered successfully.
|
||||||
fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<(), Box<dyn Error>> {
|
fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<ScaffoldOutcome, Box<dyn Error>> {
|
||||||
// 1. Template resolution: an id without a registered template fails
|
// 1. Template resolution: an id without a registered template fails
|
||||||
// here with the friendly message instead of a parse error.
|
// here with the friendly message instead of a parse error.
|
||||||
let template = templates::get(opts.template).ok_or_else(|| {
|
let template = templates::get(opts.template).ok_or_else(|| {
|
||||||
@@ -175,9 +182,10 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<(), Box<dyn Err
|
|||||||
|
|
||||||
// 5. Template post-write hook: run before the orig tarball is created,
|
// 5. Template post-write hook: run before the orig tarball is created,
|
||||||
// so files added here (rust: vendor/ + .cargo/config.toml) land
|
// so files added here (rust: vendor/ + .cargo/config.toml) land
|
||||||
// inside it.
|
// inside it. The outcome (e.g. a failed vendoring) is threaded back
|
||||||
|
// to the caller.
|
||||||
pb.set_message("Running template hooks");
|
pb.set_message("Running template hooks");
|
||||||
template.post_write(opts, &target)?;
|
let outcome = template.post_write(opts, &target)?;
|
||||||
|
|
||||||
// 6. Orig tarball (quilt only).
|
// 6. Orig tarball (quilt only).
|
||||||
if !opts.native {
|
if !opts.native {
|
||||||
@@ -193,7 +201,7 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<(), Box<dyn Err
|
|||||||
pb.set_message("Verifying");
|
pb.set_message("Verifying");
|
||||||
verify::verify(&target)?;
|
verify::verify(&target)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The success message: what was created and the next steps.
|
/// The success message: what was created and the next steps.
|
||||||
@@ -201,13 +209,16 @@ fn print_success(opts: &NewOptions) {
|
|||||||
let target =
|
let target =
|
||||||
opts.target_dir(&std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")));
|
opts.target_dir(&std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")));
|
||||||
// `display_path` yields an empty string when the target is the cwd
|
// `display_path` yields an empty string when the target is the cwd
|
||||||
// itself (Here mode): show it as `.`.
|
// itself (Here mode): `Created .` would be cryptic, so spell the
|
||||||
let display = match crate::ui::display_path(&target) {
|
// location out; the skeleton/path modes keep the `<dir>` display.
|
||||||
display if display.is_empty() => ".".to_string(),
|
let display = crate::ui::display_path(&target);
|
||||||
display => display,
|
let location = if display.is_empty() {
|
||||||
|
"package in the current directory".to_string()
|
||||||
|
} else {
|
||||||
|
display.clone()
|
||||||
};
|
};
|
||||||
log::info!(
|
log::info!(
|
||||||
"Created {display} — {} ({}-{}) for {}/{}, template '{}'",
|
"Created {location} — {} ({}-{}) for {}/{}, template '{}'",
|
||||||
opts.name,
|
opts.name,
|
||||||
opts.upstream_version,
|
opts.upstream_version,
|
||||||
opts.revision,
|
opts.revision,
|
||||||
@@ -216,7 +227,7 @@ fn print_success(opts: &NewOptions) {
|
|||||||
opts.template
|
opts.template
|
||||||
);
|
);
|
||||||
log::info!("Next steps:");
|
log::info!("Next steps:");
|
||||||
log::info!(" cd {display}");
|
log::info!(" cd {}", if display.is_empty() { "." } else { &display });
|
||||||
if opts.release {
|
if opts.release {
|
||||||
log::info!(
|
log::info!(
|
||||||
" pkh chlog # for later changes; the entry already targets {}",
|
" pkh chlog # for later changes; the entry already targets {}",
|
||||||
@@ -264,7 +275,10 @@ mod tests {
|
|||||||
|
|
||||||
/// Run `scaffold` with the cwd changed to `dir` (restored afterwards);
|
/// Run `scaffold` with the cwd changed to `dir` (restored afterwards);
|
||||||
/// must run under `#[serial]` because the cwd is process-global.
|
/// must run under `#[serial]` because the cwd is process-global.
|
||||||
fn scaffold_in(dir: &std::path::Path, opts: NewOptions) -> Result<(), Box<dyn Error>> {
|
fn scaffold_in(
|
||||||
|
dir: &std::path::Path,
|
||||||
|
opts: NewOptions,
|
||||||
|
) -> Result<ScaffoldOutcome, Box<dyn Error>> {
|
||||||
let previous = std::env::current_dir()?;
|
let previous = std::env::current_dir()?;
|
||||||
std::env::set_current_dir(dir)?;
|
std::env::set_current_dir(dir)?;
|
||||||
let result = scaffold(opts, &MultiProgress::new());
|
let result = scaffold(opts, &MultiProgress::new());
|
||||||
@@ -558,28 +572,38 @@ mod tests {
|
|||||||
/// End-to-end rust skeleton: the vendoring hook runs before the orig
|
/// End-to-end rust skeleton: the vendoring hook runs before the orig
|
||||||
/// tarball is created, so `.cargo/` (and `vendor/` when dependencies
|
/// tarball is created, so `.cargo/` (and `vendor/` when dependencies
|
||||||
/// exist) travel inside it. The vendoring step needs host cargo; on a
|
/// exist) travel inside it. The vendoring step needs host cargo; on a
|
||||||
/// cargo-less host the scaffold still succeeds with a warning.
|
/// cargo-less host the scaffold still succeeds with a warning and a
|
||||||
|
/// `vendoring_failed` outcome. Keyed against the `RUSTUP_TOOLCHAIN`
|
||||||
|
/// tests of the rust template: they mutate the process-global
|
||||||
|
/// environment the cargo shim would pick up mid-vendoring.
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
|
#[serial(RUSTUP_TOOLCHAIN)]
|
||||||
fn scaffold_rust_skeleton_vendors_before_tarball() {
|
fn scaffold_rust_skeleton_vendors_before_tarball() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
scaffold_in(
|
let outcome = scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Rust, "mytool", SourceDir::Skeleton),
|
opts(TemplateId::Rust, "mytool", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let has_cargo = crate::new::templates::find_on_path("cargo").is_some();
|
||||||
|
assert_eq!(outcome.vendoring_failed, !has_cargo);
|
||||||
|
|
||||||
let tree = dir.path().join("mytool");
|
let tree = dir.path().join("mytool");
|
||||||
assert!(tree.join("Cargo.toml").exists());
|
assert!(tree.join("Cargo.toml").exists());
|
||||||
assert!(tree.join("src/main.rs").exists());
|
assert!(tree.join("src/main.rs").exists());
|
||||||
|
// The vendoring step is what creates Cargo.lock for a skeleton.
|
||||||
|
assert_eq!(tree.join("Cargo.lock").exists(), has_cargo);
|
||||||
|
|
||||||
// rules: the vendored build overrides, no --locked on a fresh
|
// rules: the vendored build overrides, and `--locked` exactly when
|
||||||
// skeleton without Cargo.lock.
|
// the vendoring step left a lockfile behind (it appears after the
|
||||||
|
// rules were rendered, so the hook patches it in).
|
||||||
let rules = std::fs::read_to_string(tree.join("debian/rules")).unwrap();
|
let rules = std::fs::read_to_string(tree.join("debian/rules")).unwrap();
|
||||||
assert!(rules.contains("%:\n\tdh $@\n"));
|
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_build:\n\tcargo build --release --offline"));
|
||||||
assert!(rules.contains("override_dh_auto_install:\n\tinstall -Dm755 target/release/mytool debian/mytool/usr/bin/mytool"));
|
assert!(rules.contains("override_dh_auto_install:\n\tinstall -Dm755 target/release/mytool debian/mytool/usr/bin/mytool"));
|
||||||
assert!(!rules.contains("--locked"));
|
assert_eq!(rules.contains("--locked"), has_cargo);
|
||||||
|
|
||||||
// control: Architecture any + the cargo/rustc build-deps.
|
// control: Architecture any + the cargo/rustc build-deps.
|
||||||
let control = crate::debian::ControlInfo::parse(&tree.join("debian/control")).unwrap();
|
let control = crate::debian::ControlInfo::parse(&tree.join("debian/control")).unwrap();
|
||||||
@@ -591,7 +615,6 @@ mod tests {
|
|||||||
|
|
||||||
// The offline config exists when host cargo vendored the skeleton,
|
// The offline config exists when host cargo vendored the skeleton,
|
||||||
// and both it and the skeleton land inside the orig tarball.
|
// 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 {
|
if has_cargo {
|
||||||
let config = std::fs::read_to_string(tree.join(".cargo/config.toml")).unwrap();
|
let config = std::fs::read_to_string(tree.join(".cargo/config.toml")).unwrap();
|
||||||
assert!(config.contains("[source.crates-io]"), "{config}");
|
assert!(config.contains("[source.crates-io]"), "{config}");
|
||||||
|
|||||||
+55
-8
@@ -24,7 +24,7 @@ use indicatif::MultiProgress;
|
|||||||
|
|
||||||
use crate::new::detect::{self, Detection};
|
use crate::new::detect::{self, Detection};
|
||||||
use crate::new::options::{self, NewCli, NewOptions, SourceDir, TemplateId};
|
use crate::new::options::{self, NewCli, NewOptions, SourceDir, TemplateId};
|
||||||
use crate::new::templates::{self, ProbeResult};
|
use crate::new::templates::{self, ProbeResult, ScaffoldOutcome};
|
||||||
use crate::ui::prompt;
|
use crate::ui::prompt;
|
||||||
|
|
||||||
/// Answer of the "where is the source code?" question: fresh skeleton.
|
/// Answer of the "where is the source code?" question: fresh skeleton.
|
||||||
@@ -411,10 +411,17 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
/// (`pkh build`, offered yes) and the binary build (`pkh deb`, offered no —
|
/// (`pkh build`, offered yes) and the binary build (`pkh deb`, offered no —
|
||||||
/// it needs network + build deps). A failed verification build never undoes
|
/// it needs network + build deps). A failed verification build never undoes
|
||||||
/// the scaffold: the error is printed together with the manual next steps.
|
/// the scaffold: the error is printed together with the manual next steps.
|
||||||
pub async fn offer_verification(opts: &NewOptions, multi: &MultiProgress, no_verify: bool) {
|
///
|
||||||
if no_verify || !is_interactive() {
|
/// When the scaffold's vendoring step failed (`outcome`), a prominent notice
|
||||||
return;
|
/// states that the tree will not build until the dependencies are vendored —
|
||||||
}
|
/// printed with or without a TTY — and the build offer is reworded with its
|
||||||
|
/// default flipped to *no*.
|
||||||
|
pub async fn offer_verification(
|
||||||
|
opts: &NewOptions,
|
||||||
|
outcome: &ScaffoldOutcome,
|
||||||
|
multi: &MultiProgress,
|
||||||
|
no_verify: bool,
|
||||||
|
) {
|
||||||
let tree = opts.target_dir(&std::env::current_dir().unwrap_or_default());
|
let tree = opts.target_dir(&std::env::current_dir().unwrap_or_default());
|
||||||
let display = crate::ui::display_path(&tree);
|
let display = crate::ui::display_path(&tree);
|
||||||
let display = if display.is_empty() {
|
let display = if display.is_empty() {
|
||||||
@@ -423,7 +430,30 @@ pub async fn offer_verification(opts: &NewOptions, multi: &MultiProgress, no_ver
|
|||||||
display
|
display
|
||||||
};
|
};
|
||||||
|
|
||||||
let verify_source = match prompt::confirm("Verify with `pkh build` now?", true) {
|
if outcome.vendoring_failed {
|
||||||
|
// Set apart from the surrounding success output by blank lines: a
|
||||||
|
// single warning between two success lines is easy to miss.
|
||||||
|
println!();
|
||||||
|
log::warn!(
|
||||||
|
"The Cargo dependencies could NOT be vendored: this package will \
|
||||||
|
not build until the vendoring is completed by hand:\n\
|
||||||
|
\x20 1. `cd {display} && cargo vendor`\n\
|
||||||
|
\x20 2. add the printed source replacement to .cargo/config.toml, \
|
||||||
|
plus `[net] offline = true`"
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if no_verify || !is_interactive() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let build_offer = if outcome.vendoring_failed {
|
||||||
|
"Verify with `pkh build` now? (it will fail until dependencies are vendored)"
|
||||||
|
} else {
|
||||||
|
"Verify with `pkh build` now?"
|
||||||
|
};
|
||||||
|
let verify_source = match prompt::confirm(build_offer, !outcome.vendoring_failed) {
|
||||||
Ok(answer) => answer,
|
Ok(answer) => answer,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
@@ -720,8 +750,12 @@ pub fn summary_text(opts: &NewOptions) -> String {
|
|||||||
lines.push(format!(" Depends {}", opts.depends.join(", ")));
|
lines.push(format!(" Depends {}", opts.depends.join(", ")));
|
||||||
}
|
}
|
||||||
} else if opts.template == TemplateId::Rust {
|
} else if opts.template == TemplateId::Rust {
|
||||||
lines
|
// Nothing is vendored yet at this point: only announce that the
|
||||||
.push(" debian/rules cargo build --release --offline (vendored)".to_string());
|
// generation will attempt it.
|
||||||
|
lines.push(
|
||||||
|
" debian/rules cargo build --release --offline (vendored at generation)"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
lines.push(format!(" debian/rules {}", template.rules_dh_line()));
|
lines.push(format!(" debian/rules {}", template.rules_dh_line()));
|
||||||
}
|
}
|
||||||
@@ -948,6 +982,19 @@ mod tests {
|
|||||||
assert!(text.contains("debian/watch release watcher"), "{text}");
|
assert!(text.contains("debian/watch release watcher"), "{text}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The rust summary only announces the vendoring attempt of the
|
||||||
|
/// generation, it must not assert an outcome that has not been tried
|
||||||
|
/// yet (regression: it claimed "(vendored)" before generating).
|
||||||
|
#[test]
|
||||||
|
fn summary_screen_rust_does_not_presume_vendoring() {
|
||||||
|
let text = summary_text(&opts(Tid::Rust));
|
||||||
|
assert!(
|
||||||
|
text.contains("cargo build --release --offline (vendored at generation)"),
|
||||||
|
"{text}"
|
||||||
|
);
|
||||||
|
assert!(!text.contains("(vendored)"), "{text}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn answer_validators() {
|
fn answer_validators() {
|
||||||
assert!(validate_revision_answer("1").is_ok());
|
assert!(validate_revision_answer("1").is_ok());
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ impl OutputFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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, Copy, 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,
|
||||||
|
}
|
||||||
|
|
||||||
/// Metadata extracted from an existing project by [`Template::probe`], used
|
/// Metadata extracted from an existing project by [`Template::probe`], used
|
||||||
/// by the interactive wizard to pre-fill its answers (explicit flags always
|
/// by the interactive wizard to pre-fill its answers (explicit flags always
|
||||||
/// win). Every field is optional; probe failures are silent and the generic
|
/// win). Every field is optional; probe failures are silent and the generic
|
||||||
@@ -126,13 +137,16 @@ pub trait Template: Sync {
|
|||||||
/// Hook run after the generated files have been written to `tree` and
|
/// Hook run after the generated files have been written to `tree` and
|
||||||
/// before the orig tarball is created, for templates that need to run
|
/// before the orig tarball is created, for templates that need to run
|
||||||
/// host tooling over the freshly written tree (e.g. `cargo vendor`, so
|
/// host tooling over the freshly written tree (e.g. `cargo vendor`, so
|
||||||
/// the vendored sources land inside the tarball).
|
/// 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(
|
fn post_write(
|
||||||
&self,
|
&self,
|
||||||
_opts: &NewOptions,
|
_opts: &NewOptions,
|
||||||
_tree: &Path,
|
_tree: &Path,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<ScaffoldOutcome, Box<dyn std::error::Error>> {
|
||||||
Ok(())
|
Ok(ScaffoldOutcome::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+240
-22
@@ -6,14 +6,15 @@
|
|||||||
//! runs over the freshly written tree (before the orig tarball is created, so
|
//! runs over the freshly written tree (before the orig tarball is created, so
|
||||||
//! `vendor/` travels inside it), and `debian/rules` builds offline with the
|
//! `vendor/` travels inside it), and `debian/rules` builds offline with the
|
||||||
//! source replacement. When host `cargo` is missing or vendoring fails, the
|
//! source replacement. When host `cargo` is missing or vendoring fails, the
|
||||||
//! scaffold continues with a loud warning — the package will not build until
|
//! scaffold continues with a loud warning and reports
|
||||||
//! the user vendors manually.
|
//! [`ScaffoldOutcome::vendoring_failed`] to the flow — the package will not
|
||||||
|
//! build until the user vendors manually.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{OutputFile, ProbeResult, Template, find_on_path, source_dir_of};
|
use super::{OutputFile, ProbeResult, ScaffoldOutcome, Template, find_on_path, source_dir_of};
|
||||||
use crate::new::options::{NewOptions, SourceDir, TemplateId};
|
use crate::new::options::{NewOptions, SourceDir, TemplateId};
|
||||||
|
|
||||||
/// Rust project (`Cargo.toml`).
|
/// Rust project (`Cargo.toml`).
|
||||||
@@ -80,9 +81,10 @@ impl Template for Rust {
|
|||||||
|
|
||||||
/// The vendored build overrides. `--locked` is used only when the
|
/// The vendored build overrides. `--locked` is used only when the
|
||||||
/// packaged tree already carries a `Cargo.lock` (fresh skeletons have
|
/// packaged tree already carries a `Cargo.lock` (fresh skeletons have
|
||||||
/// none yet); omitting it is always safe. The built artifact of a
|
/// none yet — the vendoring hook patches the flag in once `cargo vendor`
|
||||||
/// skeleton is named after its crate (a sanitized package name) and
|
/// created it, see [`patch_rules_locked`]); omitting it is always safe.
|
||||||
/// installed under the command name.
|
/// The built artifact of a skeleton is named after its crate (a
|
||||||
|
/// sanitized package name) and installed under the command name.
|
||||||
fn rules_extra(&self, opts: &NewOptions) -> String {
|
fn rules_extra(&self, opts: &NewOptions) -> String {
|
||||||
let locked = if lockfile_present(opts) {
|
let locked = if lockfile_present(opts) {
|
||||||
" --locked"
|
" --locked"
|
||||||
@@ -127,16 +129,35 @@ impl Template for Rust {
|
|||||||
/// Vendor the Cargo dependencies into the freshly written tree: run
|
/// Vendor the Cargo dependencies into the freshly written tree: run
|
||||||
/// `cargo vendor` in it and write `.cargo/config.toml` with the printed
|
/// `cargo vendor` in it and write `.cargo/config.toml` with the printed
|
||||||
/// source replacement plus `offline = true`, so the build never touches
|
/// source replacement plus `offline = true`, so the build never touches
|
||||||
/// the network. Failures warn loudly and continue: the scaffold stays in
|
/// the network. Failures warn loudly and come back as
|
||||||
/// place, the package just will not build until vendored manually.
|
/// [`ScaffoldOutcome::vendoring_failed`] instead of failing the scaffold:
|
||||||
|
/// the tree stays in place, the package just will not build until the
|
||||||
|
/// vendoring is completed manually.
|
||||||
fn post_write(
|
fn post_write(
|
||||||
&self,
|
&self,
|
||||||
_opts: &NewOptions,
|
_opts: &NewOptions,
|
||||||
tree: &Path,
|
tree: &Path,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<ScaffoldOutcome, Box<dyn std::error::Error>> {
|
||||||
if !tree.join("Cargo.toml").exists() {
|
if !tree.join("Cargo.toml").exists() {
|
||||||
return Ok(());
|
return Ok(ScaffoldOutcome::default());
|
||||||
}
|
}
|
||||||
|
// `vendor_dependencies` reports whether the step completed; the
|
||||||
|
// outcome carries the flipped, failure-shaped flag.
|
||||||
|
let vendoring_failed = !vendor_dependencies(tree)?;
|
||||||
|
// The vendoring step is what creates `Cargo.lock` for a fresh
|
||||||
|
// skeleton, but `debian/rules` was rendered before it ran: add
|
||||||
|
// `--locked` now that the outcome is known, so the final rules use
|
||||||
|
// it exactly when the lockfile exists.
|
||||||
|
patch_rules_locked(tree)?;
|
||||||
|
Ok(ScaffoldOutcome { vendoring_failed })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The vendoring step proper: run `cargo vendor` over `tree` and write the
|
||||||
|
/// offline source replacement. Returns whether the step completed; failures
|
||||||
|
/// warn loudly and leave the tree for manual vendoring. I/O errors on the
|
||||||
|
/// freshly written tree are the exception: they fail the scaffold.
|
||||||
|
fn vendor_dependencies(tree: &Path) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
let Some(cargo) = find_on_path("cargo") else {
|
let Some(cargo) = find_on_path("cargo") else {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"cargo was not found on PATH: the Rust package will NOT build \
|
"cargo was not found on PATH: the Rust package will NOT build \
|
||||||
@@ -144,15 +165,21 @@ impl Template for Rust {
|
|||||||
tree and add the printed source replacement to \
|
tree and add the printed source replacement to \
|
||||||
.cargo/config.toml (with `[net] offline = true`)."
|
.cargo/config.toml (with `[net] offline = true`)."
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("Vendoring Cargo dependencies (`cargo vendor`) — needs one network sync");
|
log::info!("Vendoring Cargo dependencies (`cargo vendor`) — needs one network sync");
|
||||||
match std::process::Command::new(&cargo)
|
let mut command = std::process::Command::new(&cargo);
|
||||||
.arg("vendor")
|
command.arg("vendor").current_dir(tree);
|
||||||
.current_dir(tree)
|
// Pin the run to the host's *default* rustup toolchain: `cargo` usually
|
||||||
.output()
|
// is a rustup shim, so a `rust-toolchain.toml` pin of the scaffolded
|
||||||
{
|
// project would otherwise download that toolchain (or fail on broken
|
||||||
|
// rustup state) mid-scaffold. Vendoring only fetches crate sources and
|
||||||
|
// is toolchain-insensitive.
|
||||||
|
if let Some(toolchain) = vendoring_toolchain() {
|
||||||
|
command.env("RUSTUP_TOOLCHAIN", toolchain);
|
||||||
|
}
|
||||||
|
match command.output() {
|
||||||
Ok(output) if output.status.success() => {
|
Ok(output) if output.status.success() => {
|
||||||
let printed = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let printed = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
let snippet = if printed.contains("[source.") {
|
let snippet = if printed.contains("[source.") {
|
||||||
@@ -168,7 +195,7 @@ impl Template for Rust {
|
|||||||
manually (plus `[net] offline = true`).",
|
manually (plus `[net] offline = true`).",
|
||||||
config_path.display()
|
config_path.display()
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(false);
|
||||||
}
|
}
|
||||||
std::fs::create_dir_all(config_path.parent().unwrap_or(tree))?;
|
std::fs::create_dir_all(config_path.parent().unwrap_or(tree))?;
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
@@ -180,6 +207,7 @@ impl Template for Rust {
|
|||||||
fully offline",
|
fully offline",
|
||||||
config_path.display()
|
config_path.display()
|
||||||
);
|
);
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@@ -193,6 +221,7 @@ impl Template for Rust {
|
|||||||
.last()
|
.last()
|
||||||
.unwrap_or("(no output)")
|
.unwrap_or("(no output)")
|
||||||
);
|
);
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@@ -201,10 +230,88 @@ impl Template for Rust {
|
|||||||
`cargo vendor` in the tree and add the printed source \
|
`cargo vendor` in the tree and add the printed source \
|
||||||
replacement to .cargo/config.toml."
|
replacement to .cargo/config.toml."
|
||||||
);
|
);
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `RUSTUP_TOOLCHAIN` value for the vendoring run: the host's *default*
|
||||||
|
/// rustup toolchain. `None` — run with the inherited environment — when
|
||||||
|
/// rustup is not on `PATH`, is broken, prints nothing parseable, or when the
|
||||||
|
/// user already set `RUSTUP_TOOLCHAIN` (never overridden).
|
||||||
|
fn vendoring_toolchain() -> Option<String> {
|
||||||
|
if std::env::var_os("RUSTUP_TOOLCHAIN").is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rustup = find_on_path("rustup")?;
|
||||||
|
let output = std::process::Command::new(rustup)
|
||||||
|
.arg("default")
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
parse_default_toolchain(&String::from_utf8_lossy(&output.stdout))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the default toolchain name out of `rustup default` output, loosely:
|
||||||
|
/// the first token after `default toolchain:` on its line (older rustup,
|
||||||
|
/// e.g. `default toolchain: stable (override)`), or the token before
|
||||||
|
/// `(default)` (e.g. `stable-x86_64-unknown-linux-gnu (default)`).
|
||||||
|
fn parse_default_toolchain(output: &str) -> Option<String> {
|
||||||
|
for line in output.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
let name = if let Some((_, rest)) = line.split_once("default toolchain:") {
|
||||||
|
rest.split_whitespace().next()
|
||||||
|
} else if let Some((name, _)) = line.split_once(" (default)") {
|
||||||
|
Some(name.trim())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(name) = name.filter(|name| !name.is_empty()) {
|
||||||
|
return Some(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add `--locked` to the offline cargo commands of the rendered rules when
|
||||||
|
/// the tree carries a `Cargo.lock` they were not rendered for: a fresh
|
||||||
|
/// skeleton renders `debian/rules` before the vendoring step, and it is
|
||||||
|
/// exactly that step which creates the lockfile. No-op without a lockfile,
|
||||||
|
/// without rules to patch, or when the rules already carry the flag.
|
||||||
|
fn patch_rules_locked(tree: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if !tree.join("Cargo.lock").exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let path = tree.join("debian/rules");
|
||||||
|
let Ok(rules) = std::fs::read_to_string(&path) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Some(patched) = with_locked(&rules) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
std::fs::write(&path, patched)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `rules` contents with `--locked` added to the `cargo build`/`cargo test`
|
||||||
|
/// offline commands; `None` when there is nothing to patch (the flag is
|
||||||
|
/// already there, e.g. because the lockfile existed at render time).
|
||||||
|
fn with_locked(rules: &str) -> Option<String> {
|
||||||
|
if rules.contains("--locked") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let patched = rules
|
||||||
|
.replace(
|
||||||
|
"\tcargo build --release --offline\n",
|
||||||
|
"\tcargo build --release --offline --locked\n",
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"\tcargo test --release --offline\n",
|
||||||
|
"\tcargo test --release --offline --locked\n",
|
||||||
|
);
|
||||||
|
(patched != rules).then_some(patched)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the packaged tree carries a `Cargo.lock` (skeletons do not yet).
|
/// Whether the packaged tree carries a `Cargo.lock` (skeletons do not yet).
|
||||||
@@ -298,6 +405,7 @@ fn probe_cargo_toml(dir: &Path) -> Option<ProbeResult> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::new::options::{License, SourceDir};
|
use crate::new::options::{License, SourceDir};
|
||||||
|
use serial_test::serial;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts(source_dir: SourceDir) -> NewOptions {
|
fn opts(source_dir: SourceDir) -> NewOptions {
|
||||||
@@ -466,9 +574,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The vendoring hook over a zero-dependency skeleton: offline config
|
/// The vendoring hook over a zero-dependency skeleton: offline config
|
||||||
/// written, no failure (needs host cargo; without it the warning path
|
/// written, outcome reports success (needs host cargo; without it the
|
||||||
/// keeps the tree intact).
|
/// warning path keeps the tree intact and reports the failure). Keyed
|
||||||
|
/// against `vendoring_respects_the_users_toolchain`, which mutates the
|
||||||
|
/// process-global `RUSTUP_TOOLCHAIN` the cargo shim would pick up.
|
||||||
#[test]
|
#[test]
|
||||||
|
#[serial(RUSTUP_TOOLCHAIN)]
|
||||||
fn post_write_vendors_skeleton() {
|
fn post_write_vendors_skeleton() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let o = opts(SourceDir::Skeleton);
|
let o = opts(SourceDir::Skeleton);
|
||||||
@@ -479,7 +590,8 @@ mod tests {
|
|||||||
std::fs::write(path, file.contents).unwrap();
|
std::fs::write(path, file.contents).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
template.post_write(&o, dir.path()).unwrap();
|
let outcome = template.post_write(&o, dir.path()).unwrap();
|
||||||
|
assert_eq!(outcome.vendoring_failed, find_on_path("cargo").is_none());
|
||||||
|
|
||||||
if find_on_path("cargo").is_some() {
|
if find_on_path("cargo").is_some() {
|
||||||
let config = std::fs::read_to_string(dir.path().join(".cargo/config.toml")).unwrap();
|
let config = std::fs::read_to_string(dir.path().join(".cargo/config.toml")).unwrap();
|
||||||
@@ -487,12 +599,118 @@ mod tests {
|
|||||||
assert!(config.contains("replace-with = \"vendored-sources\""));
|
assert!(config.contains("replace-with = \"vendored-sources\""));
|
||||||
assert!(config.contains("[net]\noffline = true"), "{config}");
|
assert!(config.contains("[net]\noffline = true"), "{config}");
|
||||||
}
|
}
|
||||||
// An existing .cargo/config.toml is never overwritten.
|
// An existing .cargo/config.toml is never overwritten (and the
|
||||||
|
// unwriteable config counts as an incomplete vendoring).
|
||||||
let existing = dir.path().join(".cargo/config.toml");
|
let existing = dir.path().join(".cargo/config.toml");
|
||||||
if existing.exists() {
|
if existing.exists() {
|
||||||
template.post_write(&o, dir.path()).unwrap();
|
let outcome = template.post_write(&o, dir.path()).unwrap();
|
||||||
let config = std::fs::read_to_string(&existing).unwrap();
|
let config = std::fs::read_to_string(&existing).unwrap();
|
||||||
assert!(config.contains("[source.crates-io]"));
|
assert!(config.contains("[source.crates-io]"));
|
||||||
|
assert_eq!(outcome.vendoring_failed, find_on_path("cargo").is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The vendored cargo commands gain `--locked` only when the rules do
|
||||||
|
/// not carry it yet (i.e. no lockfile existed at render time).
|
||||||
|
#[test]
|
||||||
|
fn with_locked_patches_only_unlocked_rules() {
|
||||||
|
let unlocked = "#!/usr/bin/make -f\n%:\n\tdh $@\n\n\
|
||||||
|
override_dh_auto_build:\n\
|
||||||
|
\tcargo build --release --offline\n\
|
||||||
|
\n\
|
||||||
|
override_dh_auto_test:\n\
|
||||||
|
\tcargo test --release --offline\n";
|
||||||
|
let patched = with_locked(unlocked).expect("unlockable rules");
|
||||||
|
assert!(
|
||||||
|
patched.contains("\tcargo build --release --offline --locked\n"),
|
||||||
|
"{patched}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
patched.contains("\tcargo test --release --offline --locked\n"),
|
||||||
|
"{patched}"
|
||||||
|
);
|
||||||
|
// Idempotent: a second pass has nothing left to do.
|
||||||
|
assert!(with_locked(&patched).is_none());
|
||||||
|
// Rules already carrying the flag (lockfile present at render time)
|
||||||
|
// are left alone, and so are rules without cargo commands.
|
||||||
|
assert!(
|
||||||
|
with_locked("override_dh_auto_build:\n\tcargo build --release --offline --locked\n")
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(with_locked("%:\n\tdh $@\n").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The post-vendoring rules patch follows the lockfile: no `Cargo.lock`
|
||||||
|
/// in the tree, no patch; once the vendoring step created it (what it
|
||||||
|
/// does for a fresh skeleton), the offline commands become `--locked`.
|
||||||
|
#[test]
|
||||||
|
fn patch_rules_locked_follows_the_lockfile() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let rules_path = dir.path().join("debian/rules");
|
||||||
|
std::fs::create_dir_all(rules_path.parent().unwrap()).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
&rules_path,
|
||||||
|
"override_dh_auto_build:\n\tcargo build --release --offline\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
patch_rules_locked(dir.path()).unwrap();
|
||||||
|
assert!(
|
||||||
|
!std::fs::read_to_string(&rules_path)
|
||||||
|
.unwrap()
|
||||||
|
.contains("--locked")
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::write(dir.path().join("Cargo.lock"), "").unwrap();
|
||||||
|
patch_rules_locked(dir.path()).unwrap();
|
||||||
|
let rules = std::fs::read_to_string(&rules_path).unwrap();
|
||||||
|
assert!(
|
||||||
|
rules.contains("\tcargo build --release --offline --locked\n"),
|
||||||
|
"{rules}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `rustup default` output parsing: the toolchain name, loosely, from
|
||||||
|
/// both the historical (`default toolchain: …`) and the current
|
||||||
|
/// (`<name> (default)`) spellings; anything else fails soft.
|
||||||
|
#[test]
|
||||||
|
fn parse_default_toolchain_variants() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_default_toolchain("stable-x86_64-unknown-linux-gnu (default)\n"),
|
||||||
|
Some("stable-x86_64-unknown-linux-gnu".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_default_toolchain("default toolchain: stable (override)\n"),
|
||||||
|
Some("stable".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_default_toolchain("default toolchain: 1.75.0-x86_64-unknown-linux-gnu\n"),
|
||||||
|
Some("1.75.0-x86_64-unknown-linux-gnu".to_string())
|
||||||
|
);
|
||||||
|
// The line can sit among unrelated output.
|
||||||
|
assert_eq!(
|
||||||
|
parse_default_toolchain("info: syncing channel updates\nnightly (default)\n"),
|
||||||
|
Some("nightly".to_string())
|
||||||
|
);
|
||||||
|
// Nothing parseable: run without the override then.
|
||||||
|
assert_eq!(parse_default_toolchain(""), None);
|
||||||
|
assert_eq!(
|
||||||
|
parse_default_toolchain("error: no default toolchain configured\n"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `RUSTUP_TOOLCHAIN` set by the user is never overridden: the
|
||||||
|
/// vendoring run keeps the inherited environment.
|
||||||
|
#[test]
|
||||||
|
#[serial(RUSTUP_TOOLCHAIN)]
|
||||||
|
fn vendoring_respects_the_users_toolchain() {
|
||||||
|
let saved = std::env::var_os("RUSTUP_TOOLCHAIN");
|
||||||
|
unsafe { std::env::set_var("RUSTUP_TOOLCHAIN", "nightly-custom") };
|
||||||
|
assert_eq!(vendoring_toolchain(), None);
|
||||||
|
match saved {
|
||||||
|
Some(value) => unsafe { std::env::set_var("RUSTUP_TOOLCHAIN", value) },
|
||||||
|
None => unsafe { std::env::remove_var("RUSTUP_TOOLCHAIN") },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user