opentalk_version/
lib.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4//! # opentalk-version
5//!
6//! Collect a set of build information and make it available for printing.
7//!
8//! ## Example usage:
9//!
10//! Add the following to your `build.rs`
11//!
12//! ```rust
13//! fn main() -> anyhow::Result<()> {
14//!     opentalk_version::collect_build_information()
15//! }
16//! ```
17//!
18//! Once this is added, you can access the build information in your crate like this:
19//!
20//! ```ignore
21//! // This adds a `build_info` module which contains a `BuildInfo` struct that
22//! // contains all collected build information.
23//! use build_info::BuildInfo;
24//! use opentalk_version::InfoArgs;
25//!
26//! opentalk_version::build_info!();
27//!
28//! pub(super) fn print_version(args: &InfoArgs) {
29//!     let build_info = BuildInfo::new();
30//!     if let Some(text) = build_info.format(&args) {
31//!         println!("{text}");
32//!     }
33//! }
34//!
35//! ```
36
37use anyhow::Context as _;
38use clap::ArgAction;
39use vergen::{BuildBuilder, CargoBuilder, Emitter, RustcBuilder};
40use vergen_git2::Git2Builder;
41
42/// This macro will expand to a module `build_info` which contains a struct `BuildInfo`.
43/// The struct can be used to access build information.
44///
45/// # Note
46///
47/// Make sure to call [`collect_build_information`] in your [build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html).
48#[macro_export]
49macro_rules! build_info {
50    () => {
51        pub(crate) mod build_info {
52            fn profile_human() -> &'static str {
53                let profile = env!("VERGEN_CARGO_OPT_LEVEL");
54
55                // Copied from https://doc.rust-lang.org/cargo/reference/profiles.html#opt-level
56                match profile {
57                    "0" => "0, no optimizations",
58                    "1" => "1, basic optimizations",
59                    "2" => "2, some optimizations",
60                    "3" => "3, all optimizations",
61                    "s" => "'s', optimize for binary size",
62                    "z" => "'z', optimize for binary size, but also turn off loop vectorization.",
63                    profile => profile,
64                }
65            }
66
67            /// Build information about the crate.
68            #[derive(Debug, Clone)]
69            pub struct BuildInfo {
70                build_timestamp: &'static str,
71                build_version: &'static str,
72                commit_sha: Option<&'static str>,
73                commit_dirty: Option<&'static str>,
74                commit_date: Option<&'static str>,
75                commit_branch: Option<&'static str>,
76                rustc_version: &'static str,
77                rustc_channel: &'static str,
78                rustc_host_triple: &'static str,
79                cargo_target_triple: &'static str,
80                cargo_profile: &'static str,
81                license_information: Option<String>,
82            }
83
84            impl BuildInfo {
85                /// Returns a struct which contains build information.
86                pub fn new() -> Self {
87                    Self {
88                        build_timestamp: env!("VERGEN_BUILD_TIMESTAMP"),
89                        build_version: env!("CARGO_PKG_VERSION"),
90                        commit_sha: option_env!("VERGEN_GIT_SHA"),
91                        commit_dirty: option_env!("VERGEN_GIT_DIRTY"),
92                        commit_date: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
93                        commit_branch: option_env!("VERGEN_GIT_BRANCH"),
94                        rustc_version: env!("VERGEN_RUSTC_SEMVER"),
95                        rustc_channel: env!("VERGEN_RUSTC_CHANNEL"),
96                        rustc_host_triple: env!("VERGEN_RUSTC_HOST_TRIPLE"),
97                        cargo_target_triple: env!("VERGEN_CARGO_TARGET_TRIPLE"),
98                        cargo_profile: profile_human(),
99                        license_information: None,
100                    }
101                }
102
103                pub fn with_license(license_information: String) -> Self {
104                    let mut this = Self::new();
105                    this.license_information = Some(license_information);
106                    this
107                }
108            }
109
110            impl BuildInfo {
111                pub fn format(&self, args: &opentalk_version::InfoArgs) -> Option<String> {
112                    if !args.should_print() {
113                        return None;
114                    }
115                    let mut text = String::new();
116                    if (args.version) {
117                        let _ = self.version(&mut text);
118                    }
119                    if (args.license) {
120                        self.license(&mut text);
121                    }
122                    Some(text)
123                }
124
125                fn version(&self, text: &mut String) -> Result<(), std::fmt::Error> {
126                    use std::fmt::Write as _;
127
128                    write!(text, "Build Timestamp: {}\n", self.build_timestamp)?;
129                    write!(text, "Build Version: {}\n", self.build_version)?;
130                    if let Some(sha) = self.commit_sha {
131                        write!(text, "Commit SHA: {}", sha)?;
132                        match self.commit_dirty {
133                            Some("true") => write!(text, "-dirty\n")?,
134                            _ => write!(text, "\n")?,
135                        }
136                    }
137                    if let Some(commit_date) = self.commit_date {
138                        write!(text, "Commit Date: {}\n", commit_date)?;
139                    }
140
141                    if let Some(commit_branch) = self.commit_branch {
142                        write!(text, "Commit Branch: {}\n", commit_branch)?;
143                    }
144                    write!(text, "rustc Version: {}\n", self.rustc_version)?;
145                    write!(text, "rustc Channel: {}\n", self.rustc_channel)?;
146                    write!(text, "rustc Host Triple: {}\n", self.rustc_host_triple)?;
147                    write!(text, "cargo Target Triple: {}\n", self.cargo_target_triple)?;
148                    write!(text, "cargo Profile: {}\n", self.cargo_profile)
149                }
150
151                fn license(&self, text: &mut String) {
152                    if let Some(license) = self.license_information.as_deref() {
153                        text.push_str(license);
154                    }
155                }
156            }
157        }
158    };
159}
160
161#[derive(clap::Args, Debug, Clone)]
162pub struct InfoArgs {
163    /// Print long version description and exit.
164    #[arg(short('V'), long, action=ArgAction::SetTrue, help = "Print version information")]
165    pub version: bool,
166    /// Print license information and exit.
167    #[arg(short, long, action=ArgAction::SetTrue, help = "Print license information")]
168    pub license: bool,
169}
170
171impl InfoArgs {
172    pub fn should_print(&self) -> bool {
173        self.version || self.license
174    }
175}
176
177/// Collects the information that can be used later to identify the version of this crate.
178///
179/// # Note
180///
181/// This should be used inside the build script of your crate.
182pub fn collect_build_information() -> anyhow::Result<()> {
183    let mut emitter = Emitter::default();
184    let builder = emitter
185        .add_instructions(&CargoBuilder::all_cargo().context("Failed to build cargo variables")?)
186        .context("Failed to add cargo instructions")?
187        .add_instructions(&BuildBuilder::all_build().context("Failed to build builder variables")?)
188        .context("Failed to add builder variables")?
189        .add_instructions(&RustcBuilder::all_rustc().context("Failed to build rustc variables")?)
190        .context("Failed to add rustc variables")?;
191
192    if is_contained_in_git()? {
193        builder
194            .add_instructions(&Git2Builder::all_git().context("Failed to build git variables")?)
195            .context("Failed to add git variables")?;
196    }
197    builder.emit().context("Failed to emit")?;
198
199    Ok(())
200}
201
202/// Checks whether the current or one of the parent directories contains a `.git` entry.
203fn is_contained_in_git() -> anyhow::Result<bool> {
204    let current_dir = std::env::current_dir().context("Failed to get current dir")?;
205    let mut path = &*current_dir
206        .canonicalize()
207        .context("Failed to canonicalize path")?;
208    loop {
209        if path.join(".git").exists() {
210            return Ok(true);
211        }
212        let Some(parent) = path.parent() else {
213            println!("cargo::warning=No .git directory found");
214            return Ok(false);
215        };
216        path = parent;
217    }
218}