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}