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


The following commit(s) were added to refs/heads/main by this push:
     new 2b077a3  sort list valued BOM attributes for reproducible builds
2b077a3 is described below

commit 2b077a32b53097ec08719e3eb44022845419811e
Author: Stefan Bodewig <[email protected]>
AuthorDate: Sat Jul 18 14:46:28 2026 +0200

    sort list valued BOM attributes for reproducible builds
---
 changes.xml                                        |  4 ++
 src/main/org/apache/ant/cyclonedx/Component.java   | 79 ++++++++++++++++++++--
 .../org/apache/ant/cyclonedx/ComponentBomTask.java | 13 +++-
 .../apache/ant/cyclonedx/ExternalReference.java    | 30 ++++++++
 src/main/org/apache/ant/cyclonedx/License.java     | 14 ++++
 .../org/apache/ant/cyclonedx/Organization.java     |  3 +-
 src/main/org/apache/ant/cyclonedx/ToolData.java    | 17 +++++
 src/tests/antunit/component-test.xml               | 10 +--
 src/tests/antunit/componentbom-test.xml            |  4 +-
 src/tests/antunit/organization-test.xml            |  2 +-
 10 files changed, 160 insertions(+), 16 deletions(-)

diff --git a/changes.xml b/changes.xml
index 148e3ce..1ed2fb7 100644
--- a/changes.xml
+++ b/changes.xml
@@ -84,6 +84,10 @@
       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">
+      The array valued parts of the generated SBOM now have a stable
+      sort order to support reproducible builds.
+    </action>
     <action type="update">
       Updated cyclonedx-java-core library dependency to 13.0.0.
     </action>
