use std::error::Error;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
#[cfg(not(test))]
fn main() {
let metadata = match std::env::var("CARGO_MANIFEST_DIR") {
Ok(directory) => build_metadata(Path::new(&directory)),
Err(error) => BuildMetadata::unknown(&error),
};
if let Err(error) = write_metadata(&metadata, &mut io::stdout().lock()) {
eprintln!("self-stream build metadata emission failed: {error}");
std::process::exit(1);
}
}
pub fn write_metadata(metadata: &BuildMetadata, output: &mut impl Write) -> io::Result<()> {
writeln!(output, "cargo:rerun-if-changed=build.rs")?;
for path in &metadata.watched {
writeln!(output, "cargo:rerun-if-changed={}", path.display())?;
}
if let Some(warning) = &metadata.warning {
writeln!(output, "cargo:warning=SELF_STREAM_BUILD_UNKNOWN: {warning}")?;
}
writeln!(
output,
"cargo:rustc-env=LIMINAL_BUILD_COMMIT={}",
metadata.commit
)
}
pub struct BuildMetadata {
pub commit: String,
pub watched: Vec<PathBuf>,
pub warning: Option<String>,
}
impl BuildMetadata {
fn unknown(error: &dyn std::fmt::Display) -> Self {
Self {
commit: "unknown".to_owned(),
watched: Vec::new(),
warning: Some(error.to_string().replace(['\n', '\r'], " ")),
}
}
}
#[must_use]
pub fn build_metadata(directory: &Path) -> BuildMetadata {
match resolve_metadata(directory) {
Ok(metadata) => metadata,
Err(error) => BuildMetadata::unknown(&error),
}
}
fn resolve_metadata(directory: &Path) -> Result<BuildMetadata, Box<dyn Error>> {
let directory = std::fs::canonicalize(directory)?;
let workspace = workspace_root(&directory)?;
if !workspace.join(".git").try_exists()? {
return Err("workspace has no git metadata".into());
}
let head = git_output(&workspace, &["rev-parse", "--verify", "HEAD"])?;
let commit = std::str::from_utf8(&head)?.trim();
if !matches!(commit.len(), 40 | 64) || !commit.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err("self-stream build metadata git HEAD is not a commit hash".into());
}
let mut watched = vec![git_path(&workspace, "HEAD")?];
let symbolic = git_command(&workspace, &["symbolic-ref", "-q", "HEAD"])?;
if symbolic.status.success() {
let reference = std::str::from_utf8(&symbolic.stdout)?.trim();
if !reference.starts_with("refs/") || reference.contains(['\n', '\r']) {
return Err("self-stream build metadata HEAD reference is invalid".into());
}
watched.push(git_path(&workspace, reference)?);
} else if symbolic.status.code() != Some(1) || !symbolic.stderr.is_empty() {
return Err(git_failure(&symbolic).into());
}
Ok(BuildMetadata {
commit: commit.to_owned(),
watched,
warning: None,
})
}
fn workspace_root(directory: &Path) -> Result<PathBuf, Box<dyn Error>> {
for ancestor in directory.ancestors() {
let manifest = ancestor.join("Cargo.toml");
if manifest.try_exists()? {
let table: toml::Table = toml::from_str(&std::fs::read_to_string(manifest)?)?;
if table.get("workspace").is_some_and(toml::Value::is_table) {
return Ok(ancestor.to_path_buf());
}
}
}
Err("self-stream build metadata workspace root is absent".into())
}
fn git_path(workspace: &Path, name: &str) -> Result<PathBuf, Box<dyn Error>> {
let output = git_output(workspace, &["rev-parse", "--git-path", name])?;
let path = std::str::from_utf8(&output)?.trim();
if path.is_empty() || path.contains(['\n', '\r']) {
return Err("self-stream build metadata git watch path is invalid".into());
}
Ok(workspace.join(path))
}
fn git_command(directory: &Path, arguments: &[&str]) -> Result<Output, Box<dyn Error>> {
Command::new("git")
.current_dir(directory)
.args(arguments)
.output()
.map_err(|error| {
format!("self-stream build metadata git command could not start: {error}").into()
})
}
fn git_failure(output: &Output) -> String {
format!(
"self-stream build metadata git command failed ({}): {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
)
}
fn git_output(directory: &Path, arguments: &[&str]) -> Result<Vec<u8>, Box<dyn Error>> {
let output = git_command(directory, arguments)?;
if !output.status.success() {
return Err(git_failure(&output).into());
}
Ok(output.stdout)
}