new: flag rust-toolchain.toml pins in pkh new

This commit is contained in:
2026-09-16 23:59:16 +02:00
parent 84824f61c6
commit 8e06b2074d
4 changed files with 228 additions and 15 deletions
+52 -11
View File
@@ -160,6 +160,22 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
} }
let template = TemplateId::parse(cli.lang.as_deref().unwrap_or_default())?; let template = TemplateId::parse(cli.lang.as_deref().unwrap_or_default())?;
// The rust toolchain pin of the packaged project does not travel into
// the chroot build: surface it now so a too-old pin is not a surprise
// when `pkh deb` compiles with the distribution's rustc.
let toolchain_pin = if template == TemplateId::Rust {
probe.as_ref().and_then(|p| p.toolchain_pin.clone())
} else {
None
};
if let Some(pin) = &toolchain_pin {
log::info!(
"Project pins rust {pin} via rust-toolchain.toml; the chroot \
build uses the distribution's rustc and ignores the pin — \
adjust or remove it if the code needs newer compiler features"
);
}
// 3. Source location. Skipped (with the inline notice) when a confident // 3. Source location. Skipped (with the inline notice) when a confident
// detection already decided to package the current directory; a // detection already decided to package the current directory; a
// --source flag skips it too. // --source flag skips it too.
@@ -398,7 +414,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
// Summary screen + final confirmation: Ctrl+C or 'n' abort with // Summary screen + final confirmation: Ctrl+C or 'n' abort with
// nothing written (generation is all-or-nothing later anyway). // nothing written (generation is all-or-nothing later anyway).
println!("{}", summary_text(&opts)); println!("{}", summary_text(&opts, toolchain_pin.as_deref()));
if !prompt::confirm("Generate?", true)? { if !prompt::confirm("Generate?", true)? {
return Err("Aborted: nothing was written to disk.".into()); return Err("Aborted: nothing was written to disk.".into());
} }
@@ -714,12 +730,13 @@ pub fn watch_template(homepage: Option<&str>) -> Option<String> {
)) ))
} }
/// The summary screen shown before the final `Generate?` confirmation /// The pre-flight summary screen shown before the final `Generate?`
/// (spec transcript): identity line, template/license/maintainer line, the /// confirmation (spec transcript): identity line, template/license/maintainer
/// generated-file overview and — for a metapackage — the Depends payload, /// line, the generated-file overview and — for a metapackage — the Depends
/// for a skeleton — the upstream files that will be created, and for rust — /// payload, for a skeleton — the upstream files that will be created, and
/// a warning when dependencies cannot be vendored on this host. /// for rust — the probed toolchain pin (when any; the chroot build ignores
pub fn summary_text(opts: &NewOptions) -> String { /// it) plus a warning when dependencies cannot be vendored on this host.
pub fn summary_text(opts: &NewOptions, toolchain_pin: Option<&str>) -> String {
let template = templates::get(opts.template); let template = templates::get(opts.template);
let mut lines = Vec::new(); let mut lines = Vec::new();
@@ -756,6 +773,13 @@ pub fn summary_text(opts: &NewOptions) -> String {
" debian/rules cargo build --release --offline (vendored at generation)" " debian/rules cargo build --release --offline (vendored at generation)"
.to_string(), .to_string(),
); );
// A pinned rust-toolchain.toml does not reach the chroot build:
// flagged here so a too-old pin is no surprise later.
if let Some(pin) = toolchain_pin {
lines.push(format!(
" rust-toolchain {pin} (ignored by the chroot build)"
));
}
} else { } else {
lines.push(format!(" debian/rules {}", template.rules_dh_line())); lines.push(format!(" debian/rules {}", template.rules_dh_line()));
} }
@@ -923,7 +947,7 @@ mod tests {
#[test] #[test]
fn summary_screen_skeleton() { fn summary_screen_skeleton() {
let text = summary_text(&opts(Tid::Makefile)); let text = summary_text(&opts(Tid::Makefile), None);
assert!( assert!(
text.contains("mytool 0.1.0-1 · builds for ubuntu/resolute"), text.contains("mytool 0.1.0-1 · builds for ubuntu/resolute"),
"{text}" "{text}"
@@ -956,7 +980,7 @@ mod tests {
let mut o = opts(Tid::Empty); let mut o = opts(Tid::Empty);
o.depends = vec!["hello".to_string(), "hello-data (>= 1.0)".to_string()]; o.depends = vec!["hello".to_string(), "hello-data (>= 1.0)".to_string()];
o.source_dir = options::SourceDir::Here; o.source_dir = options::SourceDir::Here;
let text = summary_text(&o); let text = summary_text(&o, None);
assert!(text.contains("Architecture: all"), "{text}"); assert!(text.contains("Architecture: all"), "{text}");
assert!( assert!(
text.contains("Depends hello, hello-data (>= 1.0)"), text.contains("Depends hello, hello-data (>= 1.0)"),
@@ -973,7 +997,7 @@ mod tests {
o.release = true; o.release = true;
o.autopkgtest = true; o.autopkgtest = true;
o.watch = Some("version=4\n".to_string()); o.watch = Some("version=4\n".to_string());
let text = summary_text(&o); let text = summary_text(&o, None);
assert!(text.contains("0.1.0-1 resolute, Initial release"), "{text}"); assert!(text.contains("0.1.0-1 resolute, Initial release"), "{text}");
assert!( assert!(
text.contains("debian/tests autopkgtest smoke test"), text.contains("debian/tests autopkgtest smoke test"),
@@ -987,7 +1011,7 @@ mod tests {
/// yet (regression: it claimed "(vendored)" before generating). /// yet (regression: it claimed "(vendored)" before generating).
#[test] #[test]
fn summary_screen_rust_does_not_presume_vendoring() { fn summary_screen_rust_does_not_presume_vendoring() {
let text = summary_text(&opts(Tid::Rust)); let text = summary_text(&opts(Tid::Rust), None);
assert!( assert!(
text.contains("cargo build --release --offline (vendored at generation)"), text.contains("cargo build --release --offline (vendored at generation)"),
"{text}" "{text}"
@@ -995,6 +1019,23 @@ mod tests {
assert!(!text.contains("(vendored)"), "{text}"); assert!(!text.contains("(vendored)"), "{text}");
} }
/// A probed rust toolchain pin surfaces in the summary as its own row
/// (rust template only), flagged as ignored by the chroot build.
#[test]
fn summary_screen_shows_the_toolchain_pin() {
let text = summary_text(&opts(Tid::Rust), Some("1.98.0"));
assert!(
text.contains("rust-toolchain 1.98.0 (ignored by the chroot build)"),
"{text}"
);
// No pin, no row.
assert!(!summary_text(&opts(Tid::Rust), None).contains("rust-toolchain"));
// A pin under a template other than rust is not shown either (the
// pin only matters for a cargo build).
assert!(!summary_text(&opts(Tid::Go), Some("1.98.0")).contains("rust-toolchain"));
}
#[test] #[test]
fn answer_validators() { fn answer_validators() {
assert!(validate_revision_answer("1").is_ok()); assert!(validate_revision_answer("1").is_ok());
+5
View File
@@ -81,6 +81,11 @@ pub struct ProbeResult {
/// Installed command / binary name (e.g. the first `[[bin]]` target or /// Installed command / binary name (e.g. the first `[[bin]]` target or
/// console script). /// console script).
pub command: Option<String>, 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. /// A package template: one supported ecosystem / build system.
+1
View File
@@ -208,6 +208,7 @@ impl Template for Python {
homepage: project.homepage, homepage: project.homepage,
license: project.license, license: project.license,
command: project.script, command: project.script,
..Default::default()
}; };
if result.name.is_some() if result.name.is_some()
|| result.version.is_some() || result.version.is_some()
+170 -4
View File
@@ -118,12 +118,24 @@ impl Template for Rust {
/// Name, version, description, homepage, license and first binary from /// Name, version, description, homepage, license and first binary from
/// `cargo metadata --no-deps` (when host cargo is available), with a /// `cargo metadata --no-deps` (when host cargo is available), with a
/// minimal line-parse of `Cargo.toml` as fallback. /// minimal line-parse of `Cargo.toml` as fallback; plus the toolchain
/// channel pinned by `rust-toolchain.toml` / legacy `rust-toolchain`.
fn probe(&self, dir: &Path) -> Option<ProbeResult> { fn probe(&self, dir: &Path) -> Option<ProbeResult> {
if let Some(result) = probe_cargo_metadata(dir) { match probe_cargo_metadata(dir).or_else(|| probe_cargo_toml(dir)) {
return Some(result); Some(mut result) => {
result.toolchain_pin = toolchain_pin(dir);
Some(result)
}
// A Cargo.toml that resists reading, but a toolchain pin this
// template can still report.
None => {
let pin = toolchain_pin(dir)?;
Some(ProbeResult {
toolchain_pin: Some(pin),
..Default::default()
})
}
} }
probe_cargo_toml(dir)
} }
/// Vendor the Cargo dependencies into the freshly written tree: run /// Vendor the Cargo dependencies into the freshly written tree: run
@@ -360,6 +372,8 @@ fn probe_cargo_metadata(dir: &Path) -> Option<ProbeResult> {
homepage: field("homepage"), homepage: field("homepage"),
license: field("license"), license: field("license"),
command, command,
// Filled in by `probe` from the rust-toolchain file, not metadata.
toolchain_pin: None,
}) })
} }
@@ -401,6 +415,56 @@ fn probe_cargo_toml(dir: &Path) -> Option<ProbeResult> {
} }
} }
/// The rust toolchain channel pinned in `dir`: `rust-toolchain.toml`
/// (preferred, like rustup does) or the legacy bare `rust-toolchain`.
/// `None` without a pin file or when it names no channel.
fn toolchain_pin(dir: &Path) -> Option<String> {
let content = std::fs::read_to_string(dir.join("rust-toolchain.toml"))
.or_else(|_| std::fs::read_to_string(dir.join("rust-toolchain")))
.ok()?;
parse_toolchain_channel(&content)
}
/// Minimal line-parse of the `channel` out of a `rust-toolchain` file (no
/// TOML dependency, matching the `Cargo.toml` fallback above):
/// `channel = "…"` (single quotes work too) under the `[toolchain]` section,
/// comments and unrelated sections tolerated. Without a parseable TOML
/// channel, a legacy bare `rust-toolchain` — just the channel string on a
/// single line, no TOML syntax at all — is taken whole.
fn parse_toolchain_channel(content: &str) -> Option<String> {
let mut in_toolchain = false;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(header) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
in_toolchain = header.trim() == "toolchain";
continue;
}
if !in_toolchain {
continue;
}
if let Some((key, value)) = line.split_once('=')
&& key.trim() == "channel"
{
let value = value.trim().trim_matches('"').trim_matches('\'').trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
// Legacy bare format: the whole file is the channel name (never
// comment-only).
let bare = content.trim();
(!bare.is_empty()
&& !bare.starts_with('#')
&& !bare.contains('\n')
&& !bare.contains('=')
&& !bare.contains('['))
.then(|| bare.to_string())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -713,4 +777,106 @@ mod tests {
None => unsafe { std::env::remove_var("RUSTUP_TOOLCHAIN") }, None => unsafe { std::env::remove_var("RUSTUP_TOOLCHAIN") },
} }
} }
/// The `rust-toolchain` channel parser: quoted channel under
/// `[toolchain]` (double or single quotes, with comments and unrelated
/// sections around), the legacy bare single-line format taken whole, and
/// the failure cases (missing key, empty channel, wrong section, empty
/// file).
#[test]
fn parse_toolchain_channel_variants() {
// The canonical spelling.
assert_eq!(
parse_toolchain_channel("[toolchain]\nchannel = \"1.98.0\"\n"),
Some("1.98.0".to_string())
);
// Single quotes, no trailing newline, no spaces around `=`.
assert_eq!(
parse_toolchain_channel("[toolchain]\nchannel='nightly'"),
Some("nightly".to_string())
);
// Comments, blank lines and unrelated sections tolerated.
assert_eq!(
parse_toolchain_channel(
"# pin for CI\n\
\n\
[toolchain]\n\
# bumped by cargo update\n\
channel = \"stable\"\n\
\n\
[targets.x86_64-unknown-linux-gnu]\n"
),
Some("stable".to_string())
);
// Legacy bare `rust-toolchain`: just the channel string, no TOML.
assert_eq!(
parse_toolchain_channel("1.98.0\n"),
Some("1.98.0".to_string())
);
assert_eq!(
parse_toolchain_channel("nightly"),
Some("nightly".to_string())
);
// Missing channel key.
assert_eq!(
parse_toolchain_channel("[toolchain]\ncomponents = [\"rustfmt\"]\n"),
None
);
// Wrong section: the key is not honored (rustup would ignore it too).
assert_eq!(
parse_toolchain_channel("[unstable]\nchannel = \"1.98.0\"\n"),
None
);
// Empty channel value or file: nothing to report.
assert_eq!(
parse_toolchain_channel("[toolchain]\nchannel = \"\"\n"),
None
);
assert_eq!(parse_toolchain_channel(""), None);
assert_eq!(parse_toolchain_channel("# only a comment\n"), None);
}
/// `probe()` carries the pin next to the project metadata:
/// `rust-toolchain.toml` wins over the legacy file (as with rustup), and
/// no pin file leaves the field empty.
#[test]
fn probe_reports_the_toolchain_pin() {
let template = super::super::get(TemplateId::Rust).unwrap();
let cargo_toml = "[package]\nname = \"pinned\"\nversion = \"1.0.0\"\n";
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), cargo_toml).unwrap();
std::fs::write(
dir.path().join("rust-toolchain.toml"),
"[toolchain]\nchannel = \"1.98.0\"\n",
)
.unwrap();
let probe = template.probe(dir.path()).expect("probe result");
assert_eq!(probe.name.as_deref(), Some("pinned"));
assert_eq!(probe.toolchain_pin.as_deref(), Some("1.98.0"));
// The legacy bare file is honored when there is no toml ...
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), cargo_toml).unwrap();
std::fs::write(dir.path().join("rust-toolchain"), "nightly\n").unwrap();
assert_eq!(
template.probe(dir.path()).unwrap().toolchain_pin,
Some("nightly".to_string())
);
// ... but loses once the toml exists.
std::fs::write(
dir.path().join("rust-toolchain.toml"),
"[toolchain]\nchannel = \"1.90.0\"\n",
)
.unwrap();
assert_eq!(
template.probe(dir.path()).unwrap().toolchain_pin,
Some("1.90.0".to_string())
);
// No pin file at all.
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), cargo_toml).unwrap();
assert_eq!(template.probe(dir.path()).unwrap().toolchain_pin, None);
}
} }