Files
greptimedb/src/common/version/build.rs
T
Ning Sun 08ded45c7a feat: add common_version customization (#7869)
* feat: add product name customization

* chore: update tests
2026-03-26 10:18:06 +00:00

331 lines
10 KiB
Rust

// 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<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 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<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());
}