Skip to content

Commit

Permalink
Make QuarkusBuild not pollute Gradle's build cache
Browse files Browse the repository at this point in the history
Improve Quarkus build in Gradle, proper up-to-date mechanisms, right config-source priorities.

== Background / Motivation

Currently the `QuarkusBuild` task implementation adds even large build artifacts and unmodified dependency jars to Gradle's build cache. This pollutes the Gradle build cache quite a lot and therefore lets archived build caches become unnecessary huge.
On top of that, "relicts" from previous runs of `QuarkusBuild` using a different configuration, like a different package-type, causes multiple permutations of the cached artifacts and leads to restoring unexpected artifacts from the build cache.

This large change updates the Gradle logic to have "proper" up-to-date checks and in turn a reasonable setting of what is cached and what is not cached.

Background on how I discovered the "caching and up-to-date issue" with `QuarkusBuild`: Quarkus 2.16 added the `@CacheableTask` annotation to `QuarkusBuild`. This means that _everything_ that is generated by `QuarkusBuild` (and "declared" as an output: fast-jar directory, uber-jar, native-runner) is cached. After having Quarkus 2.16.x in our "main" branch for about 1/2 day, nearly the whole "10 GB budget" for cache artifacts in GitHub was occupied by Gradle build cache artifacts - the build cache artifacts grew from build to build - this was not the case without the `@CacheableTask` annotation.

The current inputs of the `QuarkusBuild` task are also incomplete. Those inputs do not include for example the configured `finalName`, the settings on the Gradle project and the settings on the Quarkus extension. This leads to situations that the wrong Gradle cache artifacts could be restored (think: you ask for a fast-jar, but get an uber-jar).

Another issue is that repeated runs of `quarkusBuild` against the exact same code base and dependencies leads to different outputs, the contents of the generated jars and e.g. quarkus-application.dat differ. This causes different outputs of the `QuarkusBuild` task, which cause depending tasks and projects to unnecessarily run/work again.

== Initial Goal

The initial goal of this effort was to make caching of `fast-jar` builds work nice in CI environments, I assume that's the most common package type used in CI environments for example for testing purposes. The idea here is to _not_ cache all the dependency jars, but leverage the mechanisms available in Gradle to "just collect" the dependencies. In other words (and simplified): for `fast-jar`, cache everthing "in `quarkus-app` except `lib/`.

== Updated Quarkus Gradle build implementation

There is no change for users running a `./gradlew quarkusBuild`. `QuarkusBuild` is still the task that users should execute (or depend on in their builds).

To achive the goal to make especially `fast-jar` builds "nicely cacheable" in CI, two new tasks had to be introduced:

* `QuarkusBuildDependencies` uses the dependency information from the Quarkus `ApplicationModel` to tell Gradle to collect those. This task works for `fast-jar` and `legacy-jar` and is a "no op" for other package types. This task is _never_ cacheable, but has _up-to-date_ checks.
* `QuarkusBuildCacheableAppParts` performs a Quarkus application build and collects the _cacheable_ parts of that build. This task works for `fast-jar` and `legacy-jar` and is a "no op" for other package types. This task is cacheable.
* `QuarkusBuild`, for `fast-jar` and `legacy-jar`, assembles the outputs of the above two tasks to produce the expected output. Performs a Quarkus build for other package types. See below on when this task is cacheable.

The `build/quarkus-build/` directory is used to properly separate the outputs of the above three tasks:

* `build/quarkus-build/gen/` receives the full output of a Quarkus build
* `build/quarkus-build/app/` receives the _cacheable_ parts from `build/quarkus-build/gen/`
* `build/quarkus-build/dep/` receives the dependency jars

== CI vs non-CI

