This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ant.git
The following commit(s) were added to refs/heads/master by this push:
new 928be61e7 add DateUtils#getNow as helper for reproducible builds
928be61e7 is described below
commit 928be61e77a19328ce13d44e89ddc5f77fcbe4b4
Author: Stefan Bodewig <[email protected]>
AuthorDate: Sat Jul 11 15:24:06 2026 +0200
add DateUtils#getNow as helper for reproducible builds
this mirrors what Tstamp does but we can't use it in Tstamp because
people may have overridden the protected getNow methods.
This is something I intend to use for reproducible builds of/with Ant
after 1.10.18 - and want to use in the next release of the CycloneDX
Antlib.
---
src/main/org/apache/tools/ant/util/DateUtils.java | 78 ++++++++++++
src/tests/antunit/util/dateutils-test.xml | 144 ++++++++++++++++++++++
2 files changed, 222 insertions(+)
diff --git a/src/main/org/apache/tools/ant/util/DateUtils.java
b/src/main/org/apache/tools/ant/util/DateUtils.java
index bf92bf570..31f87a9e8 100644
--- a/src/main/org/apache/tools/ant/util/DateUtils.java
+++ b/src/main/org/apache/tools/ant/util/DateUtils.java
@@ -22,13 +22,22 @@ import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.AbstractMap;
import java.util.Calendar;
+import java.time.Instant;
import java.util.Date;
import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
import java.util.TimeZone;
+import java.util.function.BiFunction;
+import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.apache.tools.ant.MagicNames;
+import org.apache.tools.ant.Project;
+
/**
* Helper methods to deal with date/time formatting with a specific
* defined format (<a href="https://www.w3.org/TR/NOTE-datetime">ISO8601</a>)
@@ -371,4 +380,73 @@ public final class DateUtils {
+ (m.group(6) == null ? "00" : m.group(6));
return iso8601WithTimeZone.get().parse(normISO);
}
+
+ /**
+ * Name od the environment variable used to set timestamps for
reproducible builds.
+ *
+ * @see "https://reproducible-builds.org/docs/source-date-epoch/"
+ * @since Ant 1.10.18
+ */
+ public 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 Ant 1.10.18
+ */
+ public 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/util/dateutils-test.xml
b/src/tests/antunit/util/dateutils-test.xml
new file mode 100644
index 000000000..f488e4fb0
--- /dev/null
+++ b/src/tests/antunit/util/dateutils-test.xml
@@ -0,0 +1,144 @@
+<?xml version="1.0"?>
+<!--
+ 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.
+-->
+<project default="antunit" xmlns:au="antlib:org.apache.ant.antunit">
+ <import file="../antunit-base.xml" />
+
+ <target name="-createNowPrinter">
+ <mkdir dir="${input}"/>
+ <mkdir dir="${output}"/>
+ <echo file="${input}/NowPrinter.java"><![CDATA[
+ import org.apache.tools.ant.BuildException;
+ import org.apache.tools.ant.Project;
+ import org.apache.tools.ant.util.DateUtils;
+ import java.text.SimpleDateFormat;
+ import java.util.*;
+ public class NowPrinter {
+ public static void main(String[] args) {
+ Project p = new Project();
+ int argsLen = args.length;
+ if (argsLen % 2 != 0) {
+ throw new BuildException("can't handle odd number of arguments");
+ }
+ for (int i = 0; i < argsLen; i += 2) {
+ p.setProperty(args[i], args[i + 1]);
+ }
+ Map.Entry<Date, Boolean> now = DateUtils.getNow(p);
+ SimpleDateFormat format = new
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+ format.setTimeZone(TimeZone.getTimeZone("UTC"));
+ System.out.println("NOW is " + format.format(now.getKey()));
+ System.out.println("REPRODUCIBLE_BUILDS is " + now.getValue());
+ }
+ }
+ ]]></echo>
+ <javac srcdir="${input}" destdir="${output}"/>
+ </target>
+
+ <target name="testGetNowMagicProperty" depends="-createNowPrinter">
+ <local name="testout"/>
+ <java classname="NowPrinter"
+ failonerror="true"
+ outputproperty="testout"
+ fork="true">
+ <classpath>
+ <pathelement location="${output}"/>
+ <pathelement path="${java.class.path}"/>
+ </classpath>
+ <arg value="ant.tstamp.now"/>
+ <arg value="86400"/>
+ </java>
+ <au:assertPropertyContains name="testout" value="NOW is
1970-01-02T00:00:00"/>
+ <au:assertPropertyContains name="testout" value="REPRODUCIBLE_BUILDS is
false"/>
+ </target>
+
+ <target name="testGetNowMagicPropertyIso" depends="-createNowPrinter">
+ <local name="testout"/>
+ <java classname="NowPrinter"
+ failonerror="true"
+ outputproperty="testout"
+ fork="true">
+ <classpath>
+ <pathelement location="${output}"/>
+ <pathelement path="${java.class.path}"/>
+ </classpath>
+ <arg value="ant.tstamp.now.iso"/>
+ <arg value="1972-04-17T08:07:00Z"/>
+ </java>
+ <au:assertPropertyContains name="testout" value="NOW is
1972-04-17T08:07:00"/>
+ <au:assertPropertyContains name="testout" value="REPRODUCIBLE_BUILDS is
false"/>
+ </target>
+
+ <target name="testGetNowMagicPropertyBoth" depends="-createNowPrinter">
+ <local name="testout"/>
+ <java classname="NowPrinter"
+ failonerror="true"
+ outputproperty="testout"
+ fork="true">
+ <classpath>
+ <pathelement location="${output}"/>
+ <pathelement path="${java.class.path}"/>
+ </classpath>
+ <arg value="ant.tstamp.now"/>
+ <arg value="86400"/>
+ <arg value="ant.tstamp.now.iso"/>
+ <arg value="1972-04-17T08:07:00Z"/>
+ </java>
+ <au:assertPropertyContains name="testout" value="NOW is
1972-04-17T08:07:00"/>
+ <au:assertPropertyContains name="testout" value="REPRODUCIBLE_BUILDS is
false"/>
+ </target>
+
+ <target name="testGetNowSourceDateEpoch" depends="-createNowPrinter">
+ <local name="testout"/>
+ <java classname="NowPrinter"
+ failonerror="true"
+ outputproperty="testout"
+ fork="true">
+ <classpath>
+ <pathelement location="${output}"/>
+ <pathelement path="${java.class.path}"/>
+ </classpath>
+ <arg value="ant.tstamp.now"/>
+ <arg value="86400"/>
+ <arg value="ant.tstamp.now.iso"/>
+ <arg value="1972-04-17T08:07:00Z"/>
+ <env key="SOURCE_DATE_EPOCH" value="1650585600"/>
+ </java>
+ <au:assertPropertyContains name="testout" value="NOW is
2022-04-22T00:00:00"/>
+ <au:assertPropertyContains name="testout" value="REPRODUCIBLE_BUILDS is
true"/>
+ </target>
+
+ <target name="testGetNowRealNow" depends="-createNowPrinter">
+ <local name="dstamp"/>
+ <local name="testout"/>
+ <!-- ignoring the time part and hoping the clock doesn't pass
+ midnight between running tstamp and NowPrinter -->
+ <tstamp>
+ <format pattern="yyyy-MM-dd" property="dstamp" timezone="UTC"/>
+ </tstamp>
+ <java classname="NowPrinter"
+ failonerror="true"
+ outputproperty="testout"
+ fork="true">
+ <classpath>
+ <pathelement location="${output}"/>
+ <pathelement path="${java.class.path}"/>
+ </classpath>
+ </java>
+ <au:assertPropertyContains name="testout" value="NOW is ${dstamp}T"/>
+ <au:assertPropertyContains name="testout" value="REPRODUCIBLE_BUILDS is
false"/>
+ </target>
+</project>