You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ant.apache.org by bo...@apache.org on 2026/08/01 16:09:40 UTC
(ant-antlibs-cyclonedx) branch support-ivy-as-component-source created (now c6fdbcd)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch support-ivy-as-component-source
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git
at c6fdbcd initial implementation of Ivy based CycloneDX resolution
This branch includes the following new commits:
new c6fdbcd initial implementation of Ivy based CycloneDX resolution
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(ant-antlibs-cyclonedx) 01/01: initial implementation of Ivy based CycloneDX resolution
Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch support-ivy-as-component-source
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git
commit c6fdbcd21dbbcc01b0015ce3df3d84a83b82e357
Author: Stefan Bodewig <bo...@apache.org>
AuthorDate: Sun Jul 19 15:12:18 2026 +0200
initial implementation of Ivy based CycloneDX resolution
---
build.xml | 60 ++++-
src/main/org/apache/ant/cyclonedx/Component.java | 112 ++++++++-
.../org/apache/ant/cyclonedx/ComponentBomTask.java | 15 +-
.../ant/cyclonedx/IvyModuleComponentResolver.java | 256 +++++++++++++++++++++
4 files changed, 434 insertions(+), 9 deletions(-)
diff --git a/build.xml b/build.xml
index d8717bc..30c5083 100644
--- a/build.xml
+++ b/build.xml
@@ -28,6 +28,19 @@ under the License.
</copy>
</target>
+ <target name="compile" depends="ready-to-compile,resolve">
+ <javac
+ srcdir="src/main"
+ destdir="${build.classes}"
+ debug="${javac.debug}"
+ source="${javac.-source}"
+ target="${javac.-target}"
+ includeantruntime="true">
+ <classpath refid="ivy.lib.path"/>
+ <classpath refid="classpath.compile"/>
+ </javac>
+ </target>
+
<target name="fetch-cyclonedx" depends="antlib">
<condition property="can.use.cyclonedx">
<isset property="jdk9+"/>
@@ -36,12 +49,19 @@ under the License.
<isset property="jdk9+"/>
</condition>
<typedef uri="antlib:org.apache.ant.cyclonedx"
- resource="org/apache/ant/cyclonedx/antlib.xml">
+ resource="org/apache/ant/cyclonedx/antlib.xml"
+ loaderRef="cdx.loader">
<classpath>
+ <path refid="ivy.lib.path"/>
<path refid="classpath.compile"/>
<pathelement location="${jarname}"/>
</classpath>
</typedef>
+ <taskdef resource="org/apache/ivy/ant/antlib.xml"
+ uri="antlib:org.apache.ivy.ant"
+ loaderRef="cdx.loader"/>
+ <ivy:resolve file="ivy.xml" xmlns:ivy="antlib:org.apache.ivy.ant"
+ conf="default" resolveId="default-only"/>
</target>
<target name="define-cyclonedx-components"
@@ -62,6 +82,44 @@ under the License.
</cdx:externalreferenceset>
</target>
+ <target name="create-antlib-sbom-ivy"
+ depends="define-cyclonedx-components"
+ if="can.use.cyclonedx">
+ <uptodate property="antlib-sbom-ok"
+ targetfile="${build.lib}/${artifact.stub}.ivy.cdx.json">
+ <srcresources>
+ <file file="${jarname}"/>
+ <file file="ivy.xml"/>
+ <file file="build.xml"/>
+ </srcresources>
+ </uptodate>
+ <cdx:componentbom
+ bomName="${artifact.stub}.ivy.cdx"
+ outputdirectory="${build.lib}"
+ format="all"
+ useComponentSupplier="true"
+ useComponentManufacturer="true"
+ unless:set="antlib-sbom-ok"
+ xmlns:unless="ant:unless"
+ xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+ <component
+ description="Apache CycloneDX Antlib"
+ publisher="The Apache Software Foundation"
+ supplierIsManufacturer="true">
+ <ivymodule conf="default" resolveId="default-only"/>
+ <file file="${jarname}"/>
+ <supplier refid="ant-pmc"/>
+ <license refid="apache-2"/>
+ <externalReferenceSet refid="ant-common-refs"/>
+ <externalReferenceSet refid="antlibs-common-ext-refs"/>
+ <externalReferenceSet refid="antlib-ext-refs"/>
+ </component>
+ <additionalComponent refid="ant"/>
+ <additionalComponent refid="ant-launcher"/>
+ <license refid="apache-2"/>
+ </cdx:componentbom>
+ </target>
+
<target name="create-antlib-sbom"
depends="define-cyclonedx-components"
if="can.use.cyclonedx">
diff --git a/src/main/org/apache/ant/cyclonedx/Component.java b/src/main/org/apache/ant/cyclonedx/Component.java
index 3b208fe..5a572dd 100644
--- a/src/main/org/apache/ant/cyclonedx/Component.java
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -78,12 +78,13 @@ public class Component extends DataType {
private List<Component> nestedComponents = new ArrayList<>();
private List<Dependency> dependencies = new ArrayList<>();
private boolean unknownDependencies = false;
- private boolean sbomLinkResolved = false;
+ private boolean resolved = false;
private List<OrganizationalContact> authors = new ArrayList<>();
private Set<String> tags = new HashSet<>();
private List<Property> properties = new ArrayList<>();
private String mimeType;
private SbomLink sbomLink;
+ private IvyModule ivyModule;
/**
* Comparator for components.
@@ -429,9 +430,26 @@ public class Component extends DataType {
*/
public SbomLink createSbomLink() {
checkChildrenAllowed();
+ if (ivyModule != null) {
+ throw new BuildException("sbomLink and ivyModule are mutually exclusive");
+ }
return sbomLink == null ? (sbomLink = new SbomLink(getProject())) : sbomLink;
}
+ /**
+ * Container for Ivy module configuration.
+ *
+ * @return container for Ivy module configuration
+ * @since CycloneDX Antlib 0.2
+ */
+ public IvyModule createIvyModule() {
+ checkChildrenAllowed();
+ if (sbomLink != null) {
+ throw new BuildException("sbomLink and ivyModule are mutually exclusive");
+ }
+ return ivyModule == null ? (ivyModule = new IvyModule()) : ivyModule;
+ }
+
/**
* Gets the name of the component.
*
@@ -569,6 +587,20 @@ public class Component extends DataType {
return externalReferences;
}
+ /**
+ * Gets whether any licenses have been explicitly configured for this component.
+ *
+ * @return whether any licenses have been explicitly configured
+ * @since CycloneDX Antlib 0.2
+ */
+ boolean hasLicenses() {
+ if (isReference()) {
+ return getRef().hasLicenses();
+ }
+ dieOnCircularReference();
+ return !licenses.isEmpty();
+ }
+
/**
* Read the linked SBOM (if any) and merge its content with the
* one already defined for this component.
@@ -583,11 +615,19 @@ public class Component extends DataType {
}
dieOnCircularReference();
- if (sbomLink != null && !sbomLinkResolved) {
- sbomLinkResolved = true;
+ if (!resolved) {
+ resolved = true;
+
+ if (sbomLink != null) {
+ SbomLinkComponentResolver resolver = new SbomLinkComponentResolver(getProject(), sbomLink);
+ return resolver.resolve(this);
+ }
+
+ if (ivyModule != null) {
+ IvyModuleComponentResolver resolver = new IvyModuleComponentResolver(ivyModule, getProject());
- SbomLinkComponentResolver resolver = new SbomLinkComponentResolver(getProject(), sbomLink);
- return resolver.resolve(this);
+ return resolver.resolve(this);
+ }
}
return Collections.emptyList();
@@ -1053,4 +1093,66 @@ public class Component extends DataType {
return createBomExternalReference;
}
}
+
+ /**
+ * Configuration for Ivy module resolution.
+ *
+ * <p>This nested element allows a Component to be populated from
+ * an Ivy module descriptor. The Ivy file should already be resolved
+ * (i.e., ivy:resolve should have been run).</p>
+ *
+ * @since CycloneDX Antlib 0.3
+ */
+ public static class IvyModule {
+ private String conf;
+ private String resolveId;
+ private Reference antIvyEngineRef;
+
+ /**
+ * Sets the configurations to take into consideration.
+ *
+ * <p>Defaults to the configurations resolved by the last resolve call, or {@code *} if no resolve was
+ * explicitly called</p>
+ *
+ * @param comma separated list of the configurations to retrieve or {@code *}.
+ */
+ public void setConf(String conf) {
+ this.conf = conf;
+ }
+
+ String getConf() {
+ return conf;
+ }
+
+ /**
+ * Sets the id which was used for a previous resolve.
+ *
+ * <p>Defaults to {@code [org].[module]}.</p>
+ *
+ * @param id which was used for a previous resolve
+ */
+ public void setResolveId(String resolveId) {
+ this.resolveId = resolveId;
+ }
+
+ String getResolveId() {
+ return resolveId;
+ }
+
+ /**
+ * Sets a reference is a different Ivy settings file than the default shall be used.
+ *
+ * <p>Defaults to {@code ivy.instance}.</p>
+ *
+ * @param ref A reference to Ivy settings that must be used by this component
+ */
+ public void setSettingsRef(Reference ref) {
+ antIvyEngineRef = ref;
+ }
+
+ Reference getSettingsRef() {
+ return antIvyEngineRef;
+ }
+
+ }
}
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index 9466583..f883001 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -324,13 +324,16 @@ public class ComponentBomTask extends Task {
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
}
+ List<Component> resolvedComponentsAdded = new ArrayList<>();
for (Component c : resolvedComponents) {
String unversionedKey = getUnversionedCoordinates(c);
if (unversionedKey == null) {
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
+ resolvedComponentsAdded.add(c);
} else if (!knownComponents.containsKey(unversionedKey)) {
knownComponents.put(unversionedKey, c.getBomRef());
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
+ resolvedComponentsAdded.add(c);
}
}
@@ -341,7 +344,7 @@ public class ComponentBomTask extends Task {
cs.sort(Component.CycloneDxComponentComparator);
bom.setComponents(cs);
- addDependencies(bom, knownComponents);
+ addDependencies(bom, knownComponents, resolvedComponentsAdded);
return bom;
}
@@ -426,7 +429,8 @@ public class ComponentBomTask extends Task {
return meta;
}
- private void addDependencies(Bom bom, Map<String, String> unversionedToVersioned) {
+ private void addDependencies(Bom bom, Map<String, String> unversionedToVersioned,
+ List<Component> resolvedComponentsAdded) {
final Set<String> bomRefs = new HashSet<>();
visitAllBomComponents(bom, c -> {
String bomRef = c.getBomRef();
@@ -438,7 +442,7 @@ public class ComponentBomTask extends Task {
});
final List<Dependency> dependencies = new ArrayList<>();
- visitAllComponents(c -> {
+ visitAllComponentsWithExtra(resolvedComponentsAdded, c -> {
String bomRef = c.getBomRef();
if (bomRef != null && !c.areDependenciesUnknown()) {
Dependency dep = new Dependency(bomRef);
@@ -466,8 +470,13 @@ public class ComponentBomTask extends Task {
}
private void visitAllComponents(Consumer<Component> visitor) {
+ visitAllComponentsWithExtra(Collections.emptyList(), visitor);
+ }
+
+ private void visitAllComponentsWithExtra(List<Component> extraComponents, Consumer<Component> visitor) {
visitAllComponents(component, visitor);
visitAllComponents(additionalComponents, visitor);
+ visitAllComponents(extraComponents, visitor);
}
private void visitAllComponents(Component c,
diff --git a/src/main/org/apache/ant/cyclonedx/IvyModuleComponentResolver.java b/src/main/org/apache/ant/cyclonedx/IvyModuleComponentResolver.java
new file mode 100644
index 0000000..43f97d8
--- /dev/null
+++ b/src/main/org/apache/ant/cyclonedx/IvyModuleComponentResolver.java
@@ -0,0 +1,256 @@
+/*
+ * 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.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.ivy.Ivy;
+import org.apache.ivy.ant.IvyAntSettings;
+import org.apache.ivy.core.module.descriptor.License;
+import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
+import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.core.report.ResolveReport;
+import org.apache.ivy.core.resolve.IvyNode;
+import org.apache.ivy.core.resolve.IvyNodeCallers.Caller;
+import org.apache.ivy.core.settings.IvySettings;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.types.Reference;
+import org.apache.tools.ant.types.resources.URLResource;
+
+/**
+ * Resolver that populates a Component from Ivy module data.
+ *
+ * <p>Assumes the Ivy file has already been resolved (i.e., the project
+ * has run ivy:resolve already). Uses the Ivy engine to resolve dependencies
+ * from the Ivy cache.</p>
+ *
+ * <p>Strongly influenced by Ivy's dependencytree task.</p>
+ *
+ * @since CycloneDX Antlib 0.2
+ */
+class IvyModuleComponentResolver {
+
+ private final Component.IvyModule ivyModule;
+ private final Project project;
+
+ IvyModuleComponentResolver(Component.IvyModule ivyModule, Project project) {
+ this.ivyModule = ivyModule;
+ this.project = project;
+ }
+
+ /**
+ * Resolves the Ivy module and populates the component with its data.
+ *
+ * @param component the component to populate
+ * @return additional components that are (transitive) dependencies of this component
+ * @throws IOException if resolution fails
+ */
+ public Collection<Component> resolve(Component component) throws IOException, BuildException {
+ Ivy ivy = createIvyInstance(component);
+ IvySettings settings = ivy.getSettings();
+
+ ResolveReport report = loadResolveReport(settings);
+ ModuleDescriptor root = report.getModuleDescriptor();
+
+ Map<ModuleRevisionId, Set<IvyNode>> dependencyTree = populateDependencyTree(settings, report);
+ fillFromModuleDescriptor(component, root, dependencyTree);
+
+ Collection<ModuleDescriptor> allDependencies = getDependencies(dependencyTree, root);
+
+ return allDependencies.stream()
+ .map(d -> toComponent(d, dependencyTree))
+ .collect(Collectors.toList());
+ }
+
+ private Ivy createIvyInstance(Component component) {
+ Reference settingRef = ivyModule.getSettingsRef();
+ IvyAntSettings engine;
+ if (settingRef == null) {
+ engine = IvyAntSettings.getDefaultInstance(component);
+ }
+ else {
+ engine = settingRef.getReferencedObject(project);
+ }
+ return engine.getConfiguredIvyInstance(component);
+ }
+
+ private ResolveReport loadResolveReport(IvySettings settings) {
+ // explicit values for organisation and module would come in here, once supported
+ String organisation = settings.getVariable("ivy.organisation");
+ if (organisation == null) {
+ throw new BuildException("no organisation provided, you need to call to <resolve/> before using this task");
+ }
+
+ String module = settings.getVariable("ivy.module");
+ if (organisation == null) {
+ throw new BuildException("no module provided, you need to call to <resolve/> before using this task");
+ }
+
+ ResolveReport report = getResolvedReport(organisation, module);
+ if (report == null) {
+ throw new BuildException("No resolution report was available," +
+ " you need to call to <resolve/> before using this task");
+ }
+ return report;
+ }
+
+ private ResolveReport getResolvedReport(String org, String module) {
+ String resolveId = ivyModule.getResolveId();
+ ResolveReport report;
+ if (resolveId != null) {
+ report = project.getReference("ivy.resolved.report."+ org + "." + resolveId);
+ } else {
+ report = project.getReference("ivy.resolved.report."+ org + "." + module);
+ }
+ if (report == null) {
+ report = project.getReference("ivy.resolved.report");
+ }
+ return report;
+ }
+
+ private Component toComponent(ModuleDescriptor md, Map<ModuleRevisionId,
+ Set<IvyNode>> dependencyTree) {
+ Component c = new Component();
+ c.setProject(project);
+ fillFromModuleDescriptor(c, md, dependencyTree);
+ return c;
+ }
+
+ private static void fillFromModuleDescriptor(Component component, ModuleDescriptor md,
+ Map<ModuleRevisionId, Set<IvyNode>> dependencyTree) {
+ ModuleRevisionId mrid = md.getModuleRevisionId();
+ if (component.getName() == null) {
+ component.setName(mrid.getName());
+ }
+ if (component.getGroup() == null) {
+ component.setGroup(mrid.getOrganisation());
+ }
+ if (component.getVersion() == null) {
+ component.setVersion(mrid.getRevision());
+ }
+ if (component.getDescription() == null && md.getDescription() != null) {
+ component.setDescription(md.getDescription());
+ }
+
+ if (!component.hasLicenses()) {
+ License[] ivyLicenses = md.getLicenses();
+ if (ivyLicenses != null) {
+ for (License ivyLicense : ivyLicenses) {
+ org.apache.ant.cyclonedx.License license = new org.apache.ant.cyclonedx.License();
+ license.setName(ivyLicense.getName());
+ if (ivyLicense.getUrl() != null) {
+ license.addConfiguredUrl(new URLResource(ivyLicense.getUrl()));
+ }
+ component.addConfiguredLicense(license);
+ }
+ }
+ }
+
+ // only add dependencies if the component doesn't already have any dependency configuration itself
+ if (!component.areDependenciesUnknown() && !component.getDependencies().iterator().hasNext()) {
+ Set<IvyNode> dependencies = dependencyTree.get(mrid);
+ if (dependencies != null) {
+ for (IvyNode n : dependencies) {
+ Component.Dependency d = new Component.Dependency();
+ d.setBomRef(getBomRef(n));
+ component.addDependency(d);
+ }
+ }
+ }
+
+ String homePage = md.getHomePage();
+ if (homePage != null && !component.getExternalReferences().stream()
+ .anyMatch(e -> e.getType().equals(org.cyclonedx.model.ExternalReference.Type.WEBSITE))) {
+ ExternalReference e = new ExternalReference();
+ e.setUrl(homePage);
+ e.setType(org.cyclonedx.model.ExternalReference.Type.WEBSITE.name());
+ component.addConfiguredExternalReference(e);
+ }
+ }
+
+ private Map<ModuleRevisionId, Set<IvyNode>> populateDependencyTree(IvySettings settings, ResolveReport report) {
+ String conf = ivyModule.getConf();
+ if (conf == null || "*".equals(conf)) {
+ conf = settings.getVariable("ivy.resolved.configurations");
+ }
+ if (conf == null) {
+ throw new BuildException("no conf provided, you need to call to <resolve/> before using this task");
+ }
+
+ Map<ModuleRevisionId, Set<IvyNode>> tree = new HashMap<>();
+ for (IvyNode dependency : report.getDependencies()) {
+ populateDependencyTree(dependency, tree, conf);
+ }
+ return tree;
+ }
+
+ private void populateDependencyTree(IvyNode node, Map<ModuleRevisionId, Set<IvyNode>> tree, String conf) {
+ if (node.isEvicted(conf)) {
+ return;
+ }
+
+ tree.computeIfAbsent(node.getId(), _ignored -> new HashSet<>());
+ for (Caller caller : node.getAllCallers()) {
+ addDependency(caller.getModuleRevisionId(), node, tree);
+ }
+ }
+
+ private void addDependency(ModuleRevisionId caller, IvyNode dependency, Map<ModuleRevisionId, Set<IvyNode>> tree) {
+ Set<IvyNode> deps = tree.computeIfAbsent(caller, _ignored -> new HashSet<>());
+ deps.add(dependency);
+ }
+
+ private Collection<ModuleDescriptor> getDependencies(Map<ModuleRevisionId, Set<IvyNode>> tree,
+ ModuleDescriptor root) {
+ Set<ModuleRevisionId> seen = new HashSet<>();
+ ModuleRevisionId rootId = root.getModuleRevisionId();
+ seen.add(rootId);
+ List<ModuleDescriptor> deps = new ArrayList<>();
+ appendDependencies(tree, rootId, deps, seen);
+ return deps;
+ }
+
+ private void appendDependencies(Map<ModuleRevisionId, Set<IvyNode>> tree, ModuleRevisionId mrid,
+ List<ModuleDescriptor> deps, Set<ModuleRevisionId> seen) {
+ Set<IvyNode> thisDeps = tree.get(mrid);
+ if (thisDeps != null) {
+ for (IvyNode d : thisDeps) {
+ ModuleRevisionId depId = d.getId();
+ if (!seen.contains(depId)) {
+ seen.add(depId);
+ deps.add(d.getDescriptor());
+ appendDependencies(tree, depId, deps, seen);
+ }
+ }
+ }
+ }
+
+ private static String getBomRef(IvyNode n) {
+ ModuleRevisionId mrid = n.getId();
+ return "pkg:maven/" + mrid.getOrganisation() + "/" + mrid.getName() + "@" + mrid.getRevision() + "?type=jar";
+ }
+}