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