diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 345a2621734..9ebf275914e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: branch: main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: ./scripts/cargo-release-publish.sh --exclude cargo-nextest + - run: ./scripts/cargo-release-publish.sh env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} @@ -53,7 +53,7 @@ jobs: branch: main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: ./scripts/cargo-release-publish.sh --exclude cargo-nextest + - run: ./scripts/cargo-release-publish.sh env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} @@ -78,7 +78,7 @@ jobs: branch: main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: ./scripts/cargo-release-publish.sh --exclude cargo-nextest + - run: ./scripts/cargo-release-publish.sh env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} @@ -277,6 +277,6 @@ jobs: - name: Run cargo-release-publish.sh run: | cd nextest - ./scripts/cargo-release-publish.sh + PUBLISH_CARGO_NEXTEST=1 ./scripts/cargo-release-publish.sh env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 9b4220c655e..13b09003901 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,7 @@ dependencies = [ "nextest-runner", "nextest-workspace-hack", "once_cell", + "os_info", "owo-colors 4.1.0", "pathdiff", "quick-junit", @@ -1796,6 +1797,7 @@ name = "nextest-workspace-hack" version = "0.1.0" dependencies = [ "backtrace", + "camino", "cc", "clap", "clap_builder", @@ -1924,6 +1926,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_info" +version = "3.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +dependencies = [ + "log", + "serde", + "windows-sys 0.52.0", +] + [[package]] name = "os_pipe" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 155428fd240..45e6ec7b75a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ nextest-metadata = { version = "0.12.1", path = "nextest-metadata" } nextest-workspace-hack = "0.1.0" nix = { version = "0.29.0", default-features = false, features = ["signal"] } once_cell = "1.19.0" +os_info = "3.8.2" owo-colors = "4.1.0" pathdiff = { version = "0.2.1", features = ["camino"] } pin-project-lite = "0.2.14" diff --git a/cargo-nextest/Cargo.toml b/cargo-nextest/Cargo.toml index 9223d9656be..17124a9a94e 100644 --- a/cargo-nextest/Cargo.toml +++ b/cargo-nextest/Cargo.toml @@ -14,7 +14,7 @@ rust-version.workspace = true [dependencies] camino.workspace = true cfg-if.workspace = true -clap = { workspace = true, features = ["derive", "env", "unicode", "wrap_help"] } +clap = { workspace = true, features = ["derive", "env", "string", "unicode", "wrap_help"] } color-eyre.workspace = true dialoguer.workspace = true duct.workspace = true @@ -31,6 +31,7 @@ nextest-metadata = { version = "=0.12.1", path = "../nextest-metadata" } nextest-runner = { version = "=0.61.0", path = "../nextest-runner" } nextest-workspace-hack.workspace = true once_cell.workspace = true +os_info.workspace = true owo-colors.workspace = true pathdiff.workspace = true quick-junit.workspace = true @@ -42,6 +43,9 @@ supports-unicode.workspace = true swrite.workspace = true thiserror.workspace = true +[build-dependencies] +camino.workspace = true + [dev-dependencies] camino-tempfile.workspace = true diff --git a/cargo-nextest/build.rs b/cargo-nextest/build.rs new file mode 100644 index 00000000000..c781d6b79e2 --- /dev/null +++ b/cargo-nextest/build.rs @@ -0,0 +1,82 @@ +use camino::Utf8Path; +use std::process::Command; + +fn main() { + commit_info(); + + let target = std::env::var("TARGET").unwrap(); + println!("cargo:rustc-env=NEXTEST_BUILD_HOST_TARGET={target}"); +} + +fn commit_info() { + println!("cargo:rerun-if-env-changed=CFG_OMIT_COMMIT_HASH"); + if std::env::var_os("CFG_OMIT_COMMIT_HASH").is_some() { + return; + } + + if let Some(info) = CommitInfo::get() { + println!("cargo:rustc-env=NEXTEST_BUILD_COMMIT_HASH={}", info.hash); + println!( + "cargo:rustc-env=NEXTEST_BUILD_COMMIT_SHORT_HASH={}", + info.short_hash, + ); + println!("cargo:rustc-env=NEXTEST_BUILD_COMMIT_DATE={}", info.date); + } +} + +struct CommitInfo { + hash: String, + short_hash: String, + date: String, +} + +impl CommitInfo { + fn get() -> Option { + // Prefer git info over crate metadata to match what Cargo and rustc do. + Self::from_git().or_else(Self::from_metadata) + } + + fn from_git() -> Option { + // cargo-nextest is one level down from the root of the repository. + if !Utf8Path::new("../.git").exists() { + return None; + } + + let output = match Command::new("git") + .arg("log") + .arg("-1") + .arg("--date=short") + .arg("--format=%H %h %cd") + .arg("--abbrev=9") + .output() + { + Ok(output) if output.status.success() => output, + _ => return None, + }; + + let stdout = String::from_utf8(output.stdout).expect("git output is ASCII"); + Self::from_string(&stdout) + } + + fn from_metadata() -> Option { + // This file is generated by scripts/cargo-release-publish.sh. + let path = Utf8Path::new("nextest-commit-info"); + if !path.exists() { + return None; + } + + println!("cargo:rerun-if-changed={}", path); + + let content = std::fs::read_to_string(path).ok()?; + Self::from_string(&content) + } + + fn from_string(s: &str) -> Option { + let mut parts = s.split_whitespace().map(|s| s.to_string()); + Some(CommitInfo { + hash: parts.next()?, + short_hash: parts.next()?, + date: parts.next()?, + }) + } +} diff --git a/cargo-nextest/src/dispatch.rs b/cargo-nextest/src/dispatch.rs index 7ee5e4ab622..ac0e77d6da6 100644 --- a/cargo-nextest/src/dispatch.rs +++ b/cargo-nextest/src/dispatch.rs @@ -5,6 +5,7 @@ use crate::{ cargo_cli::{CargoCli, CargoOptions}, output::{should_redact, OutputContext, OutputOpts, OutputWriter, StderrStyles}, reuse_build::{make_path_mapper, ArchiveFormatOpt, ReuseBuildOpts}, + version::VersionInfo, ExpectedError, Result, ReuseBuildKind, }; use camino::{Utf8Path, Utf8PathBuf}; @@ -64,7 +65,13 @@ use swrite::{swrite, SWrite}; /// This binary should typically be invoked as `cargo nextest` (in which case /// this message will not be seen), not `cargo-nextest`. #[derive(Debug, Parser)] -#[command(version, bin_name = "cargo", styles = crate::output::clap_styles::style(), max_term_width = 100)] +#[command( + version = VersionInfo::new().to_short_string(), + long_version = VersionInfo::new().to_long_string(), + bin_name = "cargo", + styles = crate::output::clap_styles::style(), + max_term_width = 100, +)] pub struct CargoNextestApp { #[clap(subcommand)] subcommand: NextestSubcommand, @@ -114,7 +121,11 @@ enum NextestSubcommand { } #[derive(Debug, Args)] -#[command(version)] +#[clap( + version = VersionInfo::new().to_short_string(), + long_version = VersionInfo::new().to_long_string(), + display_name = "cargo-nextest", +)] struct AppOpts { #[clap(flatten)] common: CommonOpts, diff --git a/cargo-nextest/src/lib.rs b/cargo-nextest/src/lib.rs index 36d8f3c0beb..969eb1d50b4 100644 --- a/cargo-nextest/src/lib.rs +++ b/cargo-nextest/src/lib.rs @@ -23,6 +23,7 @@ mod output; mod reuse_build; #[cfg(feature = "self-update")] mod update; +mod version; #[doc(hidden)] pub use dispatch::*; diff --git a/cargo-nextest/src/version.rs b/cargo-nextest/src/version.rs new file mode 100644 index 00000000000..c129d4cd5ff --- /dev/null +++ b/cargo-nextest/src/version.rs @@ -0,0 +1,78 @@ +// Copyright (c) The nextest Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::fmt::Write; + +pub(crate) struct VersionInfo { + /// Nextest's version. + version: &'static str, + + /// Information about the Git repository that nextest was built from. + /// + /// `None` if the Git repository information is not available. + commit_info: Option, +} + +impl VersionInfo { + pub(crate) const fn new() -> Self { + Self { + version: env!("CARGO_PKG_VERSION"), + commit_info: CommitInfo::from_env(), + } + } + + pub(crate) fn to_short_string(&self) -> String { + let mut s = self.version.to_string(); + + if let Some(commit_info) = &self.commit_info { + write!( + s, + " ({} {})", + commit_info.short_commit_hash, commit_info.commit_date + ) + .unwrap(); + } + + s + } + + pub(crate) fn to_long_string(&self) -> String { + let mut s = self.to_short_string(); + write!(s, "\nrelease: {}", self.version).unwrap(); + + if let Some(commit_info) = &self.commit_info { + write!(s, "\ncommit-hash: {}", commit_info.commit_hash).unwrap(); + write!(s, "\ncommit-date: {}", commit_info.commit_date).unwrap(); + } + write!(s, "\nhost: {}", env!("NEXTEST_BUILD_HOST_TARGET")).unwrap(); + write!(s, "\nos: {}", os_info::get()).unwrap(); + + s + } +} + +struct CommitInfo { + short_commit_hash: &'static str, + commit_hash: &'static str, + commit_date: &'static str, +} + +impl CommitInfo { + const fn from_env() -> Option { + let Some(short_commit_hash) = option_env!("NEXTEST_BUILD_COMMIT_SHORT_HASH") else { + return None; + }; + let Some(commit_hash) = option_env!("NEXTEST_BUILD_COMMIT_HASH") else { + return None; + }; + let Some(commit_date) = option_env!("NEXTEST_BUILD_COMMIT_DATE") else { + return None; + }; + + Some(Self { + short_commit_hash, + commit_hash, + commit_date, + }) + } +} diff --git a/integration-tests/tests/integration/main.rs b/integration-tests/tests/integration/main.rs index a92a8788990..df261a27cb0 100644 --- a/integration-tests/tests/integration/main.rs +++ b/integration-tests/tests/integration/main.rs @@ -35,6 +35,71 @@ use camino_tempfile::Utf8TempDir; use fixtures::*; use temp_project::TempProject; +#[test] +fn test_version() { + // Note that this is slightly overdetermined: details like the length of the short commit hash + // are not part of the format, and we have some flexibility in changing it. + let version_regex = + regex::Regex::new(r"^cargo-nextest (0\.9\.\d+) \(([a-f0-9]{9}) (\d{4}-\d{2}-\d{2})\)\n$") + .unwrap(); + + set_env_vars(); + + // First run nextest with -V to get a one-line version string. + let output = CargoNextestCli::new().args(["-V"]).output(); + let short_stdout = output.stdout_as_str(); + let captures = version_regex + .captures(&short_stdout) + .unwrap_or_else(|| panic!("short version matches regex: {short_stdout}")); + + let version = captures.get(1).unwrap().as_str(); + let short_hash = captures.get(2).unwrap().as_str(); + let date = captures.get(3).unwrap().as_str(); + + let output = CargoNextestCli::new().args(["--version"]).output(); + let long_stdout = output.stdout_as_str(); + + // Check that all expected lines are found. + let mut lines = long_stdout.lines(); + + // Line 1 is the version line. Check that it matches the short version line. + let version_line = lines.next().unwrap(); + assert_eq!( + version_line, + short_stdout.trim_end(), + "long version line 1 matches short version" + ); + + // Line 2 is of the form "release: 0.9.0". + let release_line = lines.next().unwrap(); + assert_eq!(release_line, format!("release: {}", version)); + + // Line 3 is the commit hash. + let commit_hash_line = lines.next().unwrap(); + assert!( + commit_hash_line.starts_with(&format!("commit-hash: {}", short_hash)), + "commit hash line matches short hash: {commit_hash_line}" + ); + + // Line 4 is the commit date. + let commit_date_line = lines.next().unwrap(); + assert_eq!(commit_date_line, format!("commit-date: {}", date)); + + // Line 5 is the host. Just check that it begins with "host: ". + let host_line = lines.next().unwrap(); + assert!( + host_line.starts_with("host: "), + "host line starts with 'host: ': {host_line}" + ); + + // Line 6 is the OS. Just check that it begins with "os: ". + let os_line = lines.next().unwrap(); + assert!( + os_line.starts_with("os: "), + "os line starts with 'os: ': {os_line}" + ); +} + #[test] fn test_list_default() { set_env_vars(); diff --git a/scripts/cargo-release-publish.sh b/scripts/cargo-release-publish.sh index 9ccefa91f43..17f9f3d9ef3 100755 --- a/scripts/cargo-release-publish.sh +++ b/scripts/cargo-release-publish.sh @@ -8,9 +8,25 @@ set -xe -o pipefail # Check out this branch, creating it if it doesn't exist. git checkout -B to-release -# --execute: actually does the release -# --no-confirm: don't ask for confirmation, since this is a non-interactive script -cargo release publish --publish --execute --no-confirm --workspace "$@" +# Publish all crates except cargo-nextest first. Do this against main so `.cargo_vcs_info.json` is +# valid. (cargo-nextest is the only crate that cares about commit info.) +cargo release publish --publish --execute --no-confirm --workspace --exclude cargo-nextest + +if [[ $PUBLISH_CARGO_NEXTEST == "1" ]]; then + # Write out commit-related metadata. This matches cargo-nextest's build.rs. + git log -1 --date=short --format="%H %h %cd" --abbrev=9 > cargo-nextest/nextest-commit-info + + # Making a commit here is important because cargo-release does not allow passing in + # --allow-dirty. But note that `nextest-commit-info` is what's on main. + # + # This does unfortunately mean that Cargo's own `.cargo_vcs_info.json` will be incorrect, but + # what can you do. + git add cargo-nextest/nextest-commit-info + git commit -m "Write out commit info for cargo-nextest" + + # Publish cargo-nextest. + cargo release publish --publish --execute --no-confirm -p cargo-nextest +fi git checkout - git branch -D to-release diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 37fac420005..06dc562e6c4 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -16,8 +16,9 @@ publish = false ### BEGIN HAKARI SECTION [dependencies] backtrace = { version = "0.3.71", features = ["gimli-symbolize"] } -clap = { version = "4.5.18", features = ["derive", "env", "unicode", "wrap_help"] } -clap_builder = { version = "4.5.18", default-features = false, features = ["color", "env", "std", "suggestions", "unicode", "usage", "wrap_help"] } +camino = { version = "1.1.9", default-features = false, features = ["serde1"] } +clap = { version = "4.5.18", features = ["derive", "env", "string", "unicode", "wrap_help"] } +clap_builder = { version = "4.5.18", default-features = false, features = ["color", "env", "std", "string", "suggestions", "unicode", "usage", "wrap_help"] } console = { version = "0.15.8" } either = { version = "1.13.0" } getrandom = { version = "0.2.15", default-features = false, features = ["std"] } @@ -34,9 +35,11 @@ xxhash-rust = { version = "0.8.12", default-features = false, features = ["xxh3" zerocopy = { version = "0.7.35", features = ["derive", "simd"] } [build-dependencies] +camino = { version = "1.1.9", default-features = false, features = ["serde1"] } cc = { version = "1.1.18", default-features = false, features = ["parallel"] } proc-macro2 = { version = "1.0.86" } quote = { version = "1.0.37" } +serde = { version = "1.0.210", features = ["alloc", "derive"] } syn = { version = "2.0.77", features = ["extra-traits", "full", "visit", "visit-mut"] } [target.x86_64-unknown-linux-gnu.dependencies] @@ -71,6 +74,6 @@ futures-sink = { version = "0.3.30", default-features = false, features = ["std" smallvec = { version = "1.13.2", default-features = false, features = ["const_new"] } tokio = { version = "1.40.0", default-features = false, features = ["io-std", "net"] } windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59.0", features = ["Win32_Globalization", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_JobObjects", "Win32_System_Pipes", "Win32_System_Threading"] } -windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52.0", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Environment", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell"] } +windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52.0", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Environment", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } ### END HAKARI SECTION