ver_stub_build/lib.rs
1//! Write the section format used by `ver-stub`, and inject it into binaries.
2//!
3//! This crate can be used from
4//! - `build.rs` scripts
5//! - Standalone binary tools, such as `ver-stub-tool`.
6//!
7//! # Quickstart
8//!
9//! Create a LinkSection, tell it what contents you want it to have, and then
10//! do something with it -- either write the bytes out to a file, or patch it
11//! into a binary that is using `ver-stub`.
12//!
13//! In your build.rs:
14//! ```ignore
15//! use ver_stub_build::LinkSection;
16//!
17//! fn main() {
18//! // Patch the `my-bin` executable, producing `my-bin.bin` in the target profile dir.
19//! LinkSection::new()
20//! .with_all_git()
21//! .patch_into_bin_dep("my-dep", "my-bin")
22//! .write_to_target_profile_dir()
23//! .unwrap();
24//!
25//! // Or with a custom output name
26//! LinkSection::new()
27//! .with_all_git()
28//! .patch_into_bin_dep("my-dep", "my-bin")
29//! .with_filename("my-custom-name")
30//! .write_to_target_profile_dir()
31//! .unwrap();
32//!
33//! // Or at a custom destination
34//! LinkSection::new()
35//! .with_all_git()
36//! .patch_into_bin_dep("my-dep", "my-bin")
37//! .write_to("dist/my-bin")
38//! .unwrap();
39//! }
40//! ```
41//!
42//! NOTE: `patch_into_bin_dep` requires cargo's unstable artifact dependencies feature.
43//! You must use nightly cargo, and enable "bindeps" in `.cargo/config.toml`.
44//! Then it finds file to be patched using `CARGO_BIN_FILE_*` env vars.
45//!
46//! More generally, you can use it without artifact dependencies, to do things
47//! similar to what ver-stub-tool does.
48//!
49//! ```ignore
50//! use ver_stub_build::LinkSection;
51//!
52//! fn main() {
53//! // Patch a binary at a specific path
54//! LinkSection::new()
55//! .with_all_git()
56//! .patch_into("/path/to/binary")
57//! .write_to_target_profile_dir()
58//! .unwrap();
59//!
60//! // Or with a custom output name
61//! LinkSection::new()
62//! .with_all_git()
63//! .patch_into("/path/to/binary")
64//! .with_filename("my-custom-name")
65//! .write_to_target_profile_dir()
66//! .unwrap();
67//!
68//! // Or just write the section data file (for use with cargo-objcopy)
69//! LinkSection::new()
70//! .with_all_git()
71//! .write_to_out_dir()
72//! .unwrap();
73//! }
74//! ```
75
76/// Cargo build script helper functions.
77mod cargo_helpers;
78
79/// Error types for ver-stub-build operations.
80mod error;
81
82/// Helpers for interacting with git
83mod git_helpers;
84
85/// LLVM tools wrapper for section manipulation.
86mod llvm_tools;
87
88/// Helper to find LLVM tools, based on code in cargo-binutils.
89mod rustc;
90
91/// Update section command for patching artifact dependency binaries.
92mod update_section;
93
94pub use error::Error;
95pub use llvm_tools::{BinaryFormat, LlvmTools, SectionInfo};
96pub use update_section::{UpdateSectionCommand, platform_section_name};
97pub use ver_stub::SECTION_NAME;
98
99use chrono::{DateTime, TimeZone, Utc};
100use std::{
101 fs,
102 path::{Path, PathBuf},
103};
104use ver_stub::{BUFFER_SIZE, Member, header_size};
105
106use cargo_helpers::{cargo_rerun_if, cargo_warning};
107use git_helpers::{
108 emit_git_rerun_if_changed, get_git_branch, get_git_commit_msg, get_git_commit_timestamp,
109 get_git_describe, get_git_sha,
110};
111
112/// Builder for configuring which git information to include in version sections.
113///
114/// Use this to select which git info to collect, then either:
115/// - Call `write_to()` or `write_to_out_dir()` to just write the section data file
116/// - Call `patch_into()` to get an `UpdateSectionCommand` for patching a binary
117#[derive(Default)]
118#[must_use]
119pub struct LinkSection {
120 include_git_sha: bool,
121 include_git_describe: bool,
122 include_git_branch: bool,
123 include_git_commit_timestamp: bool,
124 include_git_commit_date: bool,
125 include_git_commit_msg: bool,
126 include_build_timestamp: bool,
127 include_build_date: bool,
128 fail_on_error: bool,
129 custom: Option<String>,
130 buffer_size: Option<usize>,
131}
132
133impl LinkSection {
134 /// Creates a new empty `LinkSection`
135 pub fn new() -> Self {
136 Self::default()
137 }
138
139 /// Includes the git SHA (`git rev-parse HEAD`) in the section data.
140 pub fn with_git_sha(mut self) -> Self {
141 self.include_git_sha = true;
142 self
143 }
144
145 /// Includes the git describe output (`git describe --always --dirty`) in the section data.
146 pub fn with_git_describe(mut self) -> Self {
147 self.include_git_describe = true;
148 self
149 }
150
151 /// Includes the git branch name (`git rev-parse --abbrev-ref HEAD`) in the section data.
152 pub fn with_git_branch(mut self) -> Self {
153 self.include_git_branch = true;
154 self
155 }
156
157 /// Includes the git commit timestamp (RFC 3339 format) in the section data.
158 pub fn with_git_commit_timestamp(mut self) -> Self {
159 self.include_git_commit_timestamp = true;
160 self
161 }
162
163 /// Includes the git commit date (YYYY-MM-DD format) in the section data.
164 pub fn with_git_commit_date(mut self) -> Self {
165 self.include_git_commit_date = true;
166 self
167 }
168
169 /// Includes the git commit message (first line, max 100 chars) in the section data.
170 pub fn with_git_commit_msg(mut self) -> Self {
171 self.include_git_commit_msg = true;
172 self
173 }
174
175 /// Includes all git information in the section data.
176 pub fn with_all_git(mut self) -> Self {
177 self.include_git_sha = true;
178 self.include_git_describe = true;
179 self.include_git_branch = true;
180 self.include_git_commit_timestamp = true;
181 self.include_git_commit_date = true;
182 self.include_git_commit_msg = true;
183 self
184 }
185
186 /// Includes the build timestamp (RFC 3339 format, UTC) in the section data.
187 pub fn with_build_timestamp(mut self) -> Self {
188 self.include_build_timestamp = true;
189 self
190 }
191
192 /// Includes the build date (YYYY-MM-DD format, UTC) in the section data.
193 pub fn with_build_date(mut self) -> Self {
194 self.include_build_date = true;
195 self
196 }
197
198 /// Includes all build time information (timestamp and date) in the section data.
199 pub fn with_all_build_time(mut self) -> Self {
200 self.include_build_timestamp = true;
201 self.include_build_date = true;
202 self
203 }
204
205 /// Enables fail-on-error mode.
206 ///
207 /// By default, if git commands fail (e.g., `git` not found, not in a git repository,
208 /// building from a source tarball without `.git`), a `cargo:warning` is emitted and
209 /// the corresponding data is skipped. This allows builds to succeed even without git.
210 ///
211 /// When `fail_on_error()` is called, git failures will instead cause a panic,
212 /// failing the build.
213 pub fn fail_on_error(mut self) -> Self {
214 self.fail_on_error = true;
215 self
216 }
217
218 /// Sets a custom application-specific string to embed in the binary.
219 ///
220 /// This can be any string your application wants to store. The total size of all
221 /// data (including git info, timestamps, and custom string) must fit within the
222 /// buffer size (default 512 bytes). If you need more space, set the
223 /// `VER_STUB_BUFFER_SIZE` environment variable when building.
224 ///
225 /// As with any build script, you must emit `cargo:rerun-if-...` directives as
226 /// needed if you read files or environment variables to build your custom string.
227 ///
228 /// Access this at runtime with `ver_stub::custom()`.
229 pub fn with_custom(mut self, s: impl Into<String>) -> Self {
230 self.custom = Some(s.into());
231 self
232 }
233
234 /// Sets the buffer size for the section data.
235 ///
236 /// This should match the buffer size used when building the target binary.
237 /// If not set, falls back to:
238 /// 1. `VER_STUB_BUFFER_SIZE` environment variable (at runtime)
239 /// 2. The `BUFFER_SIZE` constant from ver-stub (default 512)
240 pub fn with_buffer_size(mut self, size: usize) -> Self {
241 self.buffer_size = Some(size);
242 self
243 }
244
245 /// Gets the effective buffer size to use.
246 fn effective_buffer_size(&self) -> usize {
247 self.buffer_size
248 .or_else(|| {
249 std::env::var("VER_STUB_BUFFER_SIZE")
250 .ok()
251 .and_then(|s| s.parse().ok())
252 })
253 .unwrap_or(BUFFER_SIZE)
254 }
255
256 /// Builds the section data as bytes.
257 ///
258 /// This collects all enabled version info and builds the binary section data.
259 /// Does not write to any file.
260 pub fn build_section_bytes(self) -> Vec<u8> {
261 self.check_enabled();
262
263 // Emit rerun-if-changed directives for git state (only if git data requested)
264 if self.any_git_enabled() {
265 emit_git_rerun_if_changed();
266 }
267
268 // Collect the data for each member
269 let mut member_data: [Option<String>; Member::COUNT] = Default::default();
270
271 if self.include_git_sha
272 && let Some(git_sha) = get_git_sha(self.fail_on_error)
273 {
274 eprintln!("ver-stub-build: git SHA = {}", git_sha);
275 member_data[Member::GitSha as usize] = Some(git_sha);
276 }
277
278 if self.include_git_describe
279 && let Some(git_describe) = get_git_describe(self.fail_on_error)
280 {
281 eprintln!("ver-stub-build: git describe = {}", git_describe);
282 member_data[Member::GitDescribe as usize] = Some(git_describe);
283 }
284
285 if self.include_git_branch
286 && let Some(git_branch) = get_git_branch(self.fail_on_error)
287 {
288 eprintln!("ver-stub-build: git branch = {}", git_branch);
289 member_data[Member::GitBranch as usize] = Some(git_branch);
290 }
291
292 if (self.include_git_commit_timestamp || self.include_git_commit_date)
293 && let Some(timestamp) = get_git_commit_timestamp(self.fail_on_error)
294 {
295 if self.include_git_commit_timestamp {
296 let rfc3339 = timestamp.to_rfc3339();
297 eprintln!("ver-stub-build: git commit timestamp = {}", rfc3339);
298 member_data[Member::GitCommitTimestamp as usize] = Some(rfc3339);
299 }
300 if self.include_git_commit_date {
301 let date = timestamp.date_naive().to_string();
302 eprintln!("ver-stub-build: git commit date = {}", date);
303 member_data[Member::GitCommitDate as usize] = Some(date);
304 }
305 }
306
307 if self.include_git_commit_msg
308 && let Some(msg) = get_git_commit_msg(self.fail_on_error)
309 {
310 eprintln!("ver-stub-build: git commit msg = {}", msg);
311 member_data[Member::GitCommitMsg as usize] = Some(msg);
312 }
313
314 if self.any_build_time_enabled() {
315 // Emit rerun-if-env-changed for reproducible build options
316 cargo_rerun_if("env-changed=VER_STUB_IDEMPOTENT");
317 cargo_rerun_if("env-changed=VER_STUB_BUILD_TIME");
318
319 // VER_STUB_IDEMPOTENT takes precedence: if set, never include build time
320 if std::env::var("VER_STUB_IDEMPOTENT").is_ok() {
321 eprintln!(
322 "ver-stub-build: VER_STUB_IDEMPOTENT is set, skipping build timestamp/date"
323 );
324 } else {
325 let build_time = get_build_time();
326 if self.include_build_timestamp {
327 let rfc3339 = build_time.to_rfc3339();
328 eprintln!("ver-stub-build: build timestamp = {}", rfc3339);
329 member_data[Member::BuildTimestamp as usize] = Some(rfc3339);
330 }
331 if self.include_build_date {
332 let date = build_time.date_naive().to_string();
333 eprintln!("ver-stub-build: build date = {}", date);
334 member_data[Member::BuildDate as usize] = Some(date);
335 }
336 }
337 }
338
339 if let Some(ref custom) = self.custom {
340 eprintln!("ver-stub-build: custom = {}", custom);
341 member_data[Member::Custom as usize] = Some(custom.clone());
342 }
343
344 // Build the section buffer
345 let buffer_size = self.effective_buffer_size();
346 build_section_buffer(&member_data, buffer_size)
347 }
348 /// Writes the section data file to the specified path.
349 ///
350 /// If the path is a directory, writes to `{path}/ver_stub_data`.
351 /// Otherwise writes directly to the path.
352 ///
353 /// This is useful for `cargo objcopy` workflows where you want to manually
354 /// run objcopy with the generated section file.
355 ///
356 /// Returns the path to the written file.
357 pub fn write_to(self, path: impl AsRef<Path>) -> Result<PathBuf, Error> {
358 self.write_section_to_path(path.as_ref())
359 }
360
361 /// Writes the section data file to `OUT_DIR/ver_stub_data`.
362 ///
363 /// This is a convenience method for use in build scripts.
364 ///
365 /// Returns the path to the written file.
366 pub fn write_to_out_dir(self) -> Result<PathBuf, Error> {
367 let out_dir = cargo_helpers::out_dir();
368 self.write_section_to_path(&out_dir)
369 }
370
371 /// Writes the section data file to the `target/` directory.
372 /// Returns the path to the written file (e.g., `target/ver_stub_data`).
373 ///
374 /// This is useful for `cargo objcopy` workflows where you want to run:
375 /// ```bash
376 /// cargo objcopy --release --bin my_bin -- --update-section ver_stub=target/ver_stub_data my_bin.bin
377 /// ```
378 ///
379 /// The target directory is determined by checking `CARGO_TARGET_DIR` first,
380 /// then inferring from `OUT_DIR`. The result should typically be `target/ver_stub_data`.
381 ///
382 /// When cross-compiling, it might end up in `target/<triple>/ver_stub_data`, due to
383 /// how the inference works.
384 ///
385 /// To adjust this, you can set `CARGO_TARGET_DIR` in `.cargo/config.toml`:
386 /// ```toml
387 /// [env]
388 /// CARGO_TARGET_DIR = { value = "target", relative = true }
389 /// ```
390 pub fn write_to_target_dir(self) -> Result<PathBuf, Error> {
391 let target_dir = cargo_helpers::target_dir();
392 self.write_section_to_path(&target_dir)
393 }
394
395 /// Transitions to an `UpdateSectionCommand` for patching a binary at the given path.
396 ///
397 /// # Arguments
398 /// * `binary_path` - Path to the binary to patch
399 pub fn patch_into(self, binary_path: impl AsRef<Path>) -> UpdateSectionCommand {
400 UpdateSectionCommand {
401 link_section: self,
402 bin_path: binary_path.as_ref().to_path_buf(),
403 new_name: None,
404 dry_run: false,
405 }
406 }
407
408 /// Transitions to an `UpdateSectionCommand` for patching an artifact dependency binary.
409 ///
410 /// This is a convenience method for use with Cargo's artifact dependencies feature.
411 /// It finds the binary using the `CARGO_BIN_FILE_<DEP>_<NAME>` environment variables
412 /// that Cargo sets for artifact dependencies.
413 ///
414 /// # Arguments
415 /// * `dep_name` - The name of the dependency as specified in Cargo.toml
416 /// * `bin_name` - The name of the binary within the dependency
417 pub fn patch_into_bin_dep(self, dep_name: &str, bin_name: &str) -> UpdateSectionCommand {
418 let bin_path = cargo_helpers::find_artifact_binary(dep_name, bin_name);
419 self.patch_into(bin_path)
420 }
421
422 fn any_git_enabled(&self) -> bool {
423 self.include_git_sha
424 || self.include_git_describe
425 || self.include_git_branch
426 || self.include_git_commit_timestamp
427 || self.include_git_commit_date
428 || self.include_git_commit_msg
429 }
430
431 fn any_build_time_enabled(&self) -> bool {
432 self.include_build_timestamp || self.include_build_date
433 }
434
435 fn check_enabled(&self) {
436 if !self.any_git_enabled() && !self.any_build_time_enabled() && self.custom.is_none() {
437 panic!(
438 "ver-stub-build: no version info enabled. Call with_git_sha(), with_git_describe(), \
439 with_git_branch(), with_git_commit_timestamp(), with_git_commit_date(), \
440 with_git_commit_msg(), with_all_git(), with_build_timestamp(), with_build_date(), \
441 or with_custom() before writing."
442 );
443 }
444 }
445
446 pub(crate) fn write_section_to_path(self, path: &Path) -> Result<PathBuf, Error> {
447 let buffer = self.build_section_bytes();
448
449 // Write to file - if path is a directory, append ver_stub_data
450 let output_path = if path.is_dir() {
451 path.join("ver_stub_data")
452 } else {
453 path.to_path_buf()
454 };
455 fs::write(&output_path, &buffer).map_err(|source| Error::WriteSectionFile {
456 path: output_path.clone(),
457 source,
458 })?;
459
460 Ok(output_path)
461 }
462}
463
464/// Builds the section buffer from member data.
465///
466/// Format:
467/// - First byte: number of members (Member::COUNT) for forward compatibility
468/// - Next `Member::COUNT * 2` bytes: header with end offsets (u16, little-endian, relative to header)
469/// - Remaining bytes: concatenated string data
470///
471/// Header size = 1 + Member::COUNT * 2
472///
473/// For member N:
474/// - start = header_size + end[N-1] if N > 0, else header_size
475/// - end = header_size + end[N]
476/// - If start == end, the member is not present.
477///
478/// Using relative offsets means a zero-initialized buffer reads as "all members absent".
479/// The num_members byte enables forward compatibility: old sections can be read by new code.
480fn build_section_buffer(
481 member_data: &[Option<String>; Member::COUNT],
482 buffer_size: usize,
483) -> Vec<u8> {
484 let mut buffer = vec![0u8; buffer_size];
485 let header_sz = header_size(Member::COUNT);
486
487 // First byte: number of members
488 buffer[0] = Member::COUNT as u8;
489
490 // Data starts after the header; track position relative to header_size
491 let mut relative_offset: usize = 0;
492
493 for (idx, data) in member_data.iter().enumerate() {
494 if let Some(s) = data {
495 let bytes = s.as_bytes();
496 let absolute_start = header_sz + relative_offset;
497 let absolute_end = absolute_start + bytes.len();
498
499 if absolute_end > buffer_size {
500 panic!(
501 "ver-stub-build: section data too large ({} bytes, max {}). \
502 Use with_buffer_size() or set VER_STUB_BUFFER_SIZE env var to increase.",
503 absolute_end, buffer_size
504 );
505 }
506
507 // Write the data
508 buffer[absolute_start..absolute_end].copy_from_slice(bytes);
509
510 relative_offset += bytes.len();
511 }
512
513 // Write the end offset for this member (relative to header_size)
514 // If member is not present, end == previous end, so start == end indicates "not present"
515 // Offset positions start at byte 1 (after the num_members byte)
516 let header_offset = 1 + idx * 2;
517 buffer[header_offset..header_offset + 2]
518 .copy_from_slice(&(relative_offset as u16).to_le_bytes());
519 }
520
521 buffer
522}
523
524// ============================================================================
525// Helper functions
526// ============================================================================
527
528/// Gets the build time, either from VER_STUB_BUILD_TIME env var or Utc::now().
529///
530/// If VER_STUB_BUILD_TIME is set, it tries to parse it as:
531/// 1. An integer (unix timestamp in seconds)
532/// 2. An RFC 3339 datetime string
533///
534/// This supports reproducible builds by allowing a fixed build time.
535fn get_build_time() -> DateTime<Utc> {
536 if let Ok(val) = std::env::var("VER_STUB_BUILD_TIME") {
537 // Try parsing as unix timestamp (integer) first
538 if let Ok(ts) = val.parse::<i64>() {
539 let dt = Utc.timestamp_opt(ts, 0).single().unwrap_or_else(|| {
540 panic!(
541 "ver-stub-build: VER_STUB_BUILD_TIME '{}' is not a valid unix timestamp",
542 val
543 )
544 });
545 eprintln!(
546 "ver-stub-build: using VER_STUB_BUILD_TIME={} (unix timestamp), overriding Utc::now()",
547 val
548 );
549 return dt;
550 }
551
552 // Try parsing as RFC 3339
553 if let Ok(dt) = DateTime::parse_from_rfc3339(&val) {
554 eprintln!(
555 "ver-stub-build: using VER_STUB_BUILD_TIME={} (RFC 3339), overriding Utc::now()",
556 val
557 );
558 return dt.with_timezone(&Utc);
559 }
560
561 panic!(
562 "ver-stub-build: VER_STUB_BUILD_TIME '{}' is not a valid unix timestamp or RFC 3339 datetime",
563 val
564 );
565 }
566
567 Utc::now()
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn test_build_section_buffer() {
576 let mut args = [const { None }; Member::COUNT];
577
578 args[0] = Some("asdf".into());
579
580 let buf_vec = build_section_buffer(&args, BUFFER_SIZE);
581 let buffer: &[u8; BUFFER_SIZE] = (&buf_vec[..]).try_into().unwrap();
582
583 assert_eq!(Member::get_idx_from_buffer(0, &buffer).unwrap(), "asdf");
584 for idx in 1..Member::COUNT {
585 assert!(Member::get_idx_from_buffer(idx, &buffer).is_none());
586 }
587
588 args[2] = Some("jkl;".into());
589
590 let buf_vec = build_section_buffer(&args, BUFFER_SIZE);
591 let buffer: &[u8; BUFFER_SIZE] = (&buf_vec[..]).try_into().unwrap();
592
593 assert_eq!(Member::get_idx_from_buffer(0, &buffer).unwrap(), "asdf");
594 assert!(Member::get_idx_from_buffer(1, &buffer).is_none());
595 assert_eq!(Member::get_idx_from_buffer(2, &buffer).unwrap(), "jkl;");
596 for idx in 3..Member::COUNT {
597 assert!(Member::get_idx_from_buffer(idx, &buffer).is_none());
598 }
599
600 args[5] = Some("nana".into());
601
602 let buf_vec = build_section_buffer(&args, BUFFER_SIZE);
603 let buffer: &[u8; BUFFER_SIZE] = (&buf_vec[..]).try_into().unwrap();
604
605 assert_eq!(Member::get_idx_from_buffer(0, &buffer).unwrap(), "asdf");
606 assert!(Member::get_idx_from_buffer(1, &buffer).is_none());
607 assert_eq!(Member::get_idx_from_buffer(2, &buffer).unwrap(), "jkl;");
608 assert!(Member::get_idx_from_buffer(3, &buffer).is_none());
609 assert!(Member::get_idx_from_buffer(4, &buffer).is_none());
610 assert_eq!(Member::get_idx_from_buffer(5, &buffer).unwrap(), "nana");
611 for idx in 6..Member::COUNT {
612 assert!(Member::get_idx_from_buffer(idx, &buffer).is_none());
613 }
614 }
615}