#![expect(clippy::unwrap_used)]
#![allow(
clippy::allow_attributes,
clippy::disallowed_methods,
clippy::disallowed_types
)] #![warn(missing_docs)]
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::Context as _;
mod git;
mod hashing;
mod rebuild_detector;
mod rustfmt;
pub use self::git::{git_branch, git_commit_hash, git_commit_short_hash};
pub use self::hashing::{
compute_crate_hash, compute_dir_filtered_hash, compute_dir_hash, compute_file_hash,
compute_strings_hash, iter_dir, read_versioning_hash, write_versioning_hash,
};
pub(crate) use self::rebuild_detector::Packages;
pub use self::rebuild_detector::{
get_and_track_env_var, is_tracked_env_var_set, rebuild_if_crate_changed, rerun_if_changed,
rerun_if_changed_glob, rerun_if_changed_or_doesnt_exist, write_file_if_necessary,
};
pub use self::rustfmt::rustfmt_str;
const EXPORT_GIT_FOR_DEVELOPERS: bool = false;
pub(crate) static OUTPUT_CARGO_BUILD_INSTRUCTIONS: AtomicBool = AtomicBool::new(true);
pub fn set_output_cargo_build_instructions(output_instructions: bool) {
OUTPUT_CARGO_BUILD_INSTRUCTIONS.store(output_instructions, Ordering::Relaxed);
}
pub(crate) fn should_output_cargo_build_instructions() -> bool {
OUTPUT_CARGO_BUILD_INSTRUCTIONS.load(Ordering::Relaxed)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Environment {
PublishingCrates,
RerunCI,
CondaBuild,
DeveloperInWorkspace,
UsedAsDependency,
}
impl Environment {
pub fn detect() -> Self {
let is_in_rerun_workspace = is_tracked_env_var_set("IS_IN_RERUN_WORKSPACE");
if is_tracked_env_var_set("RERUN_IS_PUBLISHING_CRATES") {
eprintln!("Environment: env-var RERUN_IS_PUBLISHING_CRATES is set");
Self::PublishingCrates
} else if is_in_rerun_workspace && std::env::var("CI").is_ok() {
eprintln!("Environment: env-var IS_IN_RERUN_WORKSPACE and CI are set");
Self::RerunCI
} else if std::env::var("CONDA_BUILD").is_ok() {
eprintln!("Environment: env-var CONDA_BUILD is set");
Self::CondaBuild
} else if is_in_rerun_workspace {
eprintln!("Environment: env-var IS_IN_RERUN_WORKSPACE is set");
Self::DeveloperInWorkspace
} else {
eprintln!("Environment: Not on CI and not in workspace");
Self::UsedAsDependency
}
}
}
pub fn export_build_info_vars_for_crate(crate_name: &str) {
let environment = Environment::detect();
let export_datetime = match environment {
Environment::PublishingCrates | Environment::RerunCI | Environment::CondaBuild => true,
Environment::DeveloperInWorkspace | Environment::UsedAsDependency => {
is_tracked_env_var_set("RERUN_ADD_BUILD_TIME_TO_BUILD_INFO")
}
};
let export_git_info = match environment {
Environment::PublishingCrates | Environment::RerunCI => true,
Environment::DeveloperInWorkspace => EXPORT_GIT_FOR_DEVELOPERS,
Environment::UsedAsDependency | Environment::CondaBuild => false,
};
if export_datetime && is_tracked_env_var_set("DATETIME") {
set_env(
"RE_BUILD_DATETIME",
&std::env::var("DATETIME").unwrap_or_default(),
);
} else if export_datetime {
set_env("RE_BUILD_DATETIME", &date_time());
rebuild_if_crate_changed(crate_name);
} else {
set_env("RE_BUILD_DATETIME", "");
}
if export_git_info && is_tracked_env_var_set("GIT_HASH") && is_tracked_env_var_set("GIT_BRANCH")
{
set_env(
"RE_BUILD_GIT_HASH",
&std::env::var("GIT_HASH").unwrap_or_default(),
);
set_env(
"RE_BUILD_GIT_BRANCH",
&std::env::var("GIT_BRANCH").unwrap_or_default(),
);
} else if export_git_info {
set_env(
"RE_BUILD_GIT_HASH",
&git::git_commit_hash().unwrap_or_default(),
);
set_env(
"RE_BUILD_GIT_BRANCH",
&git::git_branch().unwrap_or_default(),
);
git::rebuild_if_branch_or_commit_changes();
} else {
set_env("RE_BUILD_GIT_HASH", "");
set_env("RE_BUILD_GIT_BRANCH", "");
}
{
set_env("RE_BUILD_TARGET_TRIPLE", &std::env::var("TARGET").unwrap());
let (rustc, llvm) = rust_llvm_versions().unwrap_or_default();
set_env("RE_BUILD_RUSTC_VERSION", &rustc);
set_env("RE_BUILD_LLVM_VERSION", &llvm);
if environment == Environment::RerunCI {
set_env("RE_BUILD_IS_IN_RERUN_WORKSPACE", "no");
} else {
set_env(
"RE_BUILD_IS_IN_RERUN_WORKSPACE",
&std::env::var("IS_IN_RERUN_WORKSPACE").unwrap_or_default(),
);
}
}
if environment == Environment::PublishingCrates {
set_env("RE_BUILD_FEATURES", "<unknown>");
} else {
let features = enabled_features_of(crate_name);
let features = match features {
Ok(features) => features.join(" "),
Err(_err) if environment == Environment::UsedAsDependency => "<error>".to_owned(),
Err(err) => panic!("{err}"),
};
set_env("RE_BUILD_FEATURES", &features);
}
}
fn date_time() -> String {
jiff::Timestamp::now().to_string()
}
fn set_env(name: &str, value: &str) {
if should_output_cargo_build_instructions() {
println!("cargo:rustc-env={name}={value}");
}
}
fn run_command(cmd: &str, args: &[&str]) -> anyhow::Result<String> {
let output = Command::new(cmd)
.args(args)
.output()
.with_context(|| format!("running '{cmd}'"))?;
anyhow::ensure!(
output.status.success(),
"Failed to run '{cmd} {args:?}':\n{}\n{}\n",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}
fn rust_llvm_versions() -> anyhow::Result<(String, String)> {
let cmd = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into());
let args = &["-vV"];
let res = run_command(&cmd, args)?;
let mut rustc_version = None;
let mut llvm_version = None;
for line in res.lines() {
if let Some(version) = line.strip_prefix("rustc ") {
rustc_version = Some(version.to_owned());
} else if let Some(version) = line.strip_prefix("LLVM version: ") {
llvm_version = Some(version.to_owned());
}
}
Ok((
rustc_version.unwrap_or_else(|| "unknown".to_owned()),
llvm_version.unwrap_or_else(|| "unknown".to_owned()),
))
}
pub fn cargo_metadata() -> anyhow::Result<cargo_metadata::Metadata> {
anyhow::ensure!(
Environment::detect() != Environment::PublishingCrates,
"Can't get metadata during crate publishing - it would create a Cargo.lock file"
);
Ok(cargo_metadata::MetadataCommand::new()
.no_deps()
.other_options(vec!["--frozen".to_owned()])
.exec()?)
}
pub fn enabled_features_of(crate_name: &str) -> anyhow::Result<Vec<String>> {
let metadata = cargo_metadata()?;
let mut features = vec![];
for package in &metadata.packages {
if package.name.as_str() == crate_name {
for feature in package.features.keys() {
println!("Checking if feature is enabled: {feature:?}");
let feature_in_screaming_snake_case =
feature.to_ascii_uppercase().replace('-', "_");
if std::env::var(format!("CARGO_FEATURE_{feature_in_screaming_snake_case}")).is_ok()
{
features.push(feature.clone());
}
}
}
}
Ok(features)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_date_time_format() {
let timestamp = date_time();
let regex =
regex_lite::Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$").unwrap();
assert!(
regex.is_match(×tamp),
"Timestamp format is incorrect: {timestamp}"
);
}
}