|
| 1 | +use std::process::Command; |
| 2 | + |
| 3 | +fn main() { |
| 4 | + // Capture git commit SHA (full) |
| 5 | + let git_sha = run_git_command(&["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".to_string()); |
| 6 | + |
| 7 | + // Capture git commit SHA (short, 8 chars) |
| 8 | + let git_sha_short = run_git_command(&["rev-parse", "--short=8", "HEAD"]) |
| 9 | + .unwrap_or_else(|| "unknown".to_string()); |
| 10 | + |
| 11 | + // Capture git branch name |
| 12 | + let git_branch = run_git_command(&["rev-parse", "--abbrev-ref", "HEAD"]) |
| 13 | + .unwrap_or_else(|| "unknown".to_string()); |
| 14 | + |
| 15 | + // Capture git describe for semantic versioning |
| 16 | + // Format: v0.1.0-5-g7b92aa23 (tag-commits_since_tag-short_sha) |
| 17 | + // or v0.1.0 if on a tag, or v0.1.0-dirty if dirty |
| 18 | + let git_describe = run_git_command(&["describe", "--tags", "--always", "--dirty"]) |
| 19 | + .or_else(|| { |
| 20 | + // Fallback to CARGO_PKG_VERSION if no tags exist |
| 21 | + std::env::var("CARGO_PKG_VERSION").ok() |
| 22 | + }) |
| 23 | + .unwrap_or_else(|| "unknown".to_string()); |
| 24 | + |
| 25 | + // Check if repository has uncommitted changes |
| 26 | + let git_dirty = is_git_dirty(); |
| 27 | + |
| 28 | + // Capture build timestamp in ISO 8601 format (YYYY-MM-DD) |
| 29 | + let build_timestamp = std::env::var("SOURCE_DATE_EPOCH") |
| 30 | + .ok() |
| 31 | + .and_then(|epoch| { |
| 32 | + use std::time::UNIX_EPOCH; |
| 33 | + let secs = epoch.parse::<u64>().ok()?; |
| 34 | + let time = UNIX_EPOCH + std::time::Duration::from_secs(secs); |
| 35 | + Some(format_timestamp(time)) |
| 36 | + }) |
| 37 | + .unwrap_or_else(|| format_timestamp(std::time::SystemTime::now())); |
| 38 | + |
| 39 | + // Set environment variables for the build |
| 40 | + println!("cargo:rustc-env=GIT_SHA={git_sha}"); |
| 41 | + println!("cargo:rustc-env=GIT_SHA_SHORT={git_sha_short}"); |
| 42 | + println!("cargo:rustc-env=GIT_BRANCH={git_branch}"); |
| 43 | + println!("cargo:rustc-env=GIT_DESCRIBE={git_describe}"); |
| 44 | + println!("cargo:rustc-env=GIT_DIRTY={git_dirty}"); |
| 45 | + println!("cargo:rustc-env=BUILD_TIMESTAMP={build_timestamp}"); |
| 46 | + |
| 47 | + // Rerun build script if git HEAD changes |
| 48 | + println!("cargo:rerun-if-changed=.git/HEAD"); |
| 49 | + // Also rerun if the current branch ref changes |
| 50 | + if let Some(branch_ref) = run_git_command(&["symbolic-ref", "HEAD"]) { |
| 51 | + let ref_path = format!(".git/{branch_ref}"); |
| 52 | + println!("cargo:rerun-if-changed={ref_path}"); |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +/// Runs a git command and returns the output as a trimmed string, or None if the command fails. |
| 57 | +fn run_git_command(args: &[&str]) -> Option<String> { |
| 58 | + let output = Command::new("git").args(args).output().ok()?; |
| 59 | + |
| 60 | + if output.status.success() { |
| 61 | + String::from_utf8(output.stdout) |
| 62 | + .ok() |
| 63 | + .map(|s| s.trim().to_string()) |
| 64 | + .filter(|s| !s.is_empty()) |
| 65 | + } else { |
| 66 | + None |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +/// Checks if the git repository has uncommitted changes (modified, staged, or untracked files). |
| 71 | +/// Returns "true" or "false" as a string. |
| 72 | +fn is_git_dirty() -> String { |
| 73 | + // Check if there are any changes in the index or working tree |
| 74 | + // git diff-index --quiet HEAD returns non-zero if there are changes |
| 75 | + let has_changes = Command::new("git") |
| 76 | + .args(["diff-index", "--quiet", "HEAD", "--"]) |
| 77 | + .status() |
| 78 | + .map(|status| !status.success()) |
| 79 | + .unwrap_or(false); |
| 80 | + |
| 81 | + if has_changes { |
| 82 | + return "true".to_string(); |
| 83 | + } |
| 84 | + |
| 85 | + // Check for untracked files |
| 86 | + let has_untracked = run_git_command(&["ls-files", "--others", "--exclude-standard"]) |
| 87 | + .is_some_and(|output| !output.is_empty()); |
| 88 | + |
| 89 | + if has_untracked { |
| 90 | + "true".to_string() |
| 91 | + } else { |
| 92 | + "false".to_string() |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +/// Formats a `SystemTime` as an ISO 8601 date (YYYY-MM-DD). |
| 97 | +fn format_timestamp(time: std::time::SystemTime) -> String { |
| 98 | + use std::time::UNIX_EPOCH; |
| 99 | + |
| 100 | + let duration = time |
| 101 | + .duration_since(UNIX_EPOCH) |
| 102 | + .unwrap_or_else(|_| std::time::Duration::from_secs(0)); |
| 103 | + |
| 104 | + let total_secs = duration.as_secs(); |
| 105 | + // Simple date calculation (not accounting for leap seconds, but good enough) |
| 106 | + let days_since_epoch = total_secs / 86400; |
| 107 | + |
| 108 | + // Start from 1970-01-01 |
| 109 | + let mut year = 1970; |
| 110 | + let mut remaining_days = days_since_epoch; |
| 111 | + |
| 112 | + loop { |
| 113 | + let days_in_year = if is_leap_year(year) { 366 } else { 365 }; |
| 114 | + if remaining_days < days_in_year { |
| 115 | + break; |
| 116 | + } |
| 117 | + remaining_days -= days_in_year; |
| 118 | + year += 1; |
| 119 | + } |
| 120 | + |
| 121 | + let days_in_months = if is_leap_year(year) { |
| 122 | + [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] |
| 123 | + } else { |
| 124 | + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] |
| 125 | + }; |
| 126 | + |
| 127 | + let mut month = 1; |
| 128 | + let mut day = remaining_days + 1; |
| 129 | + |
| 130 | + for days_in_month in &days_in_months { |
| 131 | + if day <= *days_in_month { |
| 132 | + break; |
| 133 | + } |
| 134 | + day -= days_in_month; |
| 135 | + month += 1; |
| 136 | + } |
| 137 | + |
| 138 | + format!("{year:04}-{month:02}-{day:02}") |
| 139 | +} |
| 140 | + |
| 141 | +const fn is_leap_year(year: u64) -> bool { |
| 142 | + (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400) |
| 143 | +} |
0 commit comments