diff --git a/src/main/org/apache/ant/cyclonedx/Component.java 
b/src/main/org/apache/ant/cyclonedx/Component.java
index 95cce69..146e998 100644
--- a/src/main/org/apache/ant/cyclonedx/Component.java
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -24,6 +24,7 @@ import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -91,6 +92,42 @@ public class Component extends DataType {
     private String mimeType;
     private SbomLink sbomLink;
 
+    /**
+     * Comparator for components.
+     *
+     * <p>Sorts by bom-ref (if present) and falls back to sorting by
+     * name, then by group (if present) and version (if present).</p>
+     *
+     * @since CycloneDX Antlib 0.2
+     */
+    public static final Comparator<Component> ComponentComparator =
+        Comparator.comparing(Component::getBomRef, 
Comparator.nullsLast(Comparator.naturalOrder()))
+        .thenComparing(Component::getName)
+        .thenComparing(Component::getGroup, 
Comparator.nullsLast(Comparator.naturalOrder()))
+        .thenComparing(Component::getVersion, 
Comparator.nullsLast(Comparator.naturalOrder()));
+
+
+    /**
+     * Comparator for CycloneDX components.
+     *
+     * <p>Sorts by bom-ref (if present) and falls back to sorting by
+     * name, then by group (if present) and version (if present).</p>
+     *
+     * @since CycloneDX Antlib 0.2
+     */
+    public static final Comparator<org.cyclonedx.model.Component> 
CycloneDxComponentComparator
+        = (c1, c2) -> ComponentComparator.compare(from(c1, 
Collections.emptyList()),
+                                                  from(c2, 
Collections.emptyList()));
+
+    private static final Comparator<OrganizationalContact> 
CycloneDxOrganizationalContactComparator =
+        Comparator.comparing(OrganizationalContact::getBomRef, 
Comparator.nullsLast(Comparator.naturalOrder()))
+        .thenComparing(OrganizationalContact::getName, 
Comparator.nullsLast(Comparator.naturalOrder()))
+        .thenComparing(OrganizationalContact::getEmail, 
Comparator.nullsLast(Comparator.naturalOrder()));
+
+    private static final Comparator<Property> CycloneDxPropertyComparator =
+        Comparator.comparing(Property::getName)
+        .thenComparing(Property::getValue, 
Comparator.nullsLast(Comparator.naturalOrder()));
+
     /**
      * Sets the resource the component is about.
      *
@@ -428,6 +465,20 @@ public class Component extends DataType {
         return group;
     }
 
+    /**
+     * Gets the version of the component.
+     *
+     * @return component version
+     * @since CycloneDX Antlib 0.2
+     */
+    public String getVersion() {
+        if (isReference()) {
+            return getRef().getVersion();
+        }
+        dieOnCircularReference();
+        return version;
+    }
+
     /**
      * Gets the Package-URL (purl) of the component.
      *
@@ -475,7 +526,7 @@ public class Component extends DataType {
             return getRef().getDependencies();
         }
         dieOnCircularReference();
-        return dependencies;
+        return 
dependencies.stream().sorted(Dependency.DependencyComparator).collect(Collectors.toList());
     }
 
     /**
@@ -508,6 +559,7 @@ public class Component extends DataType {
                       .stream()
                       .flatMap(c -> c.getNestedComponents().stream())
                       .collect(Collectors.toList()));
+        result.sort(ComponentComparator);
         return result;
     }
 
@@ -692,14 +744,22 @@ public class Component extends DataType {
         } else if (!dependencies.isEmpty()) {
             throw new BuildException("a component with dependencies must 
provide a bomRef");
         }
-        component.setAuthors(authors);
-        component.setProperties(properties);
+        component.setAuthors(authors.stream()
+                             .sorted(CycloneDxOrganizationalContactComparator)
+                             .collect(Collectors.toList()));
+        component.setProperties(properties.stream()
+                                .sorted(CycloneDxPropertyComparator)
+                                .collect(Collectors.toList()));
         component.setTags(new 
Tags(tags.stream().sorted().collect(Collectors.toList())));
-        component.setExternalReferences(externalReferences);
+        component.setExternalReferences(externalReferences.stream()
+                                        
.sorted(ExternalReference.CycloneDxExternalReferenceComparator)
+                                        .collect(Collectors.toList()));
         if (!licenses.isEmpty()) {
             // would create an empty licenses node otherwise
             LicenseChoice lc = new LicenseChoice();
-            lc.setLicenses(licenses);
+            lc.setLicenses(licenses.stream()
+                           .sorted(License.CycloneDxLicenseComparator)
+                           .collect(Collectors.toList()));
             component.setLicenses(lc);
         }
         for (Component c : nestedComponents) {
@@ -916,6 +976,15 @@ public class Component extends DataType {
         private String bomRef;
         private Reference componentRef;
 
+        /**
+         * Comparator for dependencies.
+         *
+         * <p>Sorts by bom-ref.</p>
+         *
+         * @since CycloneDX Antlib 0.2
+         */
+        public static final Comparator<Dependency> DependencyComparator = 
Comparator.comparing(Dependency::getBomRef);
+
         /**
          * Identifies the dependency by its bom-ref.
          *
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java 
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index 2b23c1e..25acc67 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -36,6 +37,7 @@ import java.util.UUID;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.apache.tools.ant.BuildException;
 import org.apache.tools.ant.Task;
@@ -337,6 +339,7 @@ public class ComponentBomTask extends Task {
             cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
         }
 
+        cs.sort(Component.CycloneDxComponentComparator);
         bom.setComponents(cs);
         addDependencies(bom, knownComponents);
 
@@ -354,7 +357,10 @@ public class ComponentBomTask extends Task {
                 
tools.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
             }
             ToolInformation ti = new ToolInformation();
-            ti.setComponents(tools);
+            ti.setComponents(tools
+                             .stream()
+                             .sorted(Component.CycloneDxComponentComparator)
+                             .collect(Collectors.toList()));
             ti.setServices(antlibToolInformation.getServices());
             meta.setToolChoice(ti);
         } else {
@@ -362,7 +368,9 @@ public class ComponentBomTask extends Task {
         }
         if (!licenses.isEmpty()) {
             LicenseChoice lc = new LicenseChoice();
-            lc.setLicenses(licenses);
+            lc.setLicenses(licenses.stream()
+                           .sorted(License.CycloneDxLicenseComparator)
+                           .collect(Collectors.toList()));
             meta.setLicenses(lc);
         }
 
@@ -417,6 +425,7 @@ public class ComponentBomTask extends Task {
                 }
             });
 
+        dependencies.sort(Comparator.comparing(Dependency::getRef));
         bom.setDependencies(dependencies);
     }
 
diff --git a/src/main/org/apache/ant/cyclonedx/ExternalReference.java 
b/src/main/org/apache/ant/cyclonedx/ExternalReference.java
index 6bbf15c..66b0b70 100644
--- a/src/main/org/apache/ant/cyclonedx/ExternalReference.java
+++ b/src/main/org/apache/ant/cyclonedx/ExternalReference.java
@@ -1,5 +1,24 @@
+/*
+ *  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.Comparator;
+
 import org.apache.tools.ant.BuildException;
 
 /**
@@ -17,6 +36,17 @@ public class ExternalReference {
     private String url;
     private String type;
 
+    /**
+     * Comparator for CycloneDX external references.
+     *
+     * <p>Sorts by type then by url.</p>
+     *
+     * @since CycloneDX Antlib 0.2
+     */
+    public static final Comparator<org.cyclonedx.model.ExternalReference> 
CycloneDxExternalReferenceComparator =
+        Comparator.comparing(org.cyclonedx.model.ExternalReference::getType)
+        .thenComparing(org.cyclonedx.model.ExternalReference::getUrl);
+
     /**
      * Set the URL (actually URI) of the external reference.
      *
diff --git a/src/main/org/apache/ant/cyclonedx/License.java 
b/src/main/org/apache/ant/cyclonedx/License.java
index 3547445..6d70194 100644
--- a/src/main/org/apache/ant/cyclonedx/License.java
+++ b/src/main/org/apache/ant/cyclonedx/License.java
@@ -17,6 +17,8 @@
  */
 package org.apache.ant.cyclonedx;
 
+import java.util.Comparator;
+
 import org.apache.tools.ant.BuildException;
 import org.apache.tools.ant.types.DataType;
 import org.apache.tools.ant.types.resources.URLResource;
@@ -41,6 +43,18 @@ public class License extends DataType {
     private String name;
     private String url;
 
+    /**
+     * Comparator for CycloneDX license.
+     *
+     * <p>Sorts by id (if present) and falls back to sorting by name
+     * (if present).</p>
+     *
+     * @since CycloneDX Antlib 0.2
+     */
+    public static final Comparator<org.cyclonedx.model.License> 
CycloneDxLicenseComparator =
+        Comparator.comparing(org.cyclonedx.model.License::getId, 
Comparator.nullsLast(Comparator.naturalOrder()))
+        .thenComparing(org.cyclonedx.model.License::getName, 
Comparator.nullsLast(Comparator.naturalOrder()));
+
     /**
      * Sets the {@code id} of the license.
      *
diff --git a/src/main/org/apache/ant/cyclonedx/Organization.java 
b/src/main/org/apache/ant/cyclonedx/Organization.java
index 1b9074e..6f86e40 100644
--- a/src/main/org/apache/ant/cyclonedx/Organization.java
+++ b/src/main/org/apache/ant/cyclonedx/Organization.java
@@ -19,6 +19,7 @@ package org.apache.ant.cyclonedx;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.tools.ant.types.DataType;
 import org.apache.tools.ant.types.resources.URLResource;
@@ -76,7 +77,7 @@ public class Organization extends DataType {
             oe.setName(name);
         }
         if (!urls.isEmpty()) {
-            oe.setUrls(urls);
+            oe.setUrls(urls.stream().sorted().collect(Collectors.toList()));
         }
         return oe;
     }
diff --git a/src/main/org/apache/ant/cyclonedx/ToolData.java 
b/src/main/org/apache/ant/cyclonedx/ToolData.java
index cbbfc1c..e12b411 100644
--- a/src/main/org/apache/ant/cyclonedx/ToolData.java
+++ b/src/main/org/apache/ant/cyclonedx/ToolData.java
@@ -1,3 +1,20 @@
+/*
+ *  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.io.File;
diff --git a/src/tests/antunit/component-test.xml 
b/src/tests/antunit/component-test.xml
index ca9aa1c..c98a9c0 100644
--- a/src/tests/antunit/component-test.xml
+++ b/src/tests/antunit/component-test.xml
@@ -949,11 +949,11 @@
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.metadata.component.externalReferences.reference(type)"
-        
value="license,mailing-list,security-contact,vcs,build-system,issue-tracker,website,distribution,source-distribution,bom"/>
+        
value="vcs,issue-tracker,website,bom,mailing-list,source-distribution,distribution,license,build-system,security-contact"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.metadata.component.externalReferences.reference.url"
-        
value="https://www.apache.org/licenses/LICENSE-2.0.txt,https://ant.apache.org/mail.html,https://www.apache.org/security/,https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git,https://ci-builds.apache.org/job/Ant/job/CycloneDX%20Antlib/,https://bz.apache.org/bugzilla/buglist.cgi?component=CycloneDX%20Antlib&amp;product=Ant,https://ant.apache.org/antlibs/cyclonedx/,https://ant.apache.org/antlibs/bindownload.cgi,https://ant.apache.org/antlibs/srcdownload.cgi,https://repo1.ma
 [...]
+        
value="https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git,https://bz.apache.org/bugzilla/buglist.cgi?component=CycloneDX%20Antlib&amp;product=Ant,https://ant.apache.org/antlibs/cyclonedx/,https://repo1.maven.org/maven2/org/apache/ant/ant-cyclonedx/0.1/ant-cyclonedx-0.1-cyclonedx.json,https://ant.apache.org/mail.html,https://ant.apache.org/antlibs/srcdownload.cgi,https://ant.apache.org/antlibs/bindownload.cgi,https://www.apache.org/licenses/LICENSE-2.0.txt,https://ci-bu
 [...]
   </target>
 
   <target name="testSbomLinkDoesntAddBomExternalLinkWhenDisabled" 
depends="createMaximalComponentData">
@@ -972,7 +972,7 @@
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.metadata.component.externalReferences.reference(type)"
-        
value="license,mailing-list,security-contact,vcs,build-system,issue-tracker,website,distribution,source-distribution"/>
+        
value="vcs,issue-tracker,website,mailing-list,source-distribution,distribution,license,build-system,security-contact"/>
   </target>
 
   <target name="testSbomLinkDoesntOverrideExistingBomExternalLink" 
depends="createMaximalComponentData">
@@ -1187,7 +1187,7 @@
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.dependencies.dependency(ref)"
-        
value="pkg:maven/org.example2/[email protected]?type=jar,my-own-dependency"/>
+        
value="my-own-dependency,pkg:maven/org.example2/[email protected]?type=jar"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.dependencies.dependency.dependency(ref)"
@@ -1229,7 +1229,7 @@
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.dependencies.dependency(ref)"
-        
value="pkg:maven/org.example/[email protected]?type=jar,pkg:maven/commons-codec/[email protected]?type=jar"/>
+        
value="pkg:maven/commons-codec/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.dependencies.dependency.dependency(ref)"
diff --git a/src/tests/antunit/componentbom-test.xml 
b/src/tests/antunit/componentbom-test.xml
index 0f6e063..bc70ad7 100644
--- a/src/tests/antunit/componentbom-test.xml
+++ b/src/tests/antunit/componentbom-test.xml
@@ -426,11 +426,11 @@
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.dependencies.dependency(ref)"
-        
value="pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar"/>
+        
value="pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.dependencies.dependency.dependency(ref)"
-        
value="pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar"/>
+        
value="pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar"/>
   </target>
 
   <target name="testNestedComponents">
diff --git a/src/tests/antunit/organization-test.xml 
b/src/tests/antunit/organization-test.xml
index 26d394e..96c7d1f 100644
--- a/src/tests/antunit/organization-test.xml
+++ b/src/tests/antunit/organization-test.xml
@@ -59,7 +59,7 @@
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.metadata.component.manufacturer.url"
-        value="https://example.org/,https://example.com/"/>
+        value="https://example.com/,https://example.org/"/>
   </target>
 
   <target name="testOrganizationWorksViaReference">

Reply via email to