diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/DependencyConditionMatchesConditionalDependencyTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/DependencyConditionMatchesConditionalDependencyTest.java index 04435597762f5..51aa5660bdc3a 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/DependencyConditionMatchesConditionalDependencyTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/DependencyConditionMatchesConditionalDependencyTest.java @@ -5,14 +5,23 @@ import java.util.HashMap; import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; +import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; import io.quarkus.maven.dependency.ResolvedDependency; public class DependencyConditionMatchesConditionalDependencyTest extends BootstrapFromOriginalJarTestBase { + @Override + protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception { + var resolver = super.newAppModelResolver(currentProject); + // resolver.setIncubatingModelResolver(true); + return resolver; + } + @Override protected TsArtifact composeApplication() { diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java index c13b3c844e6de..6b20f05b9efee 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java @@ -6,7 +6,6 @@ import java.util.Collections; import java.util.List; import java.util.Properties; -import java.util.function.Supplier; import org.apache.maven.model.DependencyManagement; import org.apache.maven.model.Model; @@ -139,6 +138,10 @@ public TsArtifact addDependency(TsArtifact dep) { return addDependency(new TsDependency(dep)); } + public TsArtifact addDependency(TsArtifact dep, boolean optional) { + return addDependency(new TsDependency(dep, optional)); + } + public TsArtifact addDependency(TsArtifact dep, TsArtifact... excludes) { return addDependency(new TsDependency(dep).exclude(excludes)); } @@ -152,23 +155,23 @@ public TsArtifact addDependency(TsQuarkusExt dep, String scope) { } public TsArtifact addDependency(TsQuarkusExt dep, boolean optional) { - return addDependency(dep, () -> new TsDependency(dep.getRuntime(), optional)); + return addDependency(dep, new TsDependency(dep.getRuntime(), optional)); } public TsArtifact addDependency(TsQuarkusExt dep, String scope, boolean optional) { - return addDependency(dep, () -> new TsDependency(dep.getRuntime(), scope, optional)); + return addDependency(dep, new TsDependency(dep.getRuntime(), scope, optional)); } public TsArtifact addDependency(TsQuarkusExt dep, TsArtifact... excludes) { - return addDependency(dep, () -> new TsDependency(dep.getRuntime(), false).exclude(excludes)); + return addDependency(dep, new TsDependency(dep.getRuntime(), false).exclude(excludes)); } - private TsArtifact addDependency(TsQuarkusExt dep, Supplier dependencyFactory) { + private TsArtifact addDependency(TsQuarkusExt extDep, TsDependency dep) { if (extDeps.isEmpty()) { extDeps = new ArrayList<>(1); } - extDeps.add(dep); - return addDependency(dependencyFactory.get()); + extDeps.add(extDep); + return addDependency(dep); } public TsArtifact addDependency(TsDependency dep) { @@ -179,6 +182,33 @@ public TsArtifact addDependency(TsDependency dep) { return this; } + /** + * Adds a dependency as the first in the list. + * + * @param dep dependency to add + * @return this artifact + */ + public TsArtifact addFirstDependency(TsDependency dep) { + if (deps.isEmpty()) { + deps = new ArrayList<>(); + deps.add(dep); + } else { + deps.add(dep); + Collections.rotate(deps, 1); + } + return this; + } + + /** + * Adds a dependency as the first in the list. + * + * @param dep dependency to add + * @return this artifact + */ + public TsArtifact addFirstDependency(TsArtifact dep) { + return addFirstDependency(new TsDependency(dep)); + } + public TsArtifact addManagedDependency(TsArtifact a) { return addManagedDependency(new TsDependency(a)); } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsQuarkusExt.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsQuarkusExt.java index e5bc50e9a7489..f018ecb455821 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsQuarkusExt.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsQuarkusExt.java @@ -64,6 +64,16 @@ public TsQuarkusExt setDependencyCondition(TsQuarkusExt... exts) { return setDescriptorProp(BootstrapConstants.DEPENDENCY_CONDITION, buf.toString()); } + public TsQuarkusExt setDependencyCondition(TsArtifact... exts) { + final StringBuilder buf = new StringBuilder(); + int i = 0; + buf.append(exts[i++].getKey()); + while (i < exts.length) { + buf.append(' ').append(exts[i++].getKey()); + } + return setDescriptorProp(BootstrapConstants.DEPENDENCY_CONDITION, buf.toString()); + } + public TsQuarkusExt setDescriptorProp(String name, String value) { rtDescr.set(name, value); return this; diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesDevModelTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesDevModelTestCase.java index 20c22683e39fa..3450c3b3f0113 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesDevModelTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesDevModelTestCase.java @@ -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(false); return resolver; } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesDirectDependencyOnTransitiveDeploymentArtifactTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesDirectDependencyOnTransitiveDeploymentArtifactTestCase.java new file mode 100644 index 0000000000000..3ce00cea79e82 --- /dev/null +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesDirectDependencyOnTransitiveDeploymentArtifactTestCase.java @@ -0,0 +1,64 @@ +package io.quarkus.bootstrap.resolver.test; + +import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.CollectDependenciesBase; +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; +import io.quarkus.maven.dependency.DependencyFlags; + +public class ConditionalDependenciesDirectDependencyOnTransitiveDeploymentArtifactTestCase extends CollectDependenciesBase { + + @Override + protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception { + var resolver = super.newAppModelResolver(currentProject); + //resolver.setIncubatingModelResolver(true); + return resolver; + } + + @Override + protected void setupDependencies() { + + final TsQuarkusExt quarkusCore = new TsQuarkusExt("quarkus-core"); + install(quarkusCore); + + TsArtifact nettyNioClient = TsArtifact.jar("netty-nio-client"); + + final TsQuarkusExt nettyClientInternalExt = new TsQuarkusExt("netty-client-internal"); + nettyClientInternalExt.addDependency(quarkusCore); + nettyClientInternalExt.getRuntime().addDependency(nettyNioClient, true); + nettyClientInternalExt.setDependencyCondition(nettyNioClient); + install(nettyClientInternalExt, false); + addCollectedDep(nettyClientInternalExt.getRuntime(), + DependencyFlags.RUNTIME_CP | DependencyFlags.DEPLOYMENT_CP | DependencyFlags.RUNTIME_EXTENSION_ARTIFACT); + addCollectedDeploymentDep(nettyClientInternalExt.getDeployment()); + + final TsQuarkusExt commonExt = new TsQuarkusExt("common"); + commonExt.addDependency(quarkusCore); + commonExt.getRuntime().addDependency(nettyNioClient, true); + commonExt.getRuntime().addDependency(nettyClientInternalExt.getRuntime(), true); + commonExt.getDeployment().addDependency(nettyClientInternalExt.getDeployment(), true); + commonExt.setConditionalDeps(nettyClientInternalExt); + install(commonExt, false); + addCollectedDep(commonExt.getRuntime(), + DependencyFlags.RUNTIME_CP | DependencyFlags.DEPLOYMENT_CP | DependencyFlags.RUNTIME_EXTENSION_ARTIFACT); + addCollectedDeploymentDep(commonExt.getDeployment()); + + final TsQuarkusExt sqsExt = new TsQuarkusExt("sqs"); + sqsExt.addDependency(quarkusCore); + sqsExt.getRuntime().addDependency(commonExt.getRuntime()); + sqsExt.getRuntime().addDependency(nettyNioClient, true); + sqsExt.getDeployment().addFirstDependency(commonExt.getDeployment()); + addCollectedDep(sqsExt.getRuntime(), + DependencyFlags.RUNTIME_CP | DependencyFlags.DEPLOYMENT_CP | DependencyFlags.RUNTIME_EXTENSION_ARTIFACT); + addCollectedDeploymentDep(sqsExt.getDeployment()); + + final TsQuarkusExt messagingSqsExt = new TsQuarkusExt("messaging-sqs"); + messagingSqsExt.addDependency(quarkusCore); + messagingSqsExt.addDependency(sqsExt); + messagingSqsExt.getDeployment().addDependency(commonExt.getDeployment()); // this line breaks it + + installAsDep(messagingSqsExt); + installAsDep(nettyNioClient); + } +} diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesProdModelTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesProdModelTestCase.java index 7a470e4f666cc..31a2161630566 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesProdModelTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesProdModelTestCase.java @@ -12,7 +12,7 @@ public class ConditionalDependenciesProdModelTestCase extends CollectDependencie @Override protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception { var resolver = super.newAppModelResolver(currentProject); - resolver.setIncubatingModelResolver(false); + //resolver.setIncubatingModelResolver(false); return resolver; } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesRuntimeOnlyProdModelTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesRuntimeOnlyProdModelTestCase.java index cf246b9a85f2f..e33307941a83a 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesRuntimeOnlyProdModelTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/ConditionalDependenciesRuntimeOnlyProdModelTestCase.java @@ -14,7 +14,7 @@ public class ConditionalDependenciesRuntimeOnlyProdModelTestCase extends Collect @Override protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception { var resolver = super.newAppModelResolver(currentProject); - resolver.setIncubatingModelResolver(false); + //resolver.setIncubatingModelResolver(false); resolver.setRuntimeModelOnly(runtimeOnly); return resolver; } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/DevModeConditionalDependencyWithExtraConditionTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/DevModeConditionalDependencyWithExtraConditionTestCase.java index 77ecac97bf92f..18312969db978 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/DevModeConditionalDependencyWithExtraConditionTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/test/DevModeConditionalDependencyWithExtraConditionTestCase.java @@ -12,7 +12,7 @@ public class DevModeConditionalDependencyWithExtraConditionTestCase extends Coll @Override protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception { var resolver = super.newAppModelResolver(currentProject); - resolver.setIncubatingModelResolver(false); + //resolver.setIncubatingModelResolver(false); return resolver; } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java index 6505fab69b4cd..5a9420b20a433 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java @@ -78,8 +78,6 @@ public class ApplicationDependencyTreeResolver { // this is a temporary option, to enable the previous way of initializing runtime classpath dependencies private static final boolean CONVERGED_TREE_ONLY = PropertyUtils.getBoolean("quarkus.bootstrap.converged-tree-only", false); - private static final Artifact[] NO_ARTIFACTS = new Artifact[0]; - public static ApplicationDependencyTreeResolver newInstance() { return new ApplicationDependencyTreeResolver(); } @@ -90,7 +88,7 @@ public static Artifact getRuntimeArtifact(DependencyNode dep) { private byte walkingFlags = COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS; private final List topExtensionDeps = new ArrayList<>(); - private ExtensionDependency lastVisitedRuntimeExtNode; + private ExtensionDependency currentTopLevelExtension; private final Map allExtensions = new HashMap<>(); private List conditionalDepsToProcess = new ArrayList<>(); private final Deque> exclusionStack = new ArrayDeque<>(); @@ -433,7 +431,6 @@ private void visitRuntimeDependencies(List list) { private void visitRuntimeDependency(DependencyNode node) { final byte prevWalkingFlags = walkingFlags; - final ExtensionDependency prevLastVisitedRtExtNode = lastVisitedRuntimeExtNode; final boolean popExclusions = !node.getDependency().getExclusions().isEmpty(); if (popExclusions) { @@ -502,7 +499,6 @@ private void visitRuntimeDependency(DependencyNode node) { exclusionStack.pollLast(); } walkingFlags = prevWalkingFlags; - lastVisitedRuntimeExtNode = prevLastVisitedRtExtNode; } private ExtensionDependency getExtensionDependencyOrNull(DependencyNode node, Artifact artifact) @@ -539,11 +535,10 @@ private void visitExtensionDependency(ExtensionDependency extDep) if (isWalkingFlagOn(COLLECT_TOP_EXTENSION_RUNTIME_NODES)) { clearWalkingFlag(COLLECT_TOP_EXTENSION_RUNTIME_NODES); topExtensionDeps.add(extDep); - } - if (lastVisitedRuntimeExtNode != null) { - lastVisitedRuntimeExtNode.addExtensionDependency(extDep); - } - lastVisitedRuntimeExtNode = extDep; + currentTopLevelExtension = extDep; + } else if (currentTopLevelExtension != null) { + currentTopLevelExtension.addExtensionDependency(extDep); + } // else it'd be an unexpected situation } private void collectConditionalDependencies(ExtensionDependency dependent) @@ -622,8 +617,8 @@ private void injectDeploymentDependencies(ExtensionDependency extDep) clearReloadable(deploymentNode); } - final List deploymentDeps = deploymentNode.getChildren(); - if (!replaceDirectDepBranch(extDep, deploymentDeps)) { + extDep.replaceRuntimeExtensionNodes(deploymentNode); + if (!extDep.presentInTargetGraph) { throw new BootstrapDependencyProcessingException( "Quarkus extension deployment artifact " + deploymentNode.getArtifact() + " does not appear to depend on the corresponding runtime artifact " @@ -634,7 +629,7 @@ private void injectDeploymentDependencies(ExtensionDependency extDep) runtimeNode.setData(QUARKUS_RUNTIME_ARTIFACT, runtimeNode.getArtifact()); runtimeNode.setArtifact(deploymentNode.getArtifact()); runtimeNode.getDependency().setArtifact(deploymentNode.getArtifact()); - runtimeNode.setChildren(deploymentDeps); + runtimeNode.setChildren(deploymentNode.getChildren()); } private void clearReloadable(DependencyNode node) { @@ -647,61 +642,6 @@ private void clearReloadable(DependencyNode node) { } } - private boolean replaceDirectDepBranch(ExtensionDependency extDep, List deploymentDeps) - throws BootstrapDependencyProcessingException { - int i = 0; - DependencyNode inserted = null; - while (i < deploymentDeps.size()) { - final Artifact a = deploymentDeps.get(i).getArtifact(); - if (a == null) { - continue; - } - if (isSameKey(extDep.info.runtimeArtifact, a)) { - // we are not comparing the version in the above condition because the runtime version - // may appear to be different then the deployment one and that's ok - // e.g. the version of the runtime artifact could be managed by a BOM - // but overridden by the user in the project config. The way the deployment deps - // are resolved here, the deployment version of the runtime artifact will be the one from the BOM. - inserted = new DefaultDependencyNode(extDep.runtimeNode); - inserted.setChildren(extDep.runtimeNode.getChildren()); - deploymentDeps.set(i, inserted); - break; - } - ++i; - } - if (inserted == null) { - return false; - } - - if (extDep.runtimeExtensionDeps != null) { - for (ExtensionDependency dep : extDep.runtimeExtensionDeps) { - for (DependencyNode deploymentDep : deploymentDeps) { - if (deploymentDep == inserted) { - continue; - } - if (replaceRuntimeBranch(dep, deploymentDep.getChildren())) { - break; - } - } - } - } - - return true; - } - - private boolean replaceRuntimeBranch(ExtensionDependency extNode, List deploymentNodes) - throws BootstrapDependencyProcessingException { - if (replaceDirectDepBranch(extNode, deploymentNodes)) { - return true; - } - for (DependencyNode deploymentNode : deploymentNodes) { - if (replaceRuntimeBranch(extNode, deploymentNode.getChildren())) { - return true; - } - } - return false; - } - private DependencyNode collectDependencies(Artifact artifact, Collection exclusions, List repos) { final CollectRequest request; @@ -797,9 +737,10 @@ static ExtensionDependency get(DependencyNode node) { final DependencyNode runtimeNode; final Collection exclusions; boolean conditionalDepsQueued; - private List runtimeExtensionDeps; + private List extDeps; + private boolean presentInTargetGraph; - ExtensionDependency(ExtensionInfo info, DependencyNode node, Collection exclusions) { + private ExtensionDependency(ExtensionInfo info, DependencyNode node, Collection exclusions) { this.runtimeNode = node; this.info = info; this.exclusions = exclusions; @@ -814,11 +755,47 @@ static ExtensionDependency get(DependencyNode node) { } } - void addExtensionDependency(ExtensionDependency dep) { - if (runtimeExtensionDeps == null) { - runtimeExtensionDeps = new ArrayList<>(); + private void addExtensionDependency(ExtensionDependency dep) { + if (extDeps == null) { + extDeps = new ArrayList<>(); + } + extDeps.add(dep); + } + + private void replaceRuntimeExtensionNodes(DependencyNode deploymentNode) { + var deploymentVisitor = new OrderedDependencyVisitor(deploymentNode); + // skip the root node + deploymentVisitor.next(); + int nodesToReplace = extDeps == null ? 1 : extDeps.size() + 1; + while (deploymentVisitor.hasNext() && nodesToReplace > 0) { + deploymentVisitor.next(); + if (replaceRuntimeNode(deploymentVisitor)) { + --nodesToReplace; + } else if (extDeps != null) { + for (int i = 0; i < extDeps.size(); ++i) { + if (extDeps.get(i).replaceRuntimeNode(deploymentVisitor)) { + --nodesToReplace; + break; + } + } + } + } + } + + private boolean replaceRuntimeNode(OrderedDependencyVisitor depVisitor) { + if (!presentInTargetGraph && isSameKey(runtimeNode.getArtifact(), depVisitor.getCurrent().getArtifact())) { + // we are not comparing the version in the above condition because the runtime version + // may appear to be different from the deployment one and that's ok + // e.g. the version of the runtime artifact could be managed by a BOM + // but overridden by the user in the project config. The way the deployment deps + // are resolved here, the deployment version of the runtime artifact will be the one from the BOM. + var inserted = new DefaultDependencyNode(runtimeNode); + inserted.setChildren(runtimeNode.getChildren()); + depVisitor.replaceCurrent(inserted); + presentInTargetGraph = true; + return true; } - runtimeExtensionDeps.add(dep); + return false; } } @@ -866,6 +843,7 @@ void activate() { } else { currentChildren.addAll(originalNode.getChildren()); } + currentTopLevelExtension = null; visitRuntimeDependency(rtNode); dependent.runtimeNode.getChildren().add(rtNode); } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java index 95e1a93821119..aec70164caa23 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java @@ -71,9 +71,10 @@ public class IncubatingApplicationModelResolver { private static final String INCUBATING_MODEL_RESOLVER = "quarkus.bootstrap.incubating-model-resolver"; /* @formatter:off */ - private static final byte COLLECT_TOP_EXTENSION_RUNTIME_NODES = 0b001; - private static final byte COLLECT_DIRECT_DEPS = 0b010; - private static final byte COLLECT_RELOADABLE_MODULES = 0b100; + private static final byte COLLECT_TOP_EXTENSION_RUNTIME_NODES = 0b0001; + private static final byte COLLECT_DIRECT_DEPS = 0b0010; + private static final byte COLLECT_RELOADABLE_MODULES = 0b0100; + private static final byte COLLECT_DEPLOYMENT_INJECTION_POINTS = 0b1000; /* @formatter:on */ private static final Artifact[] NO_ARTIFACTS = new Artifact[0]; @@ -126,7 +127,7 @@ public static IncubatingApplicationModelResolver newInstance() { private final ExtensionInfo EXT_INFO_NONE = new ExtensionInfo(); - private final List topExtensionDeps = new ArrayList<>(); + private final List deploymentInjectionPoints = new ArrayList<>(); private final Map allExtensions = new ConcurrentHashMap<>(); private Collection conditionalDepsToProcess = new ConcurrentLinkedDeque<>(); @@ -201,11 +202,10 @@ public void resolve(CollectRequest collectRtDepsRequest) throws AppModelResolver DependencyNode root = resolveRuntimeDeps(collectRtDepsRequest); processRuntimeDeps(root); - final List activatedConditionalDeps = activateConditionalDeps(); - + activateConditionalDeps(); // resolve and inject deployment dependency branches for the top (first met) runtime extension nodes if (!runtimeModelOnly) { - injectDeployment(activatedConditionalDeps); + injectDeploymentDeps(); } root = normalize(resolver.getSession(), root); populateModelBuilder(root); @@ -232,11 +232,10 @@ public void resolve(CollectRequest collectRtDepsRequest) throws AppModelResolver } } - private List activateConditionalDeps() { + private void activateConditionalDeps() { if (conditionalDepsToProcess.isEmpty()) { - return List.of(); + return; } - var activatedConditionalDeps = new ArrayList(); boolean checkDependencyConditions = true; while (!conditionalDepsToProcess.isEmpty() && checkDependencyConditions) { checkDependencyConditions = false; @@ -245,7 +244,6 @@ private List activateConditionalDeps() { for (ConditionalDependency cd : unsatisfiedConditionalDeps) { if (cd.isSatisfied()) { cd.activate(); - activatedConditionalDeps.add(cd); // if a dependency was activated, the remaining not satisfied conditions should be checked again checkDependencyConditions = true; } else { @@ -253,7 +251,6 @@ private List activateConditionalDeps() { } } } - return activatedConditionalDeps; } private void populateModelBuilder(DependencyNode root) { @@ -270,43 +267,29 @@ private void populateModelBuilder(DependencyNode root) { } } - private void injectDeployment(List activatedConditionalDeps) { - final ConcurrentLinkedDeque injectQueue = new ConcurrentLinkedDeque<>(); - // non-conditional deployment branches should be added before the activated conditional ones to have consistent - // dependency graph structures - collectDeploymentDeps(injectQueue); - if (!activatedConditionalDeps.isEmpty()) { - collectConditionalDeploymentDeps(activatedConditionalDeps, injectQueue); - } - for (var inject : injectQueue) { - inject.run(); + private void injectDeploymentDeps() { + for (var dep : collectDeploymentDeps()) { + dep.injectDeploymentDependency(); } } - private void collectConditionalDeploymentDeps(List activatedConditionalDeps, - ConcurrentLinkedDeque injectQueue) { + private Collection collectDeploymentDeps() { + final ConcurrentLinkedDeque injectQueue = new ConcurrentLinkedDeque<>(); var taskRunner = new ModelResolutionTaskRunner(); - for (ConditionalDependency cd : activatedConditionalDeps) { - injectDeploymentDep(taskRunner, cd.getExtensionDependency(), injectQueue, true); + for (AppDep extDep : deploymentInjectionPoints) { + injectDeploymentDep(taskRunner, extDep, injectQueue); } taskRunner.waitForCompletion(); + return injectQueue; } - private void collectDeploymentDeps(ConcurrentLinkedDeque injectQueue) { - var taskRunner = new ModelResolutionTaskRunner(); - for (ExtensionDependency extDep : topExtensionDeps) { - injectDeploymentDep(taskRunner, extDep, injectQueue, false); - } - taskRunner.waitForCompletion(); - } - - private void injectDeploymentDep(ModelResolutionTaskRunner taskRunner, ExtensionDependency extDep, - ConcurrentLinkedDeque injectQueue, boolean conditionalDep) { + private void injectDeploymentDep(ModelResolutionTaskRunner taskRunner, AppDep extDep, + ConcurrentLinkedDeque injectQueue) { taskRunner.run(() -> { - var resolvedDep = appBuilder.getDependency(getKey(extDep.info.deploymentArtifact)); + var resolvedDep = appBuilder.getDependency(getKey(extDep.ext.info.deploymentArtifact)); if (resolvedDep == null) { - extDep.collectDeploymentDeps(); - injectQueue.add(() -> extDep.injectDeploymentNode(conditionalDep ? extDep.getParentDeploymentNode() : null)); + extDep.ext.collectDeploymentDeps(); + injectQueue.add(extDep); } else { // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath // in which case we also clear the reloadable flag on it, in case it's coming from the workspace @@ -468,7 +451,7 @@ private boolean isRuntimeArtifact(ArtifactKey key) { private void processRuntimeDeps(DependencyNode root) { final AppDep appRoot = new AppDep(root); - appRoot.walkingFlags = COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS; + appRoot.walkingFlags = COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS | COLLECT_DEPLOYMENT_INJECTION_POINTS; if (collectReloadableModules) { appRoot.walkingFlags |= COLLECT_RELOADABLE_MODULES; } @@ -615,18 +598,8 @@ void setChildFlags() { void setFlags(byte walkingFlags) { + this.walkingFlags = walkingFlags; resolvedDep.addDependencies(allDeps); - if (ext != null) { - var parentExtDep = parent; - while (parentExtDep != null) { - if (parentExtDep.ext != null) { - parentExtDep.ext.addExtensionDependency(ext); - break; - } - parentExtDep = parentExtDep.parent; - } - ext.info.ensureActivated(appBuilder); - } var existingDep = appBuilder.getDependency(resolvedDep.getKey()); if (existingDep == null) { @@ -637,13 +610,29 @@ void setFlags(byte walkingFlags) { } else if (existingDep != resolvedDep) { throw new IllegalStateException(node.getArtifact() + " is already in the model"); } - this.walkingFlags = walkingFlags; resolvedDep.setDirect(isWalkingFlagOn(COLLECT_DIRECT_DEPS)); - if (ext != null && isWalkingFlagOn(COLLECT_TOP_EXTENSION_RUNTIME_NODES)) { - resolvedDep.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); - clearWalkingFlag(COLLECT_TOP_EXTENSION_RUNTIME_NODES); - topExtensionDeps.add(ext); + if (ext != null) { + ext.info.ensureActivated(appBuilder); + if (isWalkingFlagOn(COLLECT_TOP_EXTENSION_RUNTIME_NODES)) { + resolvedDep.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); + clearWalkingFlag(COLLECT_TOP_EXTENSION_RUNTIME_NODES); + } + if (isWalkingFlagOn(COLLECT_DEPLOYMENT_INJECTION_POINTS)) { + clearWalkingFlag(COLLECT_DEPLOYMENT_INJECTION_POINTS); + ext.extDeps = new ArrayList<>(); + deploymentInjectionPoints.add(this); + } else if (!ext.presentInTargetGraph) { + var parentExtDep = parent; + while (parentExtDep != null) { + if (parentExtDep.ext != null && parentExtDep.ext.extDeps != null) { + parentExtDep.ext.addExtensionDependency(ext); + break; + } + parentExtDep = parentExtDep.parent; + } + } + ext.info.ensureActivated(appBuilder); } if (isWalkingFlagOn(COLLECT_RELOADABLE_MODULES)) { if (resolvedDep.getWorkspaceModule() != null @@ -745,6 +734,13 @@ private void collectConditionalDependencies() conditionalDep.conditionalDep.collectConditionalDependencies(); } } + + private void injectDeploymentDependency() { + // if the parent is an extension then add the deployment node as a dependency of the parent's deployment node + // (that would happen when injecting conditional dependencies) + // otherwise, the runtime module is going to be replaced with the deployment node + ext.injectDependencyDependency(parent == null ? null : (parent.ext == null ? null : parent.ext.deploymentNode)); + } } private ExtensionInfo getExtensionInfoOrNull(Artifact artifact, List repos) @@ -848,7 +844,7 @@ static ExtensionDependency get(DependencyNode node) { boolean conditionalDepsQueued; private List extDeps; private DependencyNode deploymentNode; - private DependencyNode parentNode; + private boolean presentInTargetGraph; ExtensionDependency(ExtensionInfo info, DependencyNode node, Collection exclusions) { this.runtimeNode = node; @@ -865,17 +861,6 @@ static ExtensionDependency get(DependencyNode node) { } } - DependencyNode getParentDeploymentNode() { - if (parentNode == null) { - return null; - } - var ext = ExtensionDependency.get(parentNode); - if (ext == null) { - return null; - } - return ext.deploymentNode == null ? ext.parentNode : ext.deploymentNode; - } - void addExtensionDependency(ExtensionDependency dep) { if (extDeps == null) { extDeps = new ArrayList<>(); @@ -894,7 +879,9 @@ private void collectDeploymentDeps() + "or the artifact does not have any dependencies while at least a dependency on the runtime artifact " + info.runtimeArtifact + " is expected"); } - if (!replaceDirectDepBranch(deploymentNode, true)) { + + replaceRuntimeExtensionNodes(deploymentNode); + if (!presentInTargetGraph) { throw new BootstrapDependencyProcessingException( "Quarkus extension deployment artifact " + deploymentNode.getArtifact() + " does not appear to depend on the corresponding runtime artifact " @@ -902,7 +889,7 @@ private void collectDeploymentDeps() } } - private void injectDeploymentNode(DependencyNode parentDeploymentNode) { + private void injectDependencyDependency(DependencyNode parentDeploymentNode) { if (parentDeploymentNode == null) { runtimeNode.setData(QUARKUS_RUNTIME_ARTIFACT, runtimeNode.getArtifact()); runtimeNode.setArtifact(deploymentNode.getArtifact()); @@ -912,59 +899,43 @@ private void injectDeploymentNode(DependencyNode parentDeploymentNode) { } } - private boolean replaceDirectDepBranch(DependencyNode parentNode, boolean replaceRuntimeNode) { - int i = 0; - DependencyNode inserted = null; - var childNodes = parentNode.getChildren(); - while (i < childNodes.size()) { - var node = childNodes.get(i); - final Artifact a = node.getArtifact(); - if (a != null && !hasWinner(node) && isSameKey(info.runtimeArtifact, a)) { - // we are not comparing the version in the above condition because the runtime version - // may appear to be different from the deployment one and that's ok - // e.g. the version of the runtime artifact could be managed by a BOM - // but overridden by the user in the project config. The way the deployment deps - // are resolved here, the deployment version of the runtime artifact will be the one from the BOM. - if (replaceRuntimeNode) { - inserted = new DefaultDependencyNode(runtimeNode); - inserted.setChildren(runtimeNode.getChildren()); - childNodes.set(i, inserted); - } else { - inserted = runtimeNode; - } - if (this.deploymentNode == null && this.parentNode == null) { - this.parentNode = parentNode; - } - break; + void replaceRuntimeExtensionNodes(DependencyNode deploymentNode) { + var deploymentVisitor = new OrderedDependencyVisitor(deploymentNode); + // skip the root node + deploymentVisitor.next(); + int nodesToReplace = extDeps == null ? 1 : extDeps.size() + 1; + while (deploymentVisitor.hasNext() && nodesToReplace > 0) { + var node = deploymentVisitor.next(); + if (hasWinner(node)) { + continue; } - ++i; - } - if (inserted == null) { - return false; - } - - if (extDeps != null) { - var depQueue = new ArrayList<>(childNodes); - var exts = new ArrayList<>(extDeps); - for (int j = 0; j < depQueue.size(); ++j) { - var depNode = depQueue.get(j); - if (hasWinner(depNode)) { - continue; - } - for (int k = 0; k < exts.size(); ++k) { - if (exts.get(k).replaceDirectDepBranch(depNode, replaceRuntimeNode && depNode != inserted)) { - exts.remove(k); + if (replaceRuntimeNode(deploymentVisitor)) { + --nodesToReplace; + } else if (extDeps != null) { + for (int i = 0; i < extDeps.size(); ++i) { + if (extDeps.get(i).replaceRuntimeNode(deploymentVisitor)) { + --nodesToReplace; break; } } - if (exts.isEmpty()) { - break; - } - depQueue.addAll(depNode.getChildren()); } } + } - return true; + private boolean replaceRuntimeNode(OrderedDependencyVisitor depVisitor) { + if (!presentInTargetGraph && isSameKey(runtimeNode.getArtifact(), depVisitor.getCurrent().getArtifact())) { + // we are not comparing the version in the above condition because the runtime version + // may appear to be different from the deployment one and that's ok + // e.g. the version of the runtime artifact could be managed by a BOM + // but overridden by the user in the project config. The way the deployment deps + // are resolved here, the deployment version of the runtime artifact will be the one from the BOM. + var inserted = new DefaultDependencyNode(runtimeNode); + inserted.setChildren(runtimeNode.getChildren()); + depVisitor.replaceCurrent(inserted); + presentInTargetGraph = true; + return true; + } + return false; } } @@ -980,7 +951,6 @@ private ConditionalDependency(ExtensionInfo info, AppDep parent) { rtNode.setVersionConstraint(new BootstrapArtifactVersionConstraint( new BootstrapArtifactVersion(info.runtimeArtifact.getVersion()))); rtNode.setRepositories(parent.ext.runtimeNode.getRepositories()); - conditionalDep = new AppDep(parent, rtNode); conditionalDep.ext = new ExtensionDependency(info, rtNode, parent.ext.exclusions); } @@ -1010,13 +980,15 @@ void activate() { if (collectReloadableModules) { conditionalDep.walkingFlags |= COLLECT_RELOADABLE_MODULES; } + conditionalDep.walkingFlags |= COLLECT_DEPLOYMENT_INJECTION_POINTS; + if (conditionalDep.ext.extDeps == null) { + conditionalDep.ext.extDeps = new ArrayList<>(); + } var taskRunner = new ModelResolutionTaskRunner(); conditionalDep.scheduleRuntimeVisit(taskRunner); taskRunner.waitForCompletion(); conditionalDep.setFlags(conditionalDep.walkingFlags); - if (conditionalDep.parent.resolvedDep == null) { - conditionalDep.parent.allDeps.add(conditionalDep.resolvedDep.getArtifactCoords()); - } else { + if (conditionalDep.parent.resolvedDep != null) { conditionalDep.parent.resolvedDep.addDependency(conditionalDep.resolvedDep.getArtifactCoords()); } conditionalDep.parent.ext.runtimeNode.getChildren().add(rtNode); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/OrderedDependencyVisitor.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/OrderedDependencyVisitor.java new file mode 100644 index 0000000000000..ae23a0f8c98c6 --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/OrderedDependencyVisitor.java @@ -0,0 +1,105 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.NoSuchElementException; + +import org.eclipse.aether.graph.DependencyNode; + +/** + * Walks a dependency tree by visiting dependencies in the order of their priorities + * from the perspective of version conflict resolution. + */ +class OrderedDependencyVisitor { + + private final Deque> stack = new ArrayDeque<>(); + private List currentList; + private int currentIndex = -1; + private int currentDistance; + private int totalOnCurrentDistance = 1; + private int totalOnNextDistance; + + /** + * The root of the dependency tree + * + * @param root the root of the dependency tree + */ + OrderedDependencyVisitor(DependencyNode root) { + currentList = List.of(root); + } + + /** + * Current dependency. + * + * @return current dependency + */ + DependencyNode getCurrent() { + ensureNonNegativeIndex(); + return currentList.get(currentIndex); + } + + /** + * Returns the current distance (depth) from the root to the level on which the current node is. + * + * @return current depth + */ + int getCurrentDistance() { + ensureNonNegativeIndex(); + return currentDistance; + } + + private void ensureNonNegativeIndex() { + if (currentIndex < 0) { + throw new RuntimeException("The visitor has not been positioned on the first dependency node yet"); + } + } + + /** + * Whether there are still not visited dependencies. + * + * @return true if there are still not visited dependencies, otherwise - false + */ + boolean hasNext() { + return !stack.isEmpty() + || currentIndex + 1 < currentList.size() + || !currentList.get(currentIndex).getChildren().isEmpty(); + } + + /** + * Returns the next dependency. + * + * @return the next dependency + */ + DependencyNode next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + if (currentIndex >= 0) { + var children = currentList.get(currentIndex).getChildren(); + if (!children.isEmpty()) { + stack.addLast(children); + totalOnNextDistance += children.size(); + } + if (--totalOnCurrentDistance == 0) { + ++currentDistance; + totalOnCurrentDistance = totalOnNextDistance; + totalOnNextDistance = 0; + } + } + if (++currentIndex == currentList.size()) { + currentList = stack.removeFirst(); + currentIndex = 0; + } + return currentList.get(currentIndex); + } + + /** + * Replaces the current dependency in the tree with the argument. + * + * @param newNode dependency node that should replace the current one in the tree + */ + void replaceCurrent(DependencyNode newNode) { + currentList.set(currentIndex, newNode); + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/test/java/io/quarkus/bootstrap/resolver/maven/OrderedDependencyVisitorTest.java b/independent-projects/bootstrap/maven-resolver/src/test/java/io/quarkus/bootstrap/resolver/maven/OrderedDependencyVisitorTest.java new file mode 100644 index 0000000000000..71e9b730fd3e2 --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/test/java/io/quarkus/bootstrap/resolver/maven/OrderedDependencyVisitorTest.java @@ -0,0 +1,123 @@ +package io.quarkus.bootstrap.resolver.maven; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.graph.DefaultDependencyNode; +import org.eclipse.aether.graph.DependencyNode; +import org.junit.jupiter.api.Test; + +public class OrderedDependencyVisitorTest { + + private static final String ORG_ACME = "org.acme"; + private static final String JAR = "jar"; + private static final String VERSION = "1.0"; + + @Test + public void main() { + + var root = newNode("root"); + + // direct dependencies + var colors = newNode("colors"); + var pets = newNode("pets"); + var trees = newNode("trees"); + root.setChildren(List.of(colors, pets, trees)); + + // colors + var red = newNode("red"); + var green = newNode("green"); + var blue = newNode("blue"); + colors.setChildren(List.of(red, green, blue)); + + // pets + var dog = newNode("dog"); + var cat = newNode("cat"); + pets.setChildren(List.of(dog, cat)); + // pets, puppy + var puppy = newNode("puppy"); + dog.setChildren(List.of(puppy)); + + // trees + var pine = newNode("pine"); + trees.setChildren(List.of(pine)); + + // create a visitor + var visitor = new OrderedDependencyVisitor(root); + + // assertions + assertThat(visitor.hasNext()).isTrue(); + + // distance 0 + assertThat(visitor.next()).isSameAs(root); + assertThat(visitor.getCurrent()).isSameAs(root); + assertThat(visitor.getCurrentDistance()).isEqualTo(0); + assertThat(visitor.hasNext()).isTrue(); + + // distance 1, colors + assertThat(visitor.next()).isSameAs(colors); + assertThat(visitor.getCurrent()).isSameAs(colors); + assertThat(visitor.getCurrentDistance()).isEqualTo(1); + assertThat(visitor.hasNext()).isTrue(); + + // distance 1, pets + assertThat(visitor.next()).isSameAs(pets); + assertThat(visitor.getCurrent()).isSameAs(pets); + assertThat(visitor.getCurrentDistance()).isEqualTo(1); + assertThat(visitor.hasNext()).isTrue(); + + // distance 1, trees + assertThat(visitor.next()).isSameAs(trees); + assertThat(visitor.getCurrent()).isSameAs(trees); + assertThat(visitor.getCurrentDistance()).isEqualTo(1); + assertThat(visitor.hasNext()).isTrue(); + + // distance 2, colors, red + assertThat(visitor.next()).isSameAs(red); + assertThat(visitor.getCurrent()).isSameAs(red); + assertThat(visitor.getCurrentDistance()).isEqualTo(2); + assertThat(visitor.hasNext()).isTrue(); + + // distance 2, colors, green + assertThat(visitor.next()).isSameAs(green); + assertThat(visitor.getCurrent()).isSameAs(green); + assertThat(visitor.getCurrentDistance()).isEqualTo(2); + assertThat(visitor.hasNext()).isTrue(); + + // distance 2, colors, blue + assertThat(visitor.next()).isSameAs(blue); + assertThat(visitor.getCurrent()).isSameAs(blue); + assertThat(visitor.getCurrentDistance()).isEqualTo(2); + assertThat(visitor.hasNext()).isTrue(); + + // distance 2, pets, dog + assertThat(visitor.next()).isSameAs(dog); + assertThat(visitor.getCurrent()).isSameAs(dog); + assertThat(visitor.getCurrentDistance()).isEqualTo(2); + assertThat(visitor.hasNext()).isTrue(); + + // distance 2, pets, cat + assertThat(visitor.next()).isSameAs(cat); + assertThat(visitor.getCurrent()).isSameAs(cat); + assertThat(visitor.getCurrentDistance()).isEqualTo(2); + assertThat(visitor.hasNext()).isTrue(); + + // distance 2, trees, pine + assertThat(visitor.next()).isSameAs(pine); + assertThat(visitor.getCurrent()).isSameAs(pine); + assertThat(visitor.getCurrentDistance()).isEqualTo(2); + assertThat(visitor.hasNext()).isTrue(); + + // distance 3, pets, dog, puppe + assertThat(visitor.next()).isSameAs(puppy); + assertThat(visitor.getCurrent()).isSameAs(puppy); + assertThat(visitor.getCurrentDistance()).isEqualTo(3); + assertThat(visitor.hasNext()).isFalse(); + } + + private static DependencyNode newNode(String artifactId) { + return new DefaultDependencyNode(new DefaultArtifact(ORG_ACME, artifactId, JAR, VERSION)); + } +}