Repository: cross-rs/cross
Pull request #673 (2022-03-24 21:32:04)

warn when used toolchain is out-of-date
on non-linux hosts, warn when toolchain is out of date.

# Example

```
$ cross build
Warning: using older rustc `1.61.0-nightly (9f4dc0b4d 2022-03-23)` for the target. Current active rustc on the host is `rustc 1.62.0-nightly (879aff385 2022-04-20)`.
 > Update with `rustup update --force-non-host nightly-x86_64-unknown-linux-gnu`
```

Emilgardis: Done, I think :)
Emilgardis: bors r+
bors[bot]: :-1: Rejected by too few approved reviews
Emilgardis: bors r+
bors[bot]: :-1: Rejected because of missing code owner approval
Emilgardis: oh ic, there's a mistake with `use_codeowners = true`
Emilgardis: bors r+

maybe it's because of protected branch, lets see
bors[bot]: :-1: Rejected because of missing code owner approval
Emilgardis: bors r+
bors[bot]: :-1: Rejected because of missing code owner approval
Emilgardis: > 👎 Rejected because of missing code owner approval

seriously?...
Emilgardis: bors r+ 

🤞

Patch:
diff --git a/src/main.rs b/src/main.rs
index 15df03a..e80bbc5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -277,10 +277,10 @@ fn run() -> Result<ExitStatus> {
         .iter()
         .any(|a| a == "--verbose" || a == "-v" || a == "-vv");
 
-    let version_meta =
+    let host_version_meta =
         rustc_version::version_meta().wrap_err("couldn't fetch the `rustc` version")?;
     if let Some(root) = cargo::root()? {
-        let host = version_meta.host();
+        let host = host_version_meta.host();
         let toml = toml(&root)?;
         let config = Config::new(toml);
         let target = args
@@ -288,7 +288,16 @@ fn run() -> Result<ExitStatus> {
             .or_else(|| config.target(&target_list))
             .unwrap_or_else(|| Target::from(host.triple(), &target_list));
         config.confusable_target(&target);
-        if host.is_supported(Some(&target)) {
+
+        let image_exists = match docker::image(&config, &target) {
+            Ok(_) => true,
+            Err(err) => {
+                eprintln!("Warning: {}", err);
+                false
+            }
+        };
+
+        if image_exists && host.is_supported(Some(&target)) {
             let mut sysroot = rustc::sysroot(&host, &target, verbose)?;
             let default_toolchain = sysroot
                 .file_name()
@@ -311,6 +320,15 @@ fn run() -> Result<ExitStatus> {
             if !installed_toolchains.into_iter().any(|t| t == toolchain) {
                 rustup::install_toolchain(&toolchain, verbose)?;
             }
+            // TODO: Provide a way to pick/match the toolchain version as a consumer of `cross`.
+            if let Some((rustc_version, rustc_commit)) = rustup::rustc_version(&sysroot)? {
+                warn_host_version_mismatch(
+                    &host_version_meta,
+                    &toolchain,
+                    &rustc_version,
+                    &rustc_commit,
+                )?;
+            }
 
             let available_targets = rustup::available_targets(&toolchain, verbose)?;
             let uses_xargo = config
@@ -340,14 +358,6 @@ fn run() -> Result<ExitStatus> {
                 .map(|sc| sc.needs_interpreter())
                 .unwrap_or(false);
 
-            let image_exists = match docker::image(&config, &target) {
-                Ok(_) => true,
-                Err(err) => {
-                    eprintln!("Warning: {} Falling back to `cargo` on the host.", err);
-                    false
-                }
-            };
-
             let filtered_args = if args
                 .subcommand
                 .map_or(false, |s| !s.needs_target_in_command())
@@ -374,11 +384,9 @@ fn run() -> Result<ExitStatus> {
                 args.all.clone()
             };
 
-            if image_exists
-                && target.needs_docker()
-                && args.subcommand.map(|sc| sc.needs_docker()).unwrap_or(false)
+            if target.needs_docker() && args.subcommand.map(|sc| sc.needs_docker()).unwrap_or(false)
             {
-                if version_meta.needs_interpreter()
+                if host_version_meta.needs_interpreter()
                     && needs_interpreter
                     && target.needs_interpreter()
                     && !interpreter::is_registered(&target)?
@@ -401,9 +409,54 @@ fn run() -> Result<ExitStatus> {
         }
     }
 
+    eprintln!("Warning: Falling back to `cargo` on the host.");
     cargo::run(&args.all, verbose)
 }
 
+#[derive(PartialEq, Debug)]
+pub(crate) enum VersionMatch {
+    Same,
+    OlderTarget,
+    NewerTarget,
+    Different,
+}
+
+pub(crate) fn warn_host_version_mismatch(
+    host_version_meta: &rustc_version::VersionMeta,
+    toolchain: &str,
+    rustc_version: &rustc_version::Version,
+    rustc_commit: &str,
+) -> Result<VersionMatch> {
+    let host_commit = (&host_version_meta.short_version_string)
+        .splitn(3, ' ')
+        .nth(2);
+    let rustc_commit_date = rustc_commit
+        .split_once(' ')
+        .and_then(|x| x.1.strip_suffix(')'));
+
+    // This should only hit on non Host::X86_64UnknownLinuxGnu hosts
+    if rustc_version != &host_version_meta.semver || (Some(rustc_commit) != host_commit) {
+        let versions = rustc_version.cmp(&host_version_meta.semver);
+        let dates = rustc_commit_date.cmp(&host_version_meta.commit_date.as_deref());
+
+        let rustc_warning = format!(
+            "rustc `{rustc_version} {rustc_commit}` for the target. Current active rustc on the host is `{}`",
+            host_version_meta.short_version_string
+        );
+        if versions.is_lt() || (versions.is_eq() && dates.is_lt()) {
+            eprintln!("Warning: using older {rustc_warning}.\n > Update with `rustup update --force-non-host {toolchain}`");
+            return Ok(VersionMatch::OlderTarget);
+        } else if versions.is_gt() || (versions.is_eq() && dates.is_gt()) {
+            eprintln!("Warning: using newer {rustc_warning}.\n > Update with `rustup update`");
+            return Ok(VersionMatch::NewerTarget);
+        } else {
+            eprintln!("Warning: using {rustc_warning}.");
+            return Ok(VersionMatch::Different);
+        }
+    }
+    Ok(VersionMatch::Same)
+}
+
 /// Parses the `Cross.toml` at the root of the Cargo project or from the
 /// `CROSS_CONFIG` environment variable (if any exist in either location).
 fn toml(root: &Root) -> Result<Option<CrossToml>> {
diff --git a/src/rustc.rs b/src/rustc.rs
index 47f2c2a..49ef6e4 100644
--- a/src/rustc.rs
+++ b/src/rustc.rs
@@ -45,11 +45,9 @@ pub fn target_list(verbose: bool) -> Result<TargetList> {
 pub fn sysroot(host: &Host, target: &Target, verbose: bool) -> Result<PathBuf> {
     let mut stdout = Command::new("rustc")
         .args(&["--print", "sysroot"])
-        .run_and_get_stdout(verbose)?;
-
-    if stdout.ends_with('\n') {
-        stdout.pop();
-    }
+        .run_and_get_stdout(verbose)?
+        .trim()
+        .to_owned();
 
     // On hosts other than Linux, specify the correct toolchain path.
     if host != &Host::X86_64UnknownLinuxGnu && target.needs_docker() {
diff --git a/src/rustup.rs b/src/rustup.rs
index aecd5e1..a3ca055 100644
--- a/src/rustup.rs
+++ b/src/rustup.rs
@@ -1,5 +1,8 @@
+use std::path::Path;
 use std::process::Command;
 
+use rustc_version::Version;
+
 use crate::errors::*;
 use crate::extensions::CommandExt;
 use crate::Target;
@@ -97,3 +100,29 @@ pub fn component_is_installed(component: &str, toolchain: &str, verbose: bool) -
         .lines()
         .any(|l| l.starts_with(component) && l.contains("installed")))
 }
+
+pub fn rustc_version(toolchain_path: &Path) -> Result<Option<(Version, String)>> {
+    let path = toolchain_path.join("lib/rustlib/multirust-channel-manifest.toml");
+    if path.exists() {
+        let contents = std::fs::read(&path)
+            .wrap_err_with(|| format!("couldn't open file `{}`", path.display()))?;
+        let manifest: toml::value::Table = toml::from_slice(&contents)?;
+        if let Some(rust_version) = manifest
+            .get("pkg")
+            .and_then(|pkg| pkg.get("rust"))
+            .and_then(|rust| rust.get("version"))
+            .and_then(|version| version.as_str())
+        {
+            // Field is `"{version} ({commit} {date})"`
+            if let Some((version, meta)) = rust_version.split_once(' ') {
+                return Ok(Some((
+                    Version::parse(version).wrap_err_with(|| {
+                        format!("invalid rust version found in {}", path.display())
+                    })?,
+                    meta.to_owned(),
+                )));
+            }
+        }
+    }
+    Ok(None)
+}


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index 5fa2991..90c4278 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -6,6 +6,7 @@ use std::{
 };
 
 use once_cell::sync::OnceCell;
+use rustc_version::VersionMeta;
 use serde::Deserialize;
 
 static WORKSPACE: OnceCell<PathBuf> = OnceCell::new();
@@ -51,3 +52,79 @@ pub fn walk_dir<'a>(
         e.path().extension() == Some("md".as_ref())
     })
 }
+
+#[test]
+pub fn target_mismatch() {
+    use crate::{warn_host_version_mismatch, VersionMatch};
+
+    fn make_meta(input: &str) -> VersionMeta {
+        let mut split = input.split(' ');
+        let version = split.next().unwrap();
+        let hash_short = &split.next().unwrap().strip_prefix('(').unwrap();
+        let date = split.next().unwrap().strip_suffix(")").unwrap();
+        rustc_version::version_meta_for(&format!(
+            r#"rustc {version} ({hash_short} {date})
+binary: rustc
+commit-hash: {hash_short:f<40}
+commit-date: {date}
+host: xxxx
+release: {version}
+"#
+        ))
+        .unwrap()
+    }
+
+    fn make_rustc_version(input: &str) -> (rustc_version::Version, String) {
+        let (version, meta) = input.split_once(' ').unwrap();
+        (version.parse().unwrap(), meta.to_owned())
+    }
+
+    #[track_caller]
+    fn compare(expected: VersionMatch, host: &str, targ: &str) {
+        let host_meta = dbg!(make_meta(host));
+        let target_meta = dbg!(make_rustc_version(targ));
+        assert_eq!(
+            expected,
+            warn_host_version_mismatch(&host_meta, "xxxx", &target_meta.0, &target_meta.1).unwrap(),
+            "\nhost = {}\ntarg = {}",
+            host,
+            targ
+        );
+    }
+
+    compare(
+        VersionMatch::Same,
+        "1.0.0 (11111111 2022-01-01)",
+        "1.0.0 (11111111 2022-01-01)",
+    );
+    compare(
+        VersionMatch::Same,
+        "1.0.0-nightly (11111111 2022-01-01)",
+        "1.0.0-nightly (11111111 2022-01-01)",
+    );
+    compare(
+        VersionMatch::OlderTarget,
+        "1.2.0 (22222222 2022-02-02)",
+        "1.0.0 (11111111 2022-01-01)",
+    );
+    compare(
+        VersionMatch::NewerTarget,
+        "1.0.0 (11111111 2022-01-01)",
+        "1.2.0 (22222222 2022-02-02)",
+    );
+    compare(
+        VersionMatch::Different,
+        "1.0.0-nightly (11111111 2022-01-01)",
+        "1.0.0-nightly (22222222 2022-01-01)",
+    );
+    compare(
+        VersionMatch::OlderTarget,
+        "1.0.0-nightly (22222222 2022-02-02)",
+        "1.0.0-nightly (11111111 2022-01-01)",
+    );
+    compare(
+        VersionMatch::NewerTarget,
+        "1.0.0-nightly (11111111 2022-01-01)",
+        "1.0.0-nightly (22222222 2022-02-02)",
+    );
+}