diff --git a/Cargo.lock b/Cargo.lock index f11ee4c9a3..96b5cbf5b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1463,16 +1463,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" -[[package]] -name = "build-data" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23084982b6264a75acfd469b97ce0ef9301e154e7af51fec388e695eedf01bc1" -dependencies = [ - "chrono", - "safe-regex", -] - [[package]] name = "bumpalo" version = "3.19.0" @@ -2826,11 +2816,10 @@ dependencies = [ name = "common-version" version = "1.0.0-rc.1" dependencies = [ - "build-data", "cargo-manifest", "const_format", + "git2", "serde", - "shadow-rs", ] [[package]] @@ -5669,6 +5658,19 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "git2" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +dependencies = [ + "bitflags 2.9.1", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "glob" version = "0.3.2" @@ -6706,12 +6708,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "is_debug" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407" - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -7286,6 +7282,18 @@ dependencies = [ "cc", ] +[[package]] +name = "libgit2-sys" +version = "0.18.3+1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.8.8" @@ -7350,6 +7358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", + "libc", "pkg-config", "vcpkg", ] @@ -11660,53 +11669,6 @@ dependencies = [ "serde", ] -[[package]] -name = "safe-proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "492d1a72624b0bd5b7f0193ea5834a1905534a517573a117e949e895f342906c" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "safe-quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcaa9a650f2f98ba4da0190623210c85945cb78b262709f606c57655eda173e1" -dependencies = [ - "safe-proc-macro2", -] - -[[package]] -name = "safe-regex" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5194fafa3cb9da89e0cab6dffa1f3fdded586bd6396d12be11b4cae0c7ee45c2" -dependencies = [ - "safe-regex-macro", -] - -[[package]] -name = "safe-regex-compiler" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e822ae1e61251bcfd698317c237cf83f7c57161a5dc24ee609a85697f1ed15b3" -dependencies = [ - "safe-proc-macro2", - "safe-quote", -] - -[[package]] -name = "safe-regex-macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2768de7e6ef19f59c5fd3c3ac207ef12b68a49f95e3172d67e4a04cfd992ca06" -dependencies = [ - "safe-proc-macro2", - "safe-regex-compiler", -] - [[package]] name = "safe_arch" version = "0.7.4" @@ -12261,18 +12223,6 @@ dependencies = [ "keccak", ] -[[package]] -name = "shadow-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0b6af233ae5461c3c6b30db79190ec5fbbef048ebbd5f2cbb3043464168e00" -dependencies = [ - "const_format", - "is_debug", - "time", - "tzdb", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -14489,32 +14439,6 @@ dependencies = [ "typify-impl", ] -[[package]] -name = "tz-rs" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1450bf2b99397e72070e7935c89facaa80092ac812502200375f1f7d33c71a1" - -[[package]] -name = "tzdb" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2ea5956f295449f47c0b825c5e109022ff1a6a53bb4f77682a87c2341fbf5" -dependencies = [ - "iana-time-zone", - "tz-rs", - "tzdb_data", -] - -[[package]] -name = "tzdb_data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c4c81d75033770e40fbd3643ce7472a1a9fd301f90b7139038228daf8af03ec" -dependencies = [ - "tz-rs", -] - [[package]] name = "ua-parser" version = "0.2.1" diff --git a/src/common/version/Cargo.toml b/src/common/version/Cargo.toml index adee41afd7..28ded6b240 100644 --- a/src/common/version/Cargo.toml +++ b/src/common/version/Cargo.toml @@ -13,9 +13,7 @@ codec = ["dep:serde"] [dependencies] const_format.workspace = true serde = { workspace = true, optional = true } -shadow-rs = { version = "1.2.1", default-features = false } [build-dependencies] -build-data = "0.2" cargo-manifest = "0.19" -shadow-rs = { version = "1.2.1", default-features = false, features = ["build"] } +git2 = { version = "0.20", default-features = false } diff --git a/src/common/version/build.rs b/src/common/version/build.rs index 9eb6df5fd9..f147918c21 100644 --- a/src/common/version/build.rs +++ b/src/common/version/build.rs @@ -13,80 +13,310 @@ // limitations under the License. use std::collections::BTreeSet; -use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::{env, fs, io}; -use build_data::{format_timestamp, get_source_time}; use cargo_manifest::Manifest; -use shadow_rs::{BuildPattern, CARGO_METADATA, CARGO_TREE, ShadowBuilder}; +use git2::{ErrorCode, Repository, RepositoryOpenFlags, StatusOptions}; -fn main() -> shadow_rs::SdResult<()> { - // Refresh timestamps by default in release builds. In non-release builds (debug, bench, - // etc.), skip refreshing to preserve incremental compilation. - // Set DISABLE_BUILD_INFO=1 to force-disable refreshing even in release builds. +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 disabled = env::var("DISABLE_BUILD_INFO") - .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) - .unwrap_or(false); - let refresh = profile == "release" && !disabled; + let refresh = profile == "release"; - println!("cargo:rerun-if-env-changed=DISABLE_BUILD_INFO"); - - if refresh { - println!( - "cargo:rustc-env=SOURCE_TIMESTAMP={}", - if let Ok(t) = get_source_time() { - format_timestamp(t) - } else { - "".to_string() - } - ); - build_data::set_BUILD_TIMESTAMP(); - } else { - println!("cargo:rustc-env=SOURCE_TIMESTAMP="); - println!("cargo:rustc-env=BUILD_TIMESTAMP="); - } + println!("cargo:rerun-if-env-changed=RUSTC"); // 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 src_path = env::var("CARGO_WORKSPACE_DIR").or_else(|_| env::var("CARGO_MANIFEST_DIR"))?; + 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 manifest = Manifest::from_path(PathBuf::from(&src_path).join("Cargo.toml")) - .expect("Failed to parse Cargo.toml"); - if let Some(product_version) = 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())) - }) - }) { - println!( - "cargo:rustc-env=GREPTIME_PRODUCT_VERSION={}", - product_version - ); - } else { - let version = env::var("CARGO_PKG_VERSION").unwrap(); - println!("cargo:rustc-env=GREPTIME_PRODUCT_VERSION={}", version,); + let product_version = load_product_version(&workspace_root); + println!("cargo:rustc-env=GREPTIME_PRODUCT_VERSION={product_version}"); + + 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 out_path = env::var("OUT_DIR")?; - let shadow_file = PathBuf::from(&out_path).join("shadow.rs"); + let version_state = VersionState::collect(repository.as_ref()); - // When not refreshing build info and the shadow.rs already exists, skip regenerating - // it entirely. shadow_rs always writes new BUILD_TIME* values which would change - // the file and invalidate incremental compilation even when nothing meaningful changed. - if !refresh && shadow_file.exists() { - println!("cargo:rerun-if-changed=build.rs"); - return Ok(()); - } - - let _ = ShadowBuilder::builder() - .build_pattern(BuildPattern::Lazy) - .src_path(src_path) - .out_path(out_path) - .deny_const(BTreeSet::from([CARGO_METADATA, CARGO_TREE])) - .build() - .unwrap(); + 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 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()); +} diff --git a/src/common/version/src/lib.rs b/src/common/version/src/lib.rs index 6c22073b27..82d64f532f 100644 --- a/src/common/version/src/lib.rs +++ b/src/common/version/src/lib.rs @@ -16,7 +16,9 @@ use std::fmt::Display; -shadow_rs::shadow!(build); +mod build { + include!(concat!(env!("OUT_DIR"), "/shadow.rs")); +} #[derive(Clone, Debug, PartialEq)] pub struct BuildInfo { @@ -24,8 +26,6 @@ pub struct BuildInfo { pub commit: &'static str, pub commit_short: &'static str, pub clean: bool, - pub source_time: &'static str, - pub build_time: &'static str, pub rustc: &'static str, pub target: &'static str, pub version: &'static str, @@ -55,8 +55,6 @@ pub struct OwnedBuildInfo { pub commit: String, pub commit_short: String, pub clean: bool, - pub source_time: String, - pub build_time: String, pub rustc: String, pub target: String, pub version: String, @@ -69,8 +67,6 @@ impl From for OwnedBuildInfo { commit: info.commit.to_string(), commit_short: info.commit_short.to_string(), clean: info.clean, - source_time: info.source_time.to_string(), - build_time: info.build_time.to_string(), rustc: info.rustc.to_string(), target: info.target.to_string(), version: info.version.to_string(), @@ -101,8 +97,6 @@ pub const fn build_info() -> BuildInfo { commit: build::COMMIT_HASH, commit_short: build::SHORT_COMMIT, clean: build::GIT_CLEAN, - source_time: env!("SOURCE_TIMESTAMP"), - build_time: env!("BUILD_TIMESTAMP"), rustc: build::RUST_VERSION, target: build::BUILD_TARGET, version: env!("GREPTIME_PRODUCT_VERSION"), diff --git a/src/servers/src/http/handler.rs b/src/servers/src/http/handler.rs index ca56e5234e..7ba54e3ad1 100644 --- a/src/servers/src/http/handler.rs +++ b/src/servers/src/http/handler.rs @@ -426,7 +426,6 @@ pub async fn health(Query(_params): Query) -> Json #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct StatusResponse<'a> { - pub source_time: &'a str, pub commit: &'a str, pub branch: &'a str, pub rustc_version: &'a str, @@ -442,7 +441,6 @@ pub async fn status() -> Json> { .unwrap_or_else(|_| "unknown".to_string()); let build_info = common_version::build_info(); Json(StatusResponse { - source_time: build_info.source_time, commit: build_info.commit, branch: build_info.branch, rustc_version: build_info.rustc, diff --git a/src/servers/tests/http/http_handler_test.rs b/src/servers/tests/http/http_handler_test.rs index 46bb386ee6..12465ffbc6 100644 --- a/src/servers/tests/http/http_handler_test.rs +++ b/src/servers/tests/http/http_handler_test.rs @@ -384,7 +384,6 @@ async fn test_status() { .unwrap_or_else(|_| "unknown".to_string()); let build_info = common_version::build_info(); let expected_json = http_handler::StatusResponse { - source_time: build_info.source_time, commit: build_info.commit, branch: build_info.branch, rustc_version: build_info.rustc, diff --git a/tests-integration/tests/http.rs b/tests-integration/tests/http.rs index 138d5d9ab9..68fa2a228d 100644 --- a/tests-integration/tests/http.rs +++ b/tests-integration/tests/http.rs @@ -1358,7 +1358,6 @@ pub async fn test_status_api(store_type: StorageType) { assert_eq!(res_get.status(), StatusCode::OK); let res_body = res_get.text().await; - assert!(res_body.contains("{\"source_time\"")); assert!(res_body.contains("\"commit\":")); assert!(res_body.contains("\"branch\":")); assert!(res_body.contains("\"rustc_version\":"));