From 9c5bb8310948cc796efaf56c20571659913826dd Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sun, 7 Sep 2025 12:11:35 -0700 Subject: [PATCH 1/3] Create a record class for the build constants Motivation - Makes the core code easier to understand (by centralizing the fields and reflection code) - Provides a path that could allow us to migrate our use of gversion to generate a properties file instead of a Java class (which would be much allow simpler extraction code) --- .../team2813/lib2813/util/BuildConstants.java | 16 ++++ .../lib2813/util/BuildConstantsPublisher.java | 75 +++++++--------- .../lib2813/util/BuildConstantsRecord.java | 89 +++++++++++++++++++ .../util/BuildConstantsPublisherTest.java | 50 +++++++++-- 4 files changed, 181 insertions(+), 49 deletions(-) create mode 100644 lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java create mode 100644 lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java new file mode 100644 index 00000000..fcd2a2c1 --- /dev/null +++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java @@ -0,0 +1,16 @@ +package com.team2813.lib2813.util; + +import java.time.ZonedDateTime; + +/** Holder for data collected at build time about the robot code. */ +public interface BuildConstants { + + /** The current git branch when the code was built. */ + String gitBranch(); + + /** The time the most recent commit at HEAD was submitted. */ + ZonedDateTime gitSubmitTime(); + + /** The time the code was built. */ + ZonedDateTime buildTime(); +} diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java index bde76526..c0939d1b 100644 --- a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java +++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java @@ -2,6 +2,8 @@ import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableInstance; +import java.util.Optional; +import java.util.function.Function; /** * Publishes build constants to NetworkTables. @@ -62,20 +64,11 @@ * buildConstantsPublisher.log(); * } */ -public class BuildConstantsPublisher { +public final class BuildConstantsPublisher { /** The name of the NetworkTable under which the build constants are published. */ public static final String METADATA_TABLE_NAME = "Metadata"; - private String m_mavenName; - // Don't resolve BuildConstants.MAVEN_GROUP because it is always empty - private int m_gitRevision; - // Don't resolve BuildConstants.VERSION because it is always "unspecified". - private String m_gitSha; - private String m_gitDate; - private String m_gitBranch; - private String m_buildDate; - private long m_buildUnixTime; - private int m_dirty; + private final Optional constants; /** * Constructs a BuildConstantsPublisher. @@ -87,20 +80,12 @@ public class BuildConstantsPublisher { * constants. See class description for instructions on how to generate the class. */ public BuildConstantsPublisher(Class buildConstantsClass) { - try { - m_mavenName = (String) buildConstantsClass.getDeclaredField("MAVEN_NAME").get(null); - m_gitRevision = (int) buildConstantsClass.getDeclaredField("GIT_REVISION").get(null); - m_gitSha = (String) buildConstantsClass.getDeclaredField("GIT_SHA").get(null); - m_gitDate = (String) buildConstantsClass.getDeclaredField("GIT_DATE").get(null); - m_gitBranch = (String) buildConstantsClass.getDeclaredField("GIT_BRANCH").get(null); - m_buildDate = (String) buildConstantsClass.getDeclaredField("BUILD_DATE").get(null); - m_buildUnixTime = (long) buildConstantsClass.getDeclaredField("BUILD_UNIX_TIME").get(null); - m_dirty = (int) buildConstantsClass.getDeclaredField("DIRTY").get(null); - } catch (NoSuchFieldException | IllegalAccessException e) { - // TODO(vdikov): Add a proper error logging here so that developers can catch it when it - // happens - e.printStackTrace(); - } + constants = BuildConstantsRecord.fromGeneratedClass(buildConstantsClass); + } + + /** Gets the build constants extracted from the publisher. */ + public Optional buildConstants() { + return constants.map(Function.identity()); } /** @@ -112,26 +97,32 @@ public BuildConstantsPublisher(Class buildConstantsClass) { * constants are published. */ public void publish(NetworkTableInstance ntInstance) { - NetworkTable table = ntInstance.getTable(METADATA_TABLE_NAME); - table.getStringTopic("MavenName").publish().set(m_mavenName); - table.getIntegerTopic("GitRevision").publish().set(m_gitRevision); - table.getStringTopic("GitSha").publish().set(m_gitSha); - table.getStringTopic("GitDate").publish().set(m_gitDate); - table.getStringTopic("GitBranch").publish().set(m_gitBranch); - table.getStringTopic("BuildDate").publish().set(m_buildDate); - table.getIntegerTopic("BuildUnixTime").publish().set(m_buildUnixTime); - table.getIntegerTopic("Dirty").publish().set(m_dirty); + constants.ifPresent( + values -> { + NetworkTable table = ntInstance.getTable(METADATA_TABLE_NAME); + table.getStringTopic("MavenName").publish().set(values.mavenName()); + table.getIntegerTopic("GitRevision").publish().set(values.gitRevision()); + table.getStringTopic("GitSha").publish().set(values.gitSha()); + table.getStringTopic("GitDate").publish().set(values.gitSubmitTimeString()); + table.getStringTopic("GitBranch").publish().set(values.gitBranch()); + table.getStringTopic("BuildDate").publish().set(values.buildTimeString()); + table.getIntegerTopic("BuildUnixTime").publish().set(values.buildTimeMillis()); + table.getIntegerTopic("Dirty").publish().set(values.dirty()); + }); } /** Logs the build constants to the console. */ public void log() { - System.out.println("MavenName: " + m_mavenName); - System.out.println("GitRevision: " + m_gitRevision); - System.out.println("GitSha: " + m_gitSha); - System.out.println("GitDate: " + m_gitDate); - System.out.println("GitBranch: " + m_gitBranch); - System.out.println("BuildDate: " + m_buildDate); - System.out.println("BuildUnixTime: " + m_buildUnixTime); - System.out.println("Dirty: " + m_dirty); + constants.ifPresent( + values -> { + System.out.println("MavenName: " + values.mavenName()); + System.out.println("GitRevision: " + values.gitRevision()); + System.out.println("GitSha: " + values.gitSha()); + System.out.println("GitDate: " + values.gitSubmitTimeString()); + System.out.println("GitBranch: " + values.gitBranch()); + System.out.println("BuildDate: " + values.buildTimeString()); + System.out.println("BuildUnixTime: " + values.buildTimeMillis()); + System.out.println("Dirty: " + values.dirty()); + }); } } diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java new file mode 100644 index 00000000..9bbbb848 --- /dev/null +++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java @@ -0,0 +1,89 @@ +package com.team2813.lib2813.util; + +import edu.wpi.first.wpilibj.DriverStation; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; + +record BuildConstantsRecord( + String mavenName, + int gitRevision, + String gitSha, + String gitBranch, + ZonedDateTime gitSubmitTime, + ZonedDateTime buildTime, + long buildTimeMillis, + int dirty) + implements BuildConstants { + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); + + /** + * Constructs an instance from a class generated by the gversion Gradle plugin. + * + *

Instructions for using the gversion Gradle plugin can be found buildConstantsClass, String fieldName) + throws NoSuchFieldException, IllegalAccessException { + String value = (String) buildConstantsClass.getDeclaredField(fieldName).get(null); + try { + return ZonedDateTime.parse(value, DATE_FORMATTER); + } catch (DateTimeParseException e) { + String message = + "Could not extract build constants from " + + buildConstantsClass.getSimpleName() + + " due to unparsable date-time value for " + + fieldName + + ": " + + e.getMessage(); + DriverStation.reportWarning(message, e.getStackTrace()); + return null; + } + } +} diff --git a/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java index a105e8b8..89ddce28 100644 --- a/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java +++ b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java @@ -11,12 +11,19 @@ import edu.wpi.first.networktables.NetworkTableInstance; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import org.junit.jupiter.api.Test; public class BuildConstantsPublisherTest { + // This format must be consistent with the `createVersionFile` settings in the build.gradle. + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); + // Fake constants copied and adapted from this article // https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html public final class FakeBuildConstants { @@ -29,7 +36,7 @@ public final class FakeBuildConstants { public static final String GIT_BRANCH = "main"; public static final String BUILD_DATE = "2023-10-27 12:29:57 EDT"; public static final long BUILD_UNIX_TIME = 1698424197122L; - public static final int DIRTY = 0; + public static final int DIRTY = 1; private FakeBuildConstants() {} } @@ -42,9 +49,6 @@ private FakeBuildConstants() {} */ private class DateTimeStringSubject extends Subject { private final String actual; - // The format must be consistent with the `createVersionFile` settings in the build.gradle. - private final DateTimeFormatter formatter = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); private DateTimeStringSubject(FailureMetadata metadata, String actual) { super(metadata, actual); @@ -58,10 +62,10 @@ public void parsesAsLocalDateTime() { } try { - LocalDateTime.parse(actual, formatter); + LocalDateTime.parse(actual, DATE_TIME_FORMATTER); } catch (DateTimeParseException e) { failWithActual( - fact("expected to parse as LocalDateTime with format", formatter), + fact("expected to parse as LocalDateTime with format", DATE_TIME_FORMATTER), fact("but parsing failed with", e.getMessage())); } } @@ -83,6 +87,36 @@ private Long getIntegerEntryOrDefault(NetworkTable table, String key, long defau return table.getIntegerTopic(key).getEntry(defaultValue).get(); } + @Test + public void extractsBuildConstants() { + // Arrange. + BuildConstantsPublisher publisher = new BuildConstantsPublisher(FakeBuildConstants.class); + + // Act. + var constants = publisher.buildConstants(); + + // Assert. + ZonedDateTime expectedBuildTime = + ZonedDateTime.ofInstant( + Instant.ofEpochMilli(FakeBuildConstants.BUILD_UNIX_TIME), + ZoneId.of("America/New_York")) + .withNano(0); + ZonedDateTime expectedGitCommitTime = + ZonedDateTime.parse(FakeBuildConstants.GIT_DATE, DATE_TIME_FORMATTER); + var expectedRecord = + new BuildConstantsRecord( + FakeBuildConstants.MAVEN_NAME, + FakeBuildConstants.GIT_REVISION, + FakeBuildConstants.GIT_SHA, + FakeBuildConstants.GIT_BRANCH, + expectedGitCommitTime, + expectedBuildTime, + FakeBuildConstants.BUILD_UNIX_TIME, + FakeBuildConstants.DIRTY); + + assertThat(constants).hasValue(expectedRecord); + } + @Test public void publishesBuildConstantsToNetworkTables() { // Arrange. @@ -117,7 +151,9 @@ public void publishesBuildConstantsToNetworkTables() { assertAbout(DateTimeStringSubject::new) .that(getStringEntryOrEmpty(table, "BuildDate")) .parsesAsLocalDateTime(); - assertThat(getIntegerEntryOrDefault(table, "Dirty", -1)).isAnyOf(0l, 1l); + assertThat(getIntegerEntryOrDefault(table, "Dirty", -1)).isAnyOf(0L, 1L); + + ntInstance.close(); } @Test From bf8a335750767e72dd4eb83433ced9bbcdaddddb Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sun, 7 Sep 2025 12:17:48 -0700 Subject: [PATCH 2/3] Update BuildConstantsPublisher JavaDoc to reference the WPILib docs (so it doesn't get stale) --- .../lib2813/util/BuildConstantsPublisher.java | 34 +++---------------- .../lib2813/util/BuildConstantsRecord.java | 18 ---------- 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java index c0939d1b..7d59e1ca 100644 --- a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java +++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java @@ -24,40 +24,14 @@ * *

To instantiate a BuildConstantsPublisher, a build constants class, {@code BuildConstants}, is * needs to be generated for the robot library by enabling the `gversion` plugin in the gradle build - * file. - * - *

{@code
- * plugins {
- *   ...
- *   // Plugin needed for Git Build Info
- *   // (see https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html)
- *   id 'com.peterabeles.gversion' version '1.10'
- *   ...
- * }
- * ...
- * // Generates a BuildConstants file.
- * // https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html
- * project.compileJava.dependsOn(createVersionFile)
- * def BUILD_CONSTANTS_AUTOGEN_PATH = 'build/generated/sources/build_constants/'
- * gversion {
- *     // Build inside build/ (so that it will be ignored by git due to .gitignore)
- *     // and inside build/generated/ (so that it will be ignored by our Spotless
- *     // rules).
- *     srcDir       = BUILD_CONSTANTS_AUTOGEN_PATH
- *     classPackage = 'com.team2813'
- *     className    = 'BuildConstants'
- *     dateFormat   = 'yyyy-MM-dd HH:mm:ss z'
- *     timeZone     = 'America/Los_Angeles' // Use preferred time zone
- *     indent       = '  '
- * }
- * sourceSets.main.java.srcDirs += BUILD_CONSTANTS_AUTOGEN_PATH
- * ...
- * }
+ * file. Instructions can be found in + * the WPILib documentation. * *

With the BuildConstants generation enabled, the publisher is initialized and used like this: * *

{@code
- * BuildConstantsPublisher buildConstantsPublisher(com.team2813.BuildConstants.class);
+ * BuildConstantsPublisher buildConstantsPublisher(frc.robot.BuildConstants.class);
  * // Publish the build constants to "/Metadata" on the NetworkTables
  * buildConstantsPublisher.publish(NetworkTableInstance.getDefault());
  * // Log the build constants in the robot console as well.
diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
index 9bbbb848..dbd15e0f 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
@@ -68,22 +68,4 @@ String gitSubmitTimeString() {
   String buildTimeString() {
     return DATE_FORMATTER.format(buildTime);
   }
-
-  private static ZonedDateTime extractZonedDateTime(Class buildConstantsClass, String fieldName)
-      throws NoSuchFieldException, IllegalAccessException {
-    String value = (String) buildConstantsClass.getDeclaredField(fieldName).get(null);
-    try {
-      return ZonedDateTime.parse(value, DATE_FORMATTER);
-    } catch (DateTimeParseException e) {
-      String message =
-          "Could not extract build constants from "
-              + buildConstantsClass.getSimpleName()
-              + " due to unparsable date-time value for "
-              + fieldName
-              + ": "
-              + e.getMessage();
-      DriverStation.reportWarning(message, e.getStackTrace());
-      return null;
-    }
-  }
 }

From b7ea2c104fb5d55986f63880eaee5248cf3318ac Mon Sep 17 00:00:00 2001
From: Kevin Cooney 
Date: Sun, 7 Sep 2025 12:37:43 -0700
Subject: [PATCH 3/3] Improve error message when GIT_DATE or BUILD_DATE cannot
 be parsed

---
 .../lib2813/util/BuildConstantsRecord.java    | 34 ++++++++++++++++---
 1 file changed, 29 insertions(+), 5 deletions(-)

diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
index dbd15e0f..56b2a6d5 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
@@ -30,12 +30,18 @@ record BuildConstantsRecord(
    */
   static Optional fromGeneratedClass(Class buildConstantsClass) {
     try {
+      ZonedDateTime gitDate = extractZonedDateTime(buildConstantsClass, "GIT_DATE");
+      if (gitDate == null) {
+        return Optional.empty();
+      }
+      ZonedDateTime buildDate = extractZonedDateTime(buildConstantsClass, "BUILD_DATE");
+      if (buildDate == null) {
+        return Optional.empty();
+      }
       String mavenName = (String) buildConstantsClass.getDeclaredField("MAVEN_NAME").get(null);
       int gitRevision = (int) buildConstantsClass.getDeclaredField("GIT_REVISION").get(null);
       String gitSha = (String) buildConstantsClass.getDeclaredField("GIT_SHA").get(null);
-      String gitDate = (String) buildConstantsClass.getDeclaredField("GIT_DATE").get(null);
       String gitBranch = (String) buildConstantsClass.getDeclaredField("GIT_BRANCH").get(null);
-      String buildDate = (String) buildConstantsClass.getDeclaredField("BUILD_DATE").get(null);
       long buildTimeMillis =
           (long) buildConstantsClass.getDeclaredField("BUILD_UNIX_TIME").get(null);
       int dirty = (int) buildConstantsClass.getDeclaredField("DIRTY").get(null);
@@ -46,11 +52,11 @@ static Optional fromGeneratedClass(Class buildConstants
               gitRevision,
               gitSha,
               gitBranch,
-              ZonedDateTime.parse(gitDate, DATE_FORMATTER),
-              ZonedDateTime.parse(buildDate, DATE_FORMATTER),
+              gitDate,
+              buildDate,
               buildTimeMillis,
               dirty));
-    } catch (NoSuchFieldException | IllegalAccessException | DateTimeParseException e) {
+    } catch (NoSuchFieldException | IllegalAccessException e) {
       String message =
           "Could not extract build constants from "
               + buildConstantsClass.getSimpleName()
@@ -68,4 +74,22 @@ String gitSubmitTimeString() {
   String buildTimeString() {
     return DATE_FORMATTER.format(buildTime);
   }
+
+  private static ZonedDateTime extractZonedDateTime(Class buildConstantsClass, String fieldName)
+      throws NoSuchFieldException, IllegalAccessException {
+    String value = (String) buildConstantsClass.getDeclaredField(fieldName).get(null);
+    try {
+      return ZonedDateTime.parse(value, DATE_FORMATTER);
+    } catch (DateTimeParseException e) {
+      String message =
+          "Could not extract build constants from "
+              + buildConstantsClass.getSimpleName()
+              + " due to unparsable date-time value for "
+              + fieldName
+              + ": "
+              + e.getMessage();
+      DriverStation.reportWarning(message, e.getStackTrace());
+      return null;
+    }
+  }
 }