use std::ffi::OsStr;
pub(crate) const ZERO_AR_DATE: &str = "ZERO_AR_DATE";
pub(crate) const PROFILE: &str = "PROFILE";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum ArDeterminism {
#[default]
Auto,
Always,
Off,
}
impl ArDeterminism {
pub(crate) fn parse(value: &str) -> Self {
match value {
"always" => Self::Always,
"off" => Self::Off,
_ => Self::Auto,
}
}
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Always => "always",
Self::Off => "off",
}
}
}
pub(crate) fn normalizes(mode: ArDeterminism, profile: Option<&str>, already_set: bool) -> bool {
if already_set {
return false;
}
match mode {
ArDeterminism::Off => false,
ArDeterminism::Always => true,
ArDeterminism::Auto => profile != Some("release"),
}
}
pub(crate) fn effective_zero_ar_date(
mode: ArDeterminism,
profile: Option<&str>,
inherited: Option<&OsStr>,
) -> Option<String> {
match inherited {
Some(value) => Some(key_for(value)),
None => normalizes(mode, profile, false).then(|| key_for(OsStr::new("1"))),
}
}
fn key_for(value: &OsStr) -> String {
use std::fmt::Write as _;
match value.to_str() {
Some(text) => format!("text:{text}"),
None => {
let mut key = String::from("bytes:");
for byte in value.as_encoded_bytes() {
let _ = write!(key, "{byte:02x}");
}
key
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Probe {
Stamps,
Deterministic,
Unavailable(String),
}
pub(crate) fn probe() -> Probe {
match probe_inner() {
Ok(true) => Probe::Stamps,
Ok(false) => Probe::Deterministic,
Err(error) => Probe::Unavailable(error.to_string()),
}
}
fn probe_inner() -> std::io::Result<bool> {
use std::process::{Command, Stdio};
let directory = tempfile::Builder::new().prefix("mbx-ar-probe-").tempdir()?;
let member = directory.path().join("member");
std::fs::write(&member, b"mbx archive probe")?;
let run = |program: &str, arguments: &[&std::ffi::OsStr], zero: bool| -> std::io::Result<()> {
let mut command = Command::new(program);
command
.args(arguments)
.current_dir(directory.path())
.stdout(Stdio::null())
.stderr(Stdio::null());
if zero {
command.env(ZERO_AR_DATE, "1");
} else {
command.env_remove(ZERO_AR_DATE);
}
let status = command.status()?;
if status.success() {
return Ok(());
}
Err(std::io::Error::other(format!(
"{program} exited with {status}"
)))
};
let mut digests = Vec::new();
for (name, zero) in [("zeroed.a", true), ("stamped.a", false)] {
let archive = directory.path().join(name);
run(
"ar",
&["rc".as_ref(), archive.as_os_str(), member.as_os_str()],
zero,
)?;
run("ranlib", &[archive.as_os_str()], zero)?;
digests.push(std::fs::read(&archive)?);
}
Ok(digests[0] != digests[1])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_explicit_setting_from_the_user_is_never_overridden() {
for mode in [
ArDeterminism::Auto,
ArDeterminism::Always,
ArDeterminism::Off,
] {
assert!(!normalizes(mode, Some("debug"), true));
assert!(!normalizes(mode, Some("release"), true));
}
}
#[test]
fn auto_leaves_release_archives_as_the_toolchain_made_them() {
assert!(!normalizes(ArDeterminism::Auto, Some("release"), false));
assert!(normalizes(ArDeterminism::Auto, Some("debug"), false));
assert!(normalizes(ArDeterminism::Auto, Some("bench"), false));
}
#[test]
fn a_missing_profile_is_treated_as_a_non_release_build() {
assert!(normalizes(ArDeterminism::Auto, None, false));
}
#[test]
fn always_and_off_ignore_the_profile() {
assert!(normalizes(ArDeterminism::Always, Some("release"), false));
assert!(normalizes(ArDeterminism::Always, Some("debug"), false));
assert!(!normalizes(ArDeterminism::Off, Some("release"), false));
assert!(!normalizes(ArDeterminism::Off, Some("debug"), false));
}
#[test]
fn unrecognized_settings_parse_to_the_default() {
assert_eq!(ArDeterminism::parse("always"), ArDeterminism::Always);
assert_eq!(ArDeterminism::parse("off"), ArDeterminism::Off);
assert_eq!(ArDeterminism::parse("auto"), ArDeterminism::Auto);
assert_eq!(ArDeterminism::parse("nonsense"), ArDeterminism::Auto);
assert_eq!(ArDeterminism::default(), ArDeterminism::Auto);
}
#[test]
fn the_key_reflects_what_the_script_will_read() {
assert_eq!(
effective_zero_ar_date(ArDeterminism::Auto, Some("debug"), None).as_deref(),
Some("text:1")
);
assert_eq!(
effective_zero_ar_date(ArDeterminism::Auto, Some("release"), None),
None
);
assert_eq!(
effective_zero_ar_date(ArDeterminism::Off, Some("debug"), None),
None
);
assert_eq!(
effective_zero_ar_date(ArDeterminism::Always, Some("release"), None).as_deref(),
Some("text:1")
);
}
#[test]
fn an_inherited_value_is_keyed_as_the_script_reads_it() {
for mode in [
ArDeterminism::Auto,
ArDeterminism::Always,
ArDeterminism::Off,
] {
assert_eq!(
effective_zero_ar_date(mode, Some("debug"), Some(OsStr::new("0"))).as_deref(),
Some("text:0")
);
assert_eq!(
effective_zero_ar_date(mode, Some("debug"), Some(OsStr::new("1"))).as_deref(),
Some("text:1")
);
}
}
#[cfg(unix)]
#[test]
fn a_value_that_is_not_utf8_is_keyed_rather_than_dropped() {
use std::os::unix::ffi::OsStrExt as _;
let inherited = OsStr::from_bytes(&[0x31, 0xff]);
let keyed = effective_zero_ar_date(ArDeterminism::Auto, Some("debug"), Some(inherited));
assert!(keyed.is_some());
assert_ne!(
keyed,
effective_zero_ar_date(ArDeterminism::Auto, Some("debug"), None)
);
assert_ne!(
keyed,
effective_zero_ar_date(ArDeterminism::Auto, Some("debug"), Some(OsStr::new("1")))
);
}
#[cfg(unix)]
#[test]
fn a_text_value_cannot_spell_a_byte_value() {
use std::os::unix::ffi::OsStrExt as _;
let bytes = effective_zero_ar_date(
ArDeterminism::Off,
Some("debug"),
Some(OsStr::from_bytes(&[0xc3, 0x28])),
);
let text =
effective_zero_ar_date(ArDeterminism::Off, Some("debug"), Some(OsStr::new("c328")));
assert!(bytes.is_some() && text.is_some());
assert_ne!(bytes, text);
}
#[test]
fn turning_the_policy_on_or_off_changes_the_key() {
let on = effective_zero_ar_date(ArDeterminism::Auto, Some("debug"), None);
let off = effective_zero_ar_date(ArDeterminism::Off, Some("debug"), None);
assert_ne!(on, off);
}
#[test]
fn every_mode_round_trips_through_its_configured_spelling() {
for mode in [
ArDeterminism::Auto,
ArDeterminism::Always,
ArDeterminism::Off,
] {
assert_eq!(ArDeterminism::parse(mode.as_str()), mode);
}
}
}