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 <[email protected]>
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";
+    }
+}

Reply via email to