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"; + } +}
