|
| 1 | +// Copyright 2023 The Fuchsia Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +use cargo_metadata::MetadataCommand; |
| 6 | +use rustc_version::{Channel, Version}; |
| 7 | +use std::error::Error; |
| 8 | + |
| 9 | +struct PinnedVersions { |
| 10 | + msrv: String, |
| 11 | + stable: String, |
| 12 | + nightly: String, |
| 13 | +} |
| 14 | + |
| 15 | +impl PinnedVersions { |
| 16 | + /// Attempts to extract pinned toolchain versions based on the current |
| 17 | + /// working directory. |
| 18 | + /// |
| 19 | + /// `extract_from_pwd` expects to be called from a directory which is a |
| 20 | + /// child of a Cargo workspace. It extracts the pinned versions from the |
| 21 | + /// metadata of the root package. |
| 22 | + fn extract_from_pwd() -> Result<PinnedVersions, Box<dyn Error>> { |
| 23 | + let meta = MetadataCommand::new().exec()?; |
| 24 | + // NOTE(joshlf): In theory `meta.root_package()` should work instead of |
| 25 | + // this manual search, but for some reason it breaks when called from |
| 26 | + // zerocopy-derive's tests. This works as a workaround, and it's just |
| 27 | + // test code, so I didn't bother investigating. |
| 28 | + let pkg = meta |
| 29 | + .workspace_packages() |
| 30 | + .into_iter() |
| 31 | + .find(|pkg| pkg.name == "zerocopy") |
| 32 | + .ok_or("no `zerocopy` package found; are we in a workspace?")?; |
| 33 | + let msrv = pkg |
| 34 | + .rust_version |
| 35 | + .as_ref() |
| 36 | + .ok_or("failed to find msrv: no `rust-version` key present")? |
| 37 | + .to_string(); |
| 38 | + let extract = |version_name, key| -> Result<String, String> { |
| 39 | + let value = pkg.metadata.pointer(&format!("/ci/{key}")).ok_or_else(|| { |
| 40 | + format!("failed to find {version_name}: no `metadata.ci.{key}` key present") |
| 41 | + })?; |
| 42 | + value.as_str().map(str::to_string).ok_or_else(|| format!("failed to find {version_name}: key `metadata.ci.{key}` (contents: {value:?}) failed to parse as JSON string")) |
| 43 | + }; |
| 44 | + let stable = extract("stable", "pinned-stable")?; |
| 45 | + let nightly = extract("nightly", "pinned-nightly")?; |
| 46 | + Ok(PinnedVersions { msrv, stable, nightly }) |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +#[derive(Debug)] |
| 51 | +pub enum ToolchainVersion { |
| 52 | + /// The version listed as our MSRV (ie, the `package.rust-version` key in |
| 53 | + /// `Cargo.toml`). |
| 54 | + PinnedMsrv, |
| 55 | + /// The stable version pinned in CI. |
| 56 | + PinnedStable, |
| 57 | + /// The nightly version pinned in CI |
| 58 | + PinnedNightly, |
| 59 | + /// A stable version other than the one pinned in CI. |
| 60 | + OtherStable, |
| 61 | + /// A nightly version other than the one pinned in CI. |
| 62 | + OtherNightly, |
| 63 | +} |
| 64 | + |
| 65 | +impl ToolchainVersion { |
| 66 | + /// Attempts to determine whether the current toolchain version matches one |
| 67 | + /// of the versions pinned in CI and if so, which one. |
| 68 | + pub fn extract_from_pwd() -> Result<ToolchainVersion, Box<dyn Error>> { |
| 69 | + let pinned_versions = PinnedVersions::extract_from_pwd()?; |
| 70 | + let current = rustc_version::version_meta()?; |
| 71 | + |
| 72 | + let s = match current.channel { |
| 73 | + Channel::Dev | Channel::Beta => { |
| 74 | + return Err(format!("unsupported channel: {:?}", current.channel).into()) |
| 75 | + } |
| 76 | + Channel::Nightly => { |
| 77 | + format!( |
| 78 | + "nightly-{}", |
| 79 | + current.commit_date.as_ref().ok_or("nightly channel missing commit date")? |
| 80 | + ) |
| 81 | + } |
| 82 | + Channel::Stable => { |
| 83 | + let Version { major, minor, patch, .. } = current.semver; |
| 84 | + format!("{major}.{minor}.{patch}") |
| 85 | + } |
| 86 | + }; |
| 87 | + |
| 88 | + // Due to a quirk of how Rust nightly versions are encoded and published |
| 89 | + // [1], the version as understood by rustup uses a date one day ahead of |
| 90 | + // the version as encoded in the `rustc` binary itself. |
| 91 | + // `pinned_versions` encodes the former notion of the date (as it is |
| 92 | + // meant to be passed as the `+<toolchain>` selector syntax understood |
| 93 | + // by rustup), while `current` encodes the latter notion of the date (as |
| 94 | + // it is extracted from `rustc`). Without this adjustment, toolchain |
| 95 | + // versions that should be considered equal would not be. |
| 96 | + // |
| 97 | + // [1] https://github.com/rust-lang/rust/issues/51533 |
| 98 | + let pinned_nightly_adjusted = { |
| 99 | + let desc = time::macros::format_description!("nightly-[year]-[month]-[day]"); |
| 100 | + let date = time::Date::parse(&pinned_versions.nightly, &desc).map_err(|_| { |
| 101 | + format!("failed to parse nightly version: {}", pinned_versions.nightly) |
| 102 | + })?; |
| 103 | + let adjusted = date - time::Duration::DAY; |
| 104 | + adjusted.format(&desc).unwrap() |
| 105 | + }; |
| 106 | + |
| 107 | + Ok(match s { |
| 108 | + s if s == pinned_versions.msrv => ToolchainVersion::PinnedMsrv, |
| 109 | + s if s == pinned_versions.stable => ToolchainVersion::PinnedStable, |
| 110 | + s if s == pinned_nightly_adjusted => ToolchainVersion::PinnedNightly, |
| 111 | + _ if current.channel == Channel::Stable => ToolchainVersion::OtherStable, |
| 112 | + _ if current.channel == Channel::Nightly => ToolchainVersion::OtherNightly, |
| 113 | + _ => { |
| 114 | + return Err(format!( |
| 115 | + "current toolchain ({current:?}) doesn't match any known version" |
| 116 | + ) |
| 117 | + .into()) |
| 118 | + } |
| 119 | + }) |
| 120 | + } |
| 121 | + |
| 122 | + /// Gets the name of the directory in which to store source files and |
| 123 | + /// expected output for UI tests for this toolchain version. |
| 124 | + /// |
| 125 | + /// For toolchain versions which are not pinned in CI, prints a warning to |
| 126 | + /// `stderr` which will be captured by the test harness and only printed on |
| 127 | + /// test failure. |
| 128 | + /// |
| 129 | + /// UI tests depend on the exact error messages emitted by rustc, but those |
| 130 | + /// error messages are not stable, and sometimes change between Rust |
| 131 | + /// versions. Thus, we maintain one set of UI tests for each Rust version |
| 132 | + /// that we test in CI, and we pin to specific versions in CI (a specific |
| 133 | + /// MSRV, a specific stable version, and a specific date of the nightly |
| 134 | + /// compiler). Updating those pinned versions may also require updating |
| 135 | + /// these tests. |
| 136 | + /// - `tests/ui-nightly` - Contains the source of truth for our UI test |
| 137 | + /// source files (`.rs`), and contains `.err` and `.out` files for nightly |
| 138 | + /// - `tests/ui-stable` - Contains symlinks to the `.rs` files in |
| 139 | + /// `tests/ui-nightly`, and contains `.err` and `.out` files for stable |
| 140 | + /// - `tests/ui-msrv` - Contains symlinks to the `.rs` files in |
| 141 | + /// `tests/ui-nightly`, and contains `.err` and `.out` files for MSRV |
| 142 | + pub fn get_ui_source_files_dirname_and_maybe_print_warning(&self) -> &'static str { |
| 143 | + if matches!(self, ToolchainVersion::OtherStable | ToolchainVersion::OtherNightly) { |
| 144 | + // This will be eaten by the test harness and only displayed on |
| 145 | + // failure. |
| 146 | + eprintln!("warning: current toolchain does not match any toolchain pinned in CI; this may cause spurious test failure"); |
| 147 | + } |
| 148 | + |
| 149 | + match self { |
| 150 | + ToolchainVersion::PinnedMsrv => "ui-msrv", |
| 151 | + ToolchainVersion::PinnedStable | ToolchainVersion::OtherStable => "ui-stable", |
| 152 | + ToolchainVersion::PinnedNightly | ToolchainVersion::OtherNightly => "ui-nightly", |
| 153 | + } |
| 154 | + } |
| 155 | +} |
0 commit comments