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