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 aa33e610677870..426cc86791a440 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 @@ -37,6 +37,8 @@ import io.quarkus.gradle.tasks.QuarkusAddExtension; import io.quarkus.gradle.tasks.QuarkusBuild; import io.quarkus.gradle.tasks.QuarkusBuildConfiguration; +import io.quarkus.gradle.tasks.QuarkusBuildFinish; +import io.quarkus.gradle.tasks.QuarkusBuildLibs; import io.quarkus.gradle.tasks.QuarkusDev; import io.quarkus.gradle.tasks.QuarkusGenerateCode; import io.quarkus.gradle.tasks.QuarkusGoOffline; @@ -66,6 +68,8 @@ public class QuarkusPlugin implements Plugin { public static final String QUARKUS_GENERATE_CODE_TASK_NAME = "quarkusGenerateCode"; public static final String QUARKUS_GENERATE_CODE_DEV_TASK_NAME = "quarkusGenerateCodeDev"; public static final String QUARKUS_GENERATE_CODE_TESTS_TASK_NAME = "quarkusGenerateCodeTests"; + public static final String QUARKUS_BUILD_LIBS_TASK_NAME = "quarkusLibsBuild"; + public static final String QUARKUS_BUILD_FINISH_TASK_NAME = "quarkusFinishBuild"; public static final String QUARKUS_BUILD_TASK_NAME = "quarkusBuild"; public static final String QUARKUS_DEV_TASK_NAME = "quarkusDev"; public static final String QUARKUS_REMOTE_DEV_TASK_NAME = "quarkusRemoteDev"; @@ -150,9 +154,27 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { QuarkusBuildConfiguration buildConfig = new QuarkusBuildConfiguration(project); + TaskProvider quarkusBuildLibs = tasks.register(QUARKUS_BUILD_LIBS_TASK_NAME, + QuarkusBuildLibs.class); + + TaskProvider quarkusBuildFinalize = tasks.register(QUARKUS_BUILD_FINISH_TASK_NAME, + QuarkusBuildFinish.class); + TaskProvider quarkusBuild = tasks.register(QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class, build -> { build.dependsOn(quarkusGenerateCode); + build.finalizedBy(quarkusBuildFinalize); build.getForcedProperties().set(buildConfig.getForcedProperties()); + build.getOutputs().doNotCacheIf("Caching disabled for configured package type", + t -> !((QuarkusBuild) t).isFastJarLike()); + }); + + quarkusBuildLibs.configure(task -> { + task.getOutputs().doNotCacheIf("Collecting dependencies not worth to cache", t -> true); + }); + + quarkusBuildFinalize.configure(task -> { + task.dependsOn(quarkusBuildLibs, quarkusBuild); + task.getOutputs().doNotCacheIf("Assembling task not worth to cache", t -> true); }); TaskProvider imageBuild = tasks.register(IMAGE_BUILD_TASK_NAME, ImageBuild.class, buildConfig); 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 499ca60d7a3cc2..66453a03180a92 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 @@ -3,6 +3,7 @@ import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.EnumMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; @@ -164,9 +165,12 @@ public ApplicationModel getApplicationModel() { } public ApplicationModel getApplicationModel(LaunchMode mode) { - return ToolingUtils.create(project, mode); + // Prevent duplicate computation of ApplicationModel(s), same model's needed by multiple tasks. + return applicationModels.computeIfAbsent(mode, m -> ToolingUtils.create(project, m)); } + private transient Map applicationModels = new EnumMap<>(LaunchMode.class); + /** * Returns the last file from the specified {@link FileCollection}. */ 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 1b3698e4f668a2..0671ac5e95a9c1 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 @@ -4,6 +4,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.Serializable; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -16,6 +17,7 @@ import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileSystemOperations; import org.gradle.api.java.archives.Attributes; import org.gradle.api.provider.MapProperty; import org.gradle.api.tasks.CacheableTask; @@ -34,6 +36,7 @@ import io.quarkus.bootstrap.app.QuarkusBootstrap; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.AppModelResolverException; +import io.quarkus.gradle.QuarkusPlugin; import io.quarkus.gradle.dsl.Manifest; import io.quarkus.maven.dependency.GACTV; import io.quarkus.runtime.util.StringUtil; @@ -46,6 +49,7 @@ public abstract class QuarkusBuild extends QuarkusTask { private static final String MANIFEST_ATTRIBUTES_PROPERTY_PREFIX = "quarkus.package.manifest.attributes"; private static final String OUTPUT_DIRECTORY = "quarkus.package.output-directory"; + private static final String QUARKUS_BUILD_DIR = "quarkus-build-dir"; private List ignoredEntries = new ArrayList<>(); private Manifest manifest = new Manifest(); @@ -70,6 +74,9 @@ public QuarkusBuild nativeArgs(Action> action) { return this; } + @Inject + public abstract FileSystemOperations getFileSystemOperations(); + @Optional @Input public abstract MapProperty getForcedProperties(); @@ -122,6 +129,22 @@ public Manifest getManifest() { return this.manifest; } + @Input + public String getPackageType() { + return getPropValueWithPrecedence(QuarkusPlugin.QUARKUS_PACKAGE_TYPE, java.util.Optional.of("fast-jar")); + } + + @Internal + public boolean isFastJarLike() { + switch (getPackageType()) { + case "fast-jar": + case "native": + return true; + default: + return false; + } + } + public QuarkusBuild manifest(Action action) { action.execute(this.getManifest()); return this; @@ -137,10 +160,31 @@ public File getNativeRunner() { return new File(getProject().getBuildDir(), extension().buildNativeRunnerName(Map.of())); } - @OutputDirectory + @Internal public File getFastJar() { - return new File(getProject().getBuildDir(), - this.getPropValueWithPrecedence(OUTPUT_DIRECTORY, java.util.Optional.of("quarkus-app"))); + return new File(getProject().getBuildDir(), getOutputDirectory()); + } + + /** + * Contains the Quarkus app, but without the {@code lib/} folder. Contents of this directory are not too big and + * should be cacheable. + */ + @OutputDirectory + public File getFastJarBuildDir() { + return new File(getProject().getBuildDir(), QUARKUS_BUILD_DIR); + } + + /** + * Directory into which the {@link QuarkusBuildLibs} populates the dependencies. + */ + @Internal + public File getLibsBuildDir() { + return new File(getProject().getBuildDir(), "quarkus-build-libs"); + } + + @Internal + public String getOutputDirectory() { + return getPropValueWithPrecedence(OUTPUT_DIRECTORY, java.util.Optional.of("quarkus-app")); } @TaskAction @@ -148,6 +192,10 @@ public void buildQuarkus() { final ApplicationModel appModel; final Map forcedProperties = getForcedProperties().getOrElse(Collections.emptyMap()); + // Caching and "up-to-date" checks depend on the inputs, this 'delete()' should ensure that the up-to-date + // checks work against "clean" outputs, considering that the outputs depend on the package-type. + getFileSystemOperations().delete(delete -> delete.delete(getRunnerJar(), getFastJar(), getFastJarBuildDir())); + try { appModel = extension().getAppModelResolver().resolveModel(new GACTV(getProject().getGroup().toString(), getProject().getName(), getProject().getVersion().toString())); @@ -164,10 +212,15 @@ public void buildQuarkus() { exportCustomManifestProperties(effectiveProperties); + boolean fastJarLike = isFastJarLike(); + Path quarkusBuildDir = (fastJarLike ? getFastJarBuildDir() : getProject().getBuildDir()).toPath(); + + getLogger().info("Building in target directory {}", quarkusBuildDir); + try (CuratedApplication appCreationContext = QuarkusBootstrap.builder() .setBaseClassLoader(getClass().getClassLoader()) .setExistingModel(appModel) - .setTargetDirectory(getProject().getBuildDir().toPath()) + .setTargetDirectory(quarkusBuildDir) .setBaseName(extension().finalName()) .setBuildSystemProperties(effectiveProperties) .setAppArtifact(appModel.getAppArtifact()) @@ -184,6 +237,10 @@ public void buildQuarkus() { appCreationContext.createAugmentor("io.quarkus.deployment.pkg.builditem.ProcessInheritIODisabled$Factory", Collections.emptyMap()).createProductionApplication(); + if (fastJarLike) { + Path resolvedAppBuildDir = getFastJarBuildDir().toPath().resolve(getOutputDirectory()); + getFileSystemOperations().delete(delete -> delete.delete(resolvedAppBuildDir.resolve("lib"))); + } } catch (BootstrapException e) { throw new GradleException("Failed to build a runnable JAR", e); } @@ -233,7 +290,7 @@ private String expandConfigurationKey(String shortKey) { return String.format("%s.%s", NATIVE_PROPERTY_NAMESPACE, hyphenatedKey); } - private String getPropValueWithPrecedence(final String propName, final java.util.Optional defaultValue) { + String getPropValueWithPrecedence(final String propName, final java.util.Optional defaultValue) { if (applicationProperties.isEmpty()) { SourceSet mainSourceSet = QuarkusGradleUtils.getSourceSet(getProject(), SourceSet.MAIN_SOURCE_SET_NAME); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildFinish.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildFinish.java new file mode 100644 index 00000000000000..b407927aff8813 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildFinish.java @@ -0,0 +1,71 @@ +package io.quarkus.gradle.tasks; + +import java.io.File; + +import javax.inject.Inject; + +import org.gradle.api.file.FileSystemOperations; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +/** + * Finalizes the build of a Quarkus app, combining the outputs of {@link QuarkusBuild} and {@link QuarkusBuildLibs}. + * + *

+ * This task is required to "properly" cache the output of {@link QuarkusBuild} + */ +public abstract class QuarkusBuildFinish extends QuarkusTask { + + public static final String QUARKUS_ARTIFACT_PROPERTIES = "quarkus-artifact.properties"; + + @Inject + public QuarkusBuildFinish() { + super("Finalize the Quarkus application build, prefer the 'quarkusBuild' task"); + } + + @Inject + public abstract FileSystemOperations getFileSystemOperations(); + + @Input + public String getPackageType() { + return quarkusBuild().getPackageType(); + } + + @OutputDirectory + public File getFastJar() { + return quarkusBuild().getFastJar(); + } + + @OutputFile + public File getArtifactPropertiesFile() { + return new File(getFastJar().getParentFile(), QUARKUS_ARTIFACT_PROPERTIES); + } + + @TaskAction + public void run() { + File finalDir = getFastJar(); + + QuarkusBuild quarkusBuild = quarkusBuild(); + if (!quarkusBuild.isFastJarLike()) { + getLogger().info("Nothing to do for package type {}", quarkusBuild.getPackageType()); + getFileSystemOperations().delete(delete -> delete.delete(finalDir)); + return; + } + + String outputDir = quarkusBuild.getOutputDirectory(); + File appBuildDir = new File(quarkusBuild.getFastJarBuildDir(), outputDir); + File libsBuildDir = new File(quarkusBuild.getLibsBuildDir(), outputDir); + + getLogger().info("Finalizing Quarkus build in {} from {} and {}", finalDir, appBuildDir, libsBuildDir); + + getFileSystemOperations().sync(sync -> { + sync.into(finalDir); + sync.from(appBuildDir, libsBuildDir); + }); + + getFileSystemOperations().copy( + copy -> copy.into(finalDir.getParent()).from(appBuildDir.getParent()).include(QUARKUS_ARTIFACT_PROPERTIES)); + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildLibs.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildLibs.java new file mode 100644 index 00000000000000..3cade689588509 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildLibs.java @@ -0,0 +1,123 @@ +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.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.gradle.api.GradleException; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileSystemOperations; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.ResolvedDependency; + +/** + * Collect the Quarkus app dependencies, the contents of the {@code quarkus-app/lib} folder, without making the task + * cache anything, but still provide up-to-date checks. + * + *

+ * Caching dependency jars is wasted effort and unnecessarily pollutes the Gradle build cache. + */ +public abstract class QuarkusBuildLibs extends QuarkusTask { + + @Inject + public QuarkusBuildLibs() { + super("Collect dependencies for the Quarkus application, prefer the 'quarkusBuild' task"); + } + + @Inject + public abstract FileSystemOperations getFileSystemOperations(); + + @Classpath + public FileCollection getClasspath() { + return quarkusBuild().getClasspath(); + } + + @OutputDirectory + public File getOutputDir() { + return new File(quarkusBuild().getLibsBuildDir(), quarkusBuild().getOutputDirectory()); + } + + @TaskAction + public void run() { + Path quarkusPackageOutputDir = getOutputDir().toPath(); + + getFileSystemOperations().delete(delete -> delete.delete(quarkusPackageOutputDir)); + + getLogger().info("Placing Quarkus application dependencies in {}", quarkusPackageOutputDir); + + Path libBoot = quarkusPackageOutputDir.resolve("lib/boot"); + Path libMain = quarkusPackageOutputDir.resolve("lib/main"); + try { + Files.createDirectories(libBoot); + Files.createDirectories(libMain); + } catch (IOException e) { + throw new GradleException(String.format("Failed to create directories in %s", quarkusPackageOutputDir), e); + } + + ApplicationModel appModel = extension().getApplicationModel(); + + // see https://quarkus.io/guides/class-loading-reference#configuring-class-loading + String removedArtifactsProp = quarkusBuild().getPropValueWithPrecedence("quarkus.class-loading.removed-artifacts", + Optional.of("")); + Optional optionalArtifactsProp = Optional.ofNullable( + quarkusBuild().getPropValueWithPrecedence("quarkus.class-loading.removed-artifacts", Optional.empty())); + Optional> optionalDependencies = optionalArtifactsProp.map(s -> Arrays.stream(s.split(",")) + .map(String::trim) + .filter(gact -> !gact.isEmpty()) + .map(ArtifactKey::fromString) + .collect(Collectors.toSet())); + Set removedArtifacts = Arrays.stream(removedArtifactsProp.split(",")) + .map(String::trim) + .filter(gact -> !gact.isEmpty()) + .map(ArtifactKey::fromString) + .collect(Collectors.toSet()); + + appModel.getRuntimeDependencies().stream() + .filter(appDep -> { + // copied from io.quarkus.deployment.pkg.steps.JarResultBuildStep.includeAppDep + if (!appDep.isJar()) { + return false; + } + if (appDep.isOptional()) { + return optionalDependencies.map(appArtifactKeys -> appArtifactKeys.contains(appDep.getKey())) + .orElse(true); + } + return !removedArtifacts.contains(appDep.getKey()); + }) + .map(dep -> Map.entry(dep.isFlagSet(DependencyFlags.CLASSLOADER_RUNNER_PARENT_FIRST) ? libBoot : libMain, dep)) + .peek(depAndTarget -> { + ResolvedDependency dep = depAndTarget.getValue(); + Path targetDir = depAndTarget.getKey(); + dep.getResolvedPaths().forEach(p -> { + String file = dep.getGroupId() + '.' + p.getFileName(); + Path target = targetDir.resolve(file); + if (!Files.exists(target)) { + getLogger().debug("Dependency {} : copying {} to {}", + dep.toGACTVString(), + p, target); + try { + Files.copy(p, target); + } catch (IOException e) { + throw new GradleException(String.format("Failed to copy %s to %s", p, target), e); + } + } + }); + }) + .collect(Collectors.toMap(Map.Entry::getKey, depAndTarget -> 1, Integer::sum)) + .forEach((path, count) -> getLogger().info("Copied {} files into {}", count, path)); + } +} 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 c10b0b814a4e60..9d647ed21853a3 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 @@ -14,9 +14,12 @@ import org.gradle.api.GradleException; import org.gradle.api.artifacts.Configuration; +import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.CompileClasspath; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskAction; @@ -31,6 +34,7 @@ import io.quarkus.paths.PathList; import io.quarkus.runtime.LaunchMode; +@CacheableTask public class QuarkusGenerateCode extends QuarkusTask { public static final String QUARKUS_GENERATED_SOURCES = "quarkus-generated-sources"; @@ -66,6 +70,7 @@ public void setCompileClasspath(Configuration compileClasspath) { } @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) public Set getInputDirectory() { Set inputDirectories = new HashSet<>(); 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 e16c737d6009f4..bbe365b0bd5186 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,6 +5,7 @@ import org.gradle.api.DefaultTask; +import io.quarkus.gradle.QuarkusPlugin; import io.quarkus.gradle.extension.QuarkusPluginExtension; import io.quarkus.maven.dependency.ResolvedDependency; @@ -24,6 +25,10 @@ QuarkusPluginExtension extension() { return extension; } + QuarkusBuild quarkusBuild() { + return getProject().getTasks().named(QuarkusPlugin.QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class).get(); + } + protected Properties getBuildSystemProperties(ResolvedDependency appArtifact) { final Map properties = getProject().getProperties(); final Properties realProperties = new Properties();