From b5fa4b90a7a1a9552302bbc83e07024c127345bc Mon Sep 17 00:00:00 2001 From: Aurimas Liutikas Date: Wed, 11 Dec 2024 14:47:59 -0800 Subject: [PATCH] Add a post about artifact aggregation --- Gemfile.lock | 2 +- _posts/2024-12-11-Together-In-Isolation.md | 91 ++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 _posts/2024-12-11-Together-In-Isolation.md diff --git a/Gemfile.lock b/Gemfile.lock index bbfd7c6..b5e696a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -274,7 +274,7 @@ DEPENDENCIES github-pages jekyll-feed tzinfo-data - webrick (~> 1.8.1) + webrick (~> 1.8.2) BUNDLED WITH 2.4.7 diff --git a/_posts/2024-12-11-Together-In-Isolation.md b/_posts/2024-12-11-Together-In-Isolation.md new file mode 100644 index 0000000..6aa5c95 --- /dev/null +++ b/_posts/2024-12-11-Together-In-Isolation.md @@ -0,0 +1,91 @@ +--- +layout: post +title: Together in Isolation - Isolated Project Safe Way to Aggregate Optional Artifacts +header: header-eng +--- + +One common thing to do in a Gradle project is collect artifacts from a subset of projects to produce an aggregate +artifact. An example of it might be collecting test reports from each project that have tests to create a combined +test report for all projects. Historically, someone might have done this with: + +Root project `build.gradle.kts`: +```kotlin +abstract class AggregatingTask : DefaultTask() { + @get:InputFiles + abstract val inputs: ListProperty + @get:OutputFile + abstract val output: RegularFile + @TaskAction fun aggregate() { /* process inputs */ } +} + +tasks.register("aggregatingTask") +``` + +Each project that has an artifact in their `build.gradle.kts`: +```kotlin +rootProject.tasks.named( + "aggregatingTask" +).configure { + inputs.add(reportTask.flatmap { it.reportFile }) +} +``` + +Sadly, in addition to being fragile with hardcoding of the task name/type, it is also not valid with Gradle +[Isolated Project](https://docs.gradle.org/current/userguide/isolated_projects.html) feature enabled. Is there a better +way? Yes (but it is a tad verbose)! + +Root project `build.gradle.kts`: +```kotlin +val reportConsumer = configurations.create("reportConsumer") { + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named("my-reports") + ) + } +} +// Add all subprojects to dependencies +subprojects { + reportConsumer.dependencies.add(dependencies.create(this)) +} +// Be lenient if not all projects have this artifact +val reportCollection = reportConsumer.incoming.artifactView { + lenient(true) +}.files + +abstract class AggregatingTask : DefaultTask() { + @get:InputFiles + abstract val inputs: ConfigurableFileCollection + @get:OutputFile + abstract val output: RegularFile + @TaskAction fun aggregate() { /* process inputs */ } +} +tasks.register("aggregatingTask") { + inputs.from(reportCollection) +} + +``` + +Each project that has an artifact in their `build.gradle.kts`: +```kotlin +configurations.create("reportPublisher") { + // Note, consumed and resolved are opposite of root project + isCanBeResolved = false + isCanBeConsumed = true + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named("my-reports") + ) + } +} +artifacts.add("reportPublisher", reportTask) +``` + +With this set up, root project still has the aggregating task that is able to build the report from all the artifacts +from each project, but instead of each subproject contributing to that task directly, everything flows through Gradle +artifact publishing and attribute resolution mechanisms. + +Pro-tip: to visualize what you have added to artifacts, you can use `./gradlew :projectA:outgoingVariants`