mod cargo_helpers;
mod llvm_tools;
mod rustc;
mod update_section;
pub use llvm_tools::LlvmTools;
pub use update_section::UpdateSectionCommand;
use chrono::{DateTime, FixedOffset, TimeZone, Utc};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use ver_shim::{BUFFER_SIZE, Member, header_size};
use cargo_helpers::{cargo_rerun_if, cargo_warning};
#[derive(Default)]
#[must_use]
pub struct LinkSection {
include_git_sha: bool,
include_git_describe: bool,
include_git_branch: bool,
include_git_commit_timestamp: bool,
include_git_commit_date: bool,
include_git_commit_msg: bool,
include_build_timestamp: bool,
include_build_date: bool,
fail_on_error: bool,
custom: Option<String>,
buffer_size: Option<usize>,
}
impl LinkSection {
pub fn new() -> Self {
Self::default()
}
pub fn with_git_sha(mut self) -> Self {
self.include_git_sha = true;
self
}
pub fn with_git_describe(mut self) -> Self {
self.include_git_describe = true;
self
}
pub fn with_git_branch(mut self) -> Self {
self.include_git_branch = true;
self
}
pub fn with_git_commit_timestamp(mut self) -> Self {
self.include_git_commit_timestamp = true;
self
}
pub fn with_git_commit_date(mut self) -> Self {
self.include_git_commit_date = true;
self
}
pub fn with_git_commit_msg(mut self) -> Self {
self.include_git_commit_msg = true;
self
}
pub fn with_all_git(mut self) -> Self {
self.include_git_sha = true;
self.include_git_describe = true;
self.include_git_branch = true;
self.include_git_commit_timestamp = true;
self.include_git_commit_date = true;
self.include_git_commit_msg = true;
self
}
pub fn with_build_timestamp(mut self) -> Self {
self.include_build_timestamp = true;
self
}
pub fn with_build_date(mut self) -> Self {
self.include_build_date = true;
self
}
pub fn with_all_build_time(mut self) -> Self {
self.include_build_timestamp = true;
self.include_build_date = true;
self
}
pub fn fail_on_error(mut self) -> Self {
self.fail_on_error = true;
self
}
pub fn with_custom(mut self, s: impl Into<String>) -> Self {
self.custom = Some(s.into());
self
}
pub fn with_buffer_size(mut self, size: usize) -> Self {
self.buffer_size = Some(size);
self
}
fn effective_buffer_size(&self) -> usize {
self.buffer_size
.or_else(|| {
std::env::var("VER_SHIM_BUFFER_SIZE")
.ok()
.and_then(|s| s.parse().ok())
})
.unwrap_or(BUFFER_SIZE)
}
pub fn build_section_bytes(self) -> Vec<u8> {
self.check_enabled();
if self.any_git_enabled() {
emit_git_rerun_if_changed();
}
let mut member_data: [Option<String>; Member::COUNT] = Default::default();
if self.include_git_sha
&& let Some(git_sha) = get_git_sha(self.fail_on_error)
{
eprintln!("ver-shim-build: git SHA = {}", git_sha);
member_data[Member::GitSha as usize] = Some(git_sha);
}
if self.include_git_describe
&& let Some(git_describe) = get_git_describe(self.fail_on_error)
{
eprintln!("ver-shim-build: git describe = {}", git_describe);
member_data[Member::GitDescribe as usize] = Some(git_describe);
}
if self.include_git_branch
&& let Some(git_branch) = get_git_branch(self.fail_on_error)
{
eprintln!("ver-shim-build: git branch = {}", git_branch);
member_data[Member::GitBranch as usize] = Some(git_branch);
}
if (self.include_git_commit_timestamp || self.include_git_commit_date)
&& let Some(timestamp) = get_git_commit_timestamp(self.fail_on_error)
{
if self.include_git_commit_timestamp {
let rfc3339 = timestamp.to_rfc3339();
eprintln!("ver-shim-build: git commit timestamp = {}", rfc3339);
member_data[Member::GitCommitTimestamp as usize] = Some(rfc3339);
}
if self.include_git_commit_date {
let date = timestamp.date_naive().to_string();
eprintln!("ver-shim-build: git commit date = {}", date);
member_data[Member::GitCommitDate as usize] = Some(date);
}
}
if self.include_git_commit_msg
&& let Some(msg) = get_git_commit_msg(self.fail_on_error)
{
eprintln!("ver-shim-build: git commit msg = {}", msg);
member_data[Member::GitCommitMsg as usize] = Some(msg);
}
if self.any_build_time_enabled() {
cargo_rerun_if("env-changed=VER_SHIM_IDEMPOTENT");
cargo_rerun_if("env-changed=VER_SHIM_BUILD_TIME");
if std::env::var("VER_SHIM_IDEMPOTENT").is_ok() {
eprintln!("ver-shim-build: VER_SHIM_IDEMPOTENT is set, skipping build timestamp/date");
} else {
let build_time = get_build_time();
if self.include_build_timestamp {
let rfc3339 = build_time.to_rfc3339();
eprintln!("ver-shim-build: build timestamp = {}", rfc3339);
member_data[Member::BuildTimestamp as usize] = Some(rfc3339);
}
if self.include_build_date {
let date = build_time.date_naive().to_string();
eprintln!("ver-shim-build: build date = {}", date);
member_data[Member::BuildDate as usize] = Some(date);
}
}
}
if let Some(ref custom) = self.custom {
eprintln!("ver-shim-build: custom = {}", custom);
member_data[Member::Custom as usize] = Some(custom.clone());
}
let buffer_size = self.effective_buffer_size();
build_section_buffer(&member_data, buffer_size)
}
pub fn write_to(self, path: impl AsRef<Path>) -> PathBuf {
self.write_section_to_path(path.as_ref())
}
pub fn write_to_out_dir(self) -> PathBuf {
let out_dir = cargo_helpers::out_dir();
self.write_section_to_path(&out_dir)
}
pub fn write_to_target_dir(self) -> PathBuf {
let target_dir = cargo_helpers::target_dir();
self.write_section_to_path(&target_dir)
}
pub fn patch_into(self, binary_path: impl AsRef<Path>) -> UpdateSectionCommand {
UpdateSectionCommand {
link_section: self,
bin_path: binary_path.as_ref().to_path_buf(),
new_name: None,
}
}
pub fn patch_into_bin_dep(self, dep_name: &str, bin_name: &str) -> UpdateSectionCommand {
let bin_path = cargo_helpers::find_artifact_binary(dep_name, bin_name);
self.patch_into(bin_path)
}
fn any_git_enabled(&self) -> bool {
self.include_git_sha
|| self.include_git_describe
|| self.include_git_branch
|| self.include_git_commit_timestamp
|| self.include_git_commit_date
|| self.include_git_commit_msg
}
fn any_build_time_enabled(&self) -> bool {
self.include_build_timestamp || self.include_build_date
}
fn check_enabled(&self) {
if !self.any_git_enabled() && !self.any_build_time_enabled() && self.custom.is_none() {
panic!(
"ver-shim-build: no version info enabled. Call with_git_sha(), with_git_describe(), \
with_git_branch(), with_git_commit_timestamp(), with_git_commit_date(), \
with_git_commit_msg(), with_all_git(), with_build_timestamp(), with_build_date(), \
or with_custom() before writing."
);
}
}
pub(crate) fn write_section_to_path(self, path: &Path) -> PathBuf {
let buffer = self.build_section_bytes();
let output_path = if path.is_dir() {
path.join("ver_shim_data")
} else {
path.to_path_buf()
};
fs::write(&output_path, &buffer).expect("ver-shim-build: failed to write section file");
output_path
}
}
fn build_section_buffer(member_data: &[Option<String>; Member::COUNT], buffer_size: usize) -> Vec<u8> {
let mut buffer = vec![0u8; buffer_size];
let header_sz = header_size(Member::COUNT);
buffer[0] = Member::COUNT as u8;
let mut relative_offset: usize = 0;
for (idx, data) in member_data.iter().enumerate() {
if let Some(s) = data {
let bytes = s.as_bytes();
let absolute_start = header_sz + relative_offset;
let absolute_end = absolute_start + bytes.len();
if absolute_end > buffer_size {
panic!(
"ver-shim-build: section data too large ({} bytes, max {}). \
Use with_buffer_size() or set VER_SHIM_BUFFER_SIZE env var to increase.",
absolute_end, buffer_size
);
}
buffer[absolute_start..absolute_end].copy_from_slice(bytes);
relative_offset += bytes.len();
}
let header_offset = 1 + idx * 2;
buffer[header_offset..header_offset + 2]
.copy_from_slice(&(relative_offset as u16).to_le_bytes());
}
buffer
}
fn emit_git_rerun_if_changed() {
let git_dir = match find_git_dir() {
Some(dir) => dir,
None => return,
};
let head_path = git_dir.join("HEAD");
if head_path.exists() {
cargo_rerun_if(&format!("changed={}", head_path.display()));
if let Ok(head_contents) = fs::read_to_string(&head_path) {
let head_contents = head_contents.trim();
if let Some(ref_path) = head_contents.strip_prefix("ref: ") {
let ref_file = git_dir.join(ref_path);
if ref_file.exists() {
cargo_rerun_if(&format!("changed={}", ref_file.display()));
}
}
}
}
}
fn find_git_dir() -> Option<PathBuf> {
let mut dir = std::env::current_dir().ok()?;
loop {
let git_dir = dir.join(".git");
if git_dir.is_dir() {
return Some(git_dir);
}
if !dir.pop() {
return None;
}
}
}
fn get_git_sha(fail_on_error: bool) -> Option<String> {
run_git_command(&["rev-parse", "HEAD"], fail_on_error)
}
fn get_git_describe(fail_on_error: bool) -> Option<String> {
run_git_command(&["describe", "--always", "--dirty"], fail_on_error)
}
fn get_git_branch(fail_on_error: bool) -> Option<String> {
run_git_command(&["rev-parse", "--abbrev-ref", "HEAD"], fail_on_error)
}
fn get_git_commit_timestamp(fail_on_error: bool) -> Option<DateTime<FixedOffset>> {
let timestamp_str = run_git_command(&["log", "-1", "--format=%aI"], fail_on_error)?;
match DateTime::parse_from_rfc3339(×tamp_str) {
Ok(dt) => Some(dt),
Err(e) => {
let msg = format!(
"ver-shim-build: failed to parse git timestamp '{}': {}",
timestamp_str, e
);
if fail_on_error {
panic!("{}", msg);
} else {
cargo_warning(&msg);
None
}
}
}
}
fn get_git_commit_msg(fail_on_error: bool) -> Option<String> {
let msg = run_git_command(&["log", "-1", "--format=%s"], fail_on_error)?;
Some(if msg.len() > 100 {
let mut end = 100;
while !msg.is_char_boundary(end) && end > 0 {
end -= 1;
}
msg[..end].to_string()
} else {
msg
})
}
fn get_build_time() -> DateTime<Utc> {
if let Ok(val) = std::env::var("VER_SHIM_BUILD_TIME") {
if let Ok(ts) = val.parse::<i64>() {
let dt = Utc.timestamp_opt(ts, 0).single().unwrap_or_else(|| {
panic!(
"ver-shim-build: VER_SHIM_BUILD_TIME '{}' is not a valid unix timestamp",
val
)
});
eprintln!(
"ver-shim-build: using VER_SHIM_BUILD_TIME={} (unix timestamp), overriding Utc::now()",
val
);
return dt;
}
if let Ok(dt) = DateTime::parse_from_rfc3339(&val) {
eprintln!(
"ver-shim-build: using VER_SHIM_BUILD_TIME={} (RFC 3339), overriding Utc::now()",
val
);
return dt.with_timezone(&Utc);
}
panic!(
"ver-shim-build: VER_SHIM_BUILD_TIME '{}' is not a valid unix timestamp or RFC 3339 datetime",
val
);
}
Utc::now()
}
fn run_git_command(args: &[&str], fail_on_error: bool) -> Option<String> {
let cmd = format!("git {}", args.join(" "));
let output = match Command::new("git").args(args).output() {
Ok(output) => output,
Err(e) => {
let msg = format!("ver-shim-build: failed to execute '{}': {}", cmd, e);
if fail_on_error {
panic!("{}", msg);
} else {
cargo_warning(&msg);
return None;
}
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let msg = format!(
"ver-shim-build: '{}' failed with status {}: {}",
cmd,
output.status,
stderr.trim()
);
if fail_on_error {
panic!("{}", msg);
} else {
cargo_warning(&msg);
return None;
}
}
match String::from_utf8(output.stdout) {
Ok(s) => Some(s.trim().to_string()),
Err(_) => {
let msg = format!("ver-shim-build: '{}' output is not valid UTF-8", cmd);
if fail_on_error {
panic!("{}", msg);
} else {
cargo_warning(&msg);
None
}
}
}
}