new: flag rust-toolchain.toml pins in pkh new
This commit is contained in:
+52
-11
@@ -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())?;
|
||||
|
||||
// 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
|
||||
// detection already decided to package the current directory; a
|
||||
// --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
|
||||
// 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)? {
|
||||
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
|
||||
/// (spec transcript): identity line, template/license/maintainer line, the
|
||||
/// generated-file overview and — for a metapackage — the Depends payload,
|
||||
/// for a skeleton — the upstream files that will be created, and for rust —
|
||||
/// a warning when dependencies cannot be vendored on this host.
|
||||
pub fn summary_text(opts: &NewOptions) -> String {
|
||||
/// The pre-flight summary screen shown before the final `Generate?`
|
||||
/// confirmation (spec transcript): identity line, template/license/maintainer
|
||||
/// line, the generated-file overview and — for a metapackage — the Depends
|
||||
/// payload, for a skeleton — the upstream files that will be created, and
|
||||
/// for rust — the probed toolchain pin (when any; the chroot build ignores
|
||||
/// 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 mut lines = Vec::new();
|
||||
|
||||
@@ -756,6 +773,13 @@ pub fn summary_text(opts: &NewOptions) -> String {
|
||||
" debian/rules cargo build --release --offline (vendored at generation)"
|
||||
.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 {
|
||||
lines.push(format!(" debian/rules {}", template.rules_dh_line()));
|
||||
}
|
||||
@@ -923,7 +947,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn summary_screen_skeleton() {
|
||||
let text = summary_text(&opts(Tid::Makefile));
|
||||
let text = summary_text(&opts(Tid::Makefile), None);
|
||||
assert!(
|
||||
text.contains("mytool 0.1.0-1 · builds for ubuntu/resolute"),
|
||||
"{text}"
|
||||
@@ -956,7 +980,7 @@ mod tests {
|
||||
let mut o = opts(Tid::Empty);
|
||||
o.depends = vec!["hello".to_string(), "hello-data (>= 1.0)".to_string()];
|
||||
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("Depends hello, hello-data (>= 1.0)"),
|
||||
@@ -973,7 +997,7 @@ mod tests {
|
||||
o.release = true;
|
||||
o.autopkgtest = true;
|
||||
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("debian/tests autopkgtest smoke test"),
|
||||
@@ -987,7 +1011,7 @@ mod tests {
|
||||
/// yet (regression: it claimed "(vendored)" before generating).
|
||||
#[test]
|
||||
fn summary_screen_rust_does_not_presume_vendoring() {
|
||||
let text = summary_text(&opts(Tid::Rust));
|
||||
let text = summary_text(&opts(Tid::Rust), None);
|
||||
assert!(
|
||||
text.contains("cargo build --release --offline (vendored at generation)"),
|
||||
"{text}"
|
||||
@@ -995,6 +1019,23 @@ mod tests {
|
||||
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]
|
||||
fn answer_validators() {
|
||||
assert!(validate_revision_answer("1").is_ok());
|
||||
|
||||
@@ -81,6 +81,11 @@ pub struct ProbeResult {
|
||||
/// Installed command / binary name (e.g. the first `[[bin]]` target or
|
||||
/// console script).
|
||||
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.
|
||||
|
||||
@@ -208,6 +208,7 @@ impl Template for Python {
|
||||
homepage: project.homepage,
|
||||
license: project.license,
|
||||
command: project.script,
|
||||
..Default::default()
|
||||
};
|
||||
if result.name.is_some()
|
||||
|| result.version.is_some()
|
||||
|
||||
+170
-4
@@ -118,12 +118,24 @@ impl Template for Rust {
|
||||
|
||||
/// Name, version, description, homepage, license and first binary from
|
||||
/// `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> {
|
||||
if let Some(result) = probe_cargo_metadata(dir) {
|
||||
return Some(result);
|
||||
match probe_cargo_metadata(dir).or_else(|| probe_cargo_toml(dir)) {
|
||||
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
|
||||
@@ -360,6 +372,8 @@ fn probe_cargo_metadata(dir: &Path) -> Option<ProbeResult> {
|
||||
homepage: field("homepage"),
|
||||
license: field("license"),
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -713,4 +777,106 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user