#![forbid(unsafe_code)]
#![allow(non_snake_case)]
use safe_lock::SafeLock;
use std::convert::TryFrom;
use std::ffi::OsStr;
use std::sync::atomic::{AtomicI64, Ordering};
pub struct OnceI64 {
cached_value: AtomicI64,
lock: safe_lock::SafeLock,
}
impl OnceI64 {
#[must_use]
pub const fn new() -> Self {
Self {
cached_value: AtomicI64::new(i64::MIN),
lock: SafeLock::new(),
}
}
#[allow(clippy::missing_panics_doc)]
pub fn get(&self, f: impl FnOnce() -> Result<i64, String>) -> Result<i64, String> {
let _guard = self.lock.lock().unwrap();
let value = self.cached_value.load(Ordering::Relaxed);
if value != i64::MIN {
return Ok(value);
}
let new_value = (f)()?;
self.cached_value.store(new_value, Ordering::Relaxed);
Ok(new_value)
}
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn escape_ascii(input: impl AsRef<[u8]>) -> String {
let mut result = String::new();
for byte in input.as_ref() {
for ascii_byte in core::ascii::escape_default(*byte) {
result.push_str(core::str::from_utf8(&[ascii_byte]).unwrap());
}
}
result
}
pub fn exec(cmd: impl AsRef<OsStr>, args: &[&str]) -> Result<String, String> {
let output = std::process::Command::new(cmd.as_ref())
.args(args)
.output()
.map_err(|e| {
format!(
"error executing '{} {}': {}",
cmd.as_ref().to_string_lossy(),
args.join(" "),
e
)
})?;
if !output.status.success() {
return Err(format!(
"command '{} {}' failed: exit={} stdout='{}' stderr='{}'",
cmd.as_ref().to_string_lossy(),
args.join(" "),
output
.status
.code()
.map_or_else(|| String::from("signal"), |c| c.to_string()),
escape_ascii(output.stdout),
escape_ascii(output.stderr)
));
}
let stdout = std::str::from_utf8(&output.stdout).map_err(|_| {
format!(
"command '{} {}' wrote non-utf8 bytes to stdout",
cmd.as_ref().to_string_lossy(),
args.join(" ")
)
})?;
Ok(escape_ascii(stdout.trim()).replace('"', "\\"))
}
#[must_use]
pub fn format_date(epoch: i64) -> String {
chrono::TimeZone::timestamp(&chrono::Utc, epoch, 0)
.format("%Y-%m-%dZ")
.to_string()
}
#[must_use]
pub fn format_time(epoch: i64) -> String {
chrono::TimeZone::timestamp(&chrono::Utc, epoch, 0)
.format("%H:%M:%SZ")
.to_string()
}
#[must_use]
pub fn format_timestamp(epoch: i64) -> String {
chrono::TimeZone::timestamp(&chrono::Utc, epoch, 0)
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn now() -> i64 {
static CACHED_VALUE: OnceI64 = OnceI64::new();
CACHED_VALUE
.get(|| {
Ok(i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
)
.unwrap())
})
.unwrap()
}
pub fn get_env(name: &str) -> Result<Option<String>, String> {
let value = match std::env::var(name) {
Ok(value) => value,
Err(std::env::VarError::NotPresent) => return Ok(None),
Err(std::env::VarError::NotUnicode(_)) => {
return Err(format!("env var '{}' contains non-utf8 bytes", name))
}
};
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(None);
}
Ok(Some(trimmed.to_string()))
}
pub fn get_git_commit() -> Result<String, String> {
exec("git", &["rev-parse", "HEAD"])
}
pub fn get_git_commit_short() -> Result<String, String> {
let long = get_git_commit()?;
if long.len() < 7 {
return Err(format!("got malformed commit hash from git: '{}'", long));
}
let short = &long[0..7];
Ok(short.to_string())
}
pub fn get_git_branch() -> Result<String, String> {
exec("git", &["rev-parse", "--abbrev-ref=loose", "HEAD"])
}
pub fn get_git_dirty() -> Result<bool, String> {
Ok(!exec("git", &["status", "-s"])?.is_empty())
}
pub fn get_hostname() -> Result<String, String> {
exec("hostname", &[])
}
pub fn get_rustc_version() -> Result<String, String> {
let rustc_var = std::env::var_os("RUSTC")
.filter(|s| !s.is_empty())
.ok_or_else(|| String::from("RUSTC env var is not set"))?;
exec(rustc_var, &["--version"])
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
pub enum RustChannel {
Stable,
Beta,
Nightly,
}
impl core::fmt::Display for RustChannel {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
match self {
RustChannel::Stable => write!(f, "stable"),
RustChannel::Beta => write!(f, "beta"),
RustChannel::Nightly => write!(f, "nightly"),
}
}
}
#[allow(clippy::if_not_else, clippy::range_plus_one, clippy::assign_op_pattern)]
pub fn parse_rustc_version(version: impl AsRef<str>) -> Result<(String, RustChannel), String> {
let matcher: safe_regex::Matcher3<_> =
safe_regex::regex!(br"(?:rustc )?([0-9]+\.[0-9]+\.[0-9]+)(?:(-beta)|(-nightly))?(?: .*)?");
let (semver_bytes, beta, nightly) = matcher
.match_slices(version.as_ref().trim().as_bytes())
.ok_or_else(|| format!("failed parsing rustc version: '{}'", version.as_ref()))?;
let semver = String::from_utf8(semver_bytes.to_vec()).unwrap();
let channel = if !beta.is_empty() {
RustChannel::Beta
} else if !nightly.is_empty() {
RustChannel::Nightly
} else {
RustChannel::Stable
};
Ok((semver, channel))
}
#[allow(clippy::missing_panics_doc)]
pub fn parse_rustc_semver(version: impl AsRef<str>) -> Result<String, String> {
let (semver, _channel) = parse_rustc_version(version)?;
Ok(semver)
}
#[allow(clippy::missing_panics_doc)]
pub fn parse_rustc_channel(version: impl AsRef<str>) -> Result<RustChannel, String> {
let (_semver, channel) = parse_rustc_version(version)?;
Ok(channel)
}
pub fn get_source_time() -> Result<i64, String> {
static CACHED_VALUE: OnceI64 = OnceI64::new();
CACHED_VALUE.get(|| {
if let Some(value) = get_env("SOURCE_DATE_EPOCH").unwrap() {
return value.parse().map_err(|_| {
format!(
"failed parsing env var as i64: SOURCE_DATE_EPOCH='{}'",
value
)
});
}
let stdout = exec("git", &["log", "-1", "--pretty=%ct"])?;
stdout.parse().map_err(|_| {
format!(
"failed parsing output of 'git log -1 --pretty=%ct' as i64: {}",
stdout
)
})
})
}
#[allow(clippy::missing_panics_doc)]
pub fn no_debug_rebuilds() {
if &get_env("PROFILE")
.unwrap()
.expect("PROFILE env var not set")
== "debug"
{
println!("cargo:rerun-if-env-changed=PROFILE");
}
}
pub fn set_SOURCE_DATE() {
println!(
"cargo:rustc-env=SOURCE_DATE={}",
format_date(get_source_time().unwrap())
);
}
pub fn set_SOURCE_TIME() {
println!(
"cargo:rustc-env=SOURCE_TIME={}",
format_time(get_source_time().unwrap())
);
}
pub fn set_SOURCE_TIMESTAMP() {
println!(
"cargo:rustc-env=SOURCE_TIMESTAMP={}",
format_timestamp(get_source_time().unwrap())
);
}
pub fn set_SOURCE_EPOCH_TIME() {
println!(
"cargo:rustc-env=SOURCE_EPOCH_TIME={}",
get_source_time().unwrap()
);
}
pub fn set_BUILD_DATE() {
println!("cargo:rustc-env=BUILD_DATE={}", format_date(now()));
}
pub fn set_BUILD_TIME() {
println!("cargo:rustc-env=BUILD_TIME={}", format_time(now()));
}
pub fn set_BUILD_TIMESTAMP() {
println!(
"cargo:rustc-env=BUILD_TIMESTAMP={}",
format_timestamp(now())
);
}
pub fn set_BUILD_EPOCH_TIME() {
println!("cargo:rustc-env=BUILD_EPOCH_TIME={}", now());
}
pub fn set_BUILD_HOSTNAME() {
println!("cargo:rustc-env=BUILD_HOSTNAME={}", get_hostname().unwrap());
}
pub fn set_GIT_BRANCH() {
println!("cargo:rustc-env=GIT_BRANCH={}", get_git_branch().unwrap());
}
pub fn set_GIT_COMMIT() {
println!("cargo:rustc-env=GIT_COMMIT={}", get_git_commit().unwrap());
}
pub fn set_GIT_COMMIT_SHORT() {
println!(
"cargo:rustc-env=GIT_COMMIT_SHORT={}",
get_git_commit_short().unwrap()
);
}
pub fn set_GIT_DIRTY() {
println!("cargo:rustc-env=GIT_DIRTY={}", get_git_dirty().unwrap());
}
pub fn set_RUSTC_VERSION() {
println!(
"cargo:rustc-env=RUSTC_VERSION={}",
get_rustc_version().unwrap()
);
}
pub fn set_RUSTC_VERSION_SEMVER() {
println!(
"cargo:rustc-env=RUSTC_VERSION_SEMVER={}",
parse_rustc_semver(get_rustc_version().unwrap()).unwrap()
);
}
pub fn set_RUST_CHANNEL() {
println!(
"cargo:rustc-env=RUST_CHANNEL={}",
parse_rustc_channel(get_rustc_version().unwrap()).unwrap()
);
}