diff --git a/boulder/data/macros/arch/base.yaml b/boulder/data/macros/arch/base.yaml index 003b8b3..f500570 100644 --- a/boulder/data/macros/arch/base.yaml +++ b/boulder/data/macros/arch/base.yaml @@ -58,6 +58,7 @@ actions : set -e set -x TERM="dumb"; export TERM + SOURCE_DATE_EPOCH="%(sourcedateepoch)"; export SOURCE_DATE_EPOCH PKG_CONFIG_PATH="%(pkgconfigpath)"; export PKG_CONFIG_PATH CFLAGS="%(cflags)"; export CFLAGS CGO_CFLAGS="%(cflags)"; export CGO_CFLAGS diff --git a/boulder/src/build/job/phase.rs b/boulder/src/build/job/phase.rs index 816f1d6..7c01997 100644 --- a/boulder/src/build/job/phase.rs +++ b/boulder/src/build/job/phase.rs @@ -150,6 +150,8 @@ impl Phase { parser.add_definition("compiler_cache", "/mason/ccache"); + parser.add_definition("sourcedateepoch", recipe.build_time.timestamp()); + let path = if ccache { "/usr/lib/ccache/bin:/usr/bin:/bin" } else { diff --git a/boulder/src/recipe.rs b/boulder/src/recipe.rs index bae649b..1f3f5aa 100644 --- a/boulder/src/recipe.rs +++ b/boulder/src/recipe.rs @@ -3,10 +3,12 @@ // SPDX-License-Identifier: MPL-2.0 use std::{ - fs, io, + env, fs, io, path::{Path, PathBuf}, + process::Command, }; +use chrono::{DateTime, Utc}; use thiserror::Error; use crate::architecture::{self, BuildTarget}; @@ -18,6 +20,7 @@ pub struct Recipe { pub path: PathBuf, pub source: String, pub parsed: Parsed, + pub build_time: DateTime, } impl Recipe { @@ -25,8 +28,14 @@ impl Recipe { let path = resolve_path(path)?; let source = fs::read_to_string(&path)?; let parsed = stone_recipe::from_str(&source)?; - - Ok(Self { path, source, parsed }) + let build_time = resolve_build_time(&path); + + Ok(Self { + path, + source, + parsed, + build_time, + }) } pub fn build_targets(&self) -> Vec { @@ -101,6 +110,37 @@ pub fn resolve_path(path: impl AsRef) -> Result { fs::canonicalize(&path).map_err(|_| Error::MissingRecipe(path)) } +fn resolve_build_time(path: &Path) -> DateTime { + // Propagate SOURCE_DATE_EPOCH if set + if let Ok(epoch_env) = env::var("SOURCE_DATE_EPOCH") { + if let Ok(parsed) = epoch_env.parse::() { + if let Some(timestamp) = DateTime::from_timestamp(parsed, 0) { + return timestamp; + } + } + } + + // If we are building from a git repo and have the git binary available to us then use the last commit timestamp + if let Some(recipe_dir) = path.parent() { + if let Ok(git_log) = Command::new("git") + .args(["log", "-1", "--format=\"%at\""]) + .current_dir(recipe_dir) + .output() + { + if let Ok(stdout) = String::from_utf8(git_log.stdout) { + if let Ok(parsed) = stdout.replace(['\n', '"'], "").parse::() { + if let Some(timestamp) = DateTime::from_timestamp(parsed, 0) { + return timestamp; + } + } + } + } + } + + // As a final fallback use the current time + Utc::now() +} + #[derive(Debug, Error)] pub enum Error { #[error("recipe file does not exist: {0:?}")]