use std::env;
use std::path::Path;
use std::process::Command;
const KNOWN_FEATURES: &[&str] = &[
"secrets",
"linear",
"athena",
"docker",
"kafka",
"kubernetes",
"monitoring",
"nordvpn",
"openapi-gen",
"systemd",
];
fn main() {
let manifest_dir: String = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let version: String = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string());
let repository: String =
env::var("CARGO_PKG_REPOSITORY").unwrap_or_else(|_| "https://github.com/xylex-group/xbp".to_string());
let (kind, detail, git_url, git_rev) = detect_install_source(&manifest_dir, &version, &repository);
let features = collect_enabled_features();
let features_csv = features.join(",");
let features_plus = format_features_plus(&features);
println!("cargo:rustc-env=XBP_INSTALL_SOURCE_KIND={kind}");
println!("cargo:rustc-env=XBP_INSTALL_SOURCE_DETAIL={detail}");
println!("cargo:rustc-env=XBP_INSTALL_GIT_URL={git_url}");
println!("cargo:rustc-env=XBP_INSTALL_GIT_REV={git_rev}");
println!("cargo:rustc-env=XBP_ENABLED_FEATURES={features_csv}");
println!("cargo:rustc-env=XBP_ENABLED_FEATURES_PLUS={features_plus}");
println!("cargo:rustc-env=XBP_PKG_REPOSITORY={repository}");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:rerun-if-env-changed=XBP_INSTALL_SOURCE");
println!("cargo:rerun-if-env-changed=XBP_INSTALL_GIT_URL");
println!("cargo:rerun-if-env-changed=XBP_INSTALL_GIT_REV");
}
fn collect_enabled_features() -> Vec<String> {
let mut enabled = Vec::new();
for feature in KNOWN_FEATURES {
let env_key = format!(
"CARGO_FEATURE_{}",
feature.to_ascii_uppercase().replace('-', "_")
);
if env::var_os(&env_key).is_some() {
enabled.push((*feature).to_string());
}
}
enabled
}
fn format_features_plus(features: &[String]) -> String {
const DEFAULT: &[&str] = &["secrets", "linear"];
let mut base = Vec::new();
let mut extra = Vec::new();
for feature in features {
if DEFAULT.contains(&feature.as_str()) {
base.push(feature.clone());
} else {
extra.push(format!("+{feature}"));
}
}
base.sort_by_key(|f| DEFAULT.iter().position(|d| d == f).unwrap_or(usize::MAX));
let mut out = base.join(",");
if !extra.is_empty() {
if !out.is_empty() {
out.push(' ');
}
out.push_str(&extra.join(" "));
}
if out.is_empty() {
"(none)".to_string()
} else {
out
}
}
fn detect_install_source(
manifest_dir: &str,
version: &str,
repository: &str,
) -> (String, String, String, String) {
if let Ok(forced_kind) = env::var("XBP_INSTALL_SOURCE") {
let kind = forced_kind.trim().to_ascii_lowercase();
let git_url: String = env::var("XBP_INSTALL_GIT_URL").unwrap_or_default();
let git_rev: String = env::var("XBP_INSTALL_GIT_REV").unwrap_or_default();
let detail = match kind.as_str() {
"crates.io" | "crates" | "registry" => format!("xbp@{version}"),
"git" => {
let url = if git_url.is_empty() {
repository.to_string()
} else {
git_url.clone()
};
if git_rev.is_empty() {
url
} else {
format!("{url}#{git_rev}")
}
}
_ => manifest_dir.to_string(),
};
let kind = match kind.as_str() {
"crates" | "registry" => "crates.io".to_string(),
other => other.to_string(),
};
return (kind, detail, git_url, git_rev);
}
let path = Path::new(manifest_dir);
let normalized = path.to_string_lossy().replace('\\', "/").to_ascii_lowercase();
if normalized.contains("/registry/src/")
|| normalized.contains("index.crates.io")
|| normalized.contains("crates.io-")
{
return (
"crates.io".to_string(),
format!("xbp@{version}"),
String::new(),
String::new(),
);
}
if normalized.contains("/git/checkouts/") {
let git_url = git_remote_url(path)
.or_else(|| git_remote_url(&path.join("..")))
.unwrap_or_else(|| repository.to_string());
let git_rev = git_rev_short(path).unwrap_or_default();
let detail = if git_rev.is_empty() {
git_url.clone()
} else {
format!("{git_url}#{git_rev}")
};
return ("git".to_string(), detail, git_url, git_rev);
}
let git_url = git_remote_url(path)
.or_else(|| find_git_remote_upwards(path))
.unwrap_or_default();
let git_rev = git_rev_short(path)
.or_else(|| find_git_rev_upwards(path))
.unwrap_or_default();
if !git_url.is_empty() {
let detail = if git_rev.is_empty() {
format!("{git_url} (path)")
} else {
format!("{git_url}#{git_rev} (path)")
};
return ("path".to_string(), detail, git_url, git_rev);
}
(
"path".to_string(),
manifest_dir.to_string(),
String::new(),
String::new(),
)
}
fn find_git_remote_upwards(start: &Path) -> Option<String> {
for ancestor in start.ancestors().take(6) {
if let Some(url) = git_remote_url(ancestor) {
return Some(url);
}
}
None
}
fn find_git_rev_upwards(start: &Path) -> Option<String> {
for ancestor in start.ancestors().take(6) {
if let Some(rev) = git_rev_short(ancestor) {
return Some(rev);
}
}
None
}
fn git_remote_url(dir: &Path) -> Option<String> {
if !dir.join(".git").exists() && !dir_is_inside_git_worktree(dir) {
}
let output = Command::new("git")
.args(["remote", "get-url", "origin"])
.current_dir(dir)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
if url.is_empty() {
None
} else {
Some(normalize_git_url(&url))
}
}
fn git_rev_short(dir: &Path) -> Option<String> {
let output = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.current_dir(dir)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let rev = String::from_utf8_lossy(&output.stdout).trim().to_string();
if rev.is_empty() {
None
} else {
Some(rev)
}
}
fn dir_is_inside_git_worktree(dir: &Path) -> bool {
Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(dir)
.output()
.map(|out| out.status.success())
.unwrap_or(false)
}
fn normalize_git_url(url: &str) -> String {
let trimmed = url.trim();
if let Some(rest) = trimmed.strip_prefix("git@github.com:") {
let rest = rest.trim_end_matches(".git");
return format!("https://github.com/{rest}");
}
if let Some(rest) = trimmed.strip_prefix("ssh://git@github.com/") {
let rest = rest.trim_end_matches(".git");
return format!("https://github.com/{rest}");
}
trimmed.trim_end_matches(".git").to_string()
}