diff --git a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/dependency/DeploymentClasspathBuilder.java b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/dependency/DeploymentClasspathBuilder.java index f7fef45daf664..4a04c99a2edc0 100644 --- a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/dependency/DeploymentClasspathBuilder.java +++ b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/dependency/DeploymentClasspathBuilder.java @@ -33,6 +33,8 @@ public void exportDeploymentClasspath(String configurationName) { project.getConfigurations().getByName(configurationName)); Set> extensionDependencies = collectFirstMetQuarkusExtensions(configuration); + final Set seenDeploymentDependencies = new HashSet<>(); + DependencyHandler dependencies = project.getDependencies(); for (ExtensionDependency extension : extensionDependencies) { @@ -40,8 +42,13 @@ public void exportDeploymentClasspath(String configurationName) { continue; } + if (!seenDeploymentDependencies.add(DependencyUtils.getDeploymentDependencyIdentifier(extension))) { + continue; + } + dependencies.add(deploymentConfigurationName, - DependencyUtils.createDeploymentDependency(dependencies, extension)); + DependencyUtils.createDeploymentDependency(project, dependencies, extension, + seenDeploymentDependencies)); } }); } diff --git a/devtools/gradle/gradle-model/build.gradle.kts b/devtools/gradle/gradle-model/build.gradle.kts index be5055edd3d8c..be24cf675335d 100644 --- a/devtools/gradle/gradle-model/build.gradle.kts +++ b/devtools/gradle/gradle-model/build.gradle.kts @@ -3,7 +3,10 @@ plugins { } dependencies { + implementation(libs.maven.model) + compileOnly(libs.kotlin.gradle.plugin.api) + gradleApi() } group = "io.quarkus" diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java index f3cd59c9d8f65..1e3a53544995e 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java @@ -205,8 +205,16 @@ private void setUpDeploymentConfiguration() { ConditionalDependenciesEnabler cdEnabler = new ConditionalDependenciesEnabler(project, mode, enforcedPlatforms); final Collection> allExtensions = cdEnabler.getAllExtensions(); - Set> extensions = collectFirstMetQuarkusExtensions(getRawRuntimeConfiguration(), - allExtensions); + + // linked hash set is very important here! we need to keep the first met dependencies + // at the start of the collection so dependency based exclusions stick! + // Example: this prevents pulling in hibernate-orm-deployment here if it has already been + // pulled in by hibernate-reactive where it excludes Agroal and Narayana. + // otherwise those get pulled in as well and lead to failures when trying to run + // an application with reactive DB drivers without JDBC drivers on the class path + Set> extensions = new LinkedHashSet<>( + collectFirstMetQuarkusExtensions(getRawRuntimeConfiguration(), + allExtensions)); // Add conditional extensions for (ExtensionDependency knownExtension : allExtensions) { if (knownExtension.isConditional()) { @@ -214,17 +222,29 @@ private void setUpDeploymentConfiguration() { } } + final Set seenDeploymentDependencies = new HashSet<>(); + final Set alreadyProcessed = new HashSet<>(extensions.size()); final DependencyHandler dependencies = project.getDependencies(); final Set deploymentDependencies = new HashSet<>(); + for (ExtensionDependency extension : extensions) { if (!alreadyProcessed.add(extension.getExtensionId())) { continue; } + if (!seenDeploymentDependencies.add(DependencyUtils.getDeploymentDependencyIdentifier(extension))) { + continue; + } + deploymentDependencies.add( - DependencyUtils.createDeploymentDependency(dependencies, extension)); + DependencyUtils.createDeploymentDependency( + project, + dependencies, + extension, + seenDeploymentDependencies)); } + return deploymentDependencies; }))); }); diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java index 15682e835114d..a8cb5b6e87198 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java @@ -40,6 +40,7 @@ import io.quarkus.gradle.extension.ConfigurationUtils; import io.quarkus.gradle.extension.ExtensionConstants; import io.quarkus.gradle.tooling.ToolingUtils; +import io.quarkus.gradle.tooling.pom.PomUtils; import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.GACT; @@ -349,15 +350,16 @@ public static Dependency create(DependencyHandler dependencies, String condition dependencyCoords.getVersion())); } - public static Dependency createDeploymentDependency( + public static Dependency createDeploymentDependency(Project project, DependencyHandler dependencyHandler, - ExtensionDependency dependency) { + ExtensionDependency dependency, + Set seenDeploymentDependencies) { if (dependency instanceof ProjectExtensionDependency) { ProjectExtensionDependency ped = (ProjectExtensionDependency) dependency; return createDeploymentProjectDependency(dependencyHandler, ped); } else if (dependency instanceof ArtifactExtensionDependency) { ArtifactExtensionDependency aed = (ArtifactExtensionDependency) dependency; - return createArtifactDeploymentDependency(dependencyHandler, aed); + return createArtifactDeploymentDependency(project, dependencyHandler, aed, seenDeploymentDependencies); } throw new IllegalArgumentException("Unknown ExtensionDependency type: " + dependency.getClass().getName()); @@ -376,10 +378,28 @@ private static Dependency createDeploymentProjectDependency(DependencyHandler ha } } - private static Dependency createArtifactDeploymentDependency(DependencyHandler handler, - ArtifactExtensionDependency dependency) { + private static Dependency createArtifactDeploymentDependency( + Project project, + DependencyHandler handler, + ArtifactExtensionDependency dependency, + Set seenDeploymentDependencies) { + + seenDeploymentDependencies.addAll(PomUtils.readDeploymentDependencies(project, dependency.getDeploymentModule())); + return handler.create(dependency.getDeploymentModule().getGroupId() + ":" + dependency.getDeploymentModule().getArtifactId() + ":" + dependency.getDeploymentModule().getVersion()); } + + public static String getDeploymentDependencyIdentifier(ExtensionDependency extension) { + if (extension instanceof ArtifactExtensionDependency) { + ArtifactExtensionDependency aed = (ArtifactExtensionDependency) extension; + return aed.getDeploymentModule().getGroupId() + ":" + aed.getDeploymentModule().getArtifactId(); + } else if (extension instanceof ProjectExtensionDependency) { + ProjectExtensionDependency ped = (ProjectExtensionDependency) extension; + return ped.getDeploymentModule().getGroup() + ":" + ped.getDeploymentModule().getName(); + } + + return null; + } } diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/pom/PomUtils.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/pom/PomUtils.java new file mode 100644 index 0000000000000..0f8af9b74c65c --- /dev/null +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/pom/PomUtils.java @@ -0,0 +1,69 @@ +package io.quarkus.gradle.tooling.pom; + +import java.io.FileReader; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.gradle.api.Project; +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.internal.artifacts.DefaultModuleIdentifier; +import org.gradle.api.internal.artifacts.result.DefaultResolvedArtifactResult; +import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier; +import org.gradle.maven.MavenModule; +import org.gradle.maven.MavenPomArtifact; + +import io.quarkus.maven.dependency.ArtifactCoords; + +public final class PomUtils { + public static Set readDeploymentDependencies(Project project, ArtifactCoords artifactCoords) { + @SuppressWarnings("unchecked") + ArtifactResolutionResult resolutionResult = project + .getDependencies().createArtifactResolutionQuery() + .forComponents( + new DefaultModuleComponentIdentifier( + DefaultModuleIdentifier.newId( + artifactCoords.getGroupId(), + artifactCoords.getArtifactId()), + artifactCoords.getVersion())) + .withArtifacts(MavenModule.class, MavenPomArtifact.class) + .execute(); + + MavenXpp3Reader pomReader = new MavenXpp3Reader(); + + final Set results = new HashSet<>(); + + for (ComponentArtifactsResult resolvedComponent : resolutionResult.getResolvedComponents()) { + for (ArtifactResult artifact : resolvedComponent.getArtifacts(MavenPomArtifact.class)) { + if (artifact instanceof DefaultResolvedArtifactResult) { + DefaultResolvedArtifactResult resolvedArtifact = (DefaultResolvedArtifactResult) artifact; + + Model pom; + try (FileReader fr = new FileReader(resolvedArtifact.getFile(), StandardCharsets.UTF_8)) { + pom = pomReader.read(fr); + } catch (Exception e) { + project.getLogger().warn("Failed to read pom.xml for " + resolvedArtifact, e); + continue; + } + + for (Dependency pomDependency : pom.getDependencies()) { + if (!Objects.equals("test", pomDependency.getScope())) { + results.add(pomDependency.getGroupId() + ":" + pomDependency.getArtifactId()); + } + } + } + } + } + + return results; + } + + private PomUtils() { + } +} diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index 87c07810793ae..34786d72c9233 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -5,6 +5,8 @@ plugin-publish = "1.2.1" kotlin = "1.9.22" smallrye-config = "3.5.4" +maven = "3.9.6" + junit5 = "5.10.2" assertj = "3.25.3" @@ -22,8 +24,10 @@ quarkus-project-core-extension-codestarts = { module = "io.quarkus:quarkus-proje kotlin-gradle-plugin-api = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" } smallrye-config-yaml = { module = "io.smallrye.config:smallrye-config-source-yaml", version.ref = "smallrye-config" } -jackson-databind = {module="com.fasterxml.jackson.core:jackson-databind"} -jackson-dataformat-yaml = {module="com.fasterxml.jackson.dataformat:jackson-dataformat-yaml"} +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind" } +jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" } + +maven-model = { module = "org.apache.maven:maven-model", version.ref = "maven" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit5" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api" } diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/build.gradle b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/build.gradle new file mode 100644 index 0000000000000..21deaf7f0d7ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'io.quarkus' +} + +repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy-reactive-jackson' + implementation 'io.quarkus:quarkus-hibernate-reactive-panache' + implementation 'io.quarkus:quarkus-reactive-pg-client' + implementation 'io.quarkus:quarkus-arc' + implementation 'io.quarkus:quarkus-resteasy-reactive' + testImplementation 'io.quarkus:quarkus-junit5' + testImplementation 'io.rest-assured:rest-assured' +} + +group 'org.acme' +version '1.0.0-SNAPSHOT' + +test { + systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" +} diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/gradle.properties b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/gradle.properties new file mode 100644 index 0000000000000..ec2b6ef199c2c --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/settings.gradle b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/settings.gradle new file mode 100644 index 0000000000000..b2d511f9f6a1a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} + +rootProject.name='code-with-quarkus' diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/java/org/acme/GreetingResource.java b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/java/org/acme/GreetingResource.java new file mode 100644 index 0000000000000..6938062ec8ff7 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/java/org/acme/GreetingResource.java @@ -0,0 +1,16 @@ +package org.acme; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello from RESTEasy Reactive"; + } +} diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/java/org/acme/MyEntity.java b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/java/org/acme/MyEntity.java new file mode 100644 index 0000000000000..a18f8d226cf27 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/java/org/acme/MyEntity.java @@ -0,0 +1,14 @@ + +package org.acme; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import java.util.UUID; + +@Entity +public class MyEntity { + @Id + @GeneratedValue + private UUID id; +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/resources/META-INF/resources/index.html b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000000..bd66dc4bb5121 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,285 @@ + + + + + code-with-quarkus - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive Jackson
  • +
  • Hibernate Reactive with Panache
  • +
  • Reactive PostgreSQL client (guide)
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/resources/application.properties new file mode 100644 index 0000000000000..abaaf7722ef5e --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/main/resources/application.properties @@ -0,0 +1,5 @@ +quarkus.datasource.db-kind = postgresql +quarkus.datasource.username = quarkus_test +quarkus.datasource.password = quarkus_test +quarkus.datasource.reactive.url = vertx-reactive:postgresql://localhost:5432/quarkus_test +#quarkus.datasource.jdbc=false \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/native-test/java/org/acme/GreetingResourceIT.java b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/native-test/java/org/acme/GreetingResourceIT.java new file mode 100644 index 0000000000000..cfa9d1b1aff2b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/native-test/java/org/acme/GreetingResourceIT.java @@ -0,0 +1,8 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class GreetingResourceIT extends GreetingResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/test/java/org/acme/GreetingResourceTest.java b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/test/java/org/acme/GreetingResourceTest.java new file mode 100644 index 0000000000000..1e0da3846c90e --- /dev/null +++ b/integration-tests/gradle/src/main/resources/maven-exclusion-in-extension-dependency/src/test/java/org/acme/GreetingResourceTest.java @@ -0,0 +1,21 @@ +package org.acme; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class GreetingResourceTest { + @Test + void testHelloEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(is("Hello from RESTEasy Reactive")); + } + +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/devmode/MavenExclusionInExtensionDependencyDevModeTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/devmode/MavenExclusionInExtensionDependencyDevModeTest.java new file mode 100644 index 0000000000000..ef27b3d10c9b2 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/devmode/MavenExclusionInExtensionDependencyDevModeTest.java @@ -0,0 +1,27 @@ +package io.quarkus.gradle.devmode; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This makes sure that exclusions in POM files of deployment modules are respected. + *

+ * One case where this is critical is when using quarkus-hibernate-reactive. Its deployment + * module takes care of pulling in quarkus-hibernate-orm-deployment where it excludes + * Agroal and Narayana. + *

+ * If that exclusion isn't taken into account by the Gradle plugin, it pulls in + * quarkus-hibernate-orm-deployment on its own, where those other modules aren't excluded, + * which leads to a failure at runtime because Agroal tries to initialize and looks + * for a JDBC driver which isn't typically available in reactive DB scenarios. + */ +public class MavenExclusionInExtensionDependencyDevModeTest extends QuarkusDevGradleTestBase { + @Override + protected String projectDirectoryName() { + return "maven-exclusion-in-extension-dependency"; + } + + @Override + protected void testDevMode() throws Exception { + assertThat(getHttpResponse("/hello")).contains("Hello"); + } +}