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&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&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">