chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "jcode-build-meta"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
# Single source of truth for compile-time build/version metadata.
|
||||
#
|
||||
# The build script computes the same JCODE_* values that historically lived in
|
||||
# the root crate's build.rs and emits them via `cargo:rustc-env`. Downstream
|
||||
# code reads them through the `pub const` accessors below (e.g.
|
||||
# `jcode_build_meta::VERSION`) instead of `env!("JCODE_VERSION")`, so any crate
|
||||
# in the workspace can depend on this leaf crate and obtain identical values
|
||||
# without duplicating the build script (which would double-increment the dev
|
||||
# patch counter).
|
||||
|
||||
[dependencies]
|
||||
|
||||
[build-dependencies]
|
||||
@@ -0,0 +1,319 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
// Build metadata generator for the jcode workspace.
|
||||
//
|
||||
// This is the single source of truth for the JCODE_* compile-time values that
|
||||
// previously lived in the root `jcode` crate's build.rs. It is hosted in the
|
||||
// leaf `jcode-build-meta` crate so every workspace crate can read identical
|
||||
// values (via `jcode_build_meta::*`) without duplicating this script.
|
||||
//
|
||||
// NOTE: because this crate's own package version is unrelated to jcode's, we
|
||||
// parse the root `Cargo.toml` `[package].version` for the base semver instead
|
||||
// of `CARGO_PKG_VERSION`.
|
||||
|
||||
fn main() {
|
||||
let repo_root = repo_root();
|
||||
|
||||
let pkg_version = root_package_version(&repo_root).unwrap_or_else(|| "0.0.0".to_string());
|
||||
let base_version = parse_semver(&pkg_version).unwrap_or((0, 0, 0));
|
||||
let build_semver = resolve_build_semver(base_version).unwrap_or_else(|err| {
|
||||
eprintln!("cargo:warning=failed to resolve auto build semver: {err}");
|
||||
pkg_version.clone()
|
||||
});
|
||||
let (major, minor, patch) = parse_semver(&build_semver).unwrap_or(base_version);
|
||||
let base_semver = format!("{}.{}.{}", base_version.0, base_version.1, base_version.2);
|
||||
let update_semver = if explicit_build_semver_override().is_some() {
|
||||
build_semver.clone()
|
||||
} else {
|
||||
base_semver.clone()
|
||||
};
|
||||
|
||||
let git_hash = env_or_metadata_or_git(
|
||||
&repo_root,
|
||||
"JCODE_BUILD_GIT_HASH",
|
||||
"git_hash",
|
||||
["rev-parse", "--short", "HEAD"],
|
||||
)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// Get git commit date (full datetime with timezone for accurate age calculation)
|
||||
let git_date = env_or_metadata_or_git(
|
||||
&repo_root,
|
||||
"JCODE_BUILD_GIT_DATE",
|
||||
"git_date",
|
||||
["log", "-1", "--format=%ci"],
|
||||
)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let dirty = match std::env::var("JCODE_BUILD_GIT_DIRTY") {
|
||||
Ok(value) => matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "dirty"
|
||||
),
|
||||
Err(_) => metadata_value("git_dirty")
|
||||
.map(|value| {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "dirty"
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
git_output(&repo_root, ["status", "--porcelain"]).map(|output| !output.is_empty())
|
||||
})
|
||||
.unwrap_or(false),
|
||||
};
|
||||
|
||||
// Get git tag (e.g., "v0.1.2" if HEAD is tagged, or "v0.1.2-3-gabc1234" if ahead)
|
||||
let git_tag = env_or_metadata_or_git(
|
||||
&repo_root,
|
||||
"JCODE_BUILD_GIT_TAG",
|
||||
"git_tag",
|
||||
["describe", "--tags", "--always"],
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Get recent commit messages with commit timestamps and version tag decorations.
|
||||
// Format: "hash|timestamp|decorations|subject" per line.
|
||||
// We embed a deeper window so /changelog can cover many more releases.
|
||||
let raw_log = std::env::var("JCODE_BUILD_CHANGELOG_RAW")
|
||||
.ok()
|
||||
.or_else(|| metadata_value("changelog_raw"))
|
||||
.or_else(|| git_output(&repo_root, ["log", "-700", "--format=%h|%ct|%D|%s"]))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Normalize to "hash<RS>tag<RS>timestamp<RS>subject" — extract version tag or
|
||||
// leave empty. We use ASCII record/unit separators so fields can safely
|
||||
// contain punctuation.
|
||||
let changelog = raw_log
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let mut parts = line.splitn(4, '|');
|
||||
let hash = parts.next()?;
|
||||
let timestamp = parts.next().unwrap_or("");
|
||||
let decorations = parts.next().unwrap_or("");
|
||||
let subject = parts.next()?;
|
||||
let tag = decorations
|
||||
.split(',')
|
||||
.map(|d| d.trim())
|
||||
.find(|d| d.starts_with("tag: v"))
|
||||
.and_then(|d| d.strip_prefix("tag: "))
|
||||
.unwrap_or("");
|
||||
Some(format!(
|
||||
"{}\x1e{}\x1e{}\x1e{}",
|
||||
hash, tag, timestamp, subject
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\x1f");
|
||||
|
||||
// Build version string:
|
||||
// Release: v0.2.17 (abc1234)
|
||||
// Dev: v0.2.17-dev (abc1234)
|
||||
// Dirty: v0.2.17-dev (abc1234, dirty)
|
||||
let is_release = std::env::var("JCODE_RELEASE_BUILD").is_ok();
|
||||
let version = if is_release {
|
||||
format!("v{}.{}.{} ({})", major, minor, patch, git_hash)
|
||||
} else if dirty {
|
||||
format!("v{}.{}.{}-dev ({}, dirty)", major, minor, patch, git_hash)
|
||||
} else {
|
||||
format!("v{}.{}.{}-dev ({})", major, minor, patch, git_hash)
|
||||
};
|
||||
|
||||
// Set environment variables for compilation
|
||||
println!("cargo:rustc-env=JCODE_GIT_HASH={}", git_hash);
|
||||
println!("cargo:rustc-env=JCODE_GIT_DATE={}", git_date);
|
||||
println!("cargo:rustc-env=JCODE_VERSION={}", version);
|
||||
println!("cargo:rustc-env=JCODE_SEMVER={}", build_semver);
|
||||
println!("cargo:rustc-env=JCODE_BASE_SEMVER={}", base_semver);
|
||||
println!("cargo:rustc-env=JCODE_UPDATE_SEMVER={}", update_semver);
|
||||
println!("cargo:rustc-env=JCODE_GIT_TAG={}", git_tag);
|
||||
println!("cargo:rustc-env=JCODE_CHANGELOG={}", changelog);
|
||||
println!("cargo:rustc-env=JCODE_PKG_VERSION={}", pkg_version);
|
||||
|
||||
// Forward JCODE_RELEASE_BUILD env var if set (CI sets this for release binaries)
|
||||
if std::env::var("JCODE_RELEASE_BUILD").is_ok() {
|
||||
println!("cargo:rustc-env=JCODE_RELEASE_BUILD=1");
|
||||
}
|
||||
|
||||
// Re-run only on inputs that should genuinely change the embedded metadata.
|
||||
//
|
||||
// IMPORTANT: we deliberately do NOT declare `.git/HEAD` or `.git/index` as
|
||||
// `rerun-if-changed` inputs. Those files' mtimes change on every `git add`,
|
||||
// `git status`, commit, and concurrent-agent git op. Cargo treats a build
|
||||
// script as dirty whenever any declared input is newer than the script's
|
||||
// output file, reruns it, and then force-recompiles every dependent crate
|
||||
// via StaleDepFingerprint -- even when the emitted output is byte-identical.
|
||||
// Since `jcode-build-meta` sits at the bottom of the crate graph
|
||||
// (base -> app-core -> tui -> cli all depend on it), watching the git files
|
||||
// turned routine git activity into a full-tree recompile (~18s) on every
|
||||
// incremental build. See the deterministic-semver note in
|
||||
// `resolve_build_semver` for the companion fix.
|
||||
//
|
||||
// Correctness is preserved where it matters:
|
||||
// * Release/dist builds set JCODE_RELEASE_BUILD=1 and JCODE_BUILD_SEMVER,
|
||||
// both of which DO force a rerun (declared below), so released binaries
|
||||
// always embed the exact version/hash.
|
||||
// * A `[package].version` bump touches Cargo.toml (declared below), which
|
||||
// refreshes the embedded metadata for the next build.
|
||||
// * `cargo clean` / editing this build script naturally re-runs it.
|
||||
// For ordinary dev builds the embedded git hash/dirty flag may lag the very
|
||||
// latest commit within a session; that is a cosmetic `--version` detail and
|
||||
// an acceptable trade for keeping incremental builds incremental.
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
repo_root.join("Cargo.toml").display()
|
||||
);
|
||||
println!("cargo:rerun-if-env-changed=JCODE_RELEASE_BUILD");
|
||||
println!("cargo:rerun-if-env-changed=JCODE_BUILD_SEMVER");
|
||||
// Allow callers to force a metadata refresh (e.g. install scripts) without a
|
||||
// full clean, by bumping this env var.
|
||||
println!("cargo:rerun-if-env-changed=JCODE_BUILD_GIT_HASH");
|
||||
println!("cargo:rerun-if-env-changed=JCODE_BUILD_GIT_DATE");
|
||||
println!("cargo:rerun-if-env-changed=JCODE_BUILD_GIT_DIRTY");
|
||||
println!("cargo:rerun-if-env-changed=JCODE_BUILD_GIT_TAG");
|
||||
}
|
||||
|
||||
/// Workspace root, derived from this crate's manifest dir (`crates/jcode-build-meta`).
|
||||
fn repo_root() -> PathBuf {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from("."));
|
||||
// crates/jcode-build-meta -> crates -> <repo root>
|
||||
manifest_dir
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or(manifest_dir)
|
||||
}
|
||||
|
||||
/// Parse `[package].version` from the root `Cargo.toml` without a toml dep.
|
||||
fn root_package_version(repo_root: &Path) -> Option<String> {
|
||||
let data = fs::read_to_string(repo_root.join("Cargo.toml")).ok()?;
|
||||
let mut in_package = false;
|
||||
for line in data.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with('[') {
|
||||
in_package = trimmed == "[package]";
|
||||
continue;
|
||||
}
|
||||
if in_package && let Some(rest) = trimmed.strip_prefix("version") {
|
||||
let rest = rest.trim_start();
|
||||
if let Some(rest) = rest.strip_prefix('=') {
|
||||
let value = rest.trim().trim_matches('"').to_string();
|
||||
if !value.is_empty() {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_semver(value: &str) -> Option<(u32, u32, u32)> {
|
||||
let trimmed = value.trim().trim_start_matches('v');
|
||||
let mut parts = trimmed.split('.');
|
||||
let major = parts.next()?.parse().ok()?;
|
||||
let minor = parts.next()?.parse().ok()?;
|
||||
let patch = parts.next()?.parse().ok()?;
|
||||
Some((major, minor, patch))
|
||||
}
|
||||
|
||||
fn explicit_build_semver_override() -> Option<String> {
|
||||
std::env::var("JCODE_BUILD_SEMVER")
|
||||
.ok()
|
||||
.map(|value| value.trim().trim_start_matches('v').to_string())
|
||||
.filter(|value| parse_semver(value).is_some())
|
||||
}
|
||||
|
||||
fn resolve_build_semver(base_version: (u32, u32, u32)) -> Result<String, String> {
|
||||
if let Some(explicit) = explicit_build_semver_override() {
|
||||
return Ok(explicit);
|
||||
}
|
||||
|
||||
// Dev builds derive the patch number deterministically from committed git
|
||||
// state: `base.patch + <commits since the base-version tag>`. This is a pure
|
||||
// function of HEAD, so the emitted JCODE_SEMVER/JCODE_VERSION only change when
|
||||
// an actual commit lands, NOT on every build-script rerun.
|
||||
//
|
||||
// The previous implementation incremented a persistent counter on every
|
||||
// rerun. Because the build script reruns whenever `.git/index`/`.git/HEAD`
|
||||
// change (any `git add`, commit, or concurrent agent git op), that side
|
||||
// effect churned the version string on essentially every build, which in
|
||||
// turn invalidated `jcode-build-meta` and force-recompiled the entire crate
|
||||
// graph (base -> app-core -> tui -> cli). Deriving the value deterministically
|
||||
// keeps incremental rebuilds incremental.
|
||||
let offset = commits_since_base_tag(base_version).unwrap_or(0);
|
||||
let patch = base_version.2.saturating_add(offset);
|
||||
Ok(format!("{}.{}.{}", base_version.0, base_version.1, patch))
|
||||
}
|
||||
|
||||
/// Count commits between the base-version tag (`vMAJOR.MINOR.PATCH`) and HEAD.
|
||||
/// Returns `None` when git is unavailable or the tag does not exist yet, in which
|
||||
/// case the caller falls back to the base patch (still deterministic).
|
||||
fn commits_since_base_tag(base_version: (u32, u32, u32)) -> Option<u32> {
|
||||
let repo_root = repo_root();
|
||||
let tag = format!("v{}.{}.{}", base_version.0, base_version.1, base_version.2);
|
||||
let range = format!("{tag}..HEAD");
|
||||
let out = git_output(&repo_root, ["rev-list", "--count", range.as_str()])?;
|
||||
out.trim().parse::<u32>().ok()
|
||||
}
|
||||
|
||||
fn env_or_metadata_or_git<const N: usize>(
|
||||
repo_root: &Path,
|
||||
env_name: &str,
|
||||
metadata_key: &str,
|
||||
git_args: [&str; N],
|
||||
) -> Option<String> {
|
||||
std::env::var(env_name)
|
||||
.ok()
|
||||
.or_else(|| metadata_value(metadata_key))
|
||||
.or_else(|| git_output(repo_root, git_args))
|
||||
.map(|value| value.trim().to_string())
|
||||
}
|
||||
|
||||
fn git_output<const N: usize>(repo_root: &Path, args: [&str; N]) -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.current_dir(repo_root)
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
String::from_utf8(output.stdout).ok()
|
||||
}
|
||||
|
||||
fn metadata_value(key: &str) -> Option<String> {
|
||||
let path = std::env::var("JCODE_BUILD_METADATA_FILE").ok()?;
|
||||
let data = fs::read_to_string(path).ok()?;
|
||||
let mut lines = data.lines();
|
||||
while let Some(line) = lines.next() {
|
||||
if let Some((entry_key, marker)) = line.split_once("<<") {
|
||||
if entry_key == key {
|
||||
let mut value = String::new();
|
||||
for value_line in lines.by_ref() {
|
||||
if value_line == marker {
|
||||
return Some(value);
|
||||
}
|
||||
if !value.is_empty() {
|
||||
value.push('\n');
|
||||
}
|
||||
value.push_str(value_line);
|
||||
}
|
||||
return Some(value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((entry_key, entry_value)) = line.split_once('=')
|
||||
&& entry_key == key
|
||||
{
|
||||
return Some(entry_value.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Compile-time build/version metadata for jcode.
|
||||
//!
|
||||
//! The build script (`build.rs`) computes git- and version-derived values and
|
||||
//! emits them via `cargo:rustc-env`. This module re-exposes them as `pub const`
|
||||
//! so any workspace crate can read identical values through e.g.
|
||||
//! `jcode_build_meta::VERSION` instead of `env!("JCODE_VERSION")`.
|
||||
|
||||
/// Human-readable version string, e.g. `v0.14.6-dev (abc1234)`.
|
||||
pub const VERSION: &str = env!("JCODE_VERSION");
|
||||
/// Short git hash of the build commit, e.g. `abc1234` (or `unknown`).
|
||||
pub const GIT_HASH: &str = env!("JCODE_GIT_HASH");
|
||||
/// Commit date/time of the build commit (or `unknown`).
|
||||
pub const GIT_DATE: &str = env!("JCODE_GIT_DATE");
|
||||
/// `git describe --tags --always` output (may be empty).
|
||||
pub const GIT_TAG: &str = env!("JCODE_GIT_TAG");
|
||||
/// Auto-incrementing build semver (dev) or explicit release semver.
|
||||
pub const SEMVER: &str = env!("JCODE_SEMVER");
|
||||
/// Base semver taken from the root `Cargo.toml` package version.
|
||||
pub const BASE_SEMVER: &str = env!("JCODE_BASE_SEMVER");
|
||||
/// Semver used for update comparisons.
|
||||
pub const UPDATE_SEMVER: &str = env!("JCODE_UPDATE_SEMVER");
|
||||
/// Encoded changelog (record/unit separated). See build.rs for the format.
|
||||
pub const CHANGELOG: &str = env!("JCODE_CHANGELOG");
|
||||
/// Root crate package version (mirrors the historical `CARGO_PKG_VERSION`).
|
||||
pub const PKG_VERSION: &str = env!("JCODE_PKG_VERSION");
|
||||
|
||||
/// Whether this binary was built as a release build (`JCODE_RELEASE_BUILD=1`).
|
||||
pub const fn is_release_build() -> bool {
|
||||
option_env!("JCODE_RELEASE_BUILD").is_some()
|
||||
}
|
||||
Reference in New Issue
Block a user