// Copyright 2023 Greptime Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use std::process::Command; use std::{env, fs, io}; use cargo_manifest::Manifest; use git2::{ErrorCode, Repository, RepositoryOpenFlags, StatusOptions}; const SHADOW_FILE_NAME: &str = "shadow.rs"; fn main() -> Result<(), Box> { // Refresh VCS-derived build info only in release builds. In non-release builds (debug, // bench, etc.), skip refreshing to preserve incremental compilation. let profile = env::var("PROFILE").unwrap_or_default(); let refresh = profile == "release"; println!("cargo:rerun-if-env-changed=RUSTC"); println!("cargo:rerun-if-env-changed=GREPTIME_PRODUCT_NAME"); // The "CARGO_WORKSPACE_DIR" is set manually (not by Rust itself) in Cargo config file, to // solve the problem where the "CARGO_MANIFEST_DIR" is not what we want when this repo is // made as a submodule in another repo. let workspace_dir = env::var("CARGO_WORKSPACE_DIR").or_else(|_| env::var("CARGO_MANIFEST_DIR"))?; let workspace_root = PathBuf::from(&workspace_dir); println!( "cargo:rerun-if-changed={}", workspace_root.join("Cargo.toml").display() ); let product_version = load_product_version(&workspace_root); println!("cargo:rustc-env=GREPTIME_PRODUCT_VERSION={product_version}"); let product_name = load_product_name(); println!("cargo:rustc-env=GREPTIME_PRODUCT_NAME={product_name}"); let repository = open_repository(&workspace_root); if refresh { emit_workspace_watch_list(&workspace_root, repository.as_ref())?; emit_git_watch_list(repository.as_ref()); } let version_state = VersionState::collect(repository.as_ref()); let out_dir = PathBuf::from(env::var("OUT_DIR")?); let shadow_file = out_dir.join(SHADOW_FILE_NAME); write_if_changed(&shadow_file, render_shadow_rs(&version_state))?; Ok(()) } #[derive(Debug, Clone)] struct VersionState { branch: String, commit_hash: String, short_commit: String, git_clean: bool, rust_version: String, build_target: String, } impl VersionState { fn collect(repository: Option<&Repository>) -> Self { Self { branch: git_branch(repository), commit_hash: git_commit_hash(repository), short_commit: git_short_commit(repository), git_clean: git_clean(repository), rust_version: rustc_version(), build_target: env::var("TARGET").unwrap_or_default(), } } } fn load_product_version(workspace_root: &Path) -> String { let manifest = Manifest::from_path(workspace_root.join("Cargo.toml")).expect("Failed to parse Cargo.toml"); manifest .workspace .as_ref() .and_then(|w| { w.metadata.as_ref().and_then(|m| { m.get("greptime") .and_then(|g| g.get("product_version").and_then(|v| v.as_str())) }) }) .map(str::to_string) .unwrap_or_else(|| env::var("CARGO_PKG_VERSION").unwrap()) } fn load_product_name() -> String { env::var("GREPTIME_PRODUCT_NAME").unwrap_or_else(|_| "GreptimeDB".to_string()) } fn emit_workspace_watch_list( workspace_root: &Path, repository: Option<&Repository>, ) -> Result<(), Box> { let mut watch_roots = BTreeSet::new(); if let Some(paths) = git_tracked_files(workspace_root, repository) { watch_roots.extend(tracked_watch_roots(workspace_root, &paths)); } if let Some(paths) = git_status_paths(workspace_root, repository) { watch_roots.extend(tracked_watch_roots(workspace_root, &paths)); } for path in watch_roots { println!("cargo:rerun-if-changed={}", path.display()); } Ok(()) } fn tracked_watch_roots(workspace_root: &Path, tracked_files: &[PathBuf]) -> BTreeSet { tracked_files .iter() .filter_map(|path| path.strip_prefix(workspace_root).ok()) .filter_map(|relative| { relative .components() .next() .map(|first| workspace_root.join(first)) }) .collect() } fn git_tracked_files( workspace_root: &Path, repository: Option<&Repository>, ) -> Option> { let repository = repository?; let index = match repository.index() { Ok(index) => index, Err(err) => { cargo_warning(format!( "Failed to read git index for build watch list: {err}. Git-derived build info may become stale." )); return None; } }; Some( index .iter() .filter_map(|entry| { std::str::from_utf8(entry.path.as_ref()) .ok() .map(|path| workspace_root.join(path)) }) .collect(), ) } fn git_status_paths( workspace_root: &Path, repository: Option<&Repository>, ) -> Option> { let repository = repository?; let mut options = status_options(); match repository.statuses(Some(&mut options)) { Ok(statuses) => Some( statuses .iter() .filter_map(|entry| entry.path().map(|path| workspace_root.join(path))) .collect(), ), Err(err) => { cargo_warning(format!( "Failed to read git status for build watch list: {err}. Git-derived build info may become stale." )); None } } } fn emit_git_watch_list(repository: Option<&Repository>) { let Some(repository) = repository else { return; }; let git_dir = repository.path(); for path in [ git_dir.join("HEAD"), git_dir.join("index"), git_dir.join("packed-refs"), ] { println!("cargo:rerun-if-changed={}", path.display()); } if let Some(head_ref) = repository .head() .ok() .and_then(|head| head.name().map(str::to_string)) .filter(|head_ref| head_ref.starts_with("refs/")) { println!( "cargo:rerun-if-changed={}", git_dir.join(head_ref).display() ); } } fn render_shadow_rs(version_state: &VersionState) -> String { format!( "// Code automatically generated by src/common/version/build.rs, do not edit.\n\ pub const BRANCH: &str = {branch:?};\n\ pub const COMMIT_HASH: &str = {commit_hash:?};\n\ pub const SHORT_COMMIT: &str = {short_commit:?};\n\ pub const GIT_CLEAN: bool = {git_clean};\n\ pub const RUST_VERSION: &str = {rust_version:?};\n\ pub const BUILD_TARGET: &str = {build_target:?};\n", branch = version_state.branch, commit_hash = version_state.commit_hash, short_commit = version_state.short_commit, git_clean = version_state.git_clean, rust_version = version_state.rust_version, build_target = version_state.build_target, ) } fn write_if_changed(path: &Path, content: String) -> io::Result<()> { if fs::read_to_string(path).ok().as_deref() == Some(content.as_str()) { return Ok(()); } fs::write(path, content) } fn open_repository(workspace_root: &Path) -> Option { match Repository::open_ext( workspace_root, RepositoryOpenFlags::NO_SEARCH, std::iter::empty::<&Path>(), ) { Ok(repository) => Some(repository), Err(err) if err.code() == ErrorCode::NotFound => None, Err(err) => { cargo_warning(format!( "Failed to open git repository at {}: {err}. Git-derived build info may be unavailable.", workspace_root.display() )); None } } } fn git_branch(repository: Option<&Repository>) -> String { repository .and_then(|repo| repo.head().ok()) .filter(|head| head.is_branch()) .and_then(|head| head.shorthand().map(str::to_string)) .unwrap_or_default() } fn git_commit_hash(repository: Option<&Repository>) -> String { repository .and_then(|repo| head_target(Some(repo))) .map(|oid| oid.to_string()) .unwrap_or_default() } fn git_short_commit(repository: Option<&Repository>) -> String { repository .and_then(|repo| { let object = repo.find_object(head_target(Some(repo))?, None).ok()?; object.short_id().ok()?.as_str().map(str::to_string) }) .unwrap_or_default() } fn git_clean(repository: Option<&Repository>) -> bool { let Some(repository) = repository else { return false; }; let mut options = status_options(); repository .statuses(Some(&mut options)) .map(|statuses| statuses.is_empty()) .unwrap_or(false) } fn head_target(repository: Option<&Repository>) -> Option { repository?.head().ok()?.target() } fn rustc_version() -> String { let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); Command::new(rustc) .arg("--version") .output() .ok() .filter(|output| output.status.success()) .and_then(|output| String::from_utf8(output.stdout).ok()) .map(|stdout| stdout.trim().to_string()) .unwrap_or_default() } fn status_options() -> StatusOptions { let mut options = StatusOptions::new(); options .include_untracked(true) .recurse_untracked_dirs(true) .renames_head_to_index(true); options } fn cargo_warning(message: impl AsRef) { println!("cargo:warning={}", message.as_ref()); }