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

ppkarwasz pushed a commit to branch reproducible-version-info
in repository https://gitbox.apache.org/repos/asf/logging-flume.git

commit d32d9cb00b74c98b72d34737886f0adf9f1a94fe
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Fri Aug 21 12:25:15 2026 +0200

    Derive the version metadata from the JAR manifest
    
    `saveVersion.sh` recorded the builder's user name, host, clock and a
    checksum of the working copy, so no two builds agreed and the source
    distribution, which carries no repository metadata, agreed with none.
    The same facts now come from POM-derived manifest headers, which a Git
    checkout and the source archive produce identically.
    
    `getUser()` and `getSrcChecksum()` are deprecated for removal, and
    `getRevision()` reports nothing until the build records a commit id
    again.
    
    Assisted-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_011QwMh1JvFaPBgWEGrMgMj7
---
 flume-ng-core/pom.xml                              | 111 ----------
 flume-ng-core/scripts/saveVersion.ps1              |  61 ------
 flume-ng-core/scripts/saveVersion.sh               |  69 -------
 .../java/org/apache/flume/VersionAnnotation.java   |  72 -------
 .../java/org/apache/flume/tools/VersionInfo.java   | 223 ++++++++++++++++-----
 .../org/apache/flume/tools/TestVersionInfo.java    | 138 ++++++++++---
 flume-ng-sdk/pom.xml                               |  14 ++
 flume-parent/pom.xml                               |  10 +
 8 files changed, 312 insertions(+), 386 deletions(-)

diff --git a/flume-ng-core/pom.xml b/flume-ng-core/pom.xml
index 3a9bbdd9..a26093c9 100644
--- a/flume-ng-core/pom.xml
+++ b/flume-ng-core/pom.xml
@@ -177,115 +177,4 @@
       </plugin>
     </plugins>
   </build>
-  <profiles>
-    <profile>
-      <id>not-windows</id>
-      <activation>
-        <os>
-          <family>!Windows</family>
-        </os>
-      </activation>
-      <build>
-        <plugins>
-          <plugin>
-            <groupId>org.apache.maven.plugins</groupId>
-            <artifactId>maven-antrun-plugin</artifactId>
-            <executions>
-              <execution>
-                <id>generate-version</id>
-                <goals>
-                  <goal>run</goal>
-                </goals>
-                <phase>generate-sources</phase>
-                <configuration>
-                  <target>
-                    <mkdir 
dir="${project.build.directory}/generated-sources/java" />
-                    <exec executable="sh">
-                      <arg line="${basedir}/scripts/saveVersion.sh 
${project.version} ${project.build.directory}" />
-                    </exec>
-                  </target>
-                </configuration>
-              </execution>
-            </executions>
-          </plugin>
-
-          <plugin>
-            <groupId>org.codehaus.mojo</groupId>
-            <artifactId>build-helper-maven-plugin</artifactId>
-            <executions>
-              <execution>
-                <id>add-source</id>
-                <goals>
-                  <goal>add-source</goal>
-                </goals>
-                <phase>generate-sources</phase>
-                <configuration>
-                  <sources>
-                    <source>target/generated-sources/java</source>
-                  </sources>
-                </configuration>
-              </execution>
-            </executions>
-          </plugin>
-
-        </plugins>
-      </build>
-    </profile>
-
-    <profile>
-      <id>windows</id>
-      <activation>
-        <os>
-          <family>Windows</family>
-        </os>
-      </activation>
-      <build>
-        <plugins>
-          <plugin>
-            <groupId>org.apache.maven.plugins</groupId>
-            <artifactId>maven-antrun-plugin</artifactId>
-            <executions>
-              <execution>
-                <id>generate-version</id>
-                <goals>
-                  <goal>run</goal>
-                </goals>
-                <phase>generate-sources</phase>
-                <configuration>
-                  <target>
-                    <mkdir 
dir="${project.build.directory}/generated-sources/java/org/apache/flume" />
-                    <exec executable="powershell">
-                      <arg line="-executionpolicy unrestricted -file 
${basedir}\scripts\saveVersion.ps1  ${project.version} 
${project.build.directory}" />
-                    </exec>
-                  </target>
-                </configuration>
-              </execution>
-            </executions>
-          </plugin>
-
-          <plugin>
-            <groupId>org.codehaus.mojo</groupId>
-            <artifactId>build-helper-maven-plugin</artifactId>
-            <executions>
-              <execution>
-                <id>add-source</id>
-                <goals>
-                  <goal>add-source</goal>
-                </goals>
-                <phase>generate-sources</phase>
-                <configuration>
-                  <sources>
-                    <source>target/generated-sources/java</source>
-                  </sources>
-                </configuration>
-              </execution>
-            </executions>
-          </plugin>
-
-        </plugins>
-      </build>
-    </profile>
-
-  </profiles>
-
 </project>
