linera_version/version_info/
type.rs1use 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))]
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct CrateVersion {
13 pub major: u32,
15 pub minor: u32,
17 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
54pub 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)]
59pub struct VersionInfo {
61 pub crate_version: Pretty<CrateVersion, semver::Version>,
63 pub git_commit: Hash,
65 pub git_dirty: bool,
67 pub rpc_hash: Hash,
69 pub graphql_hash: Hash,
71 pub wit_hash: Hash,
73}
74
75#[cfg(linera_version_building)]
76async_graphql::scalar!(VersionInfo);
77
78#[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#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
186pub struct ApiHashes {
187 pub rpc: String,
189 pub graphql: String,
191 pub wit: String,
193}
194
195impl VersionInfo {
196 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 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}