Skip to main content

linera_version/version_info/
type.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{io::Read as _, path::PathBuf};
5
6#[cfg(linera_version_building)]
7use crate::serde_pretty::Pretty;
8
9/// A semantic version number of a crate.
10#[cfg_attr(linera_version_building, derive(serde::Deserialize, serde::Serialize))]
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct CrateVersion {
13    /// The major version number.
14    pub major: u32,
15    /// The minor version number.
16    pub minor: u32,
17    /// The patch version number.
18    pub patch: u32,
19}
20
21impl From<semver::Version> for CrateVersion {
22    #[expect(
23        clippy::cast_possible_truncation,
24        reason = "semver components fit in u32 for any realistic version"
25    )]
26    fn from(
27        semver::Version {
28            major,
29            minor,
30            patch,
31            ..
32        }: semver::Version,
33    ) -> Self {
34        Self {
35            major: major as u32,
36            minor: minor as u32,
37            patch: patch as u32,
38        }
39    }
40}
41
42impl From<CrateVersion> for semver::Version {
43    fn from(
44        CrateVersion {
45            major,
46            minor,
47            patch,
48        }: CrateVersion,
49    ) -> Self {
50        Self::new(major as u64, minor as u64, patch as u64)
51    }
52}
53
54/// A hash of an API surface, stored as a hexadecimal string.
55pub type Hash = std::borrow::Cow<'static, str>;
56
57#[cfg_attr(linera_version_building, derive(serde::Deserialize, serde::Serialize))]
58#[derive(Clone, Debug, PartialEq, Eq, Hash)]
59/// The version info of a build of Linera.
60pub struct VersionInfo {
61    /// The crate version
62    pub crate_version: Pretty<CrateVersion, semver::Version>,
63    /// The git commit hash
64    pub git_commit: Hash,
65    /// Whether the git checkout was dirty
66    pub git_dirty: bool,
67    /// A hash of the RPC API
68    pub rpc_hash: Hash,
69    /// A hash of the GraphQL API
70    pub graphql_hash: Hash,
71    /// A hash of the WIT API
72    pub wit_hash: Hash,
73}
74
75#[cfg(linera_version_building)]
76async_graphql::scalar!(VersionInfo);
77
78/// An error that can occur while extracting version information.
79#[derive(Debug, thiserror::Error)]
80#[allow(missing_docs)]
81pub enum Error {
82    #[error("failed to interpret cargo-metadata: {0}")]
83    CargoMetadata(#[from] cargo_metadata::Error),
84    #[error("no such package: {0}")]
85    NoSuchPackage(String),
86    #[error("I/O error: {0}")]
87    IoError(#[from] std::io::Error),
88    #[error("glob error: {0}")]
89    Glob(#[from] glob::GlobError),
90    #[error("pattern error: {0}")]
91    Pattern(#[from] glob::PatternError),
92    #[error("JSON error: {0}")]
93    JsonError(#[from] serde_json::Error),
94}
95
96struct Outcome {
97    status: std::process::ExitStatus,
98    output: String,
99}
100
101fn get_hash(
102    relevant_paths: &mut Vec<PathBuf>,
103    metadata: &cargo_metadata::Metadata,
104    package: &str,
105    glob: &str,
106) -> Result<String, Error> {
107    use base64::engine::{general_purpose::STANDARD_NO_PAD, Engine as _};
108    use sha3::Digest as _;
109
110    let package_root = get_package_root(metadata, package)
111        .ok_or_else(|| Error::NoSuchPackage(package.to_owned()))?;
112    let mut hasher = sha3::Sha3_256::new();
113    let mut buffer = [0u8; 4096];
114
115    let package_glob = format!("{}/{}", package_root.display(), glob);
116
117    let mut n_file = 0;
118    for path in glob::glob(&package_glob)? {
119        let path = path?;
120        let mut file = fs_err::File::open(&path)?;
121        relevant_paths.push(path);
122        n_file += 1;
123        while file.read(&mut buffer)? != 0 {
124            hasher.update(buffer);
125        }
126    }
127    assert!(n_file > 0);
128
129    Ok(STANDARD_NO_PAD.encode(hasher.finalize()))
130}
131
132fn run(cmd: &str, args: &[&str]) -> Result<Outcome, Error> {
133    let mut cmd = std::process::Command::new(cmd);
134
135    let mut child = cmd
136        .args(args)
137        .stdout(std::process::Stdio::piped())
138        .spawn()?;
139
140    let mut output = String::new();
141    child.stdout.take().unwrap().read_to_string(&mut output)?;
142
143    Ok(Outcome {
144        status: child.wait()?,
145        output,
146    })
147}
148
149fn get_package<'r>(
150    metadata: &'r cargo_metadata::Metadata,
151    package_name: &str,
152) -> Option<&'r cargo_metadata::Package> {
153    metadata.packages.iter().find(|p| p.name == package_name)
154}
155
156fn get_package_root<'r>(
157    metadata: &'r cargo_metadata::Metadata,
158    package_name: &str,
159) -> Option<&'r std::path::Path> {
160    Some(
161        get_package(metadata, package_name)?
162            .targets
163            .first()
164            .expect("package must have at least one target")
165            .src_path
166            .ancestors()
167            .find(|p| p.join("Cargo.toml").exists())
168            .expect("package should have a Cargo.toml")
169            .as_std_path(),
170    )
171}
172
173#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
174struct CargoVcsInfo {
175    path_in_vcs: PathBuf,
176    git: CargoVcsInfoGit,
177}
178
179#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
180struct CargoVcsInfoGit {
181    sha1: String,
182}
183
184/// The hashes of the protocol's external APIs.
185#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
186pub struct ApiHashes {
187    /// A hash of the RPC API.
188    pub rpc: String,
189    /// A hash of the GraphQL API.
190    pub graphql: String,
191    /// A hash of the WIT API.
192    pub wit: String,
193}
194
195impl VersionInfo {
196    /// Extracts the version info for the current build.
197    pub fn get() -> Result<Self, Error> {
198        Self::trace_get(
199            std::path::Path::new(env!("CARGO_MANIFEST_DIR")),
200            &mut vec![],
201        )
202    }
203
204    fn trace_get(crate_dir: &std::path::Path, paths: &mut Vec<PathBuf>) -> Result<Self, Error> {
205        let metadata = cargo_metadata::MetadataCommand::new()
206            .current_dir(crate_dir)
207            .exec()?;
208
209        let crate_version = Pretty::new(
210            get_package(&metadata, env!("CARGO_PKG_NAME"))
211                .expect("this package must be in the dependency tree")
212                .version
213                .clone()
214                .into(),
215        );
216
217        let cargo_vcs_info_path = crate_dir.join(".cargo_vcs_info.json");
218        let api_hashes_path = crate_dir.join("api-hashes.json");
219        let mut git_dirty = false;
220        let git_commit = if let Ok(git_commit) = std::env::var("GIT_COMMIT") {
221            git_commit
222        } else if cargo_vcs_info_path.is_file() {
223            let cargo_vcs_info: CargoVcsInfo =
224                serde_json::from_reader(std::fs::File::open(cargo_vcs_info_path)?)?;
225            cargo_vcs_info.git.sha1
226        } else {
227            let git_outcome = run("git", &["rev-parse", "HEAD"])?;
228            if git_outcome.status.success() {
229                git_dirty = run("git", &["diff-index", "--quiet", "HEAD"])?
230                    .status
231                    .code()
232                    == Some(1);
233                git_outcome.output[..10].to_owned()
234            } else {
235                format!("v{crate_version}")
236            }
237        }
238        .into();
239
240        let api_hashes: ApiHashes = serde_json::from_reader(fs_err::File::open(api_hashes_path)?)?;
241
242        let rpc_hash = get_hash(
243            paths,
244            &metadata,
245            "linera-rpc",
246            "tests/snapshots/format__format.yaml.snap",
247        )
248        .unwrap_or(api_hashes.rpc)
249        .into();
250
251        let graphql_hash = get_hash(
252            paths,
253            &metadata,
254            "linera-service-graphql-client",
255            "gql/*.graphql",
256        )
257        .unwrap_or(api_hashes.graphql)
258        .into();
259
260        let wit_hash = get_hash(paths, &metadata, "linera-sdk", "wit/*.wit")
261            .unwrap_or(api_hashes.wit)
262            .into();
263
264        Ok(Self {
265            crate_version,
266            git_commit,
267            git_dirty,
268            rpc_hash,
269            graphql_hash,
270            wit_hash,
271        })
272    }
273
274    /// Returns the hashes of the RPC, GraphQL, and WIT APIs.
275    pub fn api_hashes(&self) -> ApiHashes {
276        ApiHashes {
277            rpc: self.rpc_hash.clone().into_owned(),
278            wit: self.wit_hash.clone().into_owned(),
279            graphql: self.graphql_hash.clone().into_owned(),
280        }
281    }
282}