Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Parent;
import org.apache.maven.model.Repository;
Expand All @@ -33,21 +35,26 @@
import org.jspecify.annotations.Nullable;

public class GradleAssistedMavenModelResolverImpl implements ModelResolver {

private final Project project;
private final Map<String, File> resolvedPomCache;

public GradleAssistedMavenModelResolverImpl(Project project) {
super();
this.project = project;
this.resolvedPomCache = new HashMap<>();
}

@Override
public ModelSource2 resolveModel(String groupId, String artifactId, String version) {
String depNotation = String.format("%s:%s:%s@pom", groupId, artifactId, version);
org.gradle.api.artifacts.Dependency dependency =
project.getDependencies().create(depNotation);
Configuration config = project.getConfigurations().detachedConfiguration(dependency);

File pomXml = config.getSingleFile();
File pomXml = resolvedPomCache.computeIfAbsent(depNotation, notation -> {
org.gradle.api.artifacts.Dependency dependency =
project.getDependencies().create(notation);
Configuration config = project.getConfigurations().detachedConfiguration(dependency);
return config.getSingleFile();
});
return new ModelSource2() {
@Override
public InputStream getInputStream() throws IOException {
Expand Down
9 changes: 3 additions & 6 deletions src/main/java/org/cyclonedx/gradle/MavenHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
import org.cyclonedx.model.ExternalReference;
import org.cyclonedx.model.LicenseChoice;
import org.cyclonedx.util.LicenseResolver;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.logging.Logger;
Expand Down Expand Up @@ -381,13 +380,11 @@ private boolean doesComponentHaveExternalReference(final Component component, fi
*
* @param pomFile
* the dependency pomFile
* @param gradleProject
* the current gradle project which gets used as the base resolver
* @param modelResolver
* the model resolver used to resolve parent POMs
* @return model for effective pom
*/
static @Nullable Model resolveEffectivePom(final @Nullable File pomFile, final Project gradleProject) {
// force the parent POMs and BOMs to be resolved
final ModelResolver modelResolver = new GradleAssistedMavenModelResolverImpl(gradleProject);
static @Nullable Model resolveEffectivePom(final @Nullable File pomFile, final ModelResolver modelResolver) {
final ModelBuildingRequest req = new DefaultModelBuildingRequest();
req.setModelResolver(modelResolver);
req.setPomFile(pomFile);
Expand Down
67 changes: 41 additions & 26 deletions src/main/java/org/cyclonedx/gradle/MavenProjectLookup.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
package org.cyclonedx.gradle;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.maven.model.Model;
import org.apache.maven.project.MavenProject;
import org.gradle.api.Project;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.result.ArtifactResolutionResult;
import org.gradle.api.artifacts.result.ArtifactResult;
import org.gradle.api.artifacts.result.ComponentArtifactsResult;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
Expand All @@ -45,10 +45,32 @@ class MavenProjectLookup {
private static final Logger LOGGER = Logging.getLogger(MavenProjectLookup.class);
private final Project project;
private final Map<ComponentIdentifier, MavenProject> cache;
private final Map<ComponentIdentifier, File> pomFileCache;
private final GradleAssistedMavenModelResolverImpl modelResolver;

MavenProjectLookup(final Project project) {
this.project = project;
this.cache = new HashMap<>();
this.pomFileCache = new HashMap<>();
this.modelResolver = new GradleAssistedMavenModelResolverImpl(project);
}

/**
* Resolves POM files for all provided component identifiers in a single batch query.
* This is significantly faster than resolving them one at a time, as it avoids
* N individual Gradle artifact resolution API calls.
*
* @param componentIds the component identifiers to resolve POM files for
*/
void batchResolvePomFiles(final Collection<ComponentIdentifier> componentIds) {
if (componentIds.isEmpty()) {
return;
}

LOGGER.info("CycloneDX: Batch resolving {} POM files", componentIds.size());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All log messages in this project follow pattern LOGGER.log("{} Message", LOG_PREFIX). It helps to identify plugin logs in Gradle build. Please use it in this PR too.

Example: https://github.com/CycloneDX/cyclonedx-gradle-plugin/blob/master/src/main/java/org/cyclonedx/gradle/CyclonedxPlugin.java#L160-L164

resolvePomFiles(componentIds);
LOGGER.info(
"CycloneDX: Batch resolved {} POM files out of {} requested", pomFileCache.size(), componentIds.size());
}

/**
Expand All @@ -74,7 +96,7 @@ class MavenProjectLookup {
final MavenProject mavenProject = MavenHelper.readPom(pomFile);
if (mavenProject != null) {
LOGGER.debug("CycloneDX: parse queried pom file for component {}", result.getId());
final Model model = MavenHelper.resolveEffectivePom(pomFile, project);
final Model model = MavenHelper.resolveEffectivePom(pomFile, modelResolver);
if (model != null) {
mavenProject.setLicenses(model.getLicenses());
}
Expand All @@ -89,32 +111,25 @@ class MavenProjectLookup {
}

@Nullable File buildMavenProject(final ComponentIdentifier id) {
if (!pomFileCache.containsKey(id)) {
resolvePomFiles(Collections.singletonList(id));
}
return pomFileCache.get(id);
}

final ArtifactResolutionResult result = project.getDependencies()
private void resolvePomFiles(final Collection<ComponentIdentifier> componentIds) {
for (final ComponentArtifactsResult componentResult : project.getDependencies()
.createArtifactResolutionQuery()
.forComponents(id)
.forComponents(componentIds)
.withArtifacts(MavenModule.class, MavenPomArtifact.class)
.execute();

final Iterator<ComponentArtifactsResult> componentIt =
result.getResolvedComponents().iterator();
if (!componentIt.hasNext()) {
return null;
}

final Iterator<ArtifactResult> artifactIt =
componentIt.next().getArtifacts(MavenPomArtifact.class).iterator();
if (!artifactIt.hasNext()) {
return null;
}

final ArtifactResult artifact = artifactIt.next();
if (artifact instanceof ResolvedArtifactResult) {
LOGGER.debug("CycloneDX: found pom file for component {}", id);
final ResolvedArtifactResult resolvedArtifact = (ResolvedArtifactResult) artifact;
return resolvedArtifact.getFile();
.execute()
.getResolvedComponents()) {
for (final ArtifactResult artifact : componentResult.getArtifacts(MavenPomArtifact.class)) {
if (artifact instanceof ResolvedArtifactResult) {
pomFileCache.put(componentResult.getId(), ((ResolvedArtifactResult) artifact).getFile());
LOGGER.debug("CycloneDX: found pom file for component {}", componentResult.getId());
}
}
}

return null;
}
}
7 changes: 6 additions & 1 deletion src/main/java/org/cyclonedx/gradle/SbomGraphProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ private SbomGraph buildSbomGraph(final Map<SbomComponentId, SbomComponent> graph
}

private Stream<Map<SbomComponentId, SbomComponent>> traverseProject() {
final DependencyGraphTraverser traverser = new DependencyGraphTraverser(getArtifacts(), mavenLookup, task);
final Map<ComponentIdentifier, File> artifacts = getArtifacts();

// Batch resolve all POM files in a single query before traversal
mavenLookup.batchResolvePomFiles(artifacts.keySet());

final DependencyGraphTraverser traverser = new DependencyGraphTraverser(artifacts, mavenLookup, task);
return getInScopeConfigurations()
.map(config -> traverser.traverseGraph(
config.getIncoming().getResolutionResult().getRoot(), project.getName(), config.getName()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,62 @@ class DependencyResolutionSpec extends Specification {
javaVersion = JavaVersion.current()
}

def "should resolve licenses across subprojects sharing transitive dependencies"() {
given:
File testDir = File.createTempDir("multi-deps-")
new File(testDir, "settings.gradle").text = """
rootProject.name = 'multi-deps'
include 'lib-a', 'lib-b'
"""
new File(testDir, "build.gradle").text = """
plugins {
id 'org.cyclonedx.bom'
id 'java'
}
group = 'com.example'
version = '1.0.0'
allprojects { repositories { mavenCentral() } }
subprojects { group = 'com.example'; version = '1.0.0' }
"""
['lib-a', 'lib-b'].each { name ->
def dir = new File(testDir, name)
dir.mkdirs()
new File(dir, "build.gradle").text = """
plugins { id 'java-library' }
dependencies {
implementation 'com.google.guava:guava:33.4.0-jre'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.3'
}
"""
}

when:
def result = GradleRunner.create()
.withProjectDir(testDir)
.withArguments(TestUtils.arguments("cyclonedxBom"))
.withPluginClasspath()
.build()

then:
result.task(":cyclonedxBom").outcome == TaskOutcome.SUCCESS
File jsonBom = new File(testDir, "build/reports/cyclonedx/bom.json")
Bom bom = new ObjectMapper().readValue(jsonBom, Bom.class)

def guava = bom.getComponents().find { it.name == 'guava' }
def jackson = bom.getComponents().find { it.name == 'jackson-databind' }

assert guava != null
assert jackson != null
assert guava.getLicenses() != null && !guava.getLicenses().getLicenses().isEmpty()
assert jackson.getLicenses() != null && !jackson.getLicenses().getLicenses().isEmpty()

and: "POM files were batch-resolved rather than individually"
result.output.contains("Batch resolved")

where:
javaVersion = JavaVersion.current()
}

private static def loadJsonBom(File file) {
return new JsonSlurper().parse(file)
}
Expand Down
Loading