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"
