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>

Reply via email to