From b542785c9125a85d64e5ef75c95f6d198c896a7c Mon Sep 17 00:00:00 2001 From: Inaki Villar Date: Mon, 12 Aug 2024 15:33:51 -0700 Subject: [PATCH] Configuration cache compatibility for QuarkusGenerateCode and partial QuarkusBuildTask --- .../build.gradle.kts | 2 +- .../java/io/quarkus/gradle/QuarkusPlugin.java | 133 +++- .../extension/QuarkusPluginExtension.java | 9 + .../tasks/AbstractQuarkusExtension.java | 6 +- .../java/io/quarkus/gradle/tasks/Deploy.java | 2 +- .../io/quarkus/gradle/tasks/ImageTask.java | 2 +- .../tasks/QuarkusApplicationModelTask.java | 614 ++++++++++++++++++ .../io/quarkus/gradle/tasks/QuarkusBuild.java | 12 +- .../tasks/QuarkusBuildCacheableAppParts.java | 2 +- .../tasks/QuarkusBuildDependencies.java | 5 +- .../gradle/tasks/QuarkusBuildTask.java | 84 ++- .../gradle/tasks/QuarkusGenerateCode.java | 44 +- .../tasks/QuarkusPluginExtensionView.java | 312 +++++++++ .../io/quarkus/gradle/tasks/QuarkusRun.java | 2 +- .../tasks/QuarkusShowEffectiveConfig.java | 10 +- .../io/quarkus/gradle/tasks/QuarkusTask.java | 13 +- .../descriptors/DefaultProjectDescriptor.java | 97 +++ .../descriptors/ProjectDescriptor.java | 27 + .../descriptors/ProjectDescriptorBuilder.java | 143 ++++ .../descriptors/QuarkusTaskDescriptor.java | 49 ++ ...ksConfigurationCacheCompatibilityTest.java | 149 +++++ .../configurationcache/main/build.gradle.kts | 31 + .../main/settings.gradle.kts | 1 + .../main/src/main/java/org/acme/Foo.java | 4 + ...ApplicationDeploymentClasspathBuilder.java | 32 +- .../GradleApplicationModelBuilder.java | 4 +- .../quarkus/gradle/tooling/ToolingUtils.java | 16 + docs/src/main/asciidoc/gradle-tooling.adoc | 30 +- .../application-files/build.gradle | 4 +- .../nativeimage/BasicJavaNativeBuildIT.java | 3 + 30 files changed, 1747 insertions(+), 95 deletions(-) create mode 100644 devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java create mode 100644 devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java create mode 100644 devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/DefaultProjectDescriptor.java create mode 100644 devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptor.java create mode 100644 devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptorBuilder.java create mode 100644 devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/QuarkusTaskDescriptor.java create mode 100644 devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/tasks/TasksConfigurationCacheCompatibilityTest.java create mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/build.gradle.kts create mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/settings.gradle.kts create mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/src/main/java/org/acme/Foo.java diff --git a/devtools/gradle/gradle-application-plugin/build.gradle.kts b/devtools/gradle/gradle-application-plugin/build.gradle.kts index 3b14b942c8e01..ba31d1989f3c3 100644 --- a/devtools/gradle/gradle-application-plugin/build.gradle.kts +++ b/devtools/gradle/gradle-application-plugin/build.gradle.kts @@ -5,7 +5,7 @@ plugins { dependencies { implementation(libs.smallrye.config.yaml) implementation("io.quarkus:quarkus-analytics-common") - + compileOnly(libs.kotlin.gradle.plugin.api) testImplementation(libs.quarkus.project.core.extension.codestarts) } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index b7fd4dcbf3802..fa3a5ba77b67b 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -1,7 +1,10 @@ package io.quarkus.gradle; +import static io.quarkus.gradle.tasks.QuarkusGradleUtils.getSourceSet; + import java.io.File; import java.nio.file.Path; +import java.util.Collections; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -24,6 +27,7 @@ import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; @@ -41,13 +45,14 @@ import io.quarkus.gradle.tasks.ImageBuild; import io.quarkus.gradle.tasks.ImagePush; import io.quarkus.gradle.tasks.QuarkusAddExtension; +import io.quarkus.gradle.tasks.QuarkusApplicationModelTask; import io.quarkus.gradle.tasks.QuarkusBuild; import io.quarkus.gradle.tasks.QuarkusBuildCacheableAppParts; import io.quarkus.gradle.tasks.QuarkusBuildDependencies; +import io.quarkus.gradle.tasks.QuarkusBuildTask; import io.quarkus.gradle.tasks.QuarkusDev; import io.quarkus.gradle.tasks.QuarkusGenerateCode; import io.quarkus.gradle.tasks.QuarkusGoOffline; -import io.quarkus.gradle.tasks.QuarkusGradleUtils; import io.quarkus.gradle.tasks.QuarkusInfo; import io.quarkus.gradle.tasks.QuarkusListCategories; import io.quarkus.gradle.tasks.QuarkusListExtensions; @@ -64,6 +69,8 @@ import io.quarkus.gradle.tooling.dependency.DependencyUtils; import io.quarkus.gradle.tooling.dependency.ExtensionDependency; import io.quarkus.gradle.tooling.dependency.ProjectExtensionDependency; +import io.quarkus.gradle.workspace.descriptors.DefaultProjectDescriptor; +import io.quarkus.gradle.workspace.descriptors.ProjectDescriptorBuilder; import io.quarkus.runtime.LaunchMode; public class QuarkusPlugin implements Plugin { @@ -163,39 +170,86 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { .getByName(ApplicationDeploymentClasspathBuilder.getBaseRuntimeConfigName(LaunchMode.DEVELOPMENT))); }); + Provider projectDescriptor = ProjectDescriptorBuilder.buildForApp(project); + ApplicationDeploymentClasspathBuilder normalClasspath = new ApplicationDeploymentClasspathBuilder(project, + LaunchMode.NORMAL); + ApplicationDeploymentClasspathBuilder testClasspath = new ApplicationDeploymentClasspathBuilder(project, + LaunchMode.TEST); + ApplicationDeploymentClasspathBuilder devClasspath = new ApplicationDeploymentClasspathBuilder(project, + LaunchMode.DEVELOPMENT); + + TaskProvider quarkusGenerateTestAppModelTask = tasks.register( + "quarkusGenerateTestAppModel", + QuarkusApplicationModelTask.class, task -> { + configureApplicationModelTask(project, task, projectDescriptor, testClasspath, LaunchMode.TEST, + "quarkus/application-model/quarkus-app-test-model.dat"); + }); + TaskProvider quarkusGenerateDevAppModelTask = tasks.register("quarkusGenerateDevAppModel", + QuarkusApplicationModelTask.class, task -> { + configureApplicationModelTask(project, task, projectDescriptor, devClasspath, LaunchMode.DEVELOPMENT, + "quarkus/application-model/quarkus-app-dev-model.dat"); + }); + + TaskProvider quarkusGenerateAppModelTask = tasks.register("quarkusGenerateAppModel", + QuarkusApplicationModelTask.class, task -> { + configureApplicationModelTask(project, task, projectDescriptor + .map(d -> d.withSourceSetView(Collections.singleton(SourceSet.MAIN_SOURCE_SET_NAME))), + normalClasspath, LaunchMode.NORMAL, + "quarkus/application-model/quarkus-app-model.dat"); + }); + // quarkusGenerateCode TaskProvider quarkusGenerateCode = tasks.register(QUARKUS_GENERATE_CODE_TASK_NAME, QuarkusGenerateCode.class, LaunchMode.NORMAL, SourceSet.MAIN_SOURCE_SET_NAME); - quarkusGenerateCode.configure(task -> configureGenerateCodeTask(task, QuarkusGenerateCode.QUARKUS_GENERATED_SOURCES)); + quarkusGenerateCode.configure(task -> configureGenerateCodeTask(task, quarkusGenerateAppModelTask, + QuarkusGenerateCode.QUARKUS_GENERATED_SOURCES)); // quarkusGenerateCodeDev TaskProvider quarkusGenerateCodeDev = tasks.register(QUARKUS_GENERATE_CODE_DEV_TASK_NAME, QuarkusGenerateCode.class, LaunchMode.DEVELOPMENT, SourceSet.MAIN_SOURCE_SET_NAME); quarkusGenerateCodeDev.configure(task -> { task.dependsOn(quarkusGenerateCode); - configureGenerateCodeTask(task, QuarkusGenerateCode.QUARKUS_GENERATED_SOURCES); + configureGenerateCodeTask(task, quarkusGenerateDevAppModelTask, QuarkusGenerateCode.QUARKUS_GENERATED_SOURCES); }); // quarkusGenerateCodeTests TaskProvider quarkusGenerateCodeTests = tasks.register(QUARKUS_GENERATE_CODE_TESTS_TASK_NAME, QuarkusGenerateCode.class, LaunchMode.TEST, SourceSet.TEST_SOURCE_SET_NAME); quarkusGenerateCodeTests.configure(task -> { task.dependsOn("compileQuarkusTestGeneratedSourcesJava"); - configureGenerateCodeTask(task, QuarkusGenerateCode.QUARKUS_TEST_GENERATED_SOURCES); + configureGenerateCodeTask(task, quarkusGenerateTestAppModelTask, + QuarkusGenerateCode.QUARKUS_TEST_GENERATED_SOURCES); }); + TaskProvider quarkusBuildAppModelTask = tasks.register("quarkusBuildAppModel", + QuarkusApplicationModelTask.class, task -> { + task.dependsOn(tasks.named(JavaPlugin.CLASSES_TASK_NAME)); + configureApplicationModelTask(project, task, projectDescriptor + .map(d -> d.withSourceSetView(Collections.singleton(SourceSet.MAIN_SOURCE_SET_NAME))), + normalClasspath, LaunchMode.NORMAL, + "quarkus/application-model/quarkus-app-model-build.dat"); + }); tasks.register(QUARKUS_SHOW_EFFECTIVE_CONFIG_TASK_NAME, QuarkusShowEffectiveConfig.class, task -> { + configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); task.setDescription("Show effective Quarkus build configuration."); }); TaskProvider quarkusBuildDependencies = tasks.register(QUARKUS_BUILD_DEP_TASK_NAME, QuarkusBuildDependencies.class, - task -> task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true)); + task -> { + configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + + task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); + task.getApplicationModel() + .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + + }); Property cacheLargeArtifacts = quarkusExt.getCacheLargeArtifacts(); TaskProvider quarkusBuildCacheableAppParts = tasks.register( QUARKUS_BUILD_APP_PARTS_TASK_NAME, QuarkusBuildCacheableAppParts.class, task -> { + configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); task.dependsOn(quarkusGenerateCode); task.getOutputs().doNotCacheIf( "Not adding uber-jars, native binaries and mutable-jar package type to Gradle " + @@ -211,6 +265,7 @@ public boolean isSatisfiedBy(Task t) { }); TaskProvider quarkusBuild = tasks.register(QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class, build -> { + configureQuarkusBuildTask(project, quarkusExt, build, quarkusBuildAppModelTask); build.dependsOn(quarkusBuildDependencies, quarkusBuildCacheableAppParts); build.getOutputs().doNotCacheIf( "Only collects and combines the outputs of " + QUARKUS_BUILD_APP_PARTS_TASK_NAME + " and " @@ -224,16 +279,41 @@ public boolean isSatisfiedBy(Task t) { }); }); - tasks.register(IMAGE_BUILD_TASK_NAME, ImageBuild.class, task -> task.finalizedBy(quarkusBuild)); + tasks.register(IMAGE_BUILD_TASK_NAME, ImageBuild.class, task -> { + configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); + task.getApplicationModel() + .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + task.finalizedBy(quarkusBuild); + }); - tasks.register(IMAGE_PUSH_TASK_NAME, ImagePush.class, task -> task.finalizedBy(quarkusBuild)); + tasks.register(IMAGE_PUSH_TASK_NAME, ImagePush.class, task -> { + configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); + task.getApplicationModel() + .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + task.finalizedBy(quarkusBuild); + }); - tasks.register(DEPLOY_TASK_NAME, Deploy.class, task -> task.finalizedBy(quarkusBuild)); + tasks.register(DEPLOY_TASK_NAME, Deploy.class, task -> { + configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); + task.getApplicationModel() + .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + task.finalizedBy(quarkusBuild); + }); TaskProvider quarkusDev = tasks.register(QUARKUS_DEV_TASK_NAME, QuarkusDev.class, devRuntimeDependencies, quarkusExt); TaskProvider quarkusRun = tasks.register(QUARKUS_RUN_TASK_NAME, QuarkusRun.class, - build -> build.dependsOn(quarkusBuild)); + build -> { + configureQuarkusBuildTask(project, quarkusExt, build, quarkusBuildAppModelTask); + build.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); + build.getApplicationModel() + .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + build.dependsOn(quarkusBuild); + + }); TaskProvider quarkusRemoteDev = tasks.register(QUARKUS_REMOTE_DEV_TASK_NAME, QuarkusRemoteDev.class, devRuntimeDependencies, quarkusExt); TaskProvider quarkusTest = tasks.register(QUARKUS_TEST_TASK_NAME, QuarkusTest.class, @@ -413,14 +493,43 @@ public void execute(Task task) { }); } - private static void configureGenerateCodeTask(QuarkusGenerateCode task, String generateSourcesDir) { - SourceSet generatedSources = QuarkusGradleUtils.getSourceSet(task.getProject(), generateSourcesDir); + private static void configureApplicationModelTask(Project project, QuarkusApplicationModelTask task, + Provider projectDescriptor, + ApplicationDeploymentClasspathBuilder classpath, + LaunchMode launchMode, String quarkusModelFile) { + task.getProjectDescriptor().set(projectDescriptor); + task.getLaunchMode().set(launchMode); + task.getOriginalClasspath().setFrom(classpath.getOriginalRuntimeClasspathAsInput()); + task.getAppClasspath().configureFrom(classpath.getRuntimeConfiguration()); + task.getPlatformConfiguration().configureFrom(classpath.getPlatformConfiguration()); + task.getDeploymentClasspath().configureFrom(classpath.getDeploymentConfiguration()); + task.getPlatformImportProperties().set(classpath.getPlatformImports().getPlatformProperties()); + task.getApplicationModel().set( + project.getLayout().getBuildDirectory() + .file(quarkusModelFile)); + + } + + private static void configureQuarkusBuildTask(Project project, QuarkusPluginExtension quarkusExt, QuarkusBuildTask task, + TaskProvider quarkusGenerateAppModelTask) { + task.getApplicationModel().set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + SourceSet mainSourceSet = getSourceSet(project, SourceSet.MAIN_SOURCE_SET_NAME); + task.setCompileClasspath(mainSourceSet.getCompileClasspath().plus(mainSourceSet.getRuntimeClasspath()) + .plus(mainSourceSet.getAnnotationProcessorPath()) + .plus(mainSourceSet.getResources())); + } + + private static void configureGenerateCodeTask(QuarkusGenerateCode task, + TaskProvider applicationModelTaskTaskProvider, String generateSourcesDir) { + SourceSet generatedSources = getSourceSet(task.getProject(), generateSourcesDir); Set sourceSetOutput = generatedSources.getOutput().filter(f -> f.getName().equals(generateSourcesDir)).getFiles(); if (sourceSetOutput.isEmpty()) { throw new GradleException("Failed to configure " + task.getPath() + ": sourceSet " + generateSourcesDir + " has no output"); } - task.getGeneratedOutputDirectory().set(generatedSources.getJava().getClassesDirectory().get().getAsFile()); + task.getApplicationModel() + .set(applicationModelTaskTaskProvider.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + task.getGeneratedOutputDirectory().set(generatedSources.getJava().getClassesDirectory()); } private void createSourceSets(Project project) { diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java index c4c543fee145b..7e45c652dbcf4 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java @@ -16,6 +16,7 @@ import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; import org.gradle.api.file.RegularFile; import org.gradle.api.plugins.JavaPlugin; @@ -165,6 +166,14 @@ public Set resourcesDir() { return getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getResources().getSrcDirs(); } + public static FileCollection combinedOutputSourceDirs(Project project) { + ConfigurableFileCollection classesDirs = project.files(); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + classesDirs.from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs()); + classesDirs.from(sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME).getOutput().getClassesDirs()); + return classesDirs; + } + public Set combinedOutputSourceDirs() { Set sourcesDirs = new LinkedHashSet<>(); SourceSetContainer sourceSets = getSourceSets(); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java index 47cd0e461a9b8..58976d97f65a9 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java @@ -39,7 +39,7 @@ public abstract class AbstractQuarkusExtension { private static final String MANIFEST_SECTIONS_PROPERTY_PREFIX = "quarkus.package.jar.manifest.sections"; private static final String MANIFEST_ATTRIBUTES_PROPERTY_PREFIX = "quarkus.package.jar.manifest.attributes"; - private static final String QUARKUS_PROFILE = "quarkus.profile"; + protected static final String QUARKUS_PROFILE = "quarkus.profile"; protected final Project project; protected final File projectDir; protected final Property finalName; @@ -249,14 +249,14 @@ private void exportCustomManifestProperties(Map properties) { } } - private String toManifestAttributeKey(String key) { + protected static String toManifestAttributeKey(String key) { if (key.contains("\"")) { throw new GradleException("Manifest entry name " + key + " is invalid. \" characters are not allowed."); } return String.format("%s.\"%s\"", MANIFEST_ATTRIBUTES_PROPERTY_PREFIX, key); } - private String toManifestSectionAttributeKey(String section, String key) { + protected static String toManifestSectionAttributeKey(String section, String key) { if (section.contains("\"")) { throw new GradleException("Manifest section name " + section + " is invalid. \" characters are not allowed."); } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java index d5b13e65e4a06..4833c5be53da2 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java @@ -84,7 +84,7 @@ public void setImageBuilder(String imageBuilder) { @Inject public Deploy() { - super("Deploy"); + super("Deploy", false); } @TaskAction diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/ImageTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/ImageTask.java index e946fad553f3e..f4b44c8f9e8dc 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/ImageTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/ImageTask.java @@ -39,7 +39,7 @@ enum Builder { } public ImageTask(String description) { - super(description); + super(description, false); } public Builder builder() { diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java new file mode 100644 index 0000000000000..3fc1afa5533e1 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java @@ -0,0 +1,614 @@ +package io.quarkus.gradle.tasks; + +import static io.quarkus.gradle.tooling.GradleApplicationModelBuilder.clearFlag; +import static io.quarkus.gradle.tooling.GradleApplicationModelBuilder.isFlagOn; +import static io.quarkus.maven.dependency.ArtifactCoords.DEFAULT_CLASSIFIER; +import static java.util.stream.Collectors.toList; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.io.UncheckedIOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.artifacts.ArtifactCollection; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.artifacts.result.DependencyResult; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; +import org.gradle.api.artifacts.result.ResolvedComponentResult; +import org.gradle.api.artifacts.result.ResolvedDependencyResult; +import org.gradle.api.artifacts.type.ArtifactTypeDefinition; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.CompileClasspath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; + +import com.google.common.base.Preconditions; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.model.ApplicationModelBuilder; +import io.quarkus.bootstrap.model.CapabilityContract; +import io.quarkus.bootstrap.model.PlatformImportsImpl; +import io.quarkus.bootstrap.resolver.AppModelResolverException; +import io.quarkus.bootstrap.workspace.ArtifactSources; +import io.quarkus.bootstrap.workspace.DefaultArtifactSources; +import io.quarkus.bootstrap.workspace.DefaultSourceDir; +import io.quarkus.bootstrap.workspace.SourceDir; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.fs.util.ZipUtils; +import io.quarkus.gradle.tooling.ToolingUtils; +import io.quarkus.gradle.workspace.descriptors.DefaultProjectDescriptor; +import io.quarkus.gradle.workspace.descriptors.ProjectDescriptor; +import io.quarkus.gradle.workspace.descriptors.ProjectDescriptor.TaskType; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactDependency; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.GACT; +import io.quarkus.maven.dependency.GACTV; +import io.quarkus.maven.dependency.GAV; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; +import io.quarkus.paths.PathCollection; +import io.quarkus.paths.PathList; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.util.HashUtil; + +public abstract class QuarkusApplicationModelTask extends DefaultTask { + + /* @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; + /* @formatter:on */ + + public static final String QUARKUS_PROJECT_DESCRIPTOR_ARTIFACT_TYPE = "quarkus-project-descriptor"; + + @Internal + public abstract RegularFileProperty getProjectBuildFile(); + + @Inject + public abstract ProjectLayout getLayout(); + + /** + * Used just to track original classpath as an input, since resolving quarkus classpath is kinda expensive, + * and we don't want to do that if task is up-to-date + */ + @CompileClasspath + public abstract ConfigurableFileCollection getOriginalClasspath(); + + @Nested + public abstract QuarkusResolvedClasspath getPlatformConfiguration(); + + @Nested + public abstract QuarkusResolvedClasspath getAppClasspath(); + + @Nested + public abstract QuarkusResolvedClasspath getDeploymentClasspath(); + + @Input + public abstract Property getLaunchMode(); + + @Input + public abstract MapProperty getPlatformImportProperties(); + + /** + * If any project task changes, we will invalidate this task anyway + */ + @Input + public abstract Property getProjectDescriptor(); + + @OutputFile + public abstract RegularFileProperty getApplicationModel(); + + public QuarkusApplicationModelTask() { + getProjectBuildFile().set(getProject().getBuildFile()); + } + + private void collectPlatforms(ResolvedDependencyResult resolvedDependency, + Map> artifactsByCapability, + PlatformImportsImpl platformImports) { + List artifacts = findArtifacts(resolvedDependency, artifactsByCapability); + ModuleVersionIdentifier moduleVersionIdentifier = resolvedDependency.getSelected().getModuleVersion(); + for (QuarkusResolvedArtifact artifact : artifacts) { + if (artifact != null && artifact.file.getName().endsWith(".properties")) { + try { + platformImports.addPlatformProperties(moduleVersionIdentifier.getGroup(), moduleVersionIdentifier.getName(), + null, "properties", moduleVersionIdentifier.getVersion(), artifact.file.toPath()); + } catch (AppModelResolverException e) { + throw new GradleException("Failed to import platform properties " + artifact.file, e); + } + } else if (artifact != null && artifact.file.getName().endsWith(".json")) { + platformImports.addPlatformDescriptor(moduleVersionIdentifier.getGroup(), moduleVersionIdentifier.getName(), + moduleVersionIdentifier.getVersion(), "json", moduleVersionIdentifier.getVersion()); + } + } + } + + @TaskAction + public void execute() throws IOException { + final ResolvedDependencyBuilder appArtifact = getProjectArtifact(); + PlatformImportsImpl platformImports = new PlatformImportsImpl(); + platformImports.setPlatformProperties(getPlatformImportProperties().get()); + Map> artifactsByCapability = getPlatformConfiguration() + .resolvedArtifactsByComponentIdentifier(); + getPlatformConfiguration().getRoot().get().getDependencies().forEach(d -> { + if (d instanceof ResolvedDependencyResult) { + collectPlatforms((ResolvedDependencyResult) d, artifactsByCapability, platformImports); + } + }); + final ApplicationModelBuilder modelBuilder = new ApplicationModelBuilder() + .setAppArtifact(appArtifact) + .setPlatformImports(platformImports) + .addReloadableWorkspaceModule(appArtifact.getKey()); + + collectDependencies(getAppClasspath(), modelBuilder, appArtifact.getWorkspaceModule().mutable()); + collectExtensionDependencies(getDeploymentClasspath(), modelBuilder); + ToolingUtils.serializeAppModel(modelBuilder.build(), getApplicationModel().get().getAsFile().toPath()); + } + + private ResolvedDependencyBuilder getProjectArtifact() { + ModuleVersionIdentifier moduleVersion = getAppClasspath().getRoot().get().getModuleVersion(); + ResolvedDependencyBuilder appArtifact = ResolvedDependencyBuilder.newInstance() + .setGroupId(moduleVersion.getGroup()) + .setArtifactId(moduleVersion.getName()) + .setVersion(moduleVersion.getVersion()); + + WorkspaceModule.Mutable mainModule = WorkspaceModule.builder() + .setModuleId(new GAV(appArtifact.getGroupId(), appArtifact.getArtifactId(), appArtifact.getVersion())) + .setModuleDir(getLayout().getProjectDirectory().getAsFile().toPath()) + .setBuildDir(getLayout().getBuildDirectory().getAsFile().get().toPath()) + .setBuildFile(getProjectBuildFile().getAsFile().get().toPath()); + + ProjectDescriptor projectDescriptor = getProjectDescriptor().get(); + initProjectModule(projectDescriptor, mainModule, ArtifactSources.MAIN, DEFAULT_CLASSIFIER); + if (getLaunchMode().get().isDevOrTest()) { + initProjectModule(projectDescriptor, mainModule, ArtifactSources.TEST, "tests"); + } + final PathList.Builder paths = PathList.builder(); + collectDestinationDirs(mainModule.getMainSources().getSourceDirs(), paths); + collectDestinationDirs(mainModule.getMainSources().getResourceDirs(), paths); + + return appArtifact.setWorkspaceModule(mainModule).setResolvedPaths(paths.build()); + } + + private static void initProjectModule(ProjectDescriptor projectDescriptor, WorkspaceModule.Mutable module, + String sourceSetName, String classifier) { + List sourceDirs = new ArrayList<>(); + List resources = new ArrayList<>(); + Set tasks = projectDescriptor.getTasksForSourceSet(sourceSetName.isEmpty() + ? SourceSet.MAIN_SOURCE_SET_NAME + : sourceSetName.equals("tests") ? SourceSet.TEST_SOURCE_SET_NAME : sourceSetName); + for (String task : tasks) { + TaskType type = projectDescriptor.getTaskType(task); + Path source = Path.of(projectDescriptor.getTaskSource(task)); + Path destDir = Path.of(projectDescriptor.getTaskDestinationDir(task)); + if (type == TaskType.COMPILE) { + sourceDirs.add(new DefaultSourceDir(source, destDir, null, Map.of("compiler", task))); + } else if (type == TaskType.RESOURCES) { + resources.add(new DefaultSourceDir(source, destDir, null)); + } + } + module.addArtifactSources(new DefaultArtifactSources(classifier, sourceDirs, resources)); + } + + private static void collectDestinationDirs(Collection sources, final PathList.Builder paths) { + for (SourceDir src : sources) { + final Path path = src.getOutputDir(); + if (paths.contains(path) || !Files.exists(path)) { + continue; + } + paths.add(path); + } + } + + private static void collectDependencies(QuarkusResolvedClasspath classpath, ApplicationModelBuilder modelBuilder, + WorkspaceModule.Mutable wsModule) { + Map> artifacts = classpath.resolvedArtifactsByComponentIdentifier(); + + Set alreadyCollectedFiles = new HashSet<>(artifacts.size()); + classpath.getRoot().get().getDependencies().forEach(d -> { + if (d instanceof ResolvedDependencyResult) { + byte flags = (byte) (COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS | COLLECT_RELOADABLE_MODULES); + collectDependencies((ResolvedDependencyResult) d, modelBuilder, artifacts, wsModule, alreadyCollectedFiles, + new HashSet<>(), flags); + } + }); + Set fileDependencies = new HashSet<>(classpath.getAllResolvedFiles().getFiles()); + + fileDependencies.removeAll(alreadyCollectedFiles); + fileDependenciesExtractor(modelBuilder, fileDependencies); + } + + private static void fileDependenciesExtractor(ApplicationModelBuilder modelBuilder, Set fileDependencies) { + // detect FS paths that are direct file dependencies and are not part of resolution graph + for (File f : fileDependencies) { + if (!f.exists()) { + continue; + } + // here we are trying to represent a direct FS path dependency + // as an artifact dependency + // SHA1 hash is used to avoid long file names in the lib dir + final String parentPath = f.getParent(); + final String group = HashUtil.sha1(parentPath == null ? f.getName() : parentPath); + String name = f.getName(); + String type = ArtifactCoords.TYPE_JAR; + if (!f.isDirectory()) { + final int dot = f.getName().lastIndexOf('.'); + if (dot > 0) { + name = f.getName().substring(0, dot); + type = f.getName().substring(dot + 1); + } + } + // hash could be a better way to represent the version + final String version = String.valueOf(f.lastModified()); + final ResolvedDependencyBuilder artifactBuilder = ResolvedDependencyBuilder.newInstance() + .setGroupId(group) + .setArtifactId(name) + .setType(type) + .setVersion(version) + .setResolvedPath(f.toPath()) + .setDirect(true) + .setRuntimeCp() + .setDeploymentCp(); + Utils.processQuarkusDependency(artifactBuilder, modelBuilder); + modelBuilder.addDependency(artifactBuilder); + } + } + + private static void collectDependencies( + ResolvedDependencyResult resolvedDependency, + ApplicationModelBuilder modelBuilder, + Map> resolvedArtifacts, + WorkspaceModule.Mutable parentModule, + Set collectedArtifactFiles, + Set processedModules, + byte flags) { + WorkspaceModule.Mutable projectModule = null; + List artifacts = findArtifacts(resolvedDependency, resolvedArtifacts); + if (artifacts.isEmpty()) { + byte finalFlags = flags; + resolvedDependency.getSelected().getDependencies().forEach((Consumer) dependencyResult -> { + if (dependencyResult instanceof ResolvedDependencyResult) { + ModuleVersionIdentifier dependencyId = Preconditions + .checkNotNull(((ResolvedDependencyResult) dependencyResult).getSelected().getModuleVersion()); + if (!processedModules.contains(new GACT(dependencyId.getGroup(), dependencyId.getName()))) { + collectDependencies((ResolvedDependencyResult) dependencyResult, modelBuilder, resolvedArtifacts, + projectModule, + collectedArtifactFiles, + processedModules, finalFlags); + } + } + }); + return; + } + + ModuleVersionIdentifier moduleVersionIdentifier = Preconditions + .checkNotNull(resolvedDependency.getSelected().getModuleVersion()); + + for (QuarkusResolvedArtifact artifact : artifacts) { + String classifier = resolveClassifier(moduleVersionIdentifier, artifact.file); + ArtifactKey artifactKey = new GACT( + moduleVersionIdentifier.getGroup(), + moduleVersionIdentifier.getName(), + classifier, + artifact.type); + if (!isDependency(artifact) || modelBuilder.getDependency(artifactKey) != null) { + continue; + } + + final ArtifactCoords depCoords = new GACTV(artifactKey, moduleVersionIdentifier.getVersion()); + ResolvedDependencyBuilder depBuilder = ResolvedDependencyBuilder.newInstance() + .setCoords(depCoords) + .setRuntimeCp() + .setDeploymentCp(); + if (isFlagOn(flags, COLLECT_DIRECT_DEPS)) { + depBuilder.setDirect(true); + flags = clearFlag(flags, COLLECT_DIRECT_DEPS); + } + if (parentModule != null) { + parentModule.addDependency(new ArtifactDependency(depCoords)); + } + + PathCollection paths = null; + + depBuilder.setResolvedPaths(paths == null ? PathList.of(artifact.file.toPath()) : paths) + .setWorkspaceModule(projectModule); + if (Utils.processQuarkusDependency(depBuilder, modelBuilder)) { + if (isFlagOn(flags, COLLECT_TOP_EXTENSION_RUNTIME_NODES)) { + depBuilder.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); + flags = clearFlag(flags, COLLECT_TOP_EXTENSION_RUNTIME_NODES); + } + flags = clearFlag(flags, COLLECT_RELOADABLE_MODULES); + } + if (!isFlagOn(flags, COLLECT_RELOADABLE_MODULES)) { + depBuilder.clearFlag(DependencyFlags.RELOADABLE); + } + modelBuilder.addDependency(depBuilder); + collectedArtifactFiles.add(artifact.file); + } + + processedModules.add(ArtifactKey.ga(moduleVersionIdentifier.getGroup(), moduleVersionIdentifier.getName())); + for (DependencyResult dependency : resolvedDependency.getSelected().getDependencies()) { + if (dependency instanceof ResolvedDependencyResult) { + ModuleVersionIdentifier dependencyId = Preconditions + .checkNotNull(((ResolvedDependencyResult) dependency).getSelected().getModuleVersion()); + if (!processedModules.contains(new GACT(dependencyId.getGroup(), dependencyId.getName()))) { + collectDependencies((ResolvedDependencyResult) dependency, modelBuilder, resolvedArtifacts, projectModule, + collectedArtifactFiles, + processedModules, flags); + } + } + } + } + + private static boolean isDependency(QuarkusResolvedArtifact a) { + return a.file.getName().endsWith(ArtifactCoords.TYPE_JAR) + || a.file.getName().endsWith(".exe") + || a.file.isDirectory(); + } + + private static void collectExtensionDependencies(QuarkusResolvedClasspath classpath, ApplicationModelBuilder modelBuilder) { + Map> artifacts = classpath.resolvedArtifactsByComponentIdentifier(); + Set alreadyVisited = new HashSet<>(); + classpath.getRoot().get().getDependencies().forEach(d -> { + if (d instanceof ResolvedDependencyResult) { + collectExtensionDependencies((ResolvedDependencyResult) d, modelBuilder, artifacts, alreadyVisited); + } + }); + } + + private static void collectExtensionDependencies( + ResolvedDependencyResult resolvedDependency, + ApplicationModelBuilder modelBuilder, + Map> resolvedArtifacts, + Set alreadyVisited) { + List artifacts = findArtifacts(resolvedDependency, resolvedArtifacts); + if (artifacts.isEmpty()) { + return; + } + + for (QuarkusResolvedArtifact artifact : artifacts) { + ModuleVersionIdentifier moduleVersionIdentifier = Preconditions + .checkNotNull(resolvedDependency.getSelected().getModuleVersion()); + + String classifier = resolveClassifier(moduleVersionIdentifier, artifact.file); + ArtifactKey artifactKey = new GACT(moduleVersionIdentifier.getGroup(), moduleVersionIdentifier.getName(), + classifier, + artifact.type); + if (!alreadyVisited.add(artifactKey)) { + return; + } + + ResolvedDependencyBuilder dep = modelBuilder.getDependency(artifactKey); + if (dep == null) { + ArtifactCoords artifactCoords = new GACTV(artifactKey, moduleVersionIdentifier.getVersion()); + dep = toDependency(artifactCoords, artifact.file); + modelBuilder.addDependency(dep); + } + dep.setDeploymentCp(); + dep.clearFlag(DependencyFlags.RELOADABLE); + + } + resolvedDependency.getSelected().getDependencies().forEach(d -> { + if (d instanceof ResolvedDependencyResult) { + collectExtensionDependencies((ResolvedDependencyResult) d, modelBuilder, resolvedArtifacts, alreadyVisited); + } + }); + } + + private static List findArtifacts( + ResolvedDependencyResult resolvedDependency, + Map> artifacts) { + return artifacts.getOrDefault(resolvedDependency.getSelected().getId(), Collections.emptyList()); + } + + private static String resolveClassifier(ModuleVersionIdentifier moduleVersionIdentifier, File file) { + String artifactIdVersion = moduleVersionIdentifier.getVersion().isEmpty() + || "unspecified".equals(moduleVersionIdentifier.getVersion()) + ? moduleVersionIdentifier.getName() + : moduleVersionIdentifier.getName() + "-" + moduleVersionIdentifier.getVersion(); + if ((file.getName().endsWith(".jar") || file.getName().endsWith(".pom") || file.getName().endsWith(".exe")) + && file.getName().startsWith(artifactIdVersion + "-")) { + int extensionLength = file.getName().endsWith(".exe") ? 4 : 4; + return file.getName().substring(artifactIdVersion.length() + 1, file.getName().length() - extensionLength); + } + return ""; + } + + static ResolvedDependencyBuilder toDependency(ArtifactCoords artifactCoords, File file, int... flags) { + int allFlags = 0; + for (int f : flags) { + allFlags |= f; + } + PathList paths = PathList.of(file.toPath()); + return ResolvedDependencyBuilder.newInstance() + .setCoords(artifactCoords) + .setResolvedPaths(paths) + .setFlags(allFlags); + } + + /** + * See example https://docs.gradle.org/current/samples/sample_tasks_with_dependency_resolution_result_inputs.html, + * to better understand how this works. + */ + public static abstract class QuarkusResolvedClasspath { + + /** + * Internal since we track defined dependencies via {@link QuarkusApplicationModelTask#getOriginalClasspath} + */ + @Internal + public abstract Property getRoot(); + + /** + * Internal since we track defined dependencies via {@link QuarkusApplicationModelTask#getOriginalClasspath} + */ + @Internal + public abstract Property getResolvedArtifactCollection(); + + /** + * TODO: Remove me + */ + @Internal + public abstract ConfigurableFileCollection getProjectDescriptors(); + + private FileCollection getAllResolvedFiles() { + return getResolvedArtifactCollection().get().getArtifactFiles(); + } + + private Map> resolvedArtifactsByComponentIdentifier() { + return getQuarkusResolvedArtifacts().stream() + .collect(Collectors.groupingBy(artifact -> artifact.getId().getComponentIdentifier())); + } + + private List getQuarkusResolvedArtifacts() { + return getResolvedArtifactCollection().get().getArtifacts().stream() + .map(this::toResolvedArtifact) + .collect(toList()); + } + + private QuarkusResolvedArtifact toResolvedArtifact(ResolvedArtifactResult result) { + String type = result.getVariant().getAttributes().getAttribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE); + File file = result.getFile(); + return new QuarkusResolvedArtifact(result.getId(), file, type); + } + + public void configureFrom(Configuration configuration) { + ResolvableDependencies resolvableDependencies = configuration.getIncoming(); + getRoot().set(resolvableDependencies.getResolutionResult().getRootComponent()); + getResolvedArtifactCollection().set(resolvableDependencies.getArtifacts()); + // TODO: Remove me, since we don't apply workspace plugin anymore, so there are no project descriptors + getProjectDescriptors().setFrom(configuration.getIncoming().artifactView(viewConfiguration -> { + // Project descriptors make sense only for projects + viewConfiguration.withVariantReselection(); + viewConfiguration.componentFilter(component -> component instanceof ProjectComponentIdentifier); + viewConfiguration.attributes(attributes -> attributes.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, + QUARKUS_PROJECT_DESCRIPTOR_ARTIFACT_TYPE)); + }).getFiles()); + } + } + + public static class QuarkusResolvedArtifact implements Serializable { + + private static final long serialVersionUID = 1L; + + private final ComponentArtifactIdentifier id; + private final String type; + private final File file; + + public QuarkusResolvedArtifact(ComponentArtifactIdentifier id, File file, String type) { + this.id = id; + this.type = type; + this.file = file; + } + + public ComponentArtifactIdentifier getId() { + return id; + } + + public String getType() { + return type; + } + + public File getFile() { + return file; + } + } + + public static class QuarkusCapability { + + } + + public static class Utils { + + public static boolean processQuarkusDependency(ResolvedDependencyBuilder artifactBuilder, + ApplicationModelBuilder modelBuilder) { + for (Path artifactPath : artifactBuilder.getResolvedPaths()) { + if (!Files.exists(artifactPath) || !artifactBuilder.getType().equals(ArtifactCoords.TYPE_JAR)) { + break; + } + if (Files.isDirectory(artifactPath)) { + return processQuarkusDir(artifactBuilder, artifactPath.resolve(BootstrapConstants.META_INF), modelBuilder); + } else { + try (FileSystem artifactFs = ZipUtils.newFileSystem(artifactPath)) { + return processQuarkusDir(artifactBuilder, artifactFs.getPath(BootstrapConstants.META_INF), + modelBuilder); + } catch (IOException e) { + throw new RuntimeException("Failed to process " + artifactPath, e); + } + } + } + return false; + } + + private static boolean processQuarkusDir(ResolvedDependencyBuilder artifactBuilder, Path quarkusDir, + ApplicationModelBuilder modelBuilder) { + if (!Files.exists(quarkusDir)) { + return false; + } + final Path quarkusDescr = quarkusDir.resolve(BootstrapConstants.DESCRIPTOR_FILE_NAME); + if (!Files.exists(quarkusDescr)) { + return false; + } + final Properties extProps = readDescriptor(quarkusDescr); + if (extProps == null) { + return false; + } + artifactBuilder.setRuntimeExtensionArtifact(); + final String extensionCoords = artifactBuilder.toGACTVString(); + modelBuilder.handleExtensionProperties(extProps, extensionCoords); + + final String providesCapabilities = extProps.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); + if (providesCapabilities != null) { + modelBuilder + .addExtensionCapabilities(CapabilityContract.of(extensionCoords, providesCapabilities, null)); + } + return true; + } + + private static Properties readDescriptor(final Path path) { + final Properties rtProps; + if (!Files.exists(path)) { + // not a platform artifact + return null; + } + rtProps = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(path)) { + rtProps.load(reader); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load extension description " + path, e); + } + return rtProps; + } + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java index b50b43b98225e..d4e7251c8be0c 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java @@ -18,7 +18,6 @@ import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.provider.ListProperty; -import org.gradle.api.provider.MapProperty; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Internal; @@ -41,7 +40,7 @@ public abstract class QuarkusBuild extends QuarkusBuildTask { @Inject public QuarkusBuild() { - super("Builds a Quarkus application."); + super("Builds a Quarkus application.", true); } @SuppressWarnings("unused") @@ -49,16 +48,11 @@ public QuarkusBuild nativeArgs(Action> action) { Map nativeArgsMap = new HashMap<>(); action.execute(nativeArgsMap); for (Map.Entry nativeArg : nativeArgsMap.entrySet()) { - getForcedProperties().put(expandConfigurationKey(nativeArg.getKey()), nativeArg.getValue().toString()); + additionalForcedProperties.put(expandConfigurationKey(nativeArg.getKey()), nativeArg.getValue().toString()); } return this; } - @Internal - public MapProperty getForcedProperties() { - return extension().forcedPropertiesProperty(); - } - @Internal public ListProperty getIgnoredEntries() { return extension().ignoredEntriesProperty(); @@ -209,7 +203,7 @@ private void runnerAndArtifactsInputs(Consumer buildInputs, Path sourceDir @SuppressWarnings("deprecation") // legacy JAR @TaskAction public void finalizeQuarkusBuild() { - if (extension().forcedPropertiesProperty().get().containsKey(QUARKUS_IGNORE_LEGACY_DEPLOY_BUILD)) { + if (getExtensionView().getForcedProperties().get().containsKey(QUARKUS_IGNORE_LEGACY_DEPLOY_BUILD)) { getLogger().info("SKIPPING finalizedBy deploy build"); return; } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildCacheableAppParts.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildCacheableAppParts.java index 2ee110cbc19fc..35d466b5528f8 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildCacheableAppParts.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildCacheableAppParts.java @@ -21,7 +21,7 @@ public abstract class QuarkusBuildCacheableAppParts extends QuarkusBuildTask { @Inject public QuarkusBuildCacheableAppParts() { super("Quarkus application build with the ability to cache the built artifacts, excluding dependencies." + - " Do not use this task directly, use '" + QuarkusPlugin.QUARKUS_BUILD_TASK_NAME + "'"); + " Do not use this task directly, use '" + QuarkusPlugin.QUARKUS_BUILD_TASK_NAME + "'", true); } @SuppressWarnings("deprecation") // legacy JAR diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildDependencies.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildDependencies.java index 1b25b1f95730c..40e811c1b02a2 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildDependencies.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildDependencies.java @@ -42,7 +42,7 @@ public abstract class QuarkusBuildDependencies extends QuarkusBuildTask { @Inject public QuarkusBuildDependencies() { super("Collect dependencies for the Quarkus application to be built. " + - "Do not use this task directly, use '" + QuarkusPlugin.QUARKUS_BUILD_TASK_NAME + "'"); + "Do not use this task directly, use '" + QuarkusPlugin.QUARKUS_BUILD_TASK_NAME + "'", true); } /** @@ -144,7 +144,8 @@ private void jarDependencies(Path libBoot, Path libMain) { } ApplicationModel appModel = resolveAppModelForBuild(); - SmallRyeConfig config = extension().buildEffectiveConfiguration(appModel.getAppArtifact()).getConfig(); + SmallRyeConfig config = getExtensionView().buildEffectiveConfiguration(appModel.getAppArtifact(), new HashMap<>()) + .getConfig(); // see https://quarkus.io/guides/class-loading-reference#configuring-class-loading Set removedArtifacts = config.getOptionalValue(CLASS_LOADING_REMOVED_ARTIFACTS, String.class) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index 8afb1473cf01d..d65757c520bd9 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -11,44 +11,52 @@ import javax.inject.Inject; import org.gradle.api.Action; -import org.gradle.api.GradleException; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileSystemOperations; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.logging.LogLevel; -import org.gradle.api.provider.ListProperty; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.StopExecutionException; +import org.gradle.util.GradleVersion; import org.gradle.workers.WorkQueue; import io.quarkus.bootstrap.model.ApplicationModel; -import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.deployment.pkg.PackageConfig; -import io.quarkus.gradle.QuarkusPlugin; import io.quarkus.gradle.tasks.worker.BuildWorker; -import io.quarkus.maven.dependency.GACTV; +import io.quarkus.gradle.tooling.ToolingUtils; import io.smallrye.config.Expressions; import io.smallrye.config.SmallRyeConfig; /** * Base class for the {@link QuarkusBuildDependencies}, {@link QuarkusBuildCacheableAppParts}, {@link QuarkusBuild} tasks */ -abstract class QuarkusBuildTask extends QuarkusTask { +public abstract class QuarkusBuildTask extends QuarkusTask { private static final String QUARKUS_BUILD_DIR = "quarkus-build"; private static final String QUARKUS_BUILD_GEN_DIR = QUARKUS_BUILD_DIR + "/gen"; private static final String QUARKUS_BUILD_APP_DIR = QUARKUS_BUILD_DIR + "/app"; private static final String QUARKUS_BUILD_DEP_DIR = QUARKUS_BUILD_DIR + "/dep"; static final String QUARKUS_ARTIFACT_PROPERTIES = "quarkus-artifact.properties"; static final String NATIVE_SOURCES = "native-sources"; + private final QuarkusPluginExtensionView extensionView; + protected final Map additionalForcedProperties = new HashMap<>(); - private final GACTV gactv; - - QuarkusBuildTask(String description) { - super(description); + QuarkusBuildTask(String description, boolean compatible) { + super(description, compatible); + this.extensionView = getProject().getObjects().newInstance(QuarkusPluginExtensionView.class, extension()); + } - gactv = new GACTV(getProject().getGroup().toString(), getProject().getName(), - getProject().getVersion().toString()); + /** + * Returns a view of the Quarkus extension that is compatible with the configuration cache. + */ + @Nested + protected QuarkusPluginExtensionView getExtensionView() { + return extensionView; } @Inject @@ -56,29 +64,34 @@ abstract class QuarkusBuildTask extends QuarkusTask { @Classpath public FileCollection getClasspath() { - return extension().classpath(); + return classpath; + } + + private FileCollection classpath = getProject().getObjects().fileCollection(); + + public void setCompileClasspath(FileCollection compileClasspath) { + this.classpath = compileClasspath; } @Input public Map getCachingRelevantInput() { - ListProperty vars = extension().getCachingRelevantProperties(); - return extension().baseConfig().cachingRelevantProperties(vars.get()); + return getExtensionView().getCachingRelevantInput().get(); } PackageConfig.JarConfig.JarType jarType() { - return extension().baseConfig().jarType(); + return getExtensionView().getJarType().get(); } boolean jarEnabled() { - return extension().baseConfig().packageConfig().jar().enabled(); + return getExtensionView().getJarEnabled().get(); } boolean nativeEnabled() { - return extension().baseConfig().nativeConfig().enabled(); + return getExtensionView().getNativeEnabled().get(); } boolean nativeSourcesOnly() { - return extension().baseConfig().nativeConfig().sourcesOnly(); + return getExtensionView().getNativeSourcesOnly().get(); } Path gradleBuildDir() { @@ -143,28 +156,28 @@ String nativeImageSourceJarDirName() { } String runnerBaseName() { - BaseConfig baseConfig = extension().baseConfig(); - return baseConfig.packageConfig().outputName().orElseGet(() -> extension().finalName()); + return getExtensionView().getRunnerName().get(); } String outputDirectory() { - BaseConfig baseConfig = extension().baseConfig(); - return baseConfig.packageConfig().outputDirectory().map(Path::toString).orElse(QuarkusPlugin.DEFAULT_OUTPUT_DIRECTORY); + return getExtensionView().getOutputDirectory().get().toString(); } private String runnerSuffix() { - BaseConfig baseConfig = extension().baseConfig(); - return baseConfig.packageConfig().computedRunnerSuffix(); + return getExtensionView().getRunnerSuffix().get(); + } + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract RegularFileProperty getApplicationModel(); + ApplicationModel resolveAppModelForBuild() { - ApplicationModel appModel; try { - appModel = extension().getAppModelResolver().resolveModel(gactv); - } catch (AppModelResolverException e) { - throw new GradleException("Failed to resolve Quarkus application model for " + getPath(), e); + return ToolingUtils.deserializeAppModel(getApplicationModel().get().getAsFile().toPath()); + } catch (IOException e) { + throw new RuntimeException(e); } - return appModel; } /** @@ -225,7 +238,8 @@ void generateBuild() { }); ApplicationModel appModel = resolveAppModelForBuild(); - SmallRyeConfig config = extension().buildEffectiveConfiguration(appModel.getAppArtifact()).getConfig(); + SmallRyeConfig config = getExtensionView() + .buildEffectiveConfiguration(appModel.getAppArtifact(), additionalForcedProperties).getConfig(); Map quarkusProperties = Expressions.withoutExpansion(() -> { Map values = new HashMap<>(); for (String key : config.getMapKeys("quarkus").values()) { @@ -252,15 +266,15 @@ void generateBuild() { .collect(Collectors.joining("\n ", "\n ", ""))); } - WorkQueue workQueue = workQueue(quarkusProperties, () -> extension().buildForkOptions); + WorkQueue workQueue = workQueue(quarkusProperties, getExtensionView().getCodeGenForkOptions().get()); workQueue.submit(BuildWorker.class, params -> { params.getBuildSystemProperties() - .putAll(extension().buildSystemProperties(appModel.getAppArtifact(), quarkusProperties)); - params.getBaseName().set(extension().finalName()); + .putAll(getExtensionView().buildSystemProperties(appModel.getAppArtifact(), quarkusProperties)); + params.getBaseName().set(getExtensionView().getFinalName()); params.getTargetDirectory().set(buildDir.toFile()); params.getAppModel().set(appModel); - params.getGradleVersion().set(getProject().getGradle().getGradleVersion()); + params.getGradleVersion().set(GradleVersion.current().getVersion()); }); workQueue.await(); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java index f3b5a930bb51a..a86e1b4e794f9 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java @@ -1,9 +1,11 @@ package io.quarkus.gradle.tasks; import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -15,19 +17,23 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; -import org.gradle.api.provider.ListProperty; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.CompileClasspath; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.TaskAction; +import org.gradle.util.GradleVersion; import org.gradle.workers.WorkQueue; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.gradle.tasks.worker.CodeGenWorker; +import io.quarkus.gradle.tooling.ToolingUtils; import io.quarkus.runtime.LaunchMode; @CacheableTask @@ -45,11 +51,20 @@ public abstract class QuarkusGenerateCode extends QuarkusTask { private final LaunchMode launchMode; private final String inputSourceSetName; + private final QuarkusPluginExtensionView extensionView; + @Inject public QuarkusGenerateCode(LaunchMode launchMode, String inputSourceSetName) { - super("Performs Quarkus pre-build preparations, such as sources generation"); + super("Performs Quarkus pre-build preparations, such as sources generation", true); this.launchMode = launchMode; this.inputSourceSetName = inputSourceSetName; + this.extensionView = getProject().getObjects().newInstance(QuarkusPluginExtensionView.class, extension()); + + } + + @Nested + protected QuarkusPluginExtensionView getExtensionView() { + return extensionView; } /** @@ -66,12 +81,6 @@ public void setCompileClasspath(Configuration compileClasspath) { this.compileClasspath = compileClasspath; } - @Input - public Map getCachingRelevantInput() { - ListProperty vars = extension().getCachingRelevantProperties(); - return extension().baseConfig().cachingRelevantProperties(vars.get()); - } - @Input Map getInternalTaskConfig() { // Necessary to distinguish the different `quarkusGenerateCode*` tasks, because the task path is _not_ @@ -99,24 +108,29 @@ public Set getInputDirectory() { return inputDirectories; } + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract RegularFileProperty getApplicationModel(); + @OutputDirectory public abstract DirectoryProperty getGeneratedOutputDirectory(); @TaskAction - public void generateCode() { - ApplicationModel appModel = extension().getApplicationModel(launchMode); - Map values = extension().buildEffectiveConfiguration(appModel.getAppArtifact()).getValues(); + public void generateCode() throws IOException { + ApplicationModel appModel = ToolingUtils.deserializeAppModel(getApplicationModel().get().getAsFile().toPath()); + Map configMap = getExtensionView() + .buildEffectiveConfiguration(appModel.getAppArtifact(), new HashMap<>()).getValues(); File outputPath = getGeneratedOutputDirectory().get().getAsFile(); getLogger().debug("Will trigger preparing sources for source directories: {} buildDir: {}", sourcesDirectories, buildDir.getAbsolutePath()); - WorkQueue workQueue = workQueue(values, () -> extension().codeGenForkOptions); + WorkQueue workQueue = workQueue(configMap, getExtensionView().getCodeGenForkOptions().get()); workQueue.submit(CodeGenWorker.class, params -> { - params.getBuildSystemProperties().putAll(values); - params.getBaseName().set(extension().finalName()); + params.getBuildSystemProperties().putAll(configMap); + params.getBaseName().set(getExtensionView().getFinalName()); params.getTargetDirectory().set(buildDir); params.getAppModel().set(appModel); params @@ -124,7 +138,7 @@ public void generateCode() { .setFrom(sourcesDirectories.stream().map(Path::toFile).collect(Collectors.toList())); params.getOutputPath().set(outputPath); params.getLaunchMode().set(launchMode); - params.getGradleVersion().set(getProject().getGradle().getGradleVersion()); + params.getGradleVersion().set(GradleVersion.current().getVersion()); }); workQueue.await(); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java new file mode 100644 index 0000000000000..f4a8e87aba3d9 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java @@ -0,0 +1,312 @@ +package io.quarkus.gradle.tasks; + +import static io.quarkus.gradle.QuarkusPlugin.BUILD_NATIVE_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.TEST_NATIVE_TASK_NAME; +import static io.quarkus.gradle.tasks.AbstractQuarkusExtension.QUARKUS_PROFILE; +import static io.quarkus.gradle.tasks.AbstractQuarkusExtension.toManifestAttributeKey; +import static io.quarkus.gradle.tasks.AbstractQuarkusExtension.toManifestSectionAttributeKey; +import static io.smallrye.common.expression.Expression.Flag.DOUBLE_COLON; +import static io.smallrye.common.expression.Expression.Flag.LENIENT_SYNTAX; +import static io.smallrye.common.expression.Expression.Flag.NO_SMART_BRACES; +import static io.smallrye.common.expression.Expression.Flag.NO_TRIM; +import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME; + +import java.io.File; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.java.archives.Attributes; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.process.JavaForkOptions; +import org.gradle.util.GradleVersion; + +import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.gradle.QuarkusPlugin; +import io.quarkus.gradle.dsl.Manifest; +import io.quarkus.gradle.extension.QuarkusPluginExtension; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.smallrye.common.expression.Expression; + +/** + * Configuration cache compatible view of Quarkus extension + */ +public abstract class QuarkusPluginExtensionView { + + @Inject + public QuarkusPluginExtensionView(Project project, QuarkusPluginExtension extension) { + project.getGradle().getTaskGraph().whenReady(taskGraph -> { + if (taskGraph.hasTask(project.getPath() + BUILD_NATIVE_TASK_NAME) + || taskGraph.hasTask(project.getPath() + TEST_NATIVE_TASK_NAME)) { + getNativeBuild().set(true); + } else { + getNativeBuild().set(false); + } + }); + getCacheLargeArtifacts().set(extension.getCacheLargeArtifacts()); + getCleanupBuildOutput().set(extension.getCleanupBuildOutput()); + getFinalName().set(extension.getFinalName()); + getCodeGenForkOptions().set(getProviderFactory().provider(() -> extension.codeGenForkOptions)); + getIgnoredEntries().set(extension.ignoredEntriesProperty()); + getMainResources().setFrom(project.getExtensions().getByType(SourceSetContainer.class).getByName(MAIN_SOURCE_SET_NAME) + .getResources().getSourceDirectories()); + getQuarkusBuildProperties().set(extension.getQuarkusBuildProperties()); + getQuarkusRelevantProjectProperties().set(getQuarkusRelevantProjectProperties(project)); + getQuarkusProfileSystemVariable().set(getProviderFactory().systemProperty(QUARKUS_PROFILE)); + getQuarkusProfileEnvVariable().set(getProviderFactory().environmentVariable("QUARKUS_PROFILE")); + getCachingRelevantInput() + .set(extension.baseConfig().cachingRelevantProperties(extension.getCachingRelevantProperties().get())); + getForcedProperties().set(extension.forcedPropertiesProperty()); + Map projectProperties = new HashMap<>(); + for (Map.Entry entry : project.getProperties().entrySet()) { + if ((entry.getKey().startsWith("quarkus.") || entry.getKey().startsWith("platform.quarkus."))) { + projectProperties.put(entry.getKey(), entry.getValue()); + } + } + getProjectProperties().set(projectProperties); + getJarEnabled().set(extension.baseConfig().packageConfig().jar().enabled()); + getManifestAttributes().set(extension.manifest().getAttributes()); + getManifestSections().set(extension.manifest().getSections()); + getNativeEnabled().set(extension.baseConfig().nativeConfig().enabled()); + getNativeSourcesOnly().set(extension.baseConfig().nativeConfig().sourcesOnly()); + getRunnerSuffix().set(extension.baseConfig().packageConfig().computedRunnerSuffix()); + getRunnerName().set(extension.baseConfig().packageConfig().outputName().orElseGet(extension::finalName)); + getOutputDirectory().set(Path.of(extension.baseConfig().packageConfig().outputDirectory().map(Path::toString) + .orElse(QuarkusPlugin.DEFAULT_OUTPUT_DIRECTORY))); + getJarType().set(extension.baseConfig().jarType()); + } + + private Provider> getQuarkusRelevantProjectProperties(Project project) { + if (GradleVersion.current().compareTo(GradleVersion.version("8.0")) >= 0) { + // This is more efficient, i.e.: configuration cache is invalidated only when quarkus properties change + return getProviderFactory().gradlePropertiesPrefixedBy("quarkus."); + } else { + return getProviderFactory().provider(() -> project.getProperties().entrySet().stream() + .filter(e -> e.getValue() != null) + .map(e -> Map.entry(e.getKey(), e.getValue().toString())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + } + } + + @Inject + public abstract ProviderFactory getProviderFactory(); + + @Input + @Optional + public abstract Property getNativeBuild(); + + @Input + public abstract Property getCacheLargeArtifacts(); + + @Input + public abstract Property getCleanupBuildOutput(); + + @Input + public abstract Property getFinalName(); + + @Input + public abstract MapProperty getProjectProperties(); + + @Nested + public abstract ListProperty> getCodeGenForkOptions(); + + @Input + @Optional + public abstract Property getJarEnabled(); + + @Input + @Optional + public abstract Property getNativeEnabled(); + + @Input + @Optional + public abstract Property getManifest(); + + @Input + @Optional + public abstract Property getNativeSourcesOnly(); + + @Input + public abstract ListProperty getIgnoredEntries(); + + @Input + public abstract MapProperty getQuarkusBuildProperties(); + + @Input + public abstract MapProperty getQuarkusRelevantProjectProperties(); + + @Internal + public abstract ConfigurableFileCollection getMainResources(); + + @Internal + public abstract Property getRunnerSuffix(); + + @Internal + public abstract Property getRunnerName(); + + @Internal + public abstract Property getOutputDirectory(); + + @Input + public abstract Property getJarType(); + + @Input + @Optional + public abstract Property getQuarkusProfileSystemVariable(); + + @Input + @Optional + public abstract Property getQuarkusProfileEnvVariable(); + + @Input + @Optional + public abstract MapProperty getCachingRelevantInput(); + + @Input + @Optional + public abstract MapProperty getForcedProperties(); + + @Input + @Optional + public abstract MapProperty getManifestAttributes(); + + @Input + @Optional + public abstract MapProperty getManifestSections(); + + private void exportCustomManifestProperties(Map properties) { + for (Map.Entry attribute : getManifestAttributes().get().entrySet()) { + properties.put(toManifestAttributeKey(attribute.getKey()), + attribute.getValue()); + } + + for (Map.Entry section : getManifestSections().get().entrySet()) { + for (Map.Entry attribute : section.getValue().entrySet()) { + properties + .put(toManifestSectionAttributeKey(section.getKey(), attribute.getKey()), attribute.getValue()); + } + } + } + + protected EffectiveConfig buildEffectiveConfiguration(ResolvedDependency appArtifact, + Map additionalForcedProperties) { + Map properties = new HashMap<>(); + exportCustomManifestProperties(properties); + + Map defaultProperties = new HashMap<>(); + String userIgnoredEntries = String.join(",", getIgnoredEntries().get()); + if (!userIgnoredEntries.isEmpty()) { + defaultProperties.put("quarkus.package.jar.user-configured-ignored-entries", userIgnoredEntries); + } + Set resourcesDirs = getMainResources().getFiles(); + defaultProperties.putIfAbsent("quarkus.application.name", appArtifact.getArtifactId()); + defaultProperties.putIfAbsent("quarkus.application.version", appArtifact.getVersion()); + + Map forced = new HashMap<>(getForcedProperties().get()); + getProjectProperties().get().forEach((k, v) -> { + forced.put(k, v.toString()); + + }); + additionalForcedProperties.forEach((k, v) -> { + forced.put(k, v.toString()); + }); + if (getNativeBuild().get()) { + forced.put("quarkus.native.enabled", "true"); + } + return EffectiveConfig.builder() + .withForcedProperties(forced) + .withTaskProperties(properties) + .withBuildProperties(getQuarkusBuildProperties().get()) + .withProjectProperties(getQuarkusRelevantProjectProperties().get()) + .withDefaultProperties(defaultProperties) + .withSourceDirectories(resourcesDirs) + .withProfile(getQuarkusProfile()) + .build(); + } + + protected Map buildSystemProperties(ResolvedDependency appArtifact, Map quarkusProperties) { + Map buildSystemProperties = new HashMap<>(); + buildSystemProperties.putIfAbsent("quarkus.application.name", appArtifact.getArtifactId()); + buildSystemProperties.putIfAbsent("quarkus.application.version", appArtifact.getVersion()); + + for (Map.Entry entry : getForcedProperties().get().entrySet()) { + if (entry.getKey().startsWith("quarkus.") || entry.getKey().startsWith("platform.quarkus.")) { + buildSystemProperties.put(entry.getKey(), entry.getValue()); + } + } + for (Map.Entry entry : getQuarkusBuildProperties().get().entrySet()) { + if (entry.getKey().startsWith("quarkus.") || entry.getKey().startsWith("platform.quarkus.")) { + buildSystemProperties.put(entry.getKey(), entry.getValue()); + } + } + for (Map.Entry entry : getProjectProperties().get().entrySet()) { + if ((entry.getKey().startsWith("quarkus.") || entry.getKey().startsWith("platform.quarkus.")) + && entry.getValue() != null) { + buildSystemProperties.put(entry.getKey(), entry.getValue().toString()); + } + } + + Set quarkusValues = new HashSet<>(); + quarkusValues.addAll(quarkusProperties.values()); + quarkusValues.addAll(buildSystemProperties.values()); + + for (String value : quarkusValues) { + Expression expression = Expression.compile(value, LENIENT_SYNTAX, NO_TRIM, NO_SMART_BRACES, DOUBLE_COLON); + for (String reference : expression.getReferencedStrings()) { + String expanded = getForcedProperties().get().get(reference); + if (expanded != null) { + buildSystemProperties.put(reference, expanded); + continue; + } + + expanded = getQuarkusBuildProperties().get().get(reference); + if (expanded != null) { + buildSystemProperties.put(reference, expanded); + continue; + } + expanded = (String) getProjectProperties().get().get(reference); + if (expanded != null) { + buildSystemProperties.put(reference, expanded); + } + } + } + return buildSystemProperties; + } + + private String getQuarkusProfile() { + String profile = getQuarkusProfileSystemVariable().getOrNull(); + if (profile == null) { + profile = getQuarkusProfileEnvVariable().getOrNull(); + } + if (profile == null) { + profile = getQuarkusBuildProperties().get().get(QUARKUS_PROFILE); + } + if (profile == null) { + Object p = getQuarkusRelevantProjectProperties().get().get(QUARKUS_PROFILE); + if (p != null) { + profile = p.toString(); + } + } + if (profile == null) { + profile = "prod"; + } + return profile; + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java index e4434f5474a1b..e65e7e8c2759b 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java @@ -48,7 +48,7 @@ public QuarkusRun() { } public QuarkusRun(String description) { - super(description); + super(description, false); final ObjectFactory objectFactory = getProject().getObjects(); mainSourceSet = getProject().getExtensions().getByType(SourceSetContainer.class) .getByName(SourceSet.MAIN_SOURCE_SET_NAME); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java index 7cf99588fa118..adc4479990c7d 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java @@ -22,6 +22,7 @@ import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; +import io.quarkus.bootstrap.model.ApplicationModel; import io.smallrye.config.SmallRyeConfig; /** @@ -33,7 +34,7 @@ public abstract class QuarkusShowEffectiveConfig extends QuarkusBuildTask { @Inject public QuarkusShowEffectiveConfig() { - super("Collect dependencies for the Quarkus application, prefer the 'quarkusBuild' task"); + super("Collect dependencies for the Quarkus application, prefer the 'quarkusBuild' task", true); this.saveConfigProperties = getProject().getObjects().property(Boolean.class).convention(Boolean.FALSE); } @@ -46,8 +47,9 @@ public Property getSaveConfigProperties() { @TaskAction public void dumpEffectiveConfiguration() { try { - EffectiveConfig effectiveConfig = extension() - .buildEffectiveConfiguration(extension().getApplicationModel().getAppArtifact()); + ApplicationModel appModel = resolveAppModelForBuild(); + EffectiveConfig effectiveConfig = getExtensionView() + .buildEffectiveConfiguration(appModel.getAppArtifact(), additionalForcedProperties); SmallRyeConfig config = effectiveConfig.getConfig(); List sourceNames = new ArrayList<>(); config.getConfigSources().forEach(configSource -> sourceNames.add(configSource.getName())); @@ -64,7 +66,7 @@ public void dumpEffectiveConfiguration() { .collect(Collectors.joining("\n ", "\n ", "\n")); getLogger().lifecycle("Effective Quarkus configuration options: {}", quarkusConfig); - String finalName = extension().finalName(); + String finalName = getExtensionView().getFinalName().get(); String jarType = config.getOptionalValue("quarkus.package.jar.type", String.class).orElse("fast-jar"); File fastJar = fastJar(); getLogger().lifecycle(""" diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java index e8dade749dc72..23068daa0d43d 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java @@ -5,7 +5,6 @@ import java.nio.file.Paths; import java.util.List; import java.util.Map; -import java.util.function.Supplier; import javax.inject.Inject; @@ -27,6 +26,10 @@ public abstract class QuarkusTask extends DefaultTask { protected final File buildDir; QuarkusTask(String description) { + this(description, false); + } + + QuarkusTask(String description, boolean configurationCacheCompatible) { setDescription(description); setGroup("quarkus"); this.extension = getProject().getExtensions().findByType(QuarkusPluginExtension.class); @@ -35,7 +38,9 @@ public abstract class QuarkusTask extends DefaultTask { // Calling this method tells Gradle that it should not fail the build. Side effect is that the configuration // cache will be at least degraded, but the build will not fail. - notCompatibleWithConfigurationCache("The Quarkus Plugin isn't compatible with the configuration cache"); + if (!configurationCacheCompatible) { + notCompatibleWithConfigurationCache("The Quarkus Plugin isn't compatible with the configuration cache"); + } } @Inject @@ -45,7 +50,7 @@ QuarkusPluginExtension extension() { return extension; } - WorkQueue workQueue(Map configMap, Supplier>> forkOptionsActions) { + WorkQueue workQueue(Map configMap, List> forkOptionsSupplier) { WorkerExecutor workerExecutor = getWorkerExecutor(); // Use process isolation by default, unless Gradle's started with its debugging system property or the @@ -55,7 +60,7 @@ WorkQueue workQueue(Map configMap, Supplier configureProcessWorkerSpec(processWorkerSpec, - configMap, forkOptionsActions.get())); + configMap, forkOptionsSupplier)); } private void configureProcessWorkerSpec(ProcessWorkerSpec processWorkerSpec, Map configMap, diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/DefaultProjectDescriptor.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/DefaultProjectDescriptor.java new file mode 100644 index 0000000000000..da56ac82b63db --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/DefaultProjectDescriptor.java @@ -0,0 +1,97 @@ +package io.quarkus.gradle.workspace.descriptors; + +import java.io.File; +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class DefaultProjectDescriptor implements Serializable, ProjectDescriptor { + + private static final long serialVersionUID = 1L; + + private final File projectDir; + private final File buildDir; + private final File buildFile; + + private final Map tasks; + private final Map> sourceSetTasks; + + public DefaultProjectDescriptor(File projectDir, File buildDir, File buildFile, Map tasks, + Map> sourceSetTasks) { + this.projectDir = projectDir; + this.buildDir = buildDir; + this.buildFile = buildFile; + this.tasks = tasks; + this.sourceSetTasks = sourceSetTasks; + } + + @Override + public File getProjectDir() { + return projectDir; + } + + @Override + public File getBuildDir() { + return buildDir; + } + + @Override + public File getBuildFile() { + return buildFile; + } + + public Map> getSourceSetTasks() { + return sourceSetTasks; + } + + public Map getTasks() { + return tasks; + } + + @Override + public Set getTasksForSourceSet(String sourceSetName) { + return sourceSetTasks.getOrDefault(sourceSetName, Collections.emptySet()); + } + + @Override + public String getTaskSource(String task) { + return tasks.get(task).getSourceDir().getAbsolutePath(); + } + + @Override + public String getTaskDestinationDir(String task) { + return tasks.get(task).getDestinationDir().getAbsolutePath(); + } + + @Override + public TaskType getTaskType(String task) { + return tasks.get(task).getTaskType(); + } + + /** + * Returns a new {@link DefaultProjectDescriptor} with only the given source sets. + */ + public DefaultProjectDescriptor withSourceSetView(Set acceptedSourceSets) { + Map> filteredSourceSets = sourceSetTasks.entrySet().stream() + .filter(e -> acceptedSourceSets.contains(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, TreeMap::new)); + Map filteredTasks = tasks.entrySet().stream() + .filter(e -> filteredSourceSets.values().stream().anyMatch(tasks -> tasks.contains(e.getKey()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, TreeMap::new)); + return new DefaultProjectDescriptor(projectDir, buildDir, buildFile, filteredTasks, filteredSourceSets); + } + + @Override + public String toString() { + return "DefaultProjectDescriptor{" + + "\nprojectDir=" + projectDir + + ",\nbuildDir=" + buildDir + + ",\nbuildFile=" + buildFile + + ",\ntasks=" + tasks + + ",\nsourceSetTasks=" + sourceSetTasks + + "\n}"; + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptor.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptor.java new file mode 100644 index 0000000000000..1f5ae038bc5ba --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptor.java @@ -0,0 +1,27 @@ +package io.quarkus.gradle.workspace.descriptors; + +import java.io.File; +import java.util.Set; + +public interface ProjectDescriptor { + + public enum TaskType { + COMPILE, + RESOURCES + } + + public File getProjectDir(); + + public File getBuildDir(); + + public File getBuildFile(); + + public Set getTasksForSourceSet(String sourceName); + + public String getTaskSource(String task); + + public String getTaskDestinationDir(String task); + + public TaskType getTaskType(String task); + +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptorBuilder.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptorBuilder.java new file mode 100644 index 0000000000000..acdd22e5661ba --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptorBuilder.java @@ -0,0 +1,143 @@ +package io.quarkus.gradle.workspace.descriptors; + +import static io.quarkus.gradle.workspace.descriptors.ProjectDescriptor.TaskType.COMPILE; +import static io.quarkus.gradle.workspace.descriptors.ProjectDescriptor.TaskType.RESOURCES; + +import java.io.File; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import org.gradle.api.Project; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.compile.AbstractCompile; +import org.gradle.api.tasks.testing.Test; +import org.gradle.language.jvm.tasks.ProcessResources; +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile; + +import com.google.common.collect.ImmutableSet; + +public class ProjectDescriptorBuilder { + + private static final Set DEPENDENCY_SOURCE_SETS = ImmutableSet.of(SourceSet.MAIN_SOURCE_SET_NAME, + SourceSet.TEST_SOURCE_SET_NAME, "test-fixtures"); + + private final File projectDir; + private final File buildDir; + private final File buildFile; + private final Map tasks; + private final Map> sourceSetTasks; + private final Set collectOnlySourceSets; + + private ProjectDescriptorBuilder(Project project, Set collectOnlySourceSets) { + this.tasks = new LinkedHashMap<>(); + this.sourceSetTasks = new LinkedHashMap<>(); + this.buildFile = project.getBuildFile(); + this.projectDir = project.getLayout().getProjectDirectory().getAsFile(); + this.buildDir = project.getLayout().getBuildDirectory().get().getAsFile(); + this.collectOnlySourceSets = collectOnlySourceSets; + } + + public static Provider buildForApp(Project target) { + ProjectDescriptorBuilder builder = new ProjectDescriptorBuilder(target, Collections.emptySet()); + target.afterEvaluate(project -> { + project.getTasks().withType(AbstractCompile.class) + .configureEach(builder::readConfigurationFor); + builder.withKotlinJvmCompileType(project); + project.getTasks().withType(ProcessResources.class) + .configureEach(builder::readConfigurationFor); + project.getTasks().withType(Test.class) + .configureEach(builder::readConfigurationFor); + }); + return target.getProviders().provider(() -> new DefaultProjectDescriptor( + builder.projectDir, + builder.buildDir, + builder.buildFile, + builder.tasks, + builder.sourceSetTasks)); + } + + public static Provider buildForDependency(Project target) { + ProjectDescriptorBuilder builder = new ProjectDescriptorBuilder(target, DEPENDENCY_SOURCE_SETS); + target.afterEvaluate(project -> { + project.getTasks().withType(AbstractCompile.class) + .configureEach(builder::readConfigurationFor); + builder.withKotlinJvmCompileType(project); + project.getTasks().withType(ProcessResources.class) + .configureEach(builder::readConfigurationFor); + }); + return target.getProviders().provider(() -> new DefaultProjectDescriptor( + builder.projectDir, + builder.buildDir, + builder.buildFile, + builder.tasks, + builder.sourceSetTasks)); + } + + private void readConfigurationFor(AbstractCompile task) { + if (task.getEnabled() && !task.getSource().isEmpty()) { + File destDir = task.getDestinationDirectory().getAsFile().get(); + task.getSource().visit(fileVisitDetails -> { + if (fileVisitDetails.getRelativePath().getParent().toString().isEmpty()) { + File srcDir = fileVisitDetails.getFile().getParentFile(); + tasks.put(task.getName(), new QuarkusTaskDescriptor(task.getName(), COMPILE, srcDir, destDir)); + SourceSetContainer sourceSets = task.getProject().getExtensions().getByType(SourceSetContainer.class); + sourceSets.stream().filter(sourceSet -> sourceSet.getOutput().getClassesDirs().contains(destDir)) + .forEach(sourceSet -> sourceSetTasks + .computeIfAbsent(sourceSet.getName(), s -> new HashSet<>()) + .add(task.getName())); + fileVisitDetails.stopVisiting(); + } + }); + } + } + + private void readConfigurationFor(Test task) { + } + + private void readConfigurationFor(ProcessResources task) { + if (task.getEnabled() && !task.getSource().isEmpty()) { + File destDir = task.getDestinationDir(); + task.getSource().getAsFileTree().visit(fileVisitDetails -> { + if (fileVisitDetails.getRelativePath().getParent().toString().isEmpty()) { + File srcDir = fileVisitDetails.getFile().getParentFile(); + tasks.put(task.getName(), new QuarkusTaskDescriptor(task.getName(), RESOURCES, srcDir, destDir)); + SourceSetContainer sourceSets = task.getProject().getExtensions().getByType(SourceSetContainer.class); + sourceSets.stream().filter(sourceSet -> destDir.equals(sourceSet.getOutput().getResourcesDir())) + .forEach(sourceSet -> sourceSetTasks + .computeIfAbsent(sourceSet.getName(), s -> new HashSet<>()) + .add(task.getName())); + fileVisitDetails.stopVisiting(); + } + }); + } + } + + private void withKotlinJvmCompileType(Project project) { + try { + Class.forName("org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile"); + project.getTasks().withType(KotlinJvmCompile.class).configureEach(this::readConfigurationFor); + } catch (ClassNotFoundException e) { + // Ignore + } + } + + private void readConfigurationFor(KotlinJvmCompile task) { + if (task.getEnabled() && !task.getSources().isEmpty()) { + File destDir = task.getDestinationDirectory().getAsFile().get(); + AtomicReference srcDir = new AtomicReference<>(); + task.getSources().getAsFileTree().visit(fileVisitDetails -> { + if (fileVisitDetails.getRelativePath().getParent().toString().isEmpty()) { + srcDir.set(fileVisitDetails.getFile().getParentFile()); + fileVisitDetails.stopVisiting(); + } + }); + tasks.put(task.getName(), new QuarkusTaskDescriptor(task.getName(), COMPILE, srcDir.get(), destDir)); + } + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/QuarkusTaskDescriptor.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/QuarkusTaskDescriptor.java new file mode 100644 index 0000000000000..2e84ed8ada1a5 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/QuarkusTaskDescriptor.java @@ -0,0 +1,49 @@ +package io.quarkus.gradle.workspace.descriptors; + +import java.io.File; +import java.io.Serializable; + +import io.quarkus.gradle.workspace.descriptors.ProjectDescriptor.TaskType; + +public class QuarkusTaskDescriptor implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String taskName; + private final TaskType taskType; + private final File sourceDir; + private final File destinationDir; + + public QuarkusTaskDescriptor(String taskName, TaskType taskType, File sourceDir, File destinationDir) { + this.taskName = taskName; + this.taskType = taskType; + this.sourceDir = sourceDir; + this.destinationDir = destinationDir; + } + + public String getTaskName() { + return taskName; + } + + public TaskType getTaskType() { + return taskType; + } + + public File getSourceDir() { + return sourceDir; + } + + public File getDestinationDir() { + return destinationDir; + } + + @Override + public String toString() { + return "QuarkusTaskDescriptor{" + + "taskName='" + taskName + '\'' + + ", taskType=" + taskType + + ", sourceDir=" + sourceDir + + ", destinationDir=" + destinationDir + + '}'; + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/tasks/TasksConfigurationCacheCompatibilityTest.java b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/tasks/TasksConfigurationCacheCompatibilityTest.java new file mode 100644 index 0000000000000..e1d0d1f957c2f --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/tasks/TasksConfigurationCacheCompatibilityTest.java @@ -0,0 +1,149 @@ +package io.quarkus.gradle.tasks; + +import static io.quarkus.gradle.QuarkusPlugin.DEPLOY_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.IMAGE_BUILD_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.IMAGE_PUSH_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.QUARKUS_BUILD_APP_PARTS_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.QUARKUS_BUILD_DEP_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.QUARKUS_BUILD_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.QUARKUS_GENERATE_CODE_DEV_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.QUARKUS_GENERATE_CODE_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.QUARKUS_GENERATE_CODE_TESTS_TASK_NAME; +import static io.quarkus.gradle.QuarkusPlugin.QUARKUS_SHOW_EFFECTIVE_CONFIG_TASK_NAME; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.apache.commons.io.FileUtils; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TasksConfigurationCacheCompatibilityTest { + + @TempDir + Path testProjectDir; + + private static Stream compatibleTasks() { + return Stream.of( + QUARKUS_GENERATE_CODE_TASK_NAME, + QUARKUS_GENERATE_CODE_DEV_TASK_NAME, + QUARKUS_BUILD_DEP_TASK_NAME, + QUARKUS_BUILD_APP_PARTS_TASK_NAME, + QUARKUS_SHOW_EFFECTIVE_CONFIG_TASK_NAME, + QUARKUS_BUILD_TASK_NAME, + QUARKUS_GENERATE_CODE_TESTS_TASK_NAME); + } + + private static Stream nonCompatibleQuarkusBuildTasks() { + return Stream.of( + IMAGE_BUILD_TASK_NAME, + IMAGE_PUSH_TASK_NAME, + DEPLOY_TASK_NAME); + } + + @Test + @Order(1) + public void quarkusBuildFooTest() throws IOException, URISyntaxException { + URL url = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/configurationcache/main"); + FileUtils.copyDirectory(new File(url.toURI()), testProjectDir.toFile()); + FileUtils.copyFile(new File("../gradle.properties"), testProjectDir.resolve("gradle.properties").toFile()); + + GradleRunner.create() + .withPluginClasspath() + .withProjectDir(testProjectDir.toFile()) + .withArguments(QUARKUS_GENERATE_CODE_TASK_NAME, "--info", "--stacktrace", "--build-cache", + "--configuration-cache") + .build(); + assertTrue(true); + } + + @ParameterizedTest + @Order(4) + @MethodSource("compatibleTasks") + public void configurationCacheIsReusedTest(String taskName) throws IOException, URISyntaxException { + URL url = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/configurationcache/main"); + FileUtils.copyDirectory(new File(url.toURI()), testProjectDir.toFile()); + FileUtils.copyFile(new File("../gradle.properties"), testProjectDir.resolve("gradle.properties").toFile()); + + buildResult(":help", "--configuration-cache"); + + BuildResult firstBuild = buildResult(taskName, "--configuration-cache"); + assertTrue(firstBuild.getOutput().contains("Configuration cache entry stored")); + + BuildResult secondBuild = buildResult(taskName, "--configuration-cache"); + assertTrue(secondBuild.getOutput().contains("Reusing configuration cache.")); + } + + @ParameterizedTest + @Order(5) + @MethodSource("compatibleTasks") + public void configurationCacheIsReusedWhenProjectIsolationIsUsedTest(String taskName) + throws IOException, URISyntaxException { + URL url = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/configurationcache/main"); + FileUtils.copyDirectory(new File(url.toURI()), testProjectDir.toFile()); + FileUtils.copyFile(new File("../gradle.properties"), testProjectDir.resolve("gradle.properties").toFile()); + + BuildResult firstBuild = buildResult(taskName, "-Dorg.gradle.unsafe.isolated-projects=true"); + assertTrue(firstBuild.getOutput().contains("Configuration cache entry stored")); + + BuildResult secondBuild = buildResult(taskName, "-Dorg.gradle.unsafe.isolated-projects=true"); + assertTrue(secondBuild.getOutput().contains("Reusing configuration cache.")); + } + + @ParameterizedTest + @Order(2) + @MethodSource("nonCompatibleQuarkusBuildTasks") + public void quarkusBuildTasksNonCompatibleWithConfigurationCacheNotFail(String taskName) + throws IOException, URISyntaxException { + URL url = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/configurationcache/main"); + FileUtils.copyDirectory(new File(url.toURI()), testProjectDir.toFile()); + FileUtils.copyFile(new File("../gradle.properties"), testProjectDir.resolve("gradle.properties").toFile()); + + BuildResult build = buildResult(taskName); + assertTrue(build.getOutput().contains("BUILD SUCCESSFUL")); + + } + + @ParameterizedTest + @MethodSource("nonCompatibleQuarkusBuildTasks") + @Order(3) + public void quarkusBuildTasksNonCompatibleWithConfigurationCacheNotFailWhenUsingConfigurationCache(String taskName) + throws IOException, URISyntaxException { + URL url = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/configurationcache/main"); + FileUtils.copyDirectory(new File(url.toURI()), testProjectDir.toFile()); + FileUtils.copyFile(new File("../gradle.properties"), testProjectDir.resolve("gradle.properties").toFile()); + + BuildResult build = buildResult(taskName, "--no-configuration-cache"); + assertTrue(build.getOutput().contains("BUILD SUCCESSFUL")); + + } + + private BuildResult buildResult(String task, String configurationCacheCommand) { + return GradleRunner.create() + .withPluginClasspath() + .withProjectDir(testProjectDir.toFile()) + .withArguments(task, "--info", "--stacktrace", "--build-cache", configurationCacheCommand) + .build(); + } + + private BuildResult buildResult(String task) { + return GradleRunner.create() + .withPluginClasspath() + .withProjectDir(testProjectDir.toFile()) + .withArguments(task, "--info", "--stacktrace", "--build-cache") + .build(); + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/build.gradle.kts b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/build.gradle.kts new file mode 100644 index 0000000000000..ff2d8c50f84f8 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + java + id("io.quarkus") +} + +buildscript { + repositories { + mavenLocal() + mavenCentral() + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation(enforcedPlatform("io.quarkus:quarkus-bom:${project.property("version")}")) + implementation("jakarta.inject:jakarta.inject-api:2.0.1") +} + +quarkus { + quarkusBuildProperties.put("quarkus.foo", "bar") + manifest { + attributes(mapOf("Manifest-Attribute" to "some-value")) + } + + // The following line is replaced by the tests in `TasksConfigurationCacheCompatibilityTest` + // ADDITIONAL_CONFIG +} diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/settings.gradle.kts b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/settings.gradle.kts new file mode 100644 index 0000000000000..c35dae34f86ef --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "configuration-cache" diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/src/main/java/org/acme/Foo.java b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/src/main/java/org/acme/Foo.java new file mode 100644 index 0000000000000..8f4e8542598f4 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/configurationcache/main/src/main/java/org/acme/Foo.java @@ -0,0 +1,4 @@ +package org.acme; + +public class Foo { +} \ No newline at end of file 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 ffc0564ff65e0..ec5fb1bf5f1e8 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 @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -19,6 +20,7 @@ import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.ResolvedDependency; import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.file.FileCollection; import org.gradle.api.internal.artifacts.dependencies.DefaultDependencyArtifact; import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency; import org.gradle.api.plugins.JavaPlugin; @@ -95,6 +97,30 @@ public static void initConfigurations(Project project) { }); } + private static Configuration[] getOriginalRuntimeClasspaths(Project project, LaunchMode mode) { + List configurationNames; + switch (mode) { + case TEST: + configurationNames = List.of(JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME); + break; + case NORMAL: + configurationNames = List.of(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + break; + case DEVELOPMENT: + configurationNames = List.of( + ToolingUtils.DEV_MODE_CONFIGURATION_NAME, + JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME, + JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + break; + default: + throw new IllegalArgumentException("Unexpected mode: " + mode); + } + ConfigurationContainer configContainer = project.getConfigurations(); + return configurationNames.stream() + .map(configContainer::getByName) + .toArray(Configuration[]::new); + } + private final Project project; private final LaunchMode mode; @@ -242,11 +268,15 @@ private void setUpCompileOnlyConfiguration() { } } + public FileCollection getOriginalRuntimeClasspathAsInput() { + return project.files((Object[]) getOriginalRuntimeClasspaths(project, mode)); + } + public Configuration getPlatformConfiguration() { return project.getConfigurations().getByName(this.platformConfigurationName); } - private Configuration getRawRuntimeConfiguration() { + public Configuration getRawRuntimeConfiguration() { return project.getConfigurations().getByName(this.runtimeConfigurationName); } diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java index 06b61927c99fd..6a423003ecfbd 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java @@ -627,11 +627,11 @@ private void addSubstitutedProject(PathList.Builder paths, File projectFile) { } } - private static boolean isFlagOn(byte walkingFlags, byte flag) { + public static boolean isFlagOn(byte walkingFlags, byte flag) { return (walkingFlags & flag) > 0; } - private static byte clearFlag(byte flags, byte flag) { + public static byte clearFlag(byte flags, byte flag) { if ((flags & flag) > 0) { flags ^= flag; } diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java index 861f1ba4c889f..5d20f796c6d51 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java @@ -1,6 +1,7 @@ package io.quarkus.gradle.tooling; import java.io.IOException; +import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -160,6 +161,21 @@ public static Path serializeAppModel(ApplicationModel appModel, Task context, bo return serializedModel; } + public static ApplicationModel deserializeAppModel(Path path) throws IOException { + try (ObjectInputStream out = new ObjectInputStream(Files.newInputStream(path))) { + return (ApplicationModel) out.readObject(); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static Path serializeAppModel(ApplicationModel appModel, Path serializedModelPath) throws IOException { + try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(serializedModelPath))) { + out.writeObject(appModel); + } + return serializedModelPath; + } + public static ApplicationModel create(Project project, LaunchMode mode) { final ModelParameter params = new ModelParameterImpl(); params.setMode(mode.toString()); diff --git a/docs/src/main/asciidoc/gradle-tooling.adoc b/docs/src/main/asciidoc/gradle-tooling.adoc index 85f1531b3ca8b..89346eb5112b4 100644 --- a/docs/src/main/asciidoc/gradle-tooling.adoc +++ b/docs/src/main/asciidoc/gradle-tooling.adoc @@ -814,5 +814,33 @@ action must be declared as an input of the task. The Quarkus Gradle plugin works with builds that have the link:https://docs.gradle.org/current/userguide/configuration_cache.html[Gradle's configuration cache] enabled, but -the configuration cache is disabled for the Quarkus tasks. This means, that the Quarkus plugin does not break such +the configuration cache is disabled for some of the Quarkus tasks. This means, that the Quarkus plugin does not break such Gradle builds. +The current state of compatibility is shown in the following table: + +[cols="2","4"] +|==== +|Quarkus task|Configuration Cache Compatibility +|`quarkusGenerateCode` |✅ +|`quarkusGenerateCodeDev`|✅ +|`quarkusGenerateCodeTests`|✅ +|`quarkusDependenciesBuild`|✅ +|`quarkusAppPartsBuild`|✅ +|`quarkusShowEffectiveConfig`|✅ +|`quarkusBuild`|✅ +|`quarkusDev`|❌ +|`quarkusRun`|❌ +|`quarkusRemoteDev`|❌ +|`quarkusTest`|❌ +|`quarkusGoOffline`|❌ +|`quarkusInfo`|❌ +|`quarkusUpdate`|❌ +|`imageBuild`|❌ +|`imagePush`|❌ +|`deploy`|❌ +|`listExtensions`|❌ +|`listCategories`|❌ +|`listPlatforms`|❌ +|`addExtension`|❌ +|`removeExtension`|❌ +|==== diff --git a/integration-tests/gradle/src/main/resources/implementation-files/application-files/build.gradle b/integration-tests/gradle/src/main/resources/implementation-files/application-files/build.gradle index 3a102c326eb0f..93b08ea565138 100644 --- a/integration-tests/gradle/src/main/resources/implementation-files/application-files/build.gradle +++ b/integration-tests/gradle/src/main/resources/implementation-files/application-files/build.gradle @@ -1,4 +1,4 @@ -import io.quarkus.gradle.tasks.QuarkusGenerateCode +import io.quarkus.gradle.tasks.QuarkusApplicationModelTask plugins { id 'io.quarkus' @@ -8,4 +8,4 @@ dependencies { implementation files("../common/build/libs/common.jar") } -tasks.withType(QuarkusGenerateCode).configureEach { t -> t.dependsOn(project(":common").tasks.named("jar")) } +tasks.withType(QuarkusApplicationModelTask).configureEach { t -> t.dependsOn(project(":common").tasks.named("jar")) } diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java index ed700225f2397..09a1d0dd5cfa6 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java @@ -19,6 +19,7 @@ public class BasicJavaNativeBuildIT extends QuarkusNativeGradleITBase { @Test public void shouldBuildNativeImage() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); + gradleConfigurationCache(false); final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative"); @@ -47,6 +48,7 @@ public void shouldBuildNativeImage() throws Exception { @Test public void shouldBuildNativeImageWithCustomName() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); + gradleConfigurationCache(false); final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.output-name=test"); @@ -77,6 +79,7 @@ public void shouldBuildNativeImageWithCustomName() throws Exception { @Test public void shouldBuildNativeImageWithCustomNameWithoutSuffix() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); + gradleConfigurationCache(false); final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.output-name=test", "-Dquarkus.package.jar.add-runner-suffix=false");