diff --git a/flume-ng-core/scripts/saveVersion.ps1 
b/flume-ng-core/scripts/saveVersion.ps1
deleted file mode 100644
index dff88134..00000000
--- a/flume-ng-core/scripts/saveVersion.ps1
+++ /dev/null
@@ -1,61 +0,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
-#
-#     http://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.
-
-# This script is used to generate the annotation of package info that
-# records the version, revision, branch, url, user and timestamp.
-
-$version=$args[0]
-$buildDirectory=$args[1]
-$outputFile= 
"$buildDirectory\generated-sources\java\org\apache\flume\package-info.java"
-$user = $Env:username
-$date = Get-Date
-
-cmd /c svn info 2>&1 | Out-Null
-if ( $LastExitCode -eq 0 ) {
-  $revision=svn info  | % { if( $_ -match "Last Changed Rev: (?<rev>.*)" ) { 
$matches['rev'];} }
-  $url=svn info  | % { if( $_ -match "URL: (?<url>.*)" ) { $matches['url'];} }
-  $branch=  if( $url -match ".*(?<branch>(branches.*)|(tags.*)|(trunk.*))" ) { 
$matches['branch']; } else { "Unknown"; }
-}
-else {
-    cmd /c git rev-parse HEAD  2>&1 | Out-Null
-    if ( $LastExitCode -eq 0 ) {
-      $revision=$(git log -1 --pretty=format:"%H")
-      $branch=$(git name-rev --name-only HEAD)
-      $remote=$(git config branch.$branch.remote)
-      $url=$(git config remote.$remote.url)
-    }
-    else {
-      revision="Unknown"
-      branch="Unknown"
-      url="file://$cwd"
-    }
-}
-
-$srcChecksum="N/A"
-
-
-$fileContent = @"
-/*
- * Generated by scripts/saveVersion.ps1
- */
-@VersionAnnotation(version="$version", revision="$revision", branch="$branch",
-                         user="$user", date="$date", url="$url",
-                         srcChecksum="$srcChecksum")
-package org.apache.flume;
-"@
-
-New-Item $outputFile -value $fileContent -force -type file
-
diff --git a/flume-ng-core/scripts/saveVersion.sh 
b/flume-ng-core/scripts/saveVersion.sh
deleted file mode 100755
index ad3f8b19..00000000
--- a/flume-ng-core/scripts/saveVersion.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/sh
-
-# 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
-#
-#     http://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.
-
-# This script is used to generate the annotation of package info that
-# records the version, revision, branch, url, user and timestamp.
-
-unset LANG
-unset LC_CTYPE
-unset LC_TIME
-version=$1
-buildDirectory=$2
-user=`whoami`
-date=`date`
-dir=`pwd`
-cwd=`dirname $dir`
-if [ -d ../.svn ]; then
-  revision=`svn info ../ | sed -n -e 's/Last Changed Rev: \(.*\)/\1/p'`
-  url=`svn info  ../ | sed -n -e 's/URL: \(.*\)/\1/p'`
-  branch=`echo $url | sed -n -e 's,.*\(branches/.*\)$,\1,p' \
-                             -e 's,.*\(tags/.*\)$,\1,p' \
-                             -e 's,.*trunk$,trunk,p'`
-elif git rev-parse HEAD 2>/dev/null > /dev/null ; then
-  revision=`git log -1 --pretty=format:"%H"`
-  hostname=`hostname`
-  branch=`git branch | sed -n -e 's/^* //p'`
-  url="git://${hostname}${cwd}"
-else
-  revision="Unknown"
-  branch="Unknown"
-  url="file://$cwd"
-fi
-
-if [ -n "$(which md5sum)" ]; then
-  srcChecksum=`find ../ -name '*.java' | grep -v generated-sources | LC_ALL=C 
sort | \
-      xargs md5sum | md5sum | cut -d ' ' -f 1`
-else
-  srcChecksum=`find ../ -name '*.java' | grep -v generated-sources | LC_ALL=C 
sort | \
-      xargs md5 | md5 | cut -d ' ' -f 1`
-fi
-
-mkdir -p $buildDirectory/generated-sources/java/org/apache/flume/
-cat << EOF | \
-  sed -e "s/VERSION/$version/" -e "s/USER/$user/" -e "s/DATE/$date/" \
-      -e "s|URL|$url|" -e "s/REV/$revision/" \
-      -e "s|BRANCH|$branch|" -e "s/SRCCHECKSUM/$srcChecksum/" \
-      > 
$buildDirectory/generated-sources/java/org/apache/flume/package-info.java
-/*
- * Generated by scripts/saveVersion.sh
- */
-@VersionAnnotation(version="VERSION", revision="REV", branch="BRANCH",
-                         user="USER", date="DATE", url="URL",
-                         srcChecksum="SRCCHECKSUM")
-package org.apache.flume;
-EOF
\ No newline at end of file
diff --git 
a/flume-ng-core/src/main/java/org/apache/flume/VersionAnnotation.java 
b/flume-ng-core/src/main/java/org/apache/flume/VersionAnnotation.java
deleted file mode 100644
index 06269ac3..00000000
--- a/flume-ng-core/src/main/java/org/apache/flume/VersionAnnotation.java
+++ /dev/null
@@ -1,72 +0,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
- *
- *      http://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.flume;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * This class is about package attribute that captures
- * version info of Flume that was compiled.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.PACKAGE)
-public @interface VersionAnnotation {
-
-    /**
-     * Get the Flume version
-     * @return the version string "1.1"
-     */
-    String version();
-
-    /**
-     * Get the subversion revision.
-     * @return the revision number as a string (eg. "100755")
-     */
-    String revision();
-
-    /**
-     * Get the branch from which this was compiled.
-     * @return The branch name, e.g. "trunk"
-     */
-    String branch();
-
-    /**
-     * Get the username that compiled Flume.
-     */
-    String user();
-
-    /**
-     * Get the date when Flume was compiled.
-     * @return the date in unix 'date' format
-     */
-    String date();
-
-    /**
-     * Get the url for the subversion repository.
-     */
-    String url();
-
-    /**
-     * Get a checksum of the source files from which
-     * Flume was compiled.
-     * @return a string that uniquely identifies the source
-     **/
-    String srcChecksum();
-}
diff --git 
a/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java 
b/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java
index f5220e4d..8e9dd475 100644
--- a/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java
+++ b/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java
@@ -16,105 +16,228 @@
  */
 package org.apache.flume.tools;
 
