|
| 1 | +//! The lowest cargo a project TELLS us it builds on: the ONE reader of the |
| 2 | +//! project's declared floor, shared by the vendored multi-version warning |
| 3 | +//! and the hosted planner's sparse-registry gate. |
| 4 | +//! |
| 5 | +//! Read offline from the project itself, in this order: the workspace |
| 6 | +//! root's `rust-version` (`[package]`, else `[workspace.package]`, and a |
| 7 | +//! `rust-version.workspace = true` inheritor reads `[workspace.package]`), |
| 8 | +//! else the toolchain rustup picks from the root: the legacy |
| 9 | +//! `rust-toolchain` file when it exists (rustup prefers it over |
| 10 | +//! `rust-toolchain.toml`, with a warning), else `rust-toolchain.toml`'s |
| 11 | +//! `[toolchain] channel`. |
| 12 | +//! |
| 13 | +//! Everything here is pure; the callers own the reads. |
| 14 | +
|
| 15 | +use toml_edit::{Document, Item, Table}; |
| 16 | + |
| 17 | +use super::manifest::package_table; |
| 18 | + |
| 19 | +/// The legacy extensionless toolchain file (a bare channel name, or the |
| 20 | +/// TOML form). rustup reads it in preference to [`TOOLCHAIN_TOML`]. |
| 21 | +pub(crate) const TOOLCHAIN_LEGACY: &str = "rust-toolchain"; |
| 22 | +/// The TOML toolchain file. |
| 23 | +pub(crate) const TOOLCHAIN_TOML: &str = "rust-toolchain.toml"; |
| 24 | + |
| 25 | +/// Days from 1970-01-01 to a proleptic Gregorian date. |
| 26 | +fn days_from_civil(year: i64, month: i64, day: i64) -> i64 { |
| 27 | + let y = if month <= 2 { year - 1 } else { year }; |
| 28 | + let era = y.div_euclid(400); |
| 29 | + let yoe = y - era * 400; |
| 30 | + let doy = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + day - 1; |
| 31 | + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; |
| 32 | + era * 146_097 + doe - 719_468 |
| 33 | +} |
| 34 | + |
| 35 | +/// The minor of a dated `nightly-YYYY-MM-DD` / `beta-YYYY-MM-DD` channel. |
| 36 | +/// Rust ships a stable release every six weeks (1.68 on 2023-03-09, 1.45 |
| 37 | +/// on 2020-07-16 — the train has not slipped since 1.0), and on that day |
| 38 | +/// beta becomes the next minor and nightly the one after. |
| 39 | +fn dated_channel_minor(spec: &str) -> Option<u32> { |
| 40 | + let (ahead, date) = if let Some(date) = spec.strip_prefix("nightly-") { |
| 41 | + (2, date) |
| 42 | + } else if let Some(date) = spec.strip_prefix("beta-") { |
| 43 | + (1, date) |
| 44 | + } else { |
| 45 | + return None; |
| 46 | + }; |
| 47 | + let mut parts = date.get(..10)?.split('-').map(str::parse::<i64>); |
| 48 | + let (year, month, day) = ( |
| 49 | + parts.next()?.ok()?, |
| 50 | + parts.next()?.ok()?, |
| 51 | + parts.next()?.ok()?, |
| 52 | + ); |
| 53 | + if !(1..=12).contains(&month) || !(1..=31).contains(&day) { |
| 54 | + return None; |
| 55 | + } |
| 56 | + let since = days_from_civil(year, month, day) - days_from_civil(2023, 3, 9); |
| 57 | + let stable = 68 + since.div_euclid(42); |
| 58 | + u32::try_from(stable + ahead).ok() |
| 59 | +} |
| 60 | + |
| 61 | +/// The `1.<minor>` of a `rust-version` or toolchain channel (`1.67`, |
| 62 | +/// `1.67.1`, `1.67-x86_64-unknown-linux-gnu`, or a dated |
| 63 | +/// `nightly-2022-06-01`); `None` for a rolling channel (`stable`, |
| 64 | +/// `nightly`, …) or anything else. |
| 65 | +fn minor_of(spec: &str) -> Option<u32> { |
| 66 | + let spec = spec.trim().trim_matches('"'); |
| 67 | + let Some(rest) = spec.strip_prefix("1.") else { |
| 68 | + return dated_channel_minor(spec); |
| 69 | + }; |
| 70 | + rest.split(['.', '-', '+']).next()?.parse().ok() |
| 71 | +} |
| 72 | + |
| 73 | +/// `[workspace.package].rust-version` of a parsed root manifest. |
| 74 | +fn workspace_rust_version(root: &Table) -> Option<&str> { |
| 75 | + root.get("workspace") |
| 76 | + .and_then(|w| w.get("package")) |
| 77 | + .and_then(|p| p.get("rust-version")) |
| 78 | + .and_then(Item::as_str) |
| 79 | +} |
| 80 | + |
| 81 | +/// The package's own `rust-version`: `Ok(Some)` for a literal, `Err(())` |
| 82 | +/// for an inheritor (`rust-version.workspace = true`, or any non-string |
| 83 | +/// value), `Ok(None)` when it has none. |
| 84 | +fn own_rust_version(root: &Table) -> Result<Option<&str>, ()> { |
| 85 | + match package_table(root).and_then(|p| p.get("rust-version")) { |
| 86 | + None => Ok(None), |
| 87 | + Some(item) => item.as_str().map(Some).ok_or(()), |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +/// The `rust-version` the root `Cargo.toml` declares. `[package]`'s own |
| 92 | +/// string wins; an inheritor, a package without one, or a virtual |
| 93 | +/// workspace reads `[workspace.package]`. |
| 94 | +pub(crate) fn root_manifest_minor(text: &str) -> Option<u32> { |
| 95 | + let doc = Document::parse(text).ok()?; |
| 96 | + let root = doc.as_table(); |
| 97 | + match own_rust_version(root) { |
| 98 | + Ok(Some(own)) => Some(own), |
| 99 | + _ => workspace_rust_version(root), |
| 100 | + } |
| 101 | + .and_then(minor_of) |
| 102 | +} |
| 103 | + |
| 104 | +/// The `rust-version` a workspace member declares: its own literal, or the |
| 105 | +/// root's `[workspace.package]` value when it inherits it. A member with |
| 106 | +/// none declares nothing (cargo does not inherit implicitly). |
| 107 | +pub(crate) fn member_manifest_minor(member: &str, root: Option<&str>) -> Option<u32> { |
| 108 | + let doc = Document::parse(member).ok()?; |
| 109 | + match own_rust_version(doc.as_table()) { |
| 110 | + Ok(Some(own)) => minor_of(own), |
| 111 | + Ok(None) => None, |
| 112 | + Err(()) => { |
| 113 | + let root = Document::parse(root?).ok()?; |
| 114 | + workspace_rust_version(root.as_table()).and_then(minor_of) |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +/// The `[toolchain] channel` of a TOML toolchain file. |
| 120 | +fn toolchain_channel(text: &str) -> Option<String> { |
| 121 | + let doc = Document::parse(text).ok()?; |
| 122 | + doc.get("toolchain") |
| 123 | + .and_then(|t| t.get("channel")) |
| 124 | + .and_then(Item::as_str) |
| 125 | + .map(str::to_string) |
| 126 | +} |
| 127 | + |
| 128 | +/// The minor of the toolchain rustup picks from the root's toolchain files |
| 129 | +/// (`None` for a file that is absent or unreadable): the legacy file when |
| 130 | +/// it exists, else the TOML one. A blank legacy text names no toolchain |
| 131 | +/// (the hosted engine records an unreadable in-memory entry as `""`), so |
| 132 | +/// it does not hide the TOML file's channel. |
| 133 | +pub(crate) fn toolchain_minor( |
| 134 | + toolchain_toml: Option<&str>, |
| 135 | + toolchain_legacy: Option<&str>, |
| 136 | +) -> Option<u32> { |
| 137 | + if let Some(legacy) = toolchain_legacy.filter(|t| !t.trim().is_empty()) { |
| 138 | + return match toolchain_channel(legacy) { |
| 139 | + Some(channel) => minor_of(&channel), |
| 140 | + None => minor_of(legacy), |
| 141 | + }; |
| 142 | + } |
| 143 | + minor_of(&toolchain_channel(toolchain_toml?)?) |
| 144 | +} |
| 145 | + |
| 146 | +/// The lowest cargo minor the project declares, from the root `Cargo.toml` |
| 147 | +/// ([`root_manifest_minor`]), else its toolchain files |
| 148 | +/// ([`toolchain_minor`]). |
| 149 | +/// |
| 150 | +/// `None` when the project declares nothing, or pins a rolling channel — |
| 151 | +/// socket-patch never runs `cargo`, so there is no other signal, and "no |
| 152 | +/// declaration" is NOT evidence of a modern cargo. |
| 153 | +pub(crate) fn declared_cargo_minor( |
| 154 | + manifest: Option<&str>, |
| 155 | + toolchain_toml: Option<&str>, |
| 156 | + toolchain_legacy: Option<&str>, |
| 157 | +) -> Option<u32> { |
| 158 | + manifest |
| 159 | + .and_then(root_manifest_minor) |
| 160 | + .or_else(|| toolchain_minor(toolchain_toml, toolchain_legacy)) |
| 161 | +} |
| 162 | + |
| 163 | +#[cfg(test)] |
| 164 | +mod tests { |
| 165 | + use super::*; |
| 166 | + |
| 167 | + fn from_manifest(text: &str) -> Option<u32> { |
| 168 | + declared_cargo_minor(Some(text), None, None) |
| 169 | + } |
| 170 | + |
| 171 | + #[test] |
| 172 | + fn reads_package_and_workspace_rust_version() { |
| 173 | + assert_eq!( |
| 174 | + from_manifest("[package]\nname = \"a\"\nrust-version = \"1.70\"\n"), |
| 175 | + Some(70) |
| 176 | + ); |
| 177 | + assert_eq!( |
| 178 | + from_manifest("\u{feff}[package]\nname = \"a\"\nrust-version = \"1.67.1\"\n"), |
| 179 | + Some(67) |
| 180 | + ); |
| 181 | + assert_eq!( |
| 182 | + from_manifest("[workspace]\n\n[workspace.package]\nrust-version = \"1.60\"\n"), |
| 183 | + Some(60) |
| 184 | + ); |
| 185 | + // The package's own value wins over the workspace default. |
| 186 | + assert_eq!( |
| 187 | + from_manifest( |
| 188 | + "[workspace.package]\nrust-version = \"1.60\"\n\n\ |
| 189 | + [package]\nname = \"a\"\nrust-version = \"1.75\"\n" |
| 190 | + ), |
| 191 | + Some(75) |
| 192 | + ); |
| 193 | + assert_eq!(from_manifest("[package]\nname = \"a\"\n"), None); |
| 194 | + assert_eq!(from_manifest("not toml ["), None); |
| 195 | + } |
| 196 | + |
| 197 | + /// #651: `rust-version.workspace = true` inherits `[workspace.package]` |
| 198 | + /// — in the dotted spelling and the inline-table one. |
| 199 | + #[test] |
| 200 | + fn workspace_inheritor_reads_workspace_package() { |
| 201 | + for spelling in [ |
| 202 | + "rust-version.workspace = true", |
| 203 | + "rust-version = { workspace = true }", |
| 204 | + ] { |
| 205 | + let text = format!( |
| 206 | + "[workspace]\n\n[workspace.package]\nrust-version = \"1.70\"\n\n\ |
| 207 | + [package]\nname = \"consumer\"\nversion = \"0.1.0\"\n{spelling}\n" |
| 208 | + ); |
| 209 | + assert_eq!(from_manifest(&text), Some(70), "{spelling}"); |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + #[test] |
| 214 | + fn falls_back_to_toolchain_files() { |
| 215 | + let toml = "[toolchain]\nchannel = \"1.67.1\"\n"; |
| 216 | + assert_eq!(declared_cargo_minor(None, Some(toml), None), Some(67)); |
| 217 | + assert_eq!(declared_cargo_minor(None, None, Some("1.56.0\n")), Some(56)); |
| 218 | + assert_eq!(declared_cargo_minor(None, None, Some(toml)), Some(67)); |
| 219 | + assert_eq!(declared_cargo_minor(None, None, Some("stable\n")), None); |
| 220 | + // rustup prefers the legacy file when both exist. |
| 221 | + let stable = "[toolchain]\nchannel = \"stable\"\n"; |
| 222 | + assert_eq!( |
| 223 | + declared_cargo_minor(None, Some(stable), Some("1.50\n")), |
| 224 | + Some(50) |
| 225 | + ); |
| 226 | + assert_eq!( |
| 227 | + declared_cargo_minor(None, Some(toml), Some("stable\n")), |
| 228 | + None |
| 229 | + ); |
| 230 | + // A blank legacy placeholder (an unreadable entry) is no toolchain. |
| 231 | + for blank in ["", " \n"] { |
| 232 | + assert_eq!( |
| 233 | + declared_cargo_minor(None, Some(toml), Some(blank)), |
| 234 | + Some(67) |
| 235 | + ); |
| 236 | + } |
| 237 | + // `rust-version` wins over the toolchain files. |
| 238 | + assert_eq!( |
| 239 | + declared_cargo_minor( |
| 240 | + Some("[package]\nname = \"a\"\nrust-version = \"1.70\"\n"), |
| 241 | + Some(toml), |
| 242 | + None |
| 243 | + ), |
| 244 | + Some(70) |
| 245 | + ); |
| 246 | + } |
| 247 | + |
| 248 | + /// A dated nightly or beta pins the cargo of that day's release train. |
| 249 | + #[test] |
| 250 | + fn dated_channels_map_to_their_release_train() { |
| 251 | + let cases = [ |
| 252 | + ("nightly-2022-06-01", Some(63)), |
| 253 | + ("beta-2022-06-01", Some(62)), |
| 254 | + // The day 1.68 shipped: nightly is 1.70, beta 1.69. |
| 255 | + ("nightly-2023-03-09", Some(70)), |
| 256 | + ("beta-2023-03-08", Some(68)), |
| 257 | + ("nightly-2020-07-16", Some(47)), |
| 258 | + ("nightly-2024-13-01", None), |
| 259 | + ("nightly", None), |
| 260 | + ("stable", None), |
| 261 | + ]; |
| 262 | + for (channel, want) in cases { |
| 263 | + let toml = format!("[toolchain]\nchannel = \"{channel}\"\n"); |
| 264 | + assert_eq!( |
| 265 | + declared_cargo_minor(None, Some(&toml), None), |
| 266 | + want, |
| 267 | + "{channel}" |
| 268 | + ); |
| 269 | + } |
| 270 | + } |
| 271 | + |
| 272 | + #[test] |
| 273 | + fn a_member_declares_its_own_or_its_inherited_rust_version() { |
| 274 | + let root = "[workspace]\n\n[workspace.package]\nrust-version = \"1.60\"\n"; |
| 275 | + let member = |rv: &str| format!("[package]\nname = \"m\"\n{rv}\n"); |
| 276 | + assert_eq!( |
| 277 | + member_manifest_minor(&member("rust-version = \"1.64\""), Some(root)), |
| 278 | + Some(64) |
| 279 | + ); |
| 280 | + assert_eq!( |
| 281 | + member_manifest_minor(&member("rust-version.workspace = true"), Some(root)), |
| 282 | + Some(60) |
| 283 | + ); |
| 284 | + assert_eq!(member_manifest_minor(&member(""), Some(root)), None); |
| 285 | + assert_eq!( |
| 286 | + member_manifest_minor(&member("rust-version.workspace = true"), None), |
| 287 | + None |
| 288 | + ); |
| 289 | + } |
| 290 | +} |
0 commit comments