Skip to content

Commit

Permalink
Support conditional dependencies on regular artifacts
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexey Loubyansky committed Nov 25, 2024
1 parent 9ff49c1 commit aac0bdf
Show file tree
Hide file tree
Showing 18 changed files with 341 additions and 169 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@
import io.quarkus.gradle.tooling.dependency.ExtensionDependency;
import io.quarkus.maven.dependency.ArtifactCoords;
import io.quarkus.maven.dependency.ArtifactKey;
import io.quarkus.maven.dependency.GACT;
import io.quarkus.runtime.LaunchMode;

public class ConditionalDependenciesEnabler {

/**
* Links dependencies to extensions
*/
private final Map<GACT, Set<ExtensionDependency<?>>> featureVariants = new HashMap<>();
private final Map<ArtifactKey, Set<ExtensionDependency<?>>> featureVariants = new HashMap<>();
/**
* Despite its name, only contains extensions which have no conditional dependencies, or have
* resolved their conditional dependencies.
Expand Down Expand Up @@ -133,10 +132,11 @@ private boolean resolveConditionalDependency(Dependency conditionalDep, LaunchMo
// Once the dependency is found, reload the extension info from within
final ExtensionDependency<?> extensionDependency = DependencyUtils.getExtensionInfoOrNull(project, artifact);
// Now check if this conditional dependency is resolved given the latest graph evolution
if (extensionDependency != null && (extensionDependency.getDependencyConditions().isEmpty()
|| exist(extensionDependency.getDependencyConditions()))) {
if (extensionDependency == null ||
(extensionDependency.getDependencyConditions().isEmpty() ||
exist(extensionDependency.getDependencyConditions()))) {
satisfied = true;
enableConditionalDependency(extensionDependency.getExtensionId());
enableConditionalDependency(artifact.getModuleVersion().getId());
break;
}
}
Expand Down Expand Up @@ -207,12 +207,12 @@ public boolean exists(ExtensionDependency<?> dependency) {
.contains(ArtifactKey.of(dependency.getGroup(), dependency.getName(), null, ArtifactCoords.TYPE_JAR));
}

private static GACT getFeatureKey(ModuleVersionIdentifier version) {
return new GACT(version.getGroup(), version.getName());
private static ArtifactKey getFeatureKey(ModuleVersionIdentifier version) {
return ArtifactKey.ga(version.getGroup(), version.getName());
}

private static GACT getFeatureKey(Dependency version) {
return new GACT(version.getGroup(), version.getName());
private static ArtifactKey getFeatureKey(Dependency version) {
return ArtifactKey.ga(version.getGroup(), version.getName());
}

private static ArtifactKey getKey(ResolvedArtifact a) {
Expand Down
69 changes: 59 additions & 10 deletions docs/src/main/asciidoc/conditional-extension-dependencies.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ include::_attributes.adoc[]
:summary: Trigger the inclusion on additional extensions based on certain conditions.
:topics: extensions

Quarkus extension dependencies are usually configured in the same way as any other project dependencies in a project's build file, e.g. the Maven `pom.xml` or the Gradle build scripts. However, Quarkus also supports types of dependencies that aren't supported out-of-the-box by Maven and Gradle. Conditional Quarkus extension dependencies falls into that category.
Quarkus extension dependencies are usually configured in the same way as any other project dependencies in a project's build file, for example the Maven `pom.xml` or the Gradle build scripts. However, Quarkus also supports types of dependencies that aren't supported out-of-the-box by Maven and Gradle. Conditional Quarkus extension dependencies is one such example.

== Conditional Dependencies

A conditional dependency is a dependency that is activated only if a certain condition is satisfied. If the condition is not satisfied then the dependency **must not** be activated. In that regard, conditional dependencies can be categorized as optional, meaning they may or may not appear in the resulting dependency graph.
A conditional dependency is a dependency that is activated only if a certain condition is satisfied. If the condition is not satisfied then the dependency **will not** be activated. In that regard, conditional dependencies can be categorized as optional, meaning they may or may not appear in the resulting dependency graph.

A typical example of a conditional dependency would be a component that should be added to the classpath **only** in case all of its required dependencies are present on the classpath. If one or more of the component's required dependencies aren't available, instead of failing, the component should simply not be added.

== Conditional Quarkus Extension Dependencies

A Quarkus extension may declare one or more conditional dependencies on other Quarkus extensions. Conditional dependencies on and from non-extension artifacts aren't supported.
A Quarkus extension may declare one or more conditional dependencies on other Quarkus extensions or regular Maven artifacts.

Let's consider the following scenario as an example: `quarkus-extension-a` has an optional dependency on `quarkus-extension-b` which should be included in a Quarkus application only if `quarkus-extension-c` is found among the application dependencies (direct or transitive). In this case, the presence of `quarkus-extension-c` is the condition which, if satisfied, will trigger inclusion of the `quarkus-extension-b` when Quarkus application dependencies are resolved.
Let's consider the following scenario as an example: `quarkus-extension-a` has an optional dependency on `quarkus-extension-b` which should be included in a Quarkus application only if `quarkus-extension-c` is found among the application dependencies (direct or transitive). In this case, the presence of `quarkus-extension-c` is the condition, which, if satisfied, will trigger inclusion of the `quarkus-extension-b` when Quarkus application dependencies are resolved.

The condition which triggers activation of an extension is configured in the extension's `META-INF/quarkus-extension.properties`, which is included in the runtime artifact of the extension. Extension developers can add the following configuration to express the condition which would have to be satisfied for the extension to be activated:

Expand Down Expand Up @@ -66,11 +66,13 @@ The condition which triggers activation of an extension is configured in the ext
<3> configuration of the dependency condition which will have to be satisfied for this extension to be added to a Quarkus application expressed as a list of artifacts that must be present among the application dependencies;
<4> an artifact key (in the format of `groupId:artifactId[:<classifier>:<extension>]` but typically simply `<groupId>:<artifactId>`) of the artifact that must be present among the application dependencies for the condition to be satisfied.

NOTE: In the example above the `artifact` used in the condition configuration happens to be a runtime Quarkus extension artifact but it could as well be any other artifact. There could also be more than one `artifact` element in the body of the `dependencyCondition`.
NOTE: In the example above the `artifact` used in the condition configuration happens to be a runtime Quarkus extension artifact but it could as well be any other artifact.

The `dependencyCondition` element may contain more than `artifact`, in which case all the configured artifacts must be present on the classpath for the condition to be satisfied.

Now, having a dependency condition recorded in the descriptor of the `quarkus-extension-b`, other extensions may declare a conditional dependency on it.

NOTE: extensions with dependency conditions present in their metadata could still appear as regular dependencies in Maven `pom.xml` and Gradle build scripts.
NOTE: extensions with dependency conditions present in their metadata could still appear as regular dependencies in Maven `pom.xml` and Gradle build scripts, in which case their conditions will simply be ignored.

A conditional dependency is configured in the runtime artifact of a Quarkus extension. In this example, the `quarkus-extension-a` will declare a conditional dependency on the `quarkus-extension-b`, which can be done in the following two ways.

Expand Down Expand Up @@ -212,7 +214,7 @@ Dev mode-only extension dependencies can be configured in the Quarkus extension
</goals>
<configuration>
<conditionalDevDependencies> <3>
<extension>org.acme:quarkus-extension-b:${b.version}</extension> <4>
<artifact>org.acme:quarkus-extension-b:${b.version}</artifact> <4>
</conditionalDevDependencies>
</configuration>
</execution>
Expand All @@ -223,11 +225,58 @@ Dev mode-only extension dependencies can be configured in the Quarkus extension
----
<1> the runtime Quarkus extension artifact ID;
<2> the goal that generates the extension descriptor which every Quarkus runtime extension project should be configured with;
<3> the dev mode conditional dependency configuration element;
<4> the artifact coordinates of conditional dependencies on extensions that should be evaluated only if an application is launched in dev mode.
<3> conditional dependencies that should be evaluated only in dev mode;
<4> the artifact coordinates of a conditional dependency.

The `quarkus-extension-b`, in this example, may or may not define its own condition to be evaluated.

If the `quarkus-extension-b` does not define a dependency condition on its own (there is no dependency condition recorded in its `META-INF/quarkus-extension.properties`), the `quarkus-extension-b` will only be added as a dependency of the `quarkus-extension-a` in dev mode but not in other modes (prod or test).

If the `quarkus-extension-b` does define a dependency condition on its own (a dependency condition recorded in its `META-INF/quarkus-extension.properties`), the `quarkus-extension-b` will be added as a dependency of the `quarkus-extension-a` in dev mode only if its condition is satisfied (the artifacts it requires are present in the application dependency graph).
If the `quarkus-extension-b` does define a dependency condition on its own (a dependency condition recorded in its `META-INF/quarkus-extension.properties`), the `quarkus-extension-b` will be added as a dependency of the `quarkus-extension-a` in dev mode only if its condition is satisfied (the artifacts it requires are present in the application dependency graph).

=== Dev mode dependencies on regular Maven artifacts

Extensions may also declare conditional dependencies on regular Maven artifacts, that are not Quarkus extensions. Given that regular Maven artifacts do not include Quarkus metadata, the condition for their inclusion is configured by an extension depending on them.

For example
[source,xml]
----
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- SKIPPED CONTENT -->
<artifactId>quarkus-extension-a</artifactId> <1>
<!-- SKIPPED CONTENT -->
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<executions>
<execution>
<phase>process-resources</phase>
<goals>
<goal>extension-descriptor</goal> <2>
</goals>
<configuration>
<conditionalDevDependencies> <3>
<artifact>org.acme:library-b:${b.version}</artifact> <4>
</conditionalDevDependencies>
</configuration>
</execution>
</executions>
</plugin>
<!-- SKIPPED CONTENT -->
----
<1> the runtime Quarkus extension artifact ID;
<2> the goal that generates the extension descriptor which every Quarkus runtime extension project should be configured with;
<3> conditional dependencies that should be evaluated only in dev mode;
<4> the artifact coordinates of a conditional dependency.

In this example `library-b` is a regular Maven artifact that will be added as a dependency of the `quarkus-extension-a` only when an application is launched in dev mode.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

import org.jboss.logging.Logger;
Expand All @@ -22,8 +21,7 @@ public class BuildToolHelper {

private final static String[] DEVMODE_REQUIRED_TASKS = new String[] { "classes" };
private final static String[] TEST_REQUIRED_TASKS = new String[] { "classes", "testClasses", "integrationTestClasses" };
private final static List<String> ENABLE_JAR_PACKAGING = Collections
.singletonList("-Dorg.gradle.java.compile-classpath-packaging=true");
private final static List<String> ENABLE_JAR_PACKAGING = List.of("-Dorg.gradle.java.compile-classpath-packaging=true");

public enum BuildTool {
MAVEN("pom.xml"),
Expand Down Expand Up @@ -61,7 +59,7 @@ public static Path getProjectDir(Path p) {
}
currentPath = currentPath.getParent();
}
log.warnv("Unable to find a project directory for {0}.", p.toString());
log.warnv("Unable to find a project directory for {0}.", p);
return null;
}

Expand All @@ -76,7 +74,7 @@ public static BuildTool findBuildTool(Path project) {
}
currentPath = currentPath.getParent();
}
log.warnv("Unable to find a build tool in {0} or in any parent.", project.toString());
log.warnv("Unable to find a build tool in {0} or in any parent.", project);
return null;
}

Expand Down Expand Up @@ -105,6 +103,11 @@ public static ApplicationModel enableGradleAppModelForTest(Path projectRoot)
return enableGradleAppModel(projectRoot, "TEST", ENABLE_JAR_PACKAGING, TEST_REQUIRED_TASKS);
}

public static ApplicationModel enableGradleAppModelForProdMode(Path projectRoot)
throws IOException, AppModelResolverException {
return enableGradleAppModel(projectRoot, "NORMAL", List.of());
}

public static ApplicationModel enableGradleAppModel(Path projectRoot, String mode, List<String> jvmArgs, String... tasks)
throws IOException, AppModelResolverException {
if (isGradleProject(projectRoot)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ public TsQuarkusExt setConditionalDeps(TsQuarkusExt... exts) {
return setDescriptorProp(BootstrapConstants.CONDITIONAL_DEPENDENCIES, buf.toString());
}

public TsQuarkusExt setConditionalDevDeps(TsQuarkusExt... exts) {
public TsQuarkusExt setConditionalDevDeps(TsArtifact... artifacts) {
final StringBuilder buf = new StringBuilder();
int i = 0;
buf.append(exts[i++].getRuntime().toString());
while (i < exts.length) {
buf.append(' ').append(exts[i++].getRuntime().toString());
buf.append(artifacts[i++]);
while (i < artifacts.length) {
buf.append(' ').append(artifacts[i++]);
}
return setDescriptorProp(BootstrapConstants.CONDITIONAL_DEV_DEPENDENCIES, buf.toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class ConditionalDependenciesDevModelTestCase extends CollectDependencies
@Override
protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception {
var resolver = super.newAppModelResolver(currentProject);
//resolver.setIncubatingModelResolver(false);
//resolver.setIncubatingModelResolver(true);
return resolver;
}

Expand Down Expand Up @@ -76,8 +76,20 @@ protected void setupDependencies() {
| DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT);
addCollectedDeploymentDep(extF.getDeployment());

final TsQuarkusExt extH = new TsQuarkusExt("ext-h");
install(extH, false);
addCollectedDep(extH.getRuntime(),
DependencyFlags.RUNTIME_CP
| DependencyFlags.DEPLOYMENT_CP
| DependencyFlags.RUNTIME_EXTENSION_ARTIFACT);
addCollectedDeploymentDep(extH.getDeployment());
final TsArtifact devOnlyLib = TsArtifact.jar("dev-only-lib");
devOnlyLib.addDependency(extH);
install(devOnlyLib, false);
addCollectedDep(devOnlyLib, DependencyFlags.RUNTIME_CP | DependencyFlags.DEPLOYMENT_CP);

final TsQuarkusExt extG = new TsQuarkusExt("ext-g");
extG.setConditionalDevDeps(extB);
extG.setConditionalDevDeps(extB.getRuntime(), devOnlyLib);
install(extG, false);
installAsDep(extG.getRuntime(),
DependencyFlags.DIRECT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ protected void setupDependencies() {
addCollectedDeploymentDep(extF.getDeployment());

final TsQuarkusExt extG = new TsQuarkusExt("ext-g");
extG.setConditionalDevDeps(extB);
extG.setConditionalDevDeps(extB.getRuntime());
install(extG, false);
installAsDep(extG.getRuntime(),
DependencyFlags.DIRECT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ protected void setupDependencies() {
install(extE, false);

final TsQuarkusExt extG = new TsQuarkusExt("ext-g");
extG.setConditionalDevDeps(extB, extC, extE);
extG.setConditionalDevDeps(extB.getRuntime(), extC.getRuntime(), extE.getRuntime());
install(extG, false);
installAsDep(extG.getRuntime(),
DependencyFlags.DIRECT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.quarkus.bootstrap.resolver.test;

import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver;
import io.quarkus.bootstrap.resolver.CollectDependenciesBase;
import io.quarkus.bootstrap.resolver.TsQuarkusExt;
import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject;
import io.quarkus.maven.dependency.DependencyFlags;

public class RequiredConditionalDependencyTest extends CollectDependenciesBase {

@Override
protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception {
var resolver = super.newAppModelResolver(currentProject);
//resolver.setIncubatingModelResolver(false);
return resolver;
}

@Override
protected void setupDependencies() {
final TsQuarkusExt extA = new TsQuarkusExt("ext-a");
installAsDep(extA);
final TsQuarkusExt extB = new TsQuarkusExt("ext-b");
extB.setDependencyCondition(extA);
install(extB, false);
addCollectedDep(extB.getRuntime(),
DependencyFlags.RUNTIME_CP
| DependencyFlags.DEPLOYMENT_CP
| DependencyFlags.RUNTIME_EXTENSION_ARTIFACT);
addCollectedDeploymentDep(extB.getDeployment());
final TsQuarkusExt extC = new TsQuarkusExt("ext-c");
extC.setConditionalDeps(extB);
install(extC, false);
addCollectedDep(extC.getRuntime(),
DependencyFlags.RUNTIME_CP
| DependencyFlags.DEPLOYMENT_CP
| DependencyFlags.RUNTIME_EXTENSION_ARTIFACT);
addCollectedDeploymentDep(extC.getDeployment());
final TsQuarkusExt extD = new TsQuarkusExt("ext-d");
extD.addDependency(extC);
install(extD, false);
addCollectedDep(extD.getRuntime(),
DependencyFlags.RUNTIME_CP
| DependencyFlags.DEPLOYMENT_CP
| DependencyFlags.RUNTIME_EXTENSION_ARTIFACT);
addCollectedDeploymentDep(extD.getDeployment());
final TsQuarkusExt extE = new TsQuarkusExt("ext-e");
extE.addDependency(extD);
extE.setDependencyCondition(extA);
install(extE, false);
addCollectedDep(extE.getRuntime(),
DependencyFlags.RUNTIME_CP
| DependencyFlags.DEPLOYMENT_CP
| DependencyFlags.RUNTIME_EXTENSION_ARTIFACT);
addCollectedDeploymentDep(extE.getDeployment());
final TsQuarkusExt extF = new TsQuarkusExt("ext-f");
extF.setConditionalDeps(extE);
installAsDep(extF);
}
}
Loading

0 comments on commit aac0bdf

Please sign in to comment.