The output of `QuarkusBuild` is, by default, cacheable in non-CI environments and _not_ cacheable in CI environments. The behavior can be explicitly overridden using the new property `cacheLargeArtifacts` property. As outlined above, caching huge artifacts is not beneficial in CI, either due to space limitations (GitHub's 10GB limit for example) or just the cost of network traffic/duration. On a developer's machine however, the cost of storing/retrieving even bigger artifacts is rather neglectible and lower than rebuilding a Quarkus application.

== Configuration

Before this change, the "priority" of configuration settings was (in most cases...) effectively: System properties, environment variables, `application.properties`, configurations in Gradle build scripts & Gradle project properties. This order is unintuitive and was changed to: system properties, environment variables, Gradle project properties, Quarkus build configuration, application.properties.  The previous code had several places in which Quarkus related configuration settings were retrieved with sometimes different priorities of the "config sources".

The whole "machinery" to get the configuration has been rewritten and encapsulated in new classes `EffectiveConfig` (effective for a Quarkus build) and `BaseConfig` (available during Gradle task configuration phase). `EffectiveConfig` is not really different from `BaseConfig, but contains the "special" settings from e.g. `ImagePush` task.

The new implementation also uses SmallRye Config and especially the existing Quarkus mechanisms to pull information out of a `PackageConfig` _object_ produced from a configuration. It turned out to be easier to reason about `PackageConfig` than "raw property values".

Support for `application.yaml/yml` has also been added.

== Workers

All calls into the "inner workings" of a Quarkus build and Quarkus code generation have been moved to separate worker processes. The code in this change to support this is pretty dead simple.

The primary reason to move that work into isolated worker processes is that all the `QuarkusBootstrap` and derived pieces know nothing about Gradle, so there is no "property like" mechanism to override settings from `application.properties/yaml/yml` with those from e.g. the `QuarkusPluginExtension`. The only option would have been to modify the system properties of the Gradle build process - but that's a no-go, especially considering other tasks running in parallel (think: two `QuarkusBuild` of different projects running at the same time). It would have been relatively easy to serialize all `QuarkusBuild` actions across a Gradle build, but why - and it would prevent using "beefy machines" to run many `QuarkusBuild`s in parallel. Another option would have been to implement a rather complex (and likely very racy) mechanism to track modifications to system properties.

As a conclusion, it wasn't just very simple to leverage the process isolation using the Gradle worker API, but it's also not bad. Gradle does reuse already spawned and compatible worker instances.

The "trick" implemented to "prioritize" configs from Gradle project properties and Quarkus extension settings is to pass the whole config as system properties to the worker.

== Logging / cache-disabled messages

A bunch of hopefully useful logging has been implemented and added.

With info-level loogging, the tasks emit at least some basic information about what they do and at least the package type being used.

== Effective config

To be able to investigate which configuration settings were effectively used, there's another new task called `quarkusShowEffectiveConfig` that shows all the `quarkus.*` properties and some more information, including the loaded `application.(properties|yaml|yml)`. This task is intended to debug build issues. The task can optionally save the effecitve config properties as a properties file in the `build/` directory, if run with the command line option `./gradlew quarkusShowEffectiveConfig --save-config-properties`.

== Test changes

None of the existing tests has changed, except the `BuildCOnfigurationTest` had to be adopted to reflect the updated order of config sources.

Relates to: #30852
  • Loading branch information
snazy committed Feb 19, 2023
1 parent 9aab978 commit e8b1d20
Show file tree
Hide file tree
Showing 64 changed files with 2,400 additions and 479 deletions.
1 change: 1 addition & 0 deletions devtools/gradle/gradle-application-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dependencies {
implementation "io.quarkus:quarkus-devtools-common:${version}"
implementation "io.quarkus:quarkus-core-deployment:${version}"
implementation "io.quarkus:quarkus-bootstrap-gradle-resolver:${version}"
implementation "io.smallrye.config:smallrye-config-source-yaml:${smallrye_config_version}"

implementation project(":gradle-model")

Expand Down
4 changes: 4 additions & 0 deletions devtools/gradle/gradle-application-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
<artifactId>quarkus-devmode-test-utils</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-source-yaml</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-gradle-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
import io.quarkus.gradle.tasks.ImagePush;
import io.quarkus.gradle.tasks.QuarkusAddExtension;
import io.quarkus.gradle.tasks.QuarkusBuild;
import io.quarkus.gradle.tasks.QuarkusBuildConfiguration;
import io.quarkus.gradle.tasks.QuarkusBuildCacheableAppParts;
import io.quarkus.gradle.tasks.QuarkusBuildDependencies;
import io.quarkus.gradle.tasks.QuarkusDev;
import io.quarkus.gradle.tasks.QuarkusGenerateCode;
import io.quarkus.gradle.tasks.QuarkusGoOffline;
Expand All @@ -46,6 +47,7 @@
import io.quarkus.gradle.tasks.QuarkusListPlatforms;
import io.quarkus.gradle.tasks.QuarkusRemoteDev;
import io.quarkus.gradle.tasks.QuarkusRemoveExtension;
import io.quarkus.gradle.tasks.QuarkusShowEffectiveConfig;
import io.quarkus.gradle.tasks.QuarkusTest;
import io.quarkus.gradle.tasks.QuarkusTestConfig;
import io.quarkus.gradle.tasks.QuarkusUpdate;
Expand All @@ -55,7 +57,10 @@
public class QuarkusPlugin implements Plugin<Project> {

public static final String ID = "io.quarkus";
public static final String QUARKUS_PACKAGE_TYPE = "quarkus.package.type";
public static final String DEFAULT_OUTPUT_DIRECTORY = "quarkus-app";
public static final String CLASS_LOADING_REMOVED_ARTIFACTS = "quarkus.class-loading.removed-artifacts";
public static final String CLASS_LOADING_PARENT_FIRST_ARTIFACTS = "quarkus.class-loading.parent-first-artifacts";
public static final String QUARKUS_ARTIFACT_PROPERTIES = "quarkus-artifact.properties";

public static final String EXTENSION_NAME = "quarkus";
public static final String LIST_EXTENSIONS_TASK_NAME = "listExtensions";
Expand All @@ -66,6 +71,9 @@ public class QuarkusPlugin implements Plugin<Project> {
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_DEP_TASK_NAME = "quarkusDependenciesBuild";
public static final String QUARKUS_BUILD_APP_PARTS_TASK_NAME = "quarkusAppPartsBuild";
public static final String QUARKUS_SHOW_EFFECTIVE_CONFIG_TASK_NAME = "quarkusShowEffectiveConfig";
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";
Expand Down Expand Up @@ -96,6 +104,7 @@ public class QuarkusPlugin implements Plugin<Project> {

private final ToolingModelBuilderRegistry registry;

@SuppressWarnings("CdiInjectionPointsInspection")
@Inject
public QuarkusPlugin(ToolingModelBuilderRegistry registry) {
this.registry = registry;
Expand Down Expand Up @@ -148,22 +157,40 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) {
TaskProvider<QuarkusGenerateCode> quarkusGenerateCodeTests = tasks.register(QUARKUS_GENERATE_CODE_TESTS_TASK_NAME,
QuarkusGenerateCode.class, task -> task.setTest(true));

QuarkusBuildConfiguration buildConfig = new QuarkusBuildConfiguration(project);
tasks.register(QUARKUS_SHOW_EFFECTIVE_CONFIG_TASK_NAME,
QuarkusShowEffectiveConfig.class, task -> {
task.setDescription("Show effective Quarkus build configuration.");
});

TaskProvider<QuarkusBuildDependencies> quarkusBuildDependencies = tasks.register(QUARKUS_BUILD_DEP_TASK_NAME,
QuarkusBuildDependencies.class,
task -> task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true));

TaskProvider<QuarkusBuildCacheableAppParts> quarkusBuildCacheableAppParts = tasks.register(
QUARKUS_BUILD_APP_PARTS_TASK_NAME,
QuarkusBuildCacheableAppParts.class, task -> {
task.dependsOn(quarkusGenerateCode);
task.getOutputs().doNotCacheIf(
"Not adding uber-jars, native binaries and mutable-jar package type to Gradle " +
"build cache by default. To allow caching of uber-jars, native binaries and mutable-jar " +
"package type, set 'cacheUberAndNativeRunners' in the 'quarkus' Gradle extension to 'true'.",
t -> !task.isCachedByDefault() && !quarkusExt.getCacheLargeArtifacts().get());
});

TaskProvider<QuarkusBuild> quarkusBuild = tasks.register(QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class, build -> {
build.dependsOn(quarkusGenerateCode);
build.getForcedProperties().set(buildConfig.getForcedProperties());
build.dependsOn(quarkusBuildDependencies, quarkusBuildCacheableAppParts);
build.getOutputs().doNotCacheIf(
"Only collects and combines the outputs of " + QUARKUS_BUILD_APP_PARTS_TASK_NAME + " and "
+ QUARKUS_BUILD_DEP_TASK_NAME + ", see 'cacheLargeArtifacts' in the 'quarkus' Gradle extension " +
"for details.",
t -> !quarkusExt.getCacheLargeArtifacts().get());
});

TaskProvider<ImageBuild> imageBuild = tasks.register(IMAGE_BUILD_TASK_NAME, ImageBuild.class, buildConfig);
imageBuild.configure(task -> {
task.finalizedBy(quarkusBuild);
});
tasks.register(IMAGE_BUILD_TASK_NAME, ImageBuild.class,
task -> task.finalizedBy(quarkusBuildCacheableAppParts, quarkusBuildDependencies, quarkusBuild));

TaskProvider<ImagePush> imagePush = tasks.register(IMAGE_PUSH_TASK_NAME, ImagePush.class, buildConfig);
imagePush.configure(task -> {
task.finalizedBy(quarkusBuild);
});
tasks.register(IMAGE_PUSH_TASK_NAME, ImagePush.class,
task -> task.finalizedBy(quarkusBuildCacheableAppParts, quarkusBuildDependencies, quarkusBuild));

TaskProvider<QuarkusDev> quarkusDev = tasks.register(QUARKUS_DEV_TASK_NAME, QuarkusDev.class, devRuntimeDependencies,
quarkusExt);
Expand All @@ -180,7 +207,6 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) {

});

configureBuildNativeTask(project);
project.getPlugins().withType(
BasePlugin.class,
basePlugin -> tasks.named(BasePlugin.ASSEMBLE_TASK_NAME, task -> task.dependsOn(quarkusBuild)));
Expand Down Expand Up @@ -242,7 +268,7 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) {
quarkusGenerateCode,
quarkusGenerateCodeTests);
});
quarkusBuild.configure(
quarkusBuildCacheableAppParts.configure(
task -> task.dependsOn(classesTask, resourcesTask, tasks.named(JavaPlugin.JAR_TASK_NAME)));

SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
Expand Down Expand Up @@ -310,7 +336,7 @@ public void execute(Task task) {
t.useJUnitPlatform();
});
// quarkusBuild is expected to run after the project has passed the tests
quarkusBuild.configure(task -> task.shouldRunAfter(tasks.withType(Test.class)));
quarkusBuildCacheableAppParts.configure(task -> task.shouldRunAfter(tasks.withType(Test.class)));

SourceSet generatedSourceSet = sourceSets.create(QuarkusGenerateCode.QUARKUS_GENERATED_SOURCES);
SourceSet generatedTestSourceSet = sourceSets.create(QuarkusGenerateCode.QUARKUS_TEST_GENERATED_SOURCES);
Expand Down Expand Up @@ -378,16 +404,6 @@ private void verifyGradleVersion() {
}
}

private void configureBuildNativeTask(Project project) {
project.getGradle().getTaskGraph().whenReady(taskGraph -> {
if (taskGraph.hasTask(project.getPath() + BUILD_NATIVE_TASK_NAME)
|| taskGraph.hasTask(project.getPath() + TEST_NATIVE_TASK_NAME)) {
project.getExtensions().getExtraProperties()
.set(QUARKUS_PACKAGE_TYPE, "native");
}
});
}

private void afterEvaluate(Project project) {

visitProjectDependencies(project, project, new HashSet<>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
Expand All @@ -25,31 +24,44 @@
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.testing.Test;
import org.gradle.jvm.tasks.Jar;
import org.gradle.process.JavaForkOptions;

import io.quarkus.bootstrap.BootstrapConstants;
import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.bootstrap.resolver.AppModelResolver;
import io.quarkus.gradle.AppModelGradleResolver;
import io.quarkus.gradle.QuarkusPlugin;
import io.quarkus.gradle.dsl.Manifest;
import io.quarkus.gradle.tasks.AbstractQuarkusExtension;
import io.quarkus.gradle.tasks.QuarkusBuild;
import io.quarkus.gradle.tasks.QuarkusGradleUtils;
import io.quarkus.gradle.tooling.ToolingUtils;
import io.quarkus.runtime.LaunchMode;

public class QuarkusPluginExtension {
private final Project project;

private final Property<String> finalName;

private final MapProperty<String, String> quarkusBuildProperties;
public abstract class QuarkusPluginExtension extends AbstractQuarkusExtension {
private final SourceSetExtension sourceSetExtension;

private final Property<Boolean> cacheLargeArtifacts;
private final Property<Boolean> cleanupBuildOutput;

public QuarkusPluginExtension(Project project) {
this.project = project;
super(project);

finalName = project.getObjects().property(String.class);
finalName.convention(project.provider(() -> String.format("%s-%s", project.getName(), project.getVersion())));
this.cleanupBuildOutput = project.getObjects().property(Boolean.class)
.convention(true);
this.cacheLargeArtifacts = project.getObjects().property(Boolean.class)
.convention(!System.getenv().containsKey("CI"));

this.sourceSetExtension = new SourceSetExtension();
this.quarkusBuildProperties = project.getObjects().mapProperty(String.class, String.class);
}

public Manifest getManifest() {
return manifest();
}

@SuppressWarnings("unused")
public void manifest(Action<Manifest> action) {
action.execute(this.getManifest());
}

public void beforeTest(Test task) {
Expand Down Expand Up @@ -86,44 +98,43 @@ public void beforeTest(Test task) {
task.environment(BootstrapConstants.TEST_TO_MAIN_MAPPINGS, fileList);
project.getLogger().debug("test dir mapping - {}", fileList);

final String nativeRunner = task.getProject().getBuildDir().toPath()
.resolve(buildNativeRunnerName(props))
.toAbsolutePath()
.toString();
QuarkusBuild quarkusBuild = project.getTasks().named(QuarkusPlugin.QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class)
.get();
String nativeRunner = quarkusBuild.getNativeRunner().toPath().toAbsolutePath().toString();
props.put("native.image.path", nativeRunner);
} catch (Exception e) {
throw new IllegalStateException("Failed to resolve deployment classpath", e);
}
}

public String buildNativeRunnerName(final Map<String, Object> taskSystemProps) {
Properties properties = new Properties(taskSystemProps.size());
properties.putAll(taskSystemProps);
quarkusBuildProperties.get().entrySet()
.forEach(buildEntry -> properties.putIfAbsent(buildEntry.getKey(), buildEntry.getValue()));
System.getProperties().entrySet()
.forEach(propEntry -> properties.putIfAbsent(propEntry.getKey(), propEntry.getValue()));
System.getenv().entrySet().forEach(
envEntry -> properties.putIfAbsent(envEntry.getKey(), envEntry.getValue()));
StringBuilder nativeRunnerName = new StringBuilder();

if (properties.containsKey("quarkus.package.output-name")) {
nativeRunnerName.append(properties.get("quarkus.package.output-name"));
} else {
nativeRunnerName.append(finalName());
}
if (!properties.containsKey("quarkus.package.add-runner-suffix")
|| (properties.containsKey("quarkus.package.add-runner-suffix")
&& Boolean.parseBoolean((String) properties.get("quarkus.package.add-runner-suffix")))) {
nativeRunnerName.append("-runner");
}
return nativeRunnerName.toString();
}

public Property<String> getFinalName() {
return finalName;
}

/**
* Whether the build output, build/*-runner[.jar] and build/quarkus-app, for other package types than the
* currently configured one are removed, default is 'true'.
*/
public Property<Boolean> getCleanupBuildOutput() {
return cleanupBuildOutput;
}

public void setCleanupBuildOutput(boolean cleanupBuildOutput) {
this.cleanupBuildOutput.set(cleanupBuildOutput);
}

/**
* Whether large build artifacts, like uber-jar and native runners, are cached. Defaults to 'false' if the 'CI' environment
* variable is set, otherwise defaults to 'true'.
*/
public Property<Boolean> getCacheLargeArtifacts() {
return cacheLargeArtifacts;
}

public void setCacheLargeArtifacts(boolean cacheLargeArtifacts) {
this.cacheLargeArtifacts.set(cacheLargeArtifacts);
}

public String finalName() {
return getFinalName().get();
}
Expand Down Expand Up @@ -167,6 +178,26 @@ public ApplicationModel getApplicationModel(LaunchMode mode) {
return ToolingUtils.create(project, mode);
}

/**
* Adds an action to configure the {@code JavaForkOptions} to build a Quarkus application.
*
* @param action configuration action
*/
@SuppressWarnings("unused")
public void buildForkOptions(Action<? super JavaForkOptions> action) {
buildForkOptions.add(action);
}

/**
* Adds an action to configure the {@code JavaForkOptions} to generate Quarkus code.
*
* @param action configuration action
*/
@SuppressWarnings("unused")
public void codeGenForkOptions(Action<? super JavaForkOptions> action) {
codeGenForkOptions.add(action);
}

/**
* Returns the last file from the specified {@link FileCollection}.
*/
Expand Down Expand Up @@ -215,6 +246,7 @@ public Path appJarOrClasses() {
return classesDir;
}

@SuppressWarnings("unused")
public MapProperty<String, String> getQuarkusBuildProperties() {
return quarkusBuildProperties;
}
Expand All @@ -226,5 +258,4 @@ public void set(String name, @Nullable String value) {
public void set(String name, Property<String> value) {
quarkusBuildProperties.put(String.format("quarkus.%s", name), value);
}

}

0 comments on commit e8b1d20

Please sign in to comment.