exfiltrate 0.3.0

An embeddable debug tool for Rust.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Captures build-time facts that `std` cannot report at runtime.
//!
//! `std::env::consts` offers `ARCH` and `OS` but never the target triple, and
//! nothing at all reports the cargo profile or the enabled feature set. Those
//! are the first three questions asked of a program you have attached a debugger
//! to, so they are baked in here rather than guessed at later.

use std::time::{SystemTime, UNIX_EPOCH};

fn main() {
    // Cargo re-runs this script when the script itself changes. Without saying
    // so explicitly, the default is to re-run on *any* source change, which
    // would move the recorded build timestamp on every edit.
    println!("cargo::rerun-if-changed=build.rs");
    println!("cargo::rerun-if-env-changed=EXFILTRATE_GIT_SHA");

    let target = std::env::var("TARGET").unwrap_or_default();
    let profile = std::env::var("PROFILE").unwrap_or_default();
    println!("cargo::rustc-env=EXFILTRATE_TARGET={target}");
    println!("cargo::rustc-env=EXFILTRATE_PROFILE={profile}");
    println!(
        "cargo::rustc-env=EXFILTRATE_BUILD_TIMESTAMP={}",
        timestamp()
    );

    // A published crate is unpacked without a repository, so this is
    // best-effort: an explicit `EXFILTRATE_GIT_SHA` wins, and otherwise a
    // missing git or a missing checkout simply leaves the field empty.
    let sha = std::env::var("EXFILTRATE_GIT_SHA")
        .ok()
        .filter(|sha| !sha.is_empty())
        .or_else(git_sha)
        .unwrap_or_default();
    println!("cargo::rustc-env=EXFILTRATE_GIT_SHA={sha}");
}

fn git_sha() -> Option<String> {
    let output = std::process::Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let sha = String::from_utf8(output.stdout).ok()?.trim().to_string();
    if sha.is_empty() { None } else { Some(sha) }
}

/// Formats the current time as `YYYY-MM-DDTHH:MM:SSZ`.
///
/// Hand-rolled rather than pulled from a date crate: a build dependency that
/// exists only to print one string is a poor trade, and the civil-date
/// conversion is short enough to check by eye.
fn timestamp() -> String {
    let Ok(since_epoch) = SystemTime::now().duration_since(UNIX_EPOCH) else {
        return String::new();
    };
    let seconds = since_epoch.as_secs() as i64;
    let days = seconds.div_euclid(86_400);
    let time_of_day = seconds.rem_euclid(86_400);
    let (year, month, day) = civil_from_days(days);
    format!(
        "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
        time_of_day / 3600,
        (time_of_day % 3600) / 60,
        time_of_day % 60
    )
}

/// Hinnant's `civil_from_days`: days since the Unix epoch to a proleptic
/// Gregorian date.
fn civil_from_days(days: i64) -> (i64, u32, u32) {
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let day_of_era = z.rem_euclid(146_097);
    let year_of_era =
        (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
    let year = year_of_era + era * 400;
    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
    let shifted_month = (5 * day_of_year + 2) / 153;
    let day = (day_of_year - (153 * shifted_month + 2) / 5 + 1) as u32;
    let month = if shifted_month < 10 {
        shifted_month + 3
    } else {
        shifted_month - 9
    } as u32;
    (if month <= 2 { year + 1 } else { year }, month, day)
}