diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index bcf1759c005c2..844727aad0f53 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -42,7 +42,6 @@ import io.quarkus.deployment.pkg.builditem.ProcessInheritIODisabled; import io.quarkus.deployment.pkg.builditem.ProcessInheritIODisabledBuildItem; import io.quarkus.deployment.steps.LocaleProcessor; -import io.quarkus.maven.dependency.GACTV; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.runtime.LocalesBuildTimeConfig; @@ -313,7 +312,7 @@ private void copyJarSourcesToLib(OutputTargetBuildItem outputTargetBuildItem, } for (ResolvedDependency depArtifact : curateOutcomeBuildItem.getApplicationModel().getRuntimeDependencies()) { - if (depArtifact.getType().equals(GACTV.TYPE_JAR)) { + if (depArtifact.isJar()) { for (Path resolvedDep : depArtifact.getResolvedPaths()) { if (!Files.isDirectory(resolvedDep)) { // Do we need to handle transformed classes? diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ArtifactCoords.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ArtifactCoords.java index 58ce084b7293c..1ba8bd4f7edfa 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ArtifactCoords.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ArtifactCoords.java @@ -18,6 +18,10 @@ public interface ArtifactCoords { ArtifactKey getKey(); + default boolean isJar() { + return TYPE_JAR.equals(getType()); + } + default String toGACTVString() { return getGroupId() + ":" + getArtifactId() + ":" + getClassifier() + ":" + getType() + ":" + getVersion(); } @@ -28,7 +32,7 @@ default String toCompactCoords() { if (!getClassifier().isEmpty()) { b.append(getClassifier()).append(':'); } - if (!TYPE_JAR.equals(getType())) { + if (!isJar()) { b.append(getType()).append(':'); } b.append(getVersion()); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java index 129541af447d8..d6b826942f491 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java @@ -8,7 +8,6 @@ import io.quarkus.bootstrap.model.AppModel; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.util.BootstrapUtils; -import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.paths.OpenPathTree; @@ -150,7 +149,7 @@ private Object runInCl(String consumerName, Map params, QuarkusC } private synchronized void processCpElement(ResolvedDependency artifact, Consumer consumer) { - if (!artifact.getType().equals(ArtifactCoords.TYPE_JAR)) { + if (!artifact.isJar()) { //avoid the need for this sort of check in multiple places consumer.accept(ClassPathElement.EMPTY); return; @@ -337,8 +336,7 @@ public QuarkusClassLoader createDeploymentClassLoader() { } } for (ResolvedDependency dependency : appModel.getDependencies()) { - if (dependency.isRuntimeCp() && - dependency.getType().equals(ArtifactCoords.TYPE_JAR) && + if (dependency.isRuntimeCp() && dependency.isJar() && (dependency.isReloadable() && appModel.getReloadableWorkspaceDependencies().contains(dependency.getKey()) || configuredClassLoading.reloadableArtifacts.contains(dependency.getKey()))) { processCpElement(dependency, element -> addCpElement(builder, dependency, element)); @@ -375,8 +373,7 @@ public QuarkusClassLoader createRuntimeClassLoader(ClassLoader base, Map addCpElement(builder, dependency, element)); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/devmode/DependenciesFilter.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/devmode/DependenciesFilter.java index 388c6db0fda70..622d4a5ed3a6a 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/devmode/DependenciesFilter.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/devmode/DependenciesFilter.java @@ -1,10 +1,13 @@ package io.quarkus.bootstrap.devmode; import io.quarkus.bootstrap.model.ApplicationModel; -import io.quarkus.maven.dependency.GACTV; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.ResolvedDependency; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.jboss.logging.Logger; public class DependenciesFilter { @@ -12,30 +15,86 @@ public class DependenciesFilter { private static final Logger log = Logger.getLogger(DependenciesFilter.class); public static List getReloadableModules(ApplicationModel appModel) { - final List reloadable = new ArrayList<>(); - if (appModel.getApplicationModule() != null) { - reloadable.add(appModel.getAppArtifact()); - } + final Map modules = new HashMap<>(); appModel.getDependencies().forEach(d -> { if (d.isReloadable()) { - reloadable.add(d); + modules.put(d.getKey(), new WorkspaceDependencies(d)); } else if (d.isWorkspaceModule()) { //if this project also contains Quarkus extensions we do no want to include these in the discovery //a bit of an edge case, but if you try and include a sample project with your extension you will //run into problems without this final StringBuilder msg = new StringBuilder(); - msg.append("Local Quarkus extension dependency "); - msg.append(d.getGroupId()).append(":").append(d.getArtifactId()).append(":"); - if (!d.getClassifier().isEmpty()) { - msg.append(d.getClassifier()).append(":"); - } - if (!GACTV.TYPE_JAR.equals(d.getType())) { - msg.append(d.getType()).append(":"); - } - msg.append(d.getVersion()).append(" will not be hot-reloadable"); + msg.append("Local Quarkus extension dependency ").append(d.toCompactCoords()) + .append(" will not be hot-reloadable"); log.warn(msg.toString()); } }); - return reloadable; + if (modules.isEmpty()) { + return appModel.getApplicationModule() == null ? List.of() : List.of(appModel.getAppArtifact()); + } + + if (appModel.getApplicationModule() != null) { + modules.put(appModel.getAppArtifact().getKey(), new WorkspaceDependencies(appModel.getAppArtifact())); + } + + // Here we are sorting the reloadable dependencies according to their interdependencies to make sure + // they are compiled in the correct order + + for (WorkspaceDependencies ad : modules.values()) { + for (Dependency dd : ad.artifact.getWorkspaceModule().getDirectDependencies()) { + final WorkspaceDependencies dep = modules.get(dd.getKey()); + if (dep != null) { + ad.addDependency(dep); + } + } + } + + final List sorted = new ArrayList<>(); + int toBeSorted; + do { + toBeSorted = 0; + for (WorkspaceDependencies ad : modules.values()) { + if (ad.sorted) { + continue; + } + if (ad.hasNotSortedDependencies()) { + ++toBeSorted; + continue; + } + ad.sorted = true; + sorted.add(ad.artifact); + } + } while (toBeSorted > 0); + + return sorted; + } + + private static class WorkspaceDependencies { + final ResolvedDependency artifact; + List deps; + boolean sorted; + + WorkspaceDependencies(ResolvedDependency artifact) { + this.artifact = artifact; + } + + boolean hasNotSortedDependencies() { + if (deps == null) { + return false; + } + for (WorkspaceDependencies d : deps) { + if (!d.sorted) { + return true; + } + } + return false; + } + + void addDependency(WorkspaceDependencies dep) { + if (deps == null) { + deps = new ArrayList<>(); + } + deps.add(dep); + } } } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java index 25cd99bc9c554..e3b46676c4180 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java @@ -374,7 +374,8 @@ public WorkspaceModule toWorkspaceModule() { moduleBuilder.setDependencyConstraints(getRawModel().getDependencyManagement() == null ? Collections.emptyList() : toArtifactDependencies(getRawModel().getDependencyManagement().getDependencies())); - moduleBuilder.setDependencies(toArtifactDependencies(getRawModel().getDependencies())); + final Model model = modelBuildingResult == null ? rawModel : modelBuildingResult.getEffectiveModel(); + moduleBuilder.setDependencies(toArtifactDependencies(model.getDependencies())); return this.module = moduleBuilder.build(); } diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java index b1c1c02867614..f8c7e7f4b2d3d 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java @@ -1194,6 +1194,39 @@ public void testThatDependencyInParentIsEvaluated() throws IOException, MavenInv runAndCheck(); } + @Test + public void testModuleCompileOrder() throws IOException, MavenInvocationException { + testDir = initProject("projects/multimodule-parent-dep", "projects/multimodule-compile-order"); + runAndCheck("-Dquarkus.bootstrap.effective-model-builder"); + + assertThat(DevModeTestUtils.getHttpResponse("/app/hello/")).isEqualTo("hello"); + + // modify classes in all the modules and make sure they are compiled in a correct order + File resource = new File(testDir, "level0/src/main/java/org/acme/level0/Level0Service.java"); + filter(resource, Collections.singletonMap("getGreeting()", "getGreeting(String name)")); + filter(resource, Collections.singletonMap("return greeting;", "return greeting + \" \" + name;")); + + resource = new File(testDir, "level1/src/main/java/org/acme/level1/Level1Service.java"); + filter(resource, Collections.singletonMap("getGreetingFromLevel0()", "getGreetingFromLevel0(String name)")); + filter(resource, Collections.singletonMap("level0Service.getGreeting()", "level0Service.getGreeting(name)")); + + resource = new File(testDir, "level2/submodule/src/main/java/org/acme/level2/submodule/Level2Service.java"); + filter(resource, Collections.singletonMap("getGreetingFromLevel1()", "getGreetingFromLevel1(String name)")); + filter(resource, + Collections.singletonMap("level1Service.getGreetingFromLevel0()", "level1Service.getGreetingFromLevel0(name)")); + + resource = new File(testDir, "runner/src/main/java/org/acme/rest/HelloResource.java"); + filter(resource, Collections.singletonMap("level0Service.getGreeting()", + "level0Service.getGreeting(\"world\")")); + filter(resource, Collections.singletonMap("level2Service.getGreetingFromLevel1()", + "level2Service.getGreetingFromLevel1(\"world\")")); + + await() + .pollDelay(300, TimeUnit.MILLISECONDS) + .atMost(1, TimeUnit.MINUTES) + .until(() -> DevModeTestUtils.getHttpResponse("/app/hello").contains("hello world")); + } + @Test public void testThatGenerateCodeGoalIsNotTriggeredIfNotConfigured() throws IOException, MavenInvocationException { testDir = initProject("projects/classic-no-generate");