This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git

commit d3a049834abb7ea0abd775307b7b46a7e1b7d7e2
Author: Stefan Bodewig <[email protected]>
AuthorDate: Sat Jul 18 09:26:14 2026 +0200

    make SBOM's timestamp honor reproducible builds requirements
---
 changes.xml                                        |  7 ++
 .../org/apache/ant/cyclonedx/ComponentBomTask.java |  2 +-
 src/main/org/apache/ant/cyclonedx/DateUtils.java   | 97 ++++++++++++++++++++++
 src/tests/antunit/componentbom-test.xml            | 25 ++++++
 4 files changed, 130 insertions(+), 1 deletion(-)

diff --git a/changes.xml b/changes.xml
index 4ced467..148e3ce 100644
--- a/changes.xml
+++ b/changes.xml
@@ -77,6 +77,13 @@
       the serial number of the generated SBOM to be set to a fixed
       value for reproducible builds.
     </action>
+    <action type="update">
+      The componentbom and archivebom tasks honor the
+      SOURCE_DATE_EPOCH environment variable as well as Ant's magic
+      properties ant.tstamp.now.iso and ant.tstamp.now and use either
+      as timestamp for the generated SBOM for reproducible builds. The
+      default still is to use the current point in time as timestamp.
+    </action>
     <action type="update">
       Updated cyclonedx-java-core library dependency to 13.0.0.
     </action>
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java 
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index 0ecde28..2b23c1e 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -345,7 +345,7 @@ public class ComponentBomTask extends Task {
 
     private Metadata createMetadata() throws IOException {
         Metadata meta = new Metadata();
-        meta.setTimestamp(new Date());
+        meta.setTimestamp(DateUtils.getNow(getProject()).getKey());
         ToolInformation antlibToolInformation = 
ToolData.getToolInformation(specVersion.getVersion());
         if (!toolComponents.isEmpty()) {
             List<org.cyclonedx.model.Component> tools =
diff --git a/src/main/org/apache/ant/cyclonedx/DateUtils.java 
b/src/main/org/apache/ant/cyclonedx/DateUtils.java
new file mode 100644
index 0000000..3752287
--- /dev/null
+++ b/src/main/org/apache/ant/cyclonedx/DateUtils.java
@@ -0,0 +1,97 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ant.cyclonedx;
+
+import java.util.AbstractMap;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TimeZone;
+import java.util.function.Function;
+import java.util.function.BiFunction;
+
+import org.apache.tools.ant.MagicNames;
+import org.apache.tools.ant.Project;
+
+final class DateUtils {
+
+    private static final String ENV_SOURCE_DATE_EPOCH = "SOURCE_DATE_EPOCH";
+
+    /**
+     * Consults the magic properties {@link MagicNames.TSTAMP_NOW_ISO} and 
{@link MagicNames.TSTAMP_NOW} as well as the
+     * environment variable {@code SOURCE_DATE_EPOCH} for predefined values of 
"now" and falls back to {@code new
+     * Date()} if neiter is set.
+     *
+     * <p>{@code SOURCE_DATE_EPOCH} takes precendence over {@link 
MagicNames.TSTAMP_NOW_ISO} which in turn takes precendence over {@link 
MagicNames.TSTAMP_NOW}.</p>
+     *
+     * @param project Project instance to use when looking up the magic 
properties.
+     * @return a tuple of "now" and a boolean flag that indicates whether 
{@code SOURCE_DATE_EPOCH} has been set.
+     * @since CycloneDX Antlib 0.2
+     */
+    // stolen from Ant 1.10.18
+    static Map.Entry<Date, Boolean> getNow(Project project) {
+        final String epoch = System.getenv(ENV_SOURCE_DATE_EPOCH);
+        if (epoch != null) {
+            // Value of SOURCE_DATE_EPOCH will be an integer, representing 
seconds.
+            try {
+                Date d = new Date(Long.parseLong(epoch) * 1000L);
+                project.log("Honouring environment variable " + 
ENV_SOURCE_DATE_EPOCH + " which has been set to " + epoch);
+                return new AbstractMap.SimpleImmutableEntry(d, true);
+            } catch(NumberFormatException e) {
+                // ignore
+                project.log("Ignoring invalid value '" + epoch + "' for " + 
ENV_SOURCE_DATE_EPOCH
+                            + " environment variable", Project.MSG_DEBUG);
+            }
+        }
+        return new AbstractMap.SimpleImmutableEntry(getNowAsDate(project), 
false);
+    }
+
+    private static Date getNowAsDate(Project p) {
+        Optional<Date> now = getNowAsDate(
+            p,
+            MagicNames.TSTAMP_NOW_ISO,
+            s -> Date.from(Instant.parse(s)),
+            (k, v) -> "magic property " + k + " ignored as '" + v + "' is not 
in valid ISO pattern"
+        );
+        if (now.isPresent()) {
+            return now.get();
+        }
+
+        now = getNowAsDate(
+            p,
+            MagicNames.TSTAMP_NOW,
+            s -> new Date(1000 * Long.parseLong(s)),
+            (k, v) -> "magic property " + k + " ignored as " + v + " is not a 
valid number"
+        );
+        return now.orElseGet(Date::new);
+    }
+
+    private static Optional<Date> getNowAsDate(Project p, String propertyName, 
Function<String, Date> map,
+                                               BiFunction<String, String, 
String> log) {
+        String property = p.getProperty(propertyName);
+        if (property != null && !property.isEmpty()) {
+            try {
+                return Optional.ofNullable(map.apply(property));
+            } catch (Exception e) {
+                p.log(log.apply(propertyName, property));
+            }
+        }
+        return Optional.empty();
+    }
+}
diff --git a/src/tests/antunit/componentbom-test.xml 
b/src/tests/antunit/componentbom-test.xml
index 02c551d..0f6e063 100644
--- a/src/tests/antunit/componentbom-test.xml
+++ b/src/tests/antunit/componentbom-test.xml
@@ -22,12 +22,19 @@
   <checksum property="sha256hash" file="${antlib.location}" 
algorithm="SHA-256"/>
 
   <target name="testToolMetadataInJsonFormat">
+    <tstamp>
+      <format timezone="UTC" pattern="yyyy-MM-dd" property="day"/>
+    </tstamp>
     <cdx:componentbom outputdirectory="${output}"
                       xmlns:cdx="antlib:org.apache.ant.cyclonedx">
       <component name="testname">
         <file file="${antlib.location}"/>
       </component>
     </cdx:componentbom>
+    <au:assertResourceContains
+        xmlns:au="antlib:org.apache.ant.antunit"
+        resource="${output}/bom.json"
+        value='"timestamp" : "${day}T'/>
     <au:assertResourceContains
         xmlns:au="antlib:org.apache.ant.antunit"
         resource="${output}/bom.json"
@@ -172,6 +179,24 @@
     </cdx:componentbom>
   </target>
 
+  <target name="testTimestampCanBeSet">
+    <local name="ant.tstamp.now.iso"/>
+    <property name="ant.tstamp.now.iso" value="1972-04-17T08:07:00Z"/>
+    <cdx:component name="testname" id="test"
+                   xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <file file="${antlib.location}"/>
+    </cdx:component>
+    <cdx:componentbom outputdirectory="${output}" format="xml"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component refid="test"/>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.timestamp"
+        value="${ant.tstamp.now.iso}"/>
+  </target>
+
   <target name="testMulipleTools">
     <cdx:componentbom
         bomName="multi"

Reply via email to