feat: replace shadow-rs with self-maintained version info (#7782)

* reimplement shadow-rs

Signed-off-by: Ruihang Xia <waynestxia@gmail.com>

* fix: remove timestamp build metadata

* fix: refresh version build metadata

* use git2

Signed-off-by: Ruihang Xia <waynestxia@gmail.com>

* warn about git failure

Signed-off-by: Ruihang Xia <waynestxia@gmail.com>

---------

Signed-off-by: Ruihang Xia <waynestxia@gmail.com>
This commit is contained in:
Ruihang Xia
2026-03-11 05:08:26 +08:00
committed by GitHub
parent b1b91e88f4
commit 9e95214fc8
7 changed files with 322 additions and 180 deletions

130
Cargo.lock generated
View File

@@ -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"

View File

@@ -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 }

View File

@@ -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<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
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<PathBuf> {
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<Vec<PathBuf>> {
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<Vec<PathBuf>> {
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<Repository> {
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<git2::Oid> {
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<str>) {
println!("cargo:warning={}", message.as_ref());
}

View File

@@ -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<BuildInfo> 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"),

View File

@@ -426,7 +426,6 @@ pub async fn health(Query(_params): Query<HealthQuery>) -> Json<HealthResponse>
#[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<StatusResponse<'static>> {
.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,

View File

@@ -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,

View File

@@ -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\":"));