-import org.apache.flume.VersionAnnotation;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.util.Properties;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
-/*
- * This class provides version info of Flume NG
+/**
+ * Provides the build metadata of the Flume artifact this class was loaded 
from.
+ *
+ * <p>The values come from the manifest of that artifact, falling back to its 
Maven descriptor when
+ * Flume has been shaded into another artifact and the manifest is no longer 
its own. Every accessor
+ * returns {@value #UNKNOWN} rather than {@code null} when the information is 
unavailable.
  */
-
 public class VersionInfo {
 
-    private static Package myPackage;
-    private static VersionAnnotation version;
+    private static final String UNKNOWN = "Unknown";
+
+    private static final String GROUP_ID = "org.apache.flume";
+    private static final String ARTIFACT_ID = "flume-ng-core";
+
+    private static final String IMPLEMENTATION_TIMESTAMP = 
"Implementation-Timestamp";
+    private static final String PURL = "Purl";
+    private static final String BUNDLE_SCM = "Bundle-SCM";
+
+    private static final String PURL_PREFIX = "pkg:maven/" + GROUP_ID + "/" + 
ARTIFACT_ID + "@";
+    private static final String CLASS_PATH = 
"org/apache/flume/tools/VersionInfo.class";
+    private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF";
+    private static final String POM_PROPERTIES_PATH =
+            "/META-INF/maven/" + GROUP_ID + "/" + ARTIFACT_ID + 
"/pom.properties";
+
+    /** Matches one {@code name=value} pair of an OSGi header, with an 
optionally quoted value. */
+    private static final Pattern SCM_ATTRIBUTE =
+            
Pattern.compile("(?:^|,)\\s*([A-Za-z0-9_-]+)\\s*=\\s*(?:\"([^\"]*)\"|([^,]*))");
+
+    private static final Attributes MANIFEST = ownManifest();
+    private static final Properties POM_PROPERTIES = pomProperties();
+
+    private static final String VERSION = version(MANIFEST, POM_PROPERTIES);
+    private static final String PURL_VALUE = purl(MANIFEST, POM_PROPERTIES);
+    private static final String URL_VALUE = 
orUnknown(scmAttribute(MANIFEST.getValue(BUNDLE_SCM), "url"));
+    private static final String TAG = 
orUnknown(scmAttribute(MANIFEST.getValue(BUNDLE_SCM), "tag"));
+    private static final String DATE = 
orUnknown(MANIFEST.getValue(IMPLEMENTATION_TIMESTAMP));
+
+    /**
+     * Reads the manifest of the artifact this class was loaded from.
+     *
+     * <p>Resolving it against the location of this class, instead of looking 
up
+     * {@code META-INF/MANIFEST.MF} on the class path, keeps another artifact 
from answering. Returns
+     * empty attributes when the manifest is missing or belongs to an artifact 
Flume was shaded into.
+     */
+    private static Attributes ownManifest() {
+        URL self = VersionInfo.class.getResource("VersionInfo.class");
+        if (self == null) {
+            return new Attributes();
+        }
+        String location = self.toString();
+        if (!location.endsWith(CLASS_PATH)) {
+            return new Attributes();
+        }
+        String root = location.substring(0, location.length() - 
CLASS_PATH.length());
+        try (InputStream stream = URI.create(root + 
MANIFEST_PATH).toURL().openStream()) {
+            Attributes attributes = new Manifest(stream).getMainAttributes();
+            return isOwn(attributes) ? attributes : new Attributes();
+        } catch (IOException | RuntimeException ignored) {
+            return new Attributes();
+        }
+    }
+
+    private static Properties pomProperties() {
+        Properties properties = new Properties();
+        try (InputStream stream = 
VersionInfo.class.getResourceAsStream(POM_PROPERTIES_PATH)) {
+            if (stream != null) {
+                properties.load(stream);
+            }
+        } catch (IOException | RuntimeException ignored) {
+            // Falls through to the empty properties.
+        }
+        return properties;
+    }
+
+    /** Tells whether the manifest describes this artifact rather than one 
Flume was shaded into. */
+    static boolean isOwn(Attributes manifest) {
+        String purl = manifest.getValue(PURL);
+        return purl != null && purl.startsWith(PURL_PREFIX);
+    }
 
-    static {
-        myPackage = VersionAnnotation.class.getPackage();
-        version = myPackage.getAnnotation(VersionAnnotation.class);
+    static String version(Attributes manifest, Properties pomProperties) {
+        String version = 
manifest.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+        return orUnknown(version != null ? version : 
pomProperties.getProperty("version"));
+    }
+
+    static String purl(Attributes manifest, Properties pomProperties) {
+        String purl = manifest.getValue(PURL);
+        if (purl == null) {
+            String groupId = pomProperties.getProperty("groupId");
+            String artifactId = pomProperties.getProperty("artifactId");
+            String version = pomProperties.getProperty("version");
+            if (groupId != null && artifactId != null && version != null) {
+                purl = "pkg:maven/" + groupId + "/" + artifactId + "@" + 
version;
+            }
+        }
+        return orUnknown(purl);
     }
 
     /**
-     * Get the meta-data for the Flume package.
-     * @return
+     * Returns one attribute of an OSGi {@code Bundle-SCM} header, or {@code 
null} if absent.
+     *
+     * <p>The header is specified by OSGi Core R8, section 3.2.1, as a comma 
separated list of
+     * {@code url}, {@code connection}, {@code developer-connection} and 
{@code tag} attributes.
      */
-    static Package getPackage() {
-        return myPackage;
+    static String scmAttribute(String header, String attribute) {
+        if (header == null) {
+            return null;
+        }
+        Matcher matcher = SCM_ATTRIBUTE.matcher(header);
+        while (matcher.find()) {
+            if (attribute.equals(matcher.group(1))) {
+                String quoted = matcher.group(2);
+                return quoted != null ? quoted : matcher.group(3).trim();
+            }
+        }
+        return null;
+    }
+
+    private static String orUnknown(String value) {
+        return value != null && !value.isEmpty() ? value : UNKNOWN;
     }
 
     /**
-     * Get the Flume version.
-     * @return the Flume version string, eg. "1.1"
+     * Gets the Flume version.
+     *
+     * @return the Flume version string, eg. "2.0.0"
      */
     public static String getVersion() {
-        return version != null ? version.version() : "Unknown";
+        return VERSION;
+    }
+
+    /**
+     * Gets the Package URL of the Flume artifact this class was loaded from.
+     *
+     * @return the Package URL, eg. 
"pkg:maven/org.apache.flume/[email protected]"
+     */
+    public static String getPurl() {
+        return PURL_VALUE;
     }
 
     /**
-     * Get the subversion revision number for the root directory
-     * @return the revision number, eg. "100755"
+     * Gets the source control revision this was built from.
+     *
+     * <p>The build no longer records a commit id, since it has to produce the 
same artifact from a
+     * Git checkout and from the source distribution, which carries no 
repository metadata.
+     *
+     * @return always "Unknown"
      */
     public static String getRevision() {
-        if (version != null && version.revision() != null && 
!version.revision().isEmpty()) {
-            return version.revision();
-        }
-        return "Unknown";
+        return UNKNOWN;
     }
 
     /**
-     * Get the branch on which this originated.
-     * @return The branch name, e.g. "trunk" or "branches/branch-1.1"
+     * Gets the source control tag or branch this was built from.
+     *
+     * @return the tag, eg. "rel/2.0.0"
      */
     public static String getBranch() {
-        return version != null ? version.branch() : "Unknown";
+        return TAG;
     }
 
     /**
-     * The date that Flume was compiled.
-     * @return the compilation date in unix date format
+     * Gets the date Flume was built.
+     *
+     * @return the build date in ISO-8601 format
      */
     public static String getDate() {
-        return version != null ? version.date() : "Unknown";
+        return DATE;
     }
 
     /**
-     * The user that compiled Flume.
-     * @return the username of the user
+     * Gets the user that compiled Flume.
+     *
+     * @return always "Unknown"
+     * @deprecated Recording the user would make the build unreproducible.
      */
+    @Deprecated(since = "2.0.0", forRemoval = true)
     public static String getUser() {
-        return version != null ? version.user() : "Unknown";
+        return UNKNOWN;
     }
 
     /**
-     * Get the subversion URL for the root Flume directory.
+     * Gets the source control URL of the Flume repository.
+     *
+     * @return the repository URL
      */
     public static String getUrl() {
-        return version != null ? version.url() : "Unknown";
+        return URL_VALUE;
     }
 
     /**
-     * Get the checksum of the source files from which Flume was
-     * built.
-     **/
+     * Gets the checksum of the source files Flume was built from.
+     *
+     * @return always "Unknown"
+     * @deprecated Use {@link #getPurl()} to identify the artifact, and verify 
it against the
+     *     checksums published with the release.
+     */
+    @Deprecated(since = "2.0.0", forRemoval = true)
     public static String getSrcChecksum() {
-        return version != null ? version.srcChecksum() : "Unknown";
+        return UNKNOWN;
     }
 
-    /**
-     * Returns the build version info which includes version,
-     * revision, user, date and source checksum
-     */
+    /** Returns the build version info, which includes the version, the tag 
and the build date. */
     public static String getBuildVersion() {
-        return VersionInfo.getVersion() + " from "
-                + VersionInfo.getRevision() + " by "
-                + VersionInfo.getUser() + " on "
-                + VersionInfo.getDate() + " source checksum "
-                + VersionInfo.getSrcChecksum();
+        return getVersion() + " from " + getBranch() + " built on " + 
getDate();
     }
 
     public static void main(String[] args) {
         System.out.println("Flume " + getVersion());
-        System.out.println("Source code repository: " + 
"https://git.apache.org/repos/asf/flume.git";);
-        System.out.println("Revision: " + getRevision());
-        System.out.println("Compiled by " + getUser() + " on " + getDate());
-        System.out.println("From source with checksum " + getSrcChecksum());
+        System.out.println("Package URL: " + getPurl());
+        System.out.println("Source code repository: " + getUrl());
+        System.out.println("Tag: " + getBranch());
+        System.out.println("Compiled on " + getDate());
     }
 }
diff --git 
a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java 
b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java
index 59457f96..66d70734 100644
--- a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java
+++ b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java
@@ -16,42 +16,134 @@
  */
 package org.apache.flume.tools;
 
-import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
+import java.time.Instant;
+import java.util.Properties;
+import java.util.jar.Attributes;
 import org.junit.Test;
 
 public class TestVersionInfo {
 
-    private static final Logger logger = LogManager.getLogger();
+    private static final String PURL = 
"pkg:maven/org.apache.flume/[email protected]";
+
+    private static final String BUNDLE_SCM = 
"url=\"https://gitbox.apache.org/repos/asf/logging-flume.git\",";
+            + 
"connection=\"scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git\",";
+            + 
"developer-connection=\"scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git\",";
+            + "tag=\"rel/2.0.0\"";
 
     /**
-     *  Make sure that Unknown is expected when no version info
+     * Checks the metadata of the artifact the test runs against.
+     *
+     * <p>BND writes the manifest to the output directory before the tests 
run, so the values are
+     * available whether Flume is loaded from a JAR or from the compiled 
classes.
      */
     @Test
-    public void testVersionInfoUnknown() {
+    public void testMetadataOfOwnArtifact() {
+        assertTrue(
+                "getVersion returned " + VersionInfo.getVersion(),
+                VersionInfo.getVersion().matches("\\d+\\.\\d+.*"));
+        assertTrue(
+                "getPurl returned " + VersionInfo.getPurl(),
+                
VersionInfo.getPurl().startsWith("pkg:maven/org.apache.flume/flume-ng-core@"));
+        assertTrue(
+                "getUrl returned " + VersionInfo.getUrl(),
+                VersionInfo.getUrl().startsWith("https://";)
+                        && VersionInfo.getUrl().contains("logging-flume"));
+        assertTrue(
+                "getBranch returned " + VersionInfo.getBranch(),
+                VersionInfo.getBranch().startsWith("rel/"));
+        // Throws if the timestamp is not ISO-8601.
+        Instant.parse(VersionInfo.getDate());
+        assertTrue(
+                "getBuildVersion returned " + VersionInfo.getBuildVersion(),
+                VersionInfo.getBuildVersion().matches(".+ from .+ built on 
.+"));
+    }
 
-        logger.debug("Flume " + VersionInfo.getVersion());
-        logger.debug("Subversion " + VersionInfo.getUrl() + " -r " + 
VersionInfo.getRevision());
-        logger.debug("Compiled by " + VersionInfo.getUser() + " on " + 
VersionInfo.getDate());
-        logger.debug("From source with checksum " + 
VersionInfo.getSrcChecksum());
-        logger.debug("Flume " + VersionInfo.getBuildVersion());
+    @Test
+    @SuppressWarnings({"deprecation", "removal"})
+    public void testUnrecordedMetadata() {
+        assertEquals("Unknown", VersionInfo.getRevision());
+        assertEquals("Unknown", VersionInfo.getUser());
+        assertEquals("Unknown", VersionInfo.getSrcChecksum());
+    }
 
-        assertTrue("getVersion returned Unknown", 
!VersionInfo.getVersion().equals("Unknown"));
-        assertTrue("getUser returned Unknown", 
!VersionInfo.getUser().equals("Unknown"));
-        assertTrue("getUrl returned Unknown", 
!VersionInfo.getUrl().equals("Unknown"));
-        assertTrue(
-                "getSrcChecksum returned Unknown", 
!VersionInfo.getSrcChecksum().equals("Unknown"));
+    @Test
+    public void testVersionPrefersTheManifest() {
+        Attributes manifest = new Attributes();
+        manifest.putValue("Implementation-Version", "2.0.0");
+        assertEquals("2.0.0", VersionInfo.version(manifest, 
pomProperties("1.11.0")));
+    }
 
-        // check getBuildVersion() return format
-        assertTrue(
-                "getBuildVersion returned unexpected format",
-                VersionInfo.getBuildVersion().matches(".+from.+by.+on.+source 
checksum.+"));
+    @Test
+    public void testVersionFallsBackToPomProperties() {
+        assertEquals("1.11.0", VersionInfo.version(new Attributes(), 
pomProperties("1.11.0")));
+    }
+
+    @Test
+    public void testVersionWithoutAnySource() {
+        assertEquals("Unknown", VersionInfo.version(new Attributes(), new 
Properties()));
+    }
+
+    @Test
+    public void testPurlPrefersTheManifest() {
+        Attributes manifest = new Attributes();
+        manifest.putValue("Purl", PURL);
+        assertEquals(PURL, VersionInfo.purl(manifest, 
pomProperties("1.11.0")));
+    }
+
+    @Test
+    public void testPurlIsBuiltFromPomProperties() {
+        assertEquals(
+                "pkg:maven/org.apache.flume/[email protected]",
+                VersionInfo.purl(new Attributes(), pomProperties("1.11.0")));
+    }
+
+    @Test
+    public void testPurlWithoutAnySource() {
+        assertEquals("Unknown", VersionInfo.purl(new Attributes(), new 
Properties()));
+    }
+
+    /** A manifest of an artifact Flume was shaded into must not be mistaken 
for our own. */
+    @Test
+    public void testForeignManifestIsRejected() {
+        Attributes foreign = new Attributes();
+        foreign.putValue("Purl", "pkg:maven/com.example/[email protected]");
+        foreign.putValue("Implementation-Version", "1.0.0");
+        assertFalse(VersionInfo.isOwn(foreign));
+        assertFalse(VersionInfo.isOwn(new Attributes()));
+
+        Attributes own = new Attributes();
+        own.putValue("Purl", PURL);
+        assertTrue(VersionInfo.isOwn(own));
+    }
+
+    @Test
+    public void testScmAttributes() {
+        assertEquals("rel/2.0.0", VersionInfo.scmAttribute(BUNDLE_SCM, "tag"));
+        assertEquals(
+                "https://gitbox.apache.org/repos/asf/logging-flume.git";, 
VersionInfo.scmAttribute(BUNDLE_SCM, "url"));
+        assertEquals(
+                
"scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git";,
+                VersionInfo.scmAttribute(BUNDLE_SCM, "developer-connection"));
+        assertNull(VersionInfo.scmAttribute(BUNDLE_SCM, "revision"));
+        assertNull(VersionInfo.scmAttribute(null, "tag"));
+    }
+
+    /** OSGi only requires quoting for values with special characters. */
+    @Test
+    public void testScmAttributeWithoutQuotes() {
+        assertEquals("rel/2.0.0", 
VersionInfo.scmAttribute("url=https://example.org,tag=rel/2.0.0";, "tag"));
+    }
 
-        // "Unknown" when build without svn or git
-        assertNotNull("getRevision returned null", VersionInfo.getRevision());
-        assertNotNull("getBranch returned null", VersionInfo.getBranch());
+    private static Properties pomProperties(String version) {
+        Properties properties = new Properties();
+        properties.setProperty("groupId", "org.apache.flume");
+        properties.setProperty("artifactId", "flume-ng-core");
+        properties.setProperty("version", version);
+        return properties;
     }
 }
diff --git a/flume-ng-sdk/pom.xml b/flume-ng-sdk/pom.xml
index 6b231c27..c5b1d20d 100644
--- a/flume-ng-sdk/pom.xml
+++ b/flume-ng-sdk/pom.xml
@@ -90,6 +90,20 @@
             <goals>
               <goal>test-jar</goal>
             </goals>
+            <configuration>
+              <!-- BND describes the main classes in a single manifest per 
module, which `logging-parent`
+                   feeds to every execution. Drop it here: the test JAR is not 
that bundle, and its
+                   Package URL needs the qualifiers of a classified artifact. 
-->
+              <archive combine.self="override">
+                <manifest>
+                  
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                  
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
+                </manifest>
+                <manifestEntries>
+                  
<Purl>pkg:maven/${project.groupId}/${project.artifactId}@${project.version}?classifier=tests&amp;type=test-jar</Purl>
+                </manifestEntries>
+              </archive>
+            </configuration>
           </execution>
         </executions>
       </plugin>
diff --git a/flume-parent/pom.xml b/flume-parent/pom.xml
index 335b5bbf..8036ba0a 100644
--- a/flume-parent/pom.xml
+++ b/flume-parent/pom.xml
@@ -84,6 +84,16 @@
     <!-- Dummy value; overwritten by CI at release time. Do not edit manually. 
-->
     
<project.build.outputTimestamp>2026-01-01T00:00:00Z</project.build.outputTimestamp>
 
+    <!-- Append build metadata to the manifest generated by BND in 
`logging-parent`.
+         `Bundle-SCM` is specified by OSGi Core R8, section 3.2.1; 
`logging-parent` strips the header
+         because inheritance assembly corrupts `project.scm.url`. Flume sets
+         `child.scm.*.inherit.append.path="false"`, so its value is correct: 
keep the header.
+         Neither the JAR File Specification nor OSGi define a header for the 
build timestamp or the
+         Package URL, so `Implementation-Timestamp` and `Purl` are 
Flume-specific. -->
+    <bnd-extra-config>-removeheaders: Bundle-DocURL,Bundle-Developers
+
+      Implementation-Timestamp: ${project.build.outputTimestamp}
+      Purl: 
pkg:maven/${project.groupId}/${project.artifactId}@${project.version}</bnd-extra-config>
     <!-- Set default encoding to UTF-8 to remove maven complaints -->
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 

Reply via email to