From 9dd084e604d30dd403485bdc785df64d7901884c Mon Sep 17 00:00:00 2001 From: Reilly Brogan Date: Mon, 8 Jul 2024 22:48:24 -0500 Subject: [PATCH] all: Add serpent_buildinfo crate and use it from boulder/moss Adds the serpent_buildinfo crate which performs several common build-metadata related tasks. Also updates boulder and moss to use the functions provided by this library instead of doing it themselves Signed-off-by: Reilly Brogan --- Cargo.lock | 10 ++ boulder/Cargo.toml | 1 + boulder/src/cli.rs | 14 +- crates/serpent_buildinfo/Cargo.toml | 13 ++ crates/serpent_buildinfo/build.rs | 171 +++++++++++++++++++++++++ crates/serpent_buildinfo/src/lib.rs | 102 +++++++++++++++ crates/serpent_buildinfo/src/values.rs | 16 +++ moss/Cargo.toml | 1 + moss/build.rs | 15 --- moss/src/cli/version.rs | 7 +- moss/src/client/mod.rs | 2 +- moss/src/environment.rs | 2 - 12 files changed, 317 insertions(+), 37 deletions(-) create mode 100644 crates/serpent_buildinfo/Cargo.toml create mode 100644 crates/serpent_buildinfo/build.rs create mode 100644 crates/serpent_buildinfo/src/lib.rs create mode 100644 crates/serpent_buildinfo/src/values.rs delete mode 100644 moss/build.rs diff --git a/Cargo.lock b/Cargo.lock index f93fc4e2..111d3695 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "serpent_buildinfo", "sha2", "stone", "stone_recipe", @@ -1414,6 +1415,7 @@ dependencies = [ "rayon", "reqwest", "serde", + "serpent_buildinfo", "sha2", "stone", "strum", @@ -2040,6 +2042,14 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serpent_buildinfo" +version = "0.1.0" +dependencies = [ + "chrono", + "thiserror", +] + [[package]] name = "sha2" version = "0.10.8" diff --git a/boulder/Cargo.toml b/boulder/Cargo.toml index 05ba3d01..5b4d34a1 100644 --- a/boulder/Cargo.toml +++ b/boulder/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true config = { path = "../crates/config" } container = { path = "../crates/container" } moss = { path = "../moss" } +serpent_buildinfo = { path = "../crates/serpent_buildinfo" } stone = { path = "../crates/stone" } stone_recipe = { path = "../crates/stone_recipe" } tui = { path = "../crates/tui" } diff --git a/boulder/src/cli.rs b/boulder/src/cli.rs index a778c439..b0d4414e 100644 --- a/boulder/src/cli.rs +++ b/boulder/src/cli.rs @@ -13,7 +13,7 @@ mod profile; mod recipe; #[derive(Debug, Parser)] -#[command(version = version())] +#[command(version = serpent_buildinfo::get_simple_version())] pub struct Command { #[command(flatten)] pub global: Global, @@ -94,15 +94,3 @@ pub enum Error { #[error("recipe")] Recipe(#[from] recipe::Error), } - -fn version() -> String { - use moss::environment; - - pub const VERSION: &str = env!("CARGO_PKG_VERSION"); - - let hash = environment::GIT_HASH - .map(|hash| format!(" ({hash})")) - .unwrap_or_default(); - - format!("{VERSION}{hash}") -} diff --git a/crates/serpent_buildinfo/Cargo.toml b/crates/serpent_buildinfo/Cargo.toml new file mode 100644 index 00000000..34fa6473 --- /dev/null +++ b/crates/serpent_buildinfo/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "serpent_buildinfo" +edition.workspace = true +version.workspace = true +rust-version.workspace = true +build = "build.rs" + +[dependencies] +chrono.workspace = true + +[build-dependencies] +chrono.workspace = true +thiserror.workspace = true diff --git a/crates/serpent_buildinfo/build.rs b/crates/serpent_buildinfo/build.rs new file mode 100644 index 00000000..9af0e853 --- /dev/null +++ b/crates/serpent_buildinfo/build.rs @@ -0,0 +1,171 @@ +// build.rs +use std::{io, os::unix::ffi::OsStringExt}; + +use chrono::{DateTime, Utc}; +use thiserror::Error; + +/// Returns value of given environment variable or error if missing. +/// +/// This also outputs necessary ‘cargo:rerun-if-env-changed’ tag to make sure +/// build script is rerun if the environment variable changes. +fn env(key: &str) -> Result { + println!("cargo:rerun-if-env-changed={}", key); + std::env::var_os(key).ok_or_else(|| Error::Env(key.to_string())) +} + +/// Calls program with given arguments and returns its standard output. If +/// calling the program fails or it exits with non-zero exit status returns an +/// error. +fn command(prog: &str, args: &[&str], cwd: Option) -> Result, Error> { + println!("cargo:rerun-if-env-changed=PATH"); + let mut cmd = std::process::Command::new(prog); + cmd.args(args); + cmd.stderr(std::process::Stdio::inherit()); + if let Some(cwd) = cwd { + cmd.current_dir(cwd); + } + let out = cmd.output()?; + if out.status.success() { + let mut stdout = out.stdout; + if let Some(b'\n') = stdout.last() { + stdout.pop(); + if let Some(b'\r') = stdout.last() { + stdout.pop(); + } + } + Ok(stdout) + } else if let Some(code) = out.status.code() { + Err(Error::CommandExit(prog.to_string(), code.to_string())) + } else { + Err(Error::CommandKilled(prog.to_string())) + } +} + +/// Checks to see if we're building from a git source and if so attempts to gather information about the git status +fn get_git_info() -> Result<(), Error> { + let pkg_dir = std::path::PathBuf::from(env("CARGO_MANIFEST_DIR")?); + let git_dir = command("git", &["rev-parse", "--git-dir"], Some(pkg_dir.clone())); + let git_dir = match git_dir { + Ok(git_dir) => { + println!("cargo:rustc-cfg=BUILDINFO_IS_GIT_BUILD"); + + std::path::PathBuf::from(std::ffi::OsString::from_vec(git_dir)) + } + Err(msg) => { + // We're not in a git repo, most likely we're building from a source archive + println!("cargo:warning=unable to determine git version (not in git repository?)"); + println!("cargo:warning={}", msg); + + // It's unlikely, but possible that someone could run git init. Might as well catch that. + println!("cargo::rerun-if-changed={}/.git", pkg_dir.display()); + return Ok(()); + } + }; + + // Make Cargo rerun us if currently checked out commit or the state of the + // working tree changes. We try to accomplish that by looking at a few + // crucial git state files. This probably may result in some false + // negatives but it’s best we’ve got. + for subpath in ["HEAD", "logs/HEAD", "index"] { + let path = git_dir.join(subpath).canonicalize()?; + println!("cargo:rerun-if-changed={}", path.display()); + } + + // Get the full git hash + let args = &["rev-parse", "--output-object-format=sha1", "HEAD"]; + let out = command("git", args, None)?; + match String::from_utf8_lossy(&out) { + std::borrow::Cow::Borrowed(full_hash) => { + println!("cargo:rustc-env=BUILDINFO_GIT_FULL_HASH={}", full_hash.trim()); + } + std::borrow::Cow::Owned(full_hash) => return Err(Error::Git(full_hash)), + } + + // Get the short git hash + let args = &["rev-parse", "--output-object-format=sha1", "--short", "HEAD"]; + let out = command("git", args, None)?; + match String::from_utf8_lossy(&out) { + std::borrow::Cow::Borrowed(short_hash) => { + println!("cargo:rustc-env=BUILDINFO_GIT_SHORT_HASH={}", short_hash.trim()); + } + std::borrow::Cow::Owned(short_hash) => return Err(Error::Git(short_hash)), + } + + // Get whether this is built from a dirty tree + let args = &["status", "--porcelain"]; + let out = command("git", args, None)?; + match String::from_utf8_lossy(&out) { + std::borrow::Cow::Borrowed(output) => match output.trim().len() { + 0 => {} + _ => println!("cargo:rustc-cfg=BUILDINFO_IS_DIRTY"), + }, + std::borrow::Cow::Owned(output) => return Err(Error::Git(output)), + } + + // Get the commit summary + let args = &["show", "--format=\"%s\"", "-s"]; + let out = command("git", args, None)?; + match String::from_utf8_lossy(&out) { + std::borrow::Cow::Borrowed(summary) => { + println!("cargo:rustc-env=BUILDINFO_GIT_SUMMARY={}", summary.trim()); + } + std::borrow::Cow::Owned(summary) => return Err(Error::Git(summary)), + } + + Ok(()) +} + +fn get_build_time() -> Result<(), Error> { + // Propagate SOURCE_DATE_EPOCH if set + if let Ok(epoch_env) = env("SOURCE_DATE_EPOCH") { + if let Ok(seconds) = epoch_env.to_string_lossy().parse::() { + if let Some(time) = DateTime::from_timestamp(seconds, 0) { + println!("cargo:rustc-env=BUILDINFO_BUILD_TIME={}", time.timestamp()); + return Ok(()); + } + } + } + + println!("cargo:rustc-env=BUILDINFO_BUILD_TIME={}", Utc::now().timestamp()); + Ok(()) +} + +fn main() { + if let Err(err) = try_main() { + eprintln!("{}", err); + std::process::exit(1); + } +} + +fn try_main() -> Result<(), Error> { + // This should include all top-level directories that contain source code or otherwise modify the build in meaningful ways + let top_level = std::path::PathBuf::from("../..").canonicalize()?; + println!("cargo::rerun-if-changed={}/boulder", top_level.display()); + println!("cargo::rerun-if-changed={}/crates", top_level.display()); + println!("cargo::rerun-if-changed={}/moss", top_level.display()); + println!("cargo::rerun-if-changed={}/test", top_level.display()); + println!("cargo::rerun-if-changed={}/Cargo.toml", top_level.display()); + + let version = env("CARGO_PKG_VERSION")?; + println!("cargo:rustc-env=BUILDINFO_VERSION={}", version.to_string_lossy()); + + get_build_time()?; + + get_git_info()?; + + Ok(()) +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("Missing `{0}` environment variable")] + Env(String), + #[error("{0}: terminated with {1}")] + CommandExit(String, String), + #[error("{0}: killed by signal")] + CommandKilled(String), + #[error("git: invalid output: {0}")] + Git(String), + #[error("io")] + Io(#[from] io::Error), +} diff --git a/crates/serpent_buildinfo/src/lib.rs b/crates/serpent_buildinfo/src/lib.rs new file mode 100644 index 00000000..3c683423 --- /dev/null +++ b/crates/serpent_buildinfo/src/lib.rs @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright © 2020-2024 Serpent OS Developers +// +// SPDX-License-Identifier: MPL-2.0 + +use std::num::ParseIntError; + +use chrono::DateTime; + +mod values; + +/// Returns the version of the project, as defined in the top-level Cargo.toml +/// +/// This will look like "0.1.0" +pub const fn get_version() -> &'static str { + values::VERSION +} + +/// Returns the build time of the project, printed in UTC time format +/// +/// If SOURCE_DATE_EPOCH is set during the build then that will be the timestamp returned +/// +/// This will look like "Tue, 9 Jul 2024 03:28:36 +0000" +pub fn get_build_time() -> Result { + let time = values::BUILD_TIME.parse::()?; + let build_time = DateTime::from_timestamp(time, 0).unwrap(); + + Ok(build_time.to_rfc2822()) +} + +/// Returns `true` if the project was built from a git source, `false` otherwise +pub const fn get_if_git_build() -> bool { + cfg!(BUILDINFO_IS_GIT_BUILD) +} + +/// Returns `true` if the project was built from a dirty git source, `false` otherwise +pub const fn get_if_git_dirty() -> bool { + cfg!(BUILDINFO_IS_DIRTY) +} + +/// Returns the git hash that the project was built from if built from a git source +/// +/// This currently returns the SHA1 hash, though eventually it will return the SHA256 one +/// +/// If built from a non-git source (like a release archive) this will return "unknown" +#[cfg(BUILDINFO_IS_GIT_BUILD)] +pub const fn get_git_full_hash() -> &'static str { + values::GIT_FULL_HASH +} + +/// Returns the git hash that the project was built from if built from a git source +/// +/// This currently returns the SHA1 hash, though eventually it will return the SHA256 one +/// +/// If built from a non-git source (like a release archive) this will return "unknown" +#[cfg(not(BUILDINFO_IS_GIT_BUILD))] +pub const fn get_git_full_hash() -> &'static str { + "unknown" +} + +/// Returns the shortened form of the git hash that this project was built from if built from git source +/// +/// If built from a non-git source (like a release archive) this will return "unknown" +#[cfg(BUILDINFO_IS_GIT_BUILD)] +pub const fn get_git_short_hash() -> &'static str { + values::GIT_SHORT_HASH +} + +/// Returns the shortened form of the git hash that this project was built from if built from git source +/// +/// If built from a non-git source (like a release archive) this will return "unknown" +#[cfg(not(BUILDINFO_IS_GIT_BUILD))] +pub const fn get_git_short_hash() -> &'static str { + "unknown" +} + +/// Returns the summary of the git commit that the project was built from +/// +/// If built from a non-git source (like a release archive) this will return "unknown" +#[cfg(BUILDINFO_IS_GIT_BUILD)] +pub const fn get_git_summary() -> &'static str { + values::GIT_SUMMARY +} + +/// Returns the summary of the git commit that the project was built from +/// +/// If built from a non-git source (like a release archive) this will return "unknown" +#[cfg(not(BUILDINFO_IS_GIT_BUILD))] +pub const fn get_git_summary() -> &'static str { + "unknown" +} + +/// For git builds this returns a string like `v0.1.0 (git 4ecad5d7e70c2cdc81350dc6b46fb55b1ccb18f5-dirty)` +/// +/// For builds from a non-git source just the version will be returned: `v0.1.0` +pub fn get_simple_version() -> String { + if cfg!(BUILDINFO_IS_GIT_BUILD) { + let dirty = if get_if_git_dirty() { "-dirty" } else { "" }; + format!("v{} (git {}{})", values::VERSION, get_git_full_hash(), dirty) + } else { + format!("v{}", values::VERSION) + } +} diff --git a/crates/serpent_buildinfo/src/values.rs b/crates/serpent_buildinfo/src/values.rs new file mode 100644 index 00000000..8c54d1ac --- /dev/null +++ b/crates/serpent_buildinfo/src/values.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright © 2020-2024 Serpent OS Developers +// +// SPDX-License-Identifier: MPL-2.0 + +pub(crate) const VERSION: &str = env!("BUILDINFO_VERSION"); + +pub(crate) const BUILD_TIME: &str = env!("BUILDINFO_BUILD_TIME"); + +#[cfg(BUILDINFO_IS_GIT_BUILD)] +pub(crate) const GIT_FULL_HASH: &str = env!("BUILDINFO_GIT_FULL_HASH"); + +#[cfg(BUILDINFO_IS_GIT_BUILD)] +pub(crate) const GIT_SHORT_HASH: &str = env!("BUILDINFO_GIT_SHORT_HASH"); + +#[cfg(BUILDINFO_IS_GIT_BUILD)] +pub(crate) const GIT_SUMMARY: &str = env!("BUILDINFO_GIT_SUMMARY"); diff --git a/moss/Cargo.toml b/moss/Cargo.toml index f124b509..e28045c6 100644 --- a/moss/Cargo.toml +++ b/moss/Cargo.toml @@ -8,6 +8,7 @@ rust-version.workspace = true config = { path = "../crates/config" } container = { path = "../crates/container" } dag = { path = "../crates/dag" } +serpent_buildinfo = { path = "../crates/serpent_buildinfo" } stone = { path = "../crates/stone" } triggers = { path = "../crates/triggers" } tui = { path = "../crates/tui" } diff --git a/moss/build.rs b/moss/build.rs deleted file mode 100644 index a7d67723..00000000 --- a/moss/build.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::process::Command; - -fn main() { - if let Ok(hash) = git_hash() { - println!("cargo:rustc-env=GIT_HASH={}", hash); - } - - println!("cargo:rerun-if-changed=../boulder/src"); - println!("cargo:rerun-if-changed=../Cargo.lock"); -} - -fn git_hash() -> Result> { - let output = Command::new("git").args(["rev-parse", "--short", "HEAD"]).output()?; - Ok(String::from_utf8(output.stdout)?) -} diff --git a/moss/src/cli/version.rs b/moss/src/cli/version.rs index 1998c13a..b32b0af1 100644 --- a/moss/src/cli/version.rs +++ b/moss/src/cli/version.rs @@ -3,7 +3,6 @@ // SPDX-License-Identifier: MPL-2.0 use clap::Command; -use moss::environment; /// Construct the Version command pub fn command() -> Command { @@ -12,9 +11,5 @@ pub fn command() -> Command { /// Print program version pub fn print() { - let hash = environment::GIT_HASH - .map(|hash| format!(" ({hash})")) - .unwrap_or_default(); - - println!("moss {}{hash}", environment::VERSION); + println!("moss {}", serpent_buildinfo::get_simple_version()); } diff --git a/moss/src/client/mod.rs b/moss/src/client/mod.rs index e731ec8d..62e48862 100644 --- a/moss/src/client/mod.rs +++ b/moss/src/client/mod.rs @@ -774,7 +774,7 @@ PRETTY_NAME="Serpent OS {version} (fstx #{tx})" ANSI_COLOR="1;35" HOME_URL="https://serpentos.com" BUG_REPORT_URL="https://github.com/serpent-os""#, - version = environment::VERSION, + version = serpent_buildinfo::get_version(), // TODO: Better id for ephemeral transactions tx = state_id.unwrap_or_default() ); diff --git a/moss/src/environment.rs b/moss/src/environment.rs index 62b94b0e..b732e0d4 100644 --- a/moss/src/environment.rs +++ b/moss/src/environment.rs @@ -3,8 +3,6 @@ // SPDX-License-Identifier: MPL-2.0 pub const NAME: &str = env!("CARGO_PKG_NAME"); -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const GIT_HASH: Option<&str> = option_env!("GIT_HASH"); /// Max concurrency for disk tasks pub const MAX_DISK_CONCURRENCY: usize = 16; /// Max concurrency for network tasks