From aa5add11a661a7b598a37201bf9097606c9b83f8 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 14 Oct 2026 17:13:05 +0200 Subject: [PATCH 1/8] docs: add Gradle agent guardrails --- AGENTS.md | 62 +++++++++++++++++++++++++++++++++++++++++++ build-logic/AGENTS.md | 42 +++++++++++++++++++++++++++++ build-logic/CLAUDE.md | 1 + buildSrc/AGENTS.md | 47 ++++++++++++++++++++++++++++++++ buildSrc/CLAUDE.md | 1 + 5 files changed, 153 insertions(+) create mode 100644 build-logic/AGENTS.md create mode 100644 build-logic/CLAUDE.md create mode 100644 buildSrc/AGENTS.md create mode 100644 buildSrc/CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 065d5f9b77c..f4bf2ebe820 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,68 @@ docs/ Developer documentation (see below) **When working on a topic above, read the linked file first** — they are the source of truth maintained by humans. +## Gradle changes + +Before editing files under `buildSrc/` or `build-logic/`, read that folder's +`AGENTS.md`. These local instructions supplement the rules below. + +### Required skill + +- Before modifying Gradle scripts, settings, properties, version catalogs, or task/plugin + source, use the official `gradle-best-practices` skill in audit mode. Scope the audit to + the requested change and its affected consumers. +- The complete installed or repository-provided skill bundle, including its reference + files, must be available. A skill name or remote link alone does not satisfy this prerequisite. +- If unavailable or incomplete, report: "gradle-best-practices is required for this change + but is unavailable." Provide the [official installation instructions](https://github.com/gradle/gradle-skills#installation). + Continue read-only investigation, but make no Gradle changes until the skill is available. +- In Codex, invoke `$gradle-best-practices`. Its `$skill-installer`, when available, can + install the `skills/gradle-best-practices` directory from `gradle/gradle-skills`. + In Claude Code, install and invoke the official plugin: + + ```text + /plugin marketplace add gradle/gradle-skills + /plugin install gradle-skills@gradle-skills + /gradle-skills:gradle-best-practices + ``` + +- Check recommendations against the pinned Gradle wrapper and the JVMs that execute the + affected code. A newer or nightly catalog may describe APIs unavailable to this build. +- Keep fixes within the requested scope. Record unrelated findings separately. An audit + does not authorise wrapper upgrades, dependency updates, or structural migrations. + +### Guardrails + +- Find affected plugin IDs, extensions, properties, tasks, fixtures, and consuming modules + before editing. Preserve their contracts and defaults unless the request changes them. +- Prefer `register`, `named`, `configureEach`, and provider composition. Avoid eager task + traversal, dependency resolution, external processes, and expensive work during configuration. + Check when providers are consumed and whether the receiving API supports them. +- Wire producer outputs into consumer inputs where data flows. Preserve the distinction + between execution dependencies, ordering, lifecycle aggregation, and finalisation. + `mustRunAfter` does not cause a prerequisite task to run. +- Keep task execution independent of the live `Project` model. Use declared properties + and supported injected services. Declare relevant inputs and distinct outputs, and choose + cacheability and path sensitivity according to the task's behaviour. +- Use public Gradle APIs. Avoid new cross-project mutable configuration and lifecycle + callbacks such as `afterEvaluate` when providers or plugin callbacks express the relationship. +- Sharing a source root, such as `buildSrc`, does not make plugins one component. Integrate + through plugin IDs, documented extensions, and Gradle model APIs. Avoid another plugin's + implementation classes, internal helpers, and incidental task layout. +- Apply required plugins explicitly; use `withPlugin` for optional integrations. Extract + shared code only for shared knowledge. Prefer small local duplication over premature coupling. +- Verify changed behaviour with focused tests and a representative consumer. When changing + configuration-cache behaviour, verify both storage and reuse. When changing incremental or + build-cache behaviour, inspect task outcomes and artifacts. Preserve validation and diagnostics. + +### Documentation boundary + +Update [docs/how_to_work_with_gradle.md](docs/how_to_work_with_gradle.md) only when a change +affects general Gradle guidance or practices broadly applicable to this repository. This +includes repository constraints that prevent following a best practice. Preserve the project +specifics intentionally documented there. Keep new plugin implementation details, local +workarounds, and task-specific procedures in source comments or local documentation. + ## Build quick reference ```shell diff --git a/build-logic/AGENTS.md b/build-logic/AGENTS.md new file mode 100644 index 00000000000..05e128b60dd --- /dev/null +++ b/build-logic/AGENTS.md @@ -0,0 +1,42 @@ +# Agent context for build-logic + +Read the [root Gradle instructions](../AGENTS.md#gradle-changes), including the mandatory +`gradle-best-practices` prerequisite, before changing build behaviour. Use the relevant +sections of the [Gradle guide](../docs/how_to_work_with_gradle.md) for explanations. + +## Scope and boundaries + +`build-logic` is an included build, registered through `pluginManagement.includeBuild`. +It owns project-facing conventions and plugins for smoke-test applications, tag registration, +and testcontainers. + +- Keep settings, repositories, plugin resolution, and dependency declarations explicit in + this build. Root-build configuration does not automatically propagate here. Reuse the + existing import of `../gradle/libs.versions.toml` and preserve repository proxy behaviour. +- Place new project-facing conventions here when they fit the existing architecture. + Keep module-specific choices in consuming modules and expose only the needed extension + properties. Follow existing placement for changes to existing plugins. +- Sharing a source root does not justify coupling plugin implementations. Follow the + root guardrails for explicit dependencies and narrow public contracts. +- Preserve each subproject's JVM target and worker requirements. Smoke-test fixtures run + nested builds with older Gradle versions; preserve their Java 21 test launcher. +- Keep optional plugin integrations independent of application order. Check both Groovy + and Kotlin consumers when changing a convention or extension API. +- Keep plugin dependencies on the build-logic classpath. Verify packaging when changing + dependencies so consumer application or agent artifacts do not gain build-only libraries. + +## Verification + +Use existing JUnit 5 tests and Kotlin fixtures. Reuse the affected module's TestKit setup, +including repository proxy handling and environment isolation. Keep language migrations +outside the requested change. Verify plugin integration independently and in composition +where applicable, plus a representative consuming module. + +Start with the affected module's tests, for example: + +```shell +./gradlew -p build-logic :smoke-test:test +``` + +For changes spanning modules, use `./gradlew :build-logic:check` from the repository root. +Run the applicable formatting checks, inspect the diff, and run `git diff --check`. diff --git a/build-logic/CLAUDE.md b/build-logic/CLAUDE.md new file mode 100644 index 00000000000..43c994c2d36 --- /dev/null +++ b/build-logic/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/buildSrc/AGENTS.md b/buildSrc/AGENTS.md new file mode 100644 index 00000000000..d40b7e495de --- /dev/null +++ b/buildSrc/AGENTS.md @@ -0,0 +1,47 @@ +# Agent context for buildSrc + +Read the [root Gradle instructions](../AGENTS.md#gradle-changes), including the mandatory +`gradle-best-practices` prerequisite, before changing build behaviour. Use the relevant +sections of the [Gradle guide](../docs/how_to_work_with_gradle.md) for explanations. + +## Scope and boundaries + +`buildSrc` is built automatically before the main build. It contains binary plugins, +precompiled conventions, shared types, and worker support, with subprojects for call-site +instrumentation and the modifiable-config agent. + +- Prefer `build-logic` for new project-facing conventions unless a shared-classloader + requirement justifies `buildSrc`. Follow existing placement for changes to existing plugins. +- Preserve Java 8 compatibility for build-time instrumentation and Muzzle workers. The Gradle + daemon JVM, compiler JVM, bytecode target, and worker JVM are separate constraints. +- Preserve plugin classloader boundaries. Do not add an optional external plugin to + `implementation` merely to make typed imports compile. Keep documented integrations such + as [JMH](src/main/kotlin/dd-trace-java.jmh-conventions.gradle.kts) working on the consumer's + plugin classpath. +- Sharing this source root does not justify coupling plugin implementations. Follow the + root guardrails for explicit dependencies and narrow public contracts. +- Check Groovy and Kotlin consumers when changing extension or convention APIs. Keep + existing defaults, repository proxies, and worker/toolchain selection working. +- Treat settings, repositories, and dependency declarations as belonging to this build. + Import the shared version catalog through the existing settings configuration. + +## Verification + +Use existing JUnit 5 tests and Kotlin fixtures for plugin behaviour. Reuse +[GradleFixture](src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt) for TestKit coverage; +keep language migrations outside the requested change. Check integrations with the involved +plugins applied independently and together where applicable. + +CLI tests require `-PrunBuildSrcTests`; a successful build with skipped tests is insufficient. +Use the fully qualified `:test` selector when filtering tests, so the filter does not reach +unrelated subprojects: + +```shell +./gradlew -p buildSrc :test -PrunBuildSrcTests --tests '*ChangedPluginTest' +./gradlew -p buildSrc spotlessCheck +``` + +Replace the example test filter with the affected test class. Run the affected subproject's +own tests when changing call-site instrumentation or the modifiable-config agent. Verify a +representative consuming module for changes to shared conventions, then inspect the diff +and run `git diff --check`. diff --git a/buildSrc/CLAUDE.md b/buildSrc/CLAUDE.md new file mode 100644 index 00000000000..43c994c2d36 --- /dev/null +++ b/buildSrc/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md From afbb780fd5f66bc79dac8e08e18275b518a83cd4 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 15 Oct 2026 15:13:36 +0200 Subject: [PATCH 2/8] docs: move shared Gradle guidance into the guide --- AGENTS.md | 62 +-------------- build-logic/AGENTS.md | 57 +++++--------- buildSrc/AGENTS.md | 58 +++++--------- docs/how_to_work_with_gradle.md | 130 +++++++++++++++++++++++++++----- 4 files changed, 153 insertions(+), 154 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f4bf2ebe820..cea154a77ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,65 +47,9 @@ docs/ Developer documentation (see below) ## Gradle changes -Before editing files under `buildSrc/` or `build-logic/`, read that folder's -`AGENTS.md`. These local instructions supplement the rules below. - -### Required skill - -- Before modifying Gradle scripts, settings, properties, version catalogs, or task/plugin - source, use the official `gradle-best-practices` skill in audit mode. Scope the audit to - the requested change and its affected consumers. -- The complete installed or repository-provided skill bundle, including its reference - files, must be available. A skill name or remote link alone does not satisfy this prerequisite. -- If unavailable or incomplete, report: "gradle-best-practices is required for this change - but is unavailable." Provide the [official installation instructions](https://github.com/gradle/gradle-skills#installation). - Continue read-only investigation, but make no Gradle changes until the skill is available. -- In Codex, invoke `$gradle-best-practices`. Its `$skill-installer`, when available, can - install the `skills/gradle-best-practices` directory from `gradle/gradle-skills`. - In Claude Code, install and invoke the official plugin: - - ```text - /plugin marketplace add gradle/gradle-skills - /plugin install gradle-skills@gradle-skills - /gradle-skills:gradle-best-practices - ``` - -- Check recommendations against the pinned Gradle wrapper and the JVMs that execute the - affected code. A newer or nightly catalog may describe APIs unavailable to this build. -- Keep fixes within the requested scope. Record unrelated findings separately. An audit - does not authorise wrapper upgrades, dependency updates, or structural migrations. - -### Guardrails - -- Find affected plugin IDs, extensions, properties, tasks, fixtures, and consuming modules - before editing. Preserve their contracts and defaults unless the request changes them. -- Prefer `register`, `named`, `configureEach`, and provider composition. Avoid eager task - traversal, dependency resolution, external processes, and expensive work during configuration. - Check when providers are consumed and whether the receiving API supports them. -- Wire producer outputs into consumer inputs where data flows. Preserve the distinction - between execution dependencies, ordering, lifecycle aggregation, and finalisation. - `mustRunAfter` does not cause a prerequisite task to run. -- Keep task execution independent of the live `Project` model. Use declared properties - and supported injected services. Declare relevant inputs and distinct outputs, and choose - cacheability and path sensitivity according to the task's behaviour. -- Use public Gradle APIs. Avoid new cross-project mutable configuration and lifecycle - callbacks such as `afterEvaluate` when providers or plugin callbacks express the relationship. -- Sharing a source root, such as `buildSrc`, does not make plugins one component. Integrate - through plugin IDs, documented extensions, and Gradle model APIs. Avoid another plugin's - implementation classes, internal helpers, and incidental task layout. -- Apply required plugins explicitly; use `withPlugin` for optional integrations. Extract - shared code only for shared knowledge. Prefer small local duplication over premature coupling. -- Verify changed behaviour with focused tests and a representative consumer. When changing - configuration-cache behaviour, verify both storage and reuse. When changing incremental or - build-cache behaviour, inspect task outcomes and artifacts. Preserve validation and diagnostics. - -### Documentation boundary - -Update [docs/how_to_work_with_gradle.md](docs/how_to_work_with_gradle.md) only when a change -affects general Gradle guidance or practices broadly applicable to this repository. This -includes repository constraints that prevent following a best practice. Preserve the project -specifics intentionally documented there. Keep new plugin implementation details, local -workarounds, and task-specific procedures in source comments or local documentation. +Follow the [Gradle guide](docs/how_to_work_with_gradle.md), including its +[required skill](docs/how_to_work_with_gradle.md#gradle-skills). Before editing files under +`buildSrc/` or `build-logic/`, also read that folder's `AGENTS.md`. ## Build quick reference diff --git a/build-logic/AGENTS.md b/build-logic/AGENTS.md index 05e128b60dd..6df2b453f6e 100644 --- a/build-logic/AGENTS.md +++ b/build-logic/AGENTS.md @@ -1,42 +1,25 @@ -# Agent context for build-logic - -Read the [root Gradle instructions](../AGENTS.md#gradle-changes), including the mandatory -`gradle-best-practices` prerequisite, before changing build behaviour. Use the relevant -sections of the [Gradle guide](../docs/how_to_work_with_gradle.md) for explanations. - -## Scope and boundaries - -`build-logic` is an included build, registered through `pluginManagement.includeBuild`. -It owns project-facing conventions and plugins for smoke-test applications, tag registration, -and testcontainers. - -- Keep settings, repositories, plugin resolution, and dependency declarations explicit in - this build. Root-build configuration does not automatically propagate here. Reuse the - existing import of `../gradle/libs.versions.toml` and preserve repository proxy behaviour. -- Place new project-facing conventions here when they fit the existing architecture. - Keep module-specific choices in consuming modules and expose only the needed extension - properties. Follow existing placement for changes to existing plugins. -- Sharing a source root does not justify coupling plugin implementations. Follow the - root guardrails for explicit dependencies and narrow public contracts. -- Preserve each subproject's JVM target and worker requirements. Smoke-test fixtures run - nested builds with older Gradle versions; preserve their Java 21 test launcher. -- Keep optional plugin integrations independent of application order. Check both Groovy - and Kotlin consumers when changing a convention or extension API. -- Keep plugin dependencies on the build-logic classpath. Verify packaging when changing - dependencies so consumer application or agent artifacts do not gain build-only libraries. - -## Verification - -Use existing JUnit 5 tests and Kotlin fixtures. Reuse the affected module's TestKit setup, -including repository proxy handling and environment isolation. Keep language migrations -outside the requested change. Verify plugin integration independently and in composition -where applicable, plus a representative consuming module. - -Start with the affected module's tests, for example: +# Working in build-logic + +Use `gradle-best-practices` before changing Gradle code here. If it is missing or incomplete, +follow the [skill setup](../docs/how_to_work_with_gradle.md#gradle-skills) before editing. + +This included build holds project conventions and the smoke-test, tag-registry and +testcontainers plugins. Its settings import `../gradle/libs.versions.toml` and configure +repository proxies separately from the main build. + +The smoke-test plugin runs nested builds with older Gradle versions. Its tests use Java 21 +for that reason; keep that launcher when changing the tests or toolchain configuration. + +## Tests + +The plugin tests use JUnit 5 and Kotlin. Reuse the module's TestKit setup, including its +repository proxy handling and environment isolation. + +Run the tests for the module you changed, for example: ```shell ./gradlew -p build-logic :smoke-test:test ``` -For changes spanning modules, use `./gradlew :build-logic:check` from the repository root. -Run the applicable formatting checks, inspect the diff, and run `git diff --check`. +For changes across modules, `./gradlew :build-logic:check` runs the included build's checks +from the repository root. diff --git a/buildSrc/AGENTS.md b/buildSrc/AGENTS.md index d40b7e495de..61bec149c03 100644 --- a/buildSrc/AGENTS.md +++ b/buildSrc/AGENTS.md @@ -1,47 +1,25 @@ -# Agent context for buildSrc +# Working in buildSrc -Read the [root Gradle instructions](../AGENTS.md#gradle-changes), including the mandatory -`gradle-best-practices` prerequisite, before changing build behaviour. Use the relevant -sections of the [Gradle guide](../docs/how_to_work_with_gradle.md) for explanations. +Use `gradle-best-practices` before changing Gradle code here. If it is missing or incomplete, +follow the [skill setup](../docs/how_to_work_with_gradle.md#gradle-skills) before editing. -## Scope and boundaries +New project-facing conventions usually belong in `build-logic`. This folder still holds +plugins and shared types that need the `buildSrc` classloader. Keep changes to existing +plugins where they are unless the task includes moving them. -`buildSrc` is built automatically before the main build. It contains binary plugins, -precompiled conventions, shared types, and worker support, with subprojects for call-site -instrumentation and the modifiable-config agent. +The Java 8 target in `build.gradle.kts` is needed by build-time instrumentation and Muzzle +workers. A newer Gradle daemon or compiler JVM does not remove that requirement. -- Prefer `build-logic` for new project-facing conventions unless a shared-classloader - requirement justifies `buildSrc`. Follow existing placement for changes to existing plugins. -- Preserve Java 8 compatibility for build-time instrumentation and Muzzle workers. The Gradle - daemon JVM, compiler JVM, bytecode target, and worker JVM are separate constraints. -- Preserve plugin classloader boundaries. Do not add an optional external plugin to - `implementation` merely to make typed imports compile. Keep documented integrations such - as [JMH](src/main/kotlin/dd-trace-java.jmh-conventions.gradle.kts) working on the consumer's - plugin classpath. -- Sharing this source root does not justify coupling plugin implementations. Follow the - root guardrails for explicit dependencies and narrow public contracts. -- Check Groovy and Kotlin consumers when changing extension or convention APIs. Keep - existing defaults, repository proxies, and worker/toolchain selection working. -- Treat settings, repositories, and dependency declarations as belonging to this build. - Import the shared version catalog through the existing settings configuration. +JMH uses the consuming build's plugin classpath. Read the comments in +[its convention](src/main/kotlin/dd-trace-java.jmh-conventions.gradle.kts) before changing +that integration or adding JMH to `implementation`. -## Verification +## Tests -Use existing JUnit 5 tests and Kotlin fixtures for plugin behaviour. Reuse -[GradleFixture](src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt) for TestKit coverage; -keep language migrations outside the requested change. Check integrations with the involved -plugins applied independently and together where applicable. +The plugin tests use JUnit 5 and Kotlin. Reuse +[GradleFixture](src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt) for TestKit tests. +CLI tests need `-PrunBuildSrcTests`; see [the test commands](../docs/how_to_work_with_gradle.md#running-build-logic-tests). +Keep the `:test` selector when filtering so the filter does not reach unrelated subprojects. -CLI tests require `-PrunBuildSrcTests`; a successful build with skipped tests is insufficient. -Use the fully qualified `:test` selector when filtering tests, so the filter does not reach -unrelated subprojects: - -```shell -./gradlew -p buildSrc :test -PrunBuildSrcTests --tests '*ChangedPluginTest' -./gradlew -p buildSrc spotlessCheck -``` - -Replace the example test filter with the affected test class. Run the affected subproject's -own tests when changing call-site instrumentation or the modifiable-config agent. Verify a -representative consuming module for changes to shared conventions, then inspect the diff -and run `git diff --check`. +Changes to `call-site-instrumentation-plugin` or `modifiable-config-agent` also need their +own subproject tests. Run `./gradlew -p buildSrc spotlessCheck` for formatting. diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index b0f8ef43d9a..ca46e3056b8 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -1,10 +1,17 @@ # How to Work with Gradle -This guide covers the fundamentals of working with Gradle in this project. Understanding these concepts will help you navigate the build system and contribute effectively. +This guide covers Gradle practices and how they apply in this repository, including where +our current build cannot yet follow a best practice. The project-specific sections explain +conventions used across the build. + +Update this guide when a change affects that shared advice. Keep implementation notes, +workarounds and procedures for a single plugin or task with its source or local documentation. ## Quick Reference **Core Concepts:** + +- [Gradle skills](#gradle-skills) - Installing and using the required skill for agent-assisted changes - [Gradle Files](#what-are-gradle-files) — Groovy vs Kotlin DSL - [Build Lifecycle](#gradle-build-lifecycle) — Initialization → Configuration → Execution phases - [Tasks](#gradle-tasks) — Lifecycle tasks, inputs/outputs, lazy configuration @@ -25,6 +32,46 @@ This guide covers the fundamentals of working with Gradle in this project. Under > [!TIP] > First time with Gradle? Start with [Gradle Files](#what-are-gradle-files), [Build Lifecycle](#gradle-build-lifecycle), and [Tasks](#gradle-tasks). +## Gradle skills + +Gradle publishes a [best-practices skill](https://github.com/gradle/gradle-skills) for reviewing +build scripts and plugin code. Run it in audit mode before an agent changes Gradle code in +this repository. Review the code being changed and the modules that use it. Keep fixes +within the requested task and report unrelated findings separately. + +Install the whole skill, including its reference files. If it is missing or incomplete, +tell the contributor and point them to the setup below. Read-only investigation can continue; +Gradle edits must wait until the skill is available. A link to the skill does not install it. + +Check the skill's catalog version against our Gradle wrapper and the JVMs that run the +affected code. Some recommendations may describe newer or nightly APIs. + +### Codex + +Ask the built-in [skill installer](https://learn.chatgpt.com/docs/build-skills#install-curated-skills-for-local-use) +to install the skill from Gradle's repository: + +```text +$skill-installer install https://github.com/gradle/gradle-skills/tree/main/skills/gradle-best-practices +``` + +Then invoke it with `$gradle-best-practices`. If it does not appear after installation, +restart the session. + +### Claude Code + +Install Gradle's plugin: + +```text +/plugin marketplace add gradle/gradle-skills +/plugin install gradle-skills@gradle-skills +``` + +Invoke the skill with `/gradle-skills:gradle-best-practices`. Each contributor installs the +plugin for their own setup; naming it in repository instructions does not fetch it. +See the [upstream installation guide](https://github.com/gradle/gradle-skills#installation) +for other agents and installation options. + ## What are Gradle Files? Gradle builds are defined through a set of build scripts. These scripts can be written in two Domain Specific Languages (DSLs): **Groovy DSL** and **Kotlin DSL**. @@ -161,12 +208,16 @@ Only tasks required to complete the requested goal are executed. In a well-organized Gradle project, build logic lives in specific places: -| Location | Purpose | -|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `settings.gradle.kts` | Project structure, repository settings, plugin management | -| `build.gradle.kts` | Project-specific build configuration | -| `buildSrc/` | Build logic automatically included by Gradle; contains convention plugins and shared configuration. It's possible to use different location(s) but it requires explicit declaration(s). | -| `gradle/` | Version catalogs and wrapper files and script plugins | +| Location | Purpose | +|-----------------------|--------------------------------------------------------------------| +| `settings.gradle.kts` | Project structure, repositories and plugin management | +| `build.gradle.kts` | Configuration for one project | +| `buildSrc/` | Build logic Gradle makes available automatically to the main build | +| `build-logic/` | Build logic included explicitly through plugin management | +| `gradle/` | Version catalogs, wrapper files and remaining script plugins | + +`buildSrc` and included builds have their own settings and dependencies. Repositories and +version catalogs from the main build are not inherited; configure or import them explicitly. > [!CAUTION] > Script plugins are not recommended. The best practice for developing our build logic in plugins is @@ -174,6 +225,16 @@ In a well-organized Gradle project, build logic lives in specific places: ### Running Build Logic Tests +Use unit tests for helpers and TestKit for behaviour that depends on Gradle, such as task +wiring and plugin application. Reuse the existing fixtures. When changing how plugins work +together, test the relevant application orders and try a module that uses them. +See Gradle's [plugin testing guide](https://docs.gradle.org/current/userguide/testing_gradle_plugins.html). + +Check task outcomes and generated files as well as the build result. For configuration-cache +changes, run the same task twice with `--configuration-cache` and check that the second run +reuses the entry. For incremental changes, check that unchanged inputs leave the task +`UP-TO-DATE` and changed inputs update its output. Use a clean fixture to check `FROM-CACHE`. + Running `buildSrc/` tests within IntelliJ IDEA works without special treatment because the build detects the injected `idea.active` system property. @@ -253,6 +314,10 @@ In custom tasks, use annotations to declare inputs and outputs: More annotations are documented [here](https://docs.gradle.org/current/userguide/implementing_custom_tasks.html#annotating_inputs_and_outputs). +Give each task its own outputs and choose path sensitivity to match what it reads. For custom +task classes, use `@CacheableTask` when the outputs are reproducible from the declared inputs, +or `@DisableCachingByDefault` with a reason when caching is unsuitable. + ##### In custom tasks declared with DSL For ad-hoc tasks or when you can't use annotations, declare inputs and outputs programmatically via the `inputs` and `outputs` properties: @@ -333,6 +398,17 @@ When running Gradle tasks, you'll see status labels indicating what happened dur | `NO-SOURCE` | The task had no input files to process | +### Task Dependencies + +When a task reads another task's output, pass the output provider to the consuming task's +input. Gradle can then infer the dependency; see [Using Providers](#using-providers). +Keep an explicit `dependsOn` when a prerequisite has no output to wire, or when a lifecycle +task collects other tasks. + +`mustRunAfter` and `shouldRunAfter` only order tasks already selected to run. They do not +schedule a prerequisite. Use `finalizedBy` for work such as cleanup that must follow a task, +including when that task fails. See [Controlling Task Execution](https://docs.gradle.org/current/userguide/controlling_task_execution.html). + ### Tasks tips - **Prefer `Sync` over `Copy`**: `Sync` mirrors the source to the destination by removing files that no longer exist @@ -1131,6 +1207,23 @@ common configuration patterns and can be applied like any other plugin. > Convention plugins promote consistency across modules. Instead of copy-pasting configuration, > define it once and apply it everywhere. +### Keeping Plugins Independent + +Plugins in the same source root, including `buildSrc`, still have separate responsibilities. +Use plugin IDs, documented extensions and Gradle model APIs to connect them. Depending on +another plugin's implementation classes, internal helpers or undocumented task names makes +either plugin harder to change. Share code when it represents the same knowledge and should change together; +similar-looking code alone is not a reason to join two plugins. + +If a plugin requires another plugin, apply it explicitly with `pluginManager.apply`. +For an optional integration, use `pluginManager.withPlugin` so it works in either application +order. A one-time `hasPlugin` check misses plugins applied later. + +Use public Gradle APIs and keep configuration in the plugin that owns it. Prefer providers +and plugin callbacks to `afterEvaluate` or changing another project's model. Keep plugin +dependencies on the build classpath so they do not become application or agent dependencies. +See Gradle's [plugin development guide](https://docs.gradle.org/current/userguide/implementing_gradle_plugins_binary.html). + ### Project Convention Plugins Files ending in `.gradle.kts` placed in `buildSrc/src/main/kotlin/` target `Project` and can configure tasks, dependencies, and extensions. The `buildSrc/` directory is automatically included by Gradle before the main build. @@ -1368,21 +1461,22 @@ tasks.withType().configureEach { ### Using Providers -```Gradle Kotlin DSL -// ❌ Eager - version is read immediately -tasks.register("myJar") { - archiveVersion.set(project.version.toString()) -} +Pass providers through to properties and file collections that accept them. Use `map` to +transform a value and `flatMap` when the result is itself a provider, such as a task's +output property. + +With the Java plugin applied, this copies its JAR without resolving the path during configuration: -// ✅ Lazy - version is read when the jar task runs -tasks.register("myJar") { - archiveVersion.set(project.provider { project.version.toString() }) +```Gradle Kotlin DSL +tasks.register("stageJar") { + from(tasks.named("jar").flatMap { it.archiveFile }) + into(layout.buildDirectory.dir("staged")) } ``` -> [!TIP] -> In Gradle Kotlin DSL (and Groovy DSL) you can use `=` instead of `set(...)`, e.g. -> `archiveVersion = project.version.toString()` +Check the receiving API before passing it a provider. An `Any` or `Object` parameter may +turn it into a string instead of reading its value. For JVM arguments, see +[Lazy JVM Arguments and System Properties](#lazy-jvm-arguments-and-system-properties). ### Lazy JVM Arguments and System Properties From 8040547f56d3abe443eec19a1e75147ae99e7229 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 20 Oct 2026 14:11:40 +0200 Subject: [PATCH 3/8] docs: clarify project dependencies in build logic --- docs/how_to_work_with_gradle.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index ca46e3056b8..5e6b83c18d4 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -1224,6 +1224,20 @@ and plugin callbacks to `afterEvaluate` or changing another project's model. Kee dependencies on the build classpath so they do not become application or agent dependencies. See Gradle's [plugin development guide](https://docs.gradle.org/current/userguide/implementing_gradle_plugins_binary.html). +### Dependencies on Main Projects + +As a rule of thumb, keep build tooling independent of production-module code. Avoid importing +or copying production classes into plugin implementations. Supply required files and artifacts +through declared task inputs. Document why an exception is needed. + +Keep reusable plugins independent of project names and repository layout. Put references to +concrete project paths in module-specific conventions or the consuming build. + +A convention can still declare [project dependencies](https://docs.gradle.org/current/userguide/multi_project_builds.html) +for the project it configures. These belong to the consuming project, rather than the build +tooling's own dependencies. Gradle orders the corresponding build tasks, which works with +[`buildSrc` being compiled before the main build scripts are evaluated](https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html). + ### Project Convention Plugins Files ending in `.gradle.kts` placed in `buildSrc/src/main/kotlin/` target `Project` and can configure tasks, dependencies, and extensions. The `buildSrc/` directory is automatically included by Gradle before the main build. From e100ae4bf4f4f1b527e8f3713008d2e911e5a671 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 20 Oct 2026 14:44:11 +0200 Subject: [PATCH 4/8] docs: centralise build logic placement advice --- buildSrc/AGENTS.md | 4 +--- docs/how_to_work_with_gradle.md | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/buildSrc/AGENTS.md b/buildSrc/AGENTS.md index 61bec149c03..4d5896a04be 100644 --- a/buildSrc/AGENTS.md +++ b/buildSrc/AGENTS.md @@ -3,9 +3,7 @@ Use `gradle-best-practices` before changing Gradle code here. If it is missing or incomplete, follow the [skill setup](../docs/how_to_work_with_gradle.md#gradle-skills) before editing. -New project-facing conventions usually belong in `build-logic`. This folder still holds -plugins and shared types that need the `buildSrc` classloader. Keep changes to existing -plugins where they are unless the task includes moving them. +For new conventions, follow the [build logic placement advice](../docs/how_to_work_with_gradle.md#build-logic-location). The Java 8 target in `build.gradle.kts` is needed by build-time instrumentation and Muzzle workers. A newer Gradle daemon or compiler JVM does not remove that requirement. diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index 5e6b83c18d4..d1210c17d19 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -219,6 +219,10 @@ In a well-organized Gradle project, build logic lives in specific places: `buildSrc` and included builds have their own settings and dependencies. Repositories and version catalogs from the main build are not inherited; configure or import them explicitly. +New project-facing conventions usually belong in `build-logic`. `buildSrc` still holds +plugins and shared types that need its classloader. Keep changes to existing plugins +where they are unless the task includes moving them. + > [!CAUTION] > Script plugins are not recommended. The best practice for developing our build logic in plugins is > to create _convention plugins_ or _binary plugins_. From 47d23f4a2885fc30441ebb7be9f81f7d001f9be5 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 20 Oct 2026 15:23:45 +0200 Subject: [PATCH 5/8] docs: clarify Gradle guidance and skill setup --- docs/how_to_work_with_gradle.md | 42 +++++++-------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index d1210c17d19..e3e6f62ef2f 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -1,11 +1,10 @@ # How to Work with Gradle -This guide covers Gradle practices and how they apply in this repository, including where -our current build cannot yet follow a best practice. The project-specific sections explain -conventions used across the build. +This guide covers Gradle best practices, how we apply them in this repository, and the +constraints that currently require exceptions. -Update this guide when a change affects that shared advice. Keep implementation notes, -workarounds and procedures for a single plugin or task with its source or local documentation. +Update it when a change affects that shared advice. Keep details specific to one plugin +or task beside its source or in local documentation. ## Quick Reference @@ -40,38 +39,15 @@ this repository. Review the code being changed and the modules that use it. Keep within the requested task and report unrelated findings separately. Install the whole skill, including its reference files. If it is missing or incomplete, -tell the contributor and point them to the setup below. Read-only investigation can continue; -Gradle edits must wait until the skill is available. A link to the skill does not install it. +read the [upstream installation instructions](https://github.com/gradle/gradle-skills#installation) +and help the contributor install it for their agent. If installation cannot be completed +in this session, provide the relevant commands and explain what is blocking it. +Read-only investigation can continue; wait until the complete skill is available before +editing Gradle code. Check the skill's catalog version against our Gradle wrapper and the JVMs that run the affected code. Some recommendations may describe newer or nightly APIs. -### Codex - -Ask the built-in [skill installer](https://learn.chatgpt.com/docs/build-skills#install-curated-skills-for-local-use) -to install the skill from Gradle's repository: - -```text -$skill-installer install https://github.com/gradle/gradle-skills/tree/main/skills/gradle-best-practices -``` - -Then invoke it with `$gradle-best-practices`. If it does not appear after installation, -restart the session. - -### Claude Code - -Install Gradle's plugin: - -```text -/plugin marketplace add gradle/gradle-skills -/plugin install gradle-skills@gradle-skills -``` - -Invoke the skill with `/gradle-skills:gradle-best-practices`. Each contributor installs the -plugin for their own setup; naming it in repository instructions does not fetch it. -See the [upstream installation guide](https://github.com/gradle/gradle-skills#installation) -for other agents and installation options. - ## What are Gradle Files? Gradle builds are defined through a set of build scripts. These scripts can be written in two Domain Specific Languages (DSLs): **Groovy DSL** and **Kotlin DSL**. From 7f056e4a31965522c7c665ff4a43292b143b6ef8 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 21 Oct 2026 11:34:51 +0200 Subject: [PATCH 6/8] docs: Improve wording --- docs/how_to_work_with_gradle.md | 221 ++++++++++++++++++++------------ 1 file changed, 137 insertions(+), 84 deletions(-) diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index e3e6f62ef2f..6883742948c 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -1,10 +1,11 @@ # How to Work with Gradle This guide covers Gradle best practices, how we apply them in this repository, and the -constraints that currently require exceptions. +constraints that currently require exceptions. Understanding these concepts will help +you navigate the build system and contribute effectively. -Update it when a change affects that shared advice. Keep details specific to one plugin -or task beside its source or in local documentation. +The guide describes shared practices and repository constraints. Details specific to one +plugin or task belong beside its source or in local documentation. ## Quick Reference @@ -34,30 +35,27 @@ or task beside its source or in local documentation. ## Gradle skills Gradle publishes a [best-practices skill](https://github.com/gradle/gradle-skills) for reviewing -build scripts and plugin code. Run it in audit mode before an agent changes Gradle code in -this repository. Review the code being changed and the modules that use it. Keep fixes -within the requested task and report unrelated findings separately. +build scripts and plugin code. In this repository, agent-assisted Gradle code changes require +an audit of the affected code and its consuming modules with this skill. The requested task +defines the scope of fixes; unrelated findings are reported separately. -Install the whole skill, including its reference files. If it is missing or incomplete, -read the [upstream installation instructions](https://github.com/gradle/gradle-skills#installation) -and help the contributor install it for their agent. If installation cannot be completed -in this session, provide the relevant commands and explain what is blocking it. -Read-only investigation can continue; wait until the complete skill is available before -editing Gradle code. +The [upstream installation instructions](https://github.com/gradle/gradle-skills#installation) +cover supported agents. A complete installation includes the skill's reference files. +Without them, only read-only investigation is available until installation is complete. -Check the skill's catalog version against our Gradle wrapper and the JVMs that run the -affected code. Some recommendations may describe newer or nightly APIs. +The skill's catalog may describe newer or nightly APIs. Its recommendations need to match +our Gradle wrapper version and the JVMs that run the affected code. ## What are Gradle Files? -Gradle builds are defined through a set of build scripts. These scripts can be written in two Domain Specific Languages (DSLs): **Groovy DSL** and **Kotlin DSL**. +Gradle builds are defined through a set of build scripts. These scripts can be written in two Domain Specific Languages (DSLs): **Groovy DSL** (`*.gradle`) and **Kotlin DSL** (`*.gradle.kts`) . ### Groovy DSL The original Gradle DSL uses Groovy syntax. Files use the `.gradle` extension. Avoid Groovy DSL if you can, small project, however since the project still rely -heaviliy on _script plugins_ written in Groovy, this is not always straighforward to use Kotlin DSL. +heavily on _script plugins_ written in Groovy, this is not always straightforward to use Kotlin DSL. ```Gradle plugins { @@ -116,19 +114,39 @@ Gradle executes builds in distinct phases. Understanding this lifecycle is essen ### 1. Initialization Phase -Gradle determines which projects are part of the build. It executes: +Gradle runs init scripts, then evaluates the settings script to determine which projects belong +to the build. -- **`init.gradle`** (or scripts in `~/.gradle/init.d/`): Global initialization scripts that run before any project is evaluated -- **`settings.gradle.kts`**: Defines the main repository project structure and discovers subprojects +- **[Init scripts](https://docs.gradle.org/current/userguide/init_scripts.html)** can configure + a build without changing its build scripts. They can be passed with `--init-script` or loaded + from the Gradle user home's `init.d/` directory (`~/.gradle/init.d/` by default). -```Gradle Kotlin DSL -// settings.gradle.kts -rootProject.name = "my-project" + For example, an init script can enable offline dependency resolution: + + ```Gradle Kotlin DSL + // offline.init.gradle.kts + gradle.startParameter.isOffline = true + ``` + + ```bash + ./gradlew --init-script offline.init.gradle.kts build + ``` + + This invocation uses cached dependencies and fails if a required dependency is missing. -include("module-a") -include("module-b") -include("module-c:submodule") -``` + +- **`settings.gradle.kts`** defines the root project and includes subprojects. + + The settings script defines the project structure: + + ```Gradle Kotlin DSL + // settings.gradle.kts + rootProject.name = "my-project" + + include(":module-a") + include(":module-b") + include(":module-c:submodule") + ``` ### 2. Configuration Phase @@ -184,36 +202,59 @@ Only tasks required to complete the requested goal are executed. In a well-organized Gradle project, build logic lives in specific places: -| Location | Purpose | -|-----------------------|--------------------------------------------------------------------| -| `settings.gradle.kts` | Project structure, repositories and plugin management | -| `build.gradle.kts` | Configuration for one project | -| `buildSrc/` | Build logic Gradle makes available automatically to the main build | -| `build-logic/` | Build logic included explicitly through plugin management | -| `gradle/` | Version catalogs, wrapper files and remaining script plugins | +| Location | Purpose | +|-----------------------|----------------------------------------------------------------| +| `settings.gradle.kts` | Project structure, repositories and plugin management | +| `build.gradle.kts` | Configuration for one project | +| `buildSrc/` | Legacy build logic, automatically available to the main build | +| `build-logic/` | Build logic included explicitly through plugin management | +| `gradle/` | Version catalogs, wrapper files and remaining *script plugins* | `buildSrc` and included builds have their own settings and dependencies. Repositories and -version catalogs from the main build are not inherited; configure or import them explicitly. +version catalogs from the main build are not inherited, so each build has its own declarations +or explicit imports. + +Gradle [recommends an included `build-logic` build](https://docs.gradle.org/current/userguide/best_practices_structuring_builds.html#favor_composite_builds) +for shared conventions. Its plugins have explicit dependencies and are added only to the +projects that use them. +This limits classpath coupling and can reduce build script recompilation when build logic +changes. Plugins can also be built and tested independently. -New project-facing conventions usually belong in `build-logic`. `buildSrc` still holds -plugins and shared types that need its classloader. Keep changes to existing plugins -where they are unless the task includes moving them. +`buildSrc` is the legacy location for our build logic and remains supported by Gradle. + +Project build scripts and applied *script plugins* load `buildSrc` classes through a common +parent classloader. We are replacing script plugins with convention plugins, using `buildSrc` +as an intermediate step for conventions applied across all modules through existing scripts. +Those scripts still need access to shared classes through that classloader, which currently +prevents those classes from moving into `build-logic`. Independent conventions applied on +demand can go directly into `build-logic`: explicit plugin requests provide their classpath +to the consuming projects. > [!CAUTION] -> Script plugins are not recommended. The best practice for developing our build logic in plugins is -> to create _convention plugins_ or _binary plugins_. +> *Script plugins* are not recommended. Shared build logic belongs in _convention plugins_ +> or _binary plugins_. ### Running Build Logic Tests -Use unit tests for helpers and TestKit for behaviour that depends on Gradle, such as task -wiring and plugin application. Reuse the existing fixtures. When changing how plugins work -together, test the relevant application orders and try a module that uses them. +Unit tests cover helpers, while Gradle's TestKit covers behaviour that depends on Gradle. +Simple declarative conventions may not need dedicated tests. Larger plugins with conditional +behaviour benefit from functional tests covering: + +- Plugin application, including different application orders for interacting plugins +- Configuration roles and dependency resolution +- Task wiring, outcomes and generated files +- Behaviour in a consuming module + +Existing fixtures provide the setup for these tests. Isolated Projects support is not currently +required, but avoiding cross-project state reduces future migration work. See Gradle's [plugin testing guide](https://docs.gradle.org/current/userguide/testing_gradle_plugins.html). -Check task outcomes and generated files as well as the build result. For configuration-cache -changes, run the same task twice with `--configuration-cache` and check that the second run -reuses the entry. For incremental changes, check that unchanged inputs leave the task -`UP-TO-DATE` and changed inputs update its output. Use a clean fixture to check `FROM-CACHE`. +Repeated runs exercise different behaviour: + +- `--rerun` on a task, or `--rerun-tasks` for all selected tasks, forces execution again. +- Two identical runs with `--configuration-cache` show whether the second reuses the entry. +- Unchanged inputs leave an incremental task `UP-TO-DATE`; changed inputs update its output. +- Restoring outputs from a populated build cache into a clean fixture produces `FROM-CACHE`. Running `buildSrc/` tests within IntelliJ IDEA works without special treatment because the build detects the injected `idea.active` system property. @@ -294,9 +335,10 @@ In custom tasks, use annotations to declare inputs and outputs: More annotations are documented [here](https://docs.gradle.org/current/userguide/implementing_custom_tasks.html#annotating_inputs_and_outputs). -Give each task its own outputs and choose path sensitivity to match what it reads. For custom -task classes, use `@CacheableTask` when the outputs are reproducible from the declared inputs, -or `@DisableCachingByDefault` with a reason when caching is unsuitable. +Each task owns its outputs, and path sensitivity reflects which parts of an input path affect +the result. A custom task class can opt into build caching with `@CacheableTask` when its +outputs are reproducible from its declared inputs. `@DisableCachingByDefault` records why +caching is unsuitable otherwise. ##### In custom tasks declared with DSL @@ -380,13 +422,15 @@ When running Gradle tasks, you'll see status labels indicating what happened dur ### Task Dependencies -When a task reads another task's output, pass the output provider to the consuming task's -input. Gradle can then infer the dependency; see [Using Providers](#using-providers). -Keep an explicit `dependsOn` when a prerequisite has no output to wire, or when a lifecycle -task collects other tasks. +Gradle infers a task dependency when a producing task's output provider is connected to a +consuming task's input. The declared inputs and outputs also support up-to-date checks; +see [Using Providers](#using-providers). + +An explicit `dependsOn` schedules a prerequisite even when no artifact flows between tasks. +Lifecycle tasks such as `check` and `build` use it to collect other tasks. `mustRunAfter` and `shouldRunAfter` only order tasks already selected to run. They do not -schedule a prerequisite. Use `finalizedBy` for work such as cleanup that must follow a task, +schedule a prerequisite. `finalizedBy` schedules follow-up work such as cleanup, including when that task fails. See [Controlling Task Execution](https://docs.gradle.org/current/userguide/controlling_task_execution.html). ### Tasks tips @@ -1187,36 +1231,27 @@ common configuration patterns and can be applied like any other plugin. > Convention plugins promote consistency across modules. Instead of copy-pasting configuration, > define it once and apply it everywhere. -### Keeping Plugins Independent - -Plugins in the same source root, including `buildSrc`, still have separate responsibilities. -Use plugin IDs, documented extensions and Gradle model APIs to connect them. Depending on -another plugin's implementation classes, internal helpers or undocumented task names makes -either plugin harder to change. Share code when it represents the same knowledge and should change together; -similar-looking code alone is not a reason to join two plugins. +### Plugin Boundaries -If a plugin requires another plugin, apply it explicitly with `pluginManager.apply`. -For an optional integration, use `pluginManager.withPlugin` so it works in either application -order. A one-time `hasPlugin` check misses plugins applied later. +Plugins retain separate responsibilities even within `buildSrc`. Plugin IDs, documented +extensions, properties, task paths and public Gradle APIs let them interact without depending +on each other's implementation details. Shared code is useful when it represents knowledge +that must change together across plugins. -Use public Gradle APIs and keep configuration in the plugin that owns it. Prefer providers -and plugin callbacks to `afterEvaluate` or changing another project's model. Keep plugin -dependencies on the build classpath so they do not become application or agent dependencies. See Gradle's [plugin development guide](https://docs.gradle.org/current/userguide/implementing_gradle_plugins_binary.html). ### Dependencies on Main Projects -As a rule of thumb, keep build tooling independent of production-module code. Avoid importing -or copying production classes into plugin implementations. Supply required files and artifacts -through declared task inputs. Document why an exception is needed. +Build tooling remains independent of production code. The tasks it registers can consume +production artifacts through declared inputs. Convention plugins can also declare +[project dependencies](https://docs.gradle.org/current/userguide/multi_project_builds.html) +for the projects they configure. -Keep reusable plugins independent of project names and repository layout. Put references to -concrete project paths in module-specific conventions or the consuming build. +Concrete project paths usually belong in the consuming build, keeping reusable plugins +independent of the repository layout. Exceptions have a documented reason. -A convention can still declare [project dependencies](https://docs.gradle.org/current/userguide/multi_project_builds.html) -for the project it configures. These belong to the consuming project, rather than the build -tooling's own dependencies. Gradle orders the corresponding build tasks, which works with -[`buildSrc` being compiled before the main build scripts are evaluated](https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html). +[`buildSrc` is compiled before the main build scripts are evaluated](https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html). +Gradle schedules tasks for project dependencies as part of the main build. ### Project Convention Plugins @@ -1333,7 +1368,7 @@ Script plugins are standalone `.gradle` or `.gradle.kts` files that can be appli apply(from = "$rootDir/gradle/some-script.gradle") ``` -As warned, don't wrtite new ones, use convention plugins instead ! +As warned, don't write new ones, use convention plugins instead ! - **No type safety**: When written in Groovy DSL, you lose IDE support and compile-time checking - **Mixed DSL confusion**: Projects often end up with a mix of Groovy and Kotlin scripts @@ -1455,22 +1490,40 @@ tasks.withType().configureEach { ### Using Providers -Pass providers through to properties and file collections that accept them. Use `map` to -transform a value and `flatMap` when the result is itself a provider, such as a task's -output property. +A provider represents a value that Gradle can query when needed. Properties and file +collections that accept providers retain this connection without reading the value immediately. +`map` transforms a value; `flatMap` connects to another provider, such as a task's output property. -With the Java plugin applied, this copies its JAR without resolving the path during configuration: +With the Java plugin applied, these examples show eager and lazy access to its JAR: ```Gradle Kotlin DSL +// ❌ Eager - reads the archive path while configuring this task +tasks.register("stageJarEager") { + from(tasks.named("jar").get().archiveFile.get()) + into(layout.buildDirectory.dir("staged-eager")) +} + +// ✅ Lazy - preserves the output provider and its dependency on the jar task tasks.register("stageJar") { from(tasks.named("jar").flatMap { it.archiveFile }) into(layout.buildDirectory.dir("staged")) } ``` -Check the receiving API before passing it a provider. An `Any` or `Object` parameter may -turn it into a string instead of reading its value. For JVM arguments, see -[Lazy JVM Arguments and System Properties](#lazy-jvm-arguments-and-system-properties). +The lazy version also carries the producer dependency, so Gradle runs `jar` before `stageJar`. +The eager version passes only a file path and loses that dependency information. + +> [!TIP] +> Kotlin DSL supports `=` for Gradle lazy properties, for example `archiveFileName = provider { "app.jar" }`. +> This has the same effect as `archiveFileName.set(provider { "app.jar" })`. + +Provider support depends on the receiving API. Some APIs with `Any` or `Object` parameters +convert a provider to a string instead of reading its value. JVM arguments are one example; +see [Lazy JVM Arguments and System Properties](#lazy-jvm-arguments-and-system-properties). + +> [!TIP] +> Existing `afterEvaluate` blocks can often be refactored away using providers, +> lazy task configuration and plugin callbacks. ### Lazy JVM Arguments and System Properties From 81b6bf6ed22a10cb65c37e74dc20b6c27409f3c2 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 21 Oct 2026 14:43:07 +0200 Subject: [PATCH 7/8] docs: Improve correctness and wording on the lazy API usage --- docs/how_to_work_with_gradle.md | 310 ++++++++++++++++++++++++-------- 1 file changed, 237 insertions(+), 73 deletions(-) diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index 6883742948c..bd35fbb2eff 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -433,6 +433,8 @@ Lifecycle tasks such as `check` and `build` use it to collect other tasks. schedule a prerequisite. `finalizedBy` schedules follow-up work such as cleanup, including when that task fails. See [Controlling Task Execution](https://docs.gradle.org/current/userguide/controlling_task_execution.html). +Avoid usage of `TaskDependency` interface. + ### Tasks tips - **Prefer `Sync` over `Copy`**: `Sync` mirrors the source to the destination by removing files that no longer exist @@ -1381,43 +1383,54 @@ maintainability and performance. For module build files, use a `dd-trace-java.mo ## Gradle Lazy API -Each time Gradle is invoked it project **must** go through the Gradle **Configuration phase**, in which it -evaluates all build scripts of the participating projects. Any expensive actions in this phase will be run -_every single time_. - -This means inefficient configuration directly impacts developer experience by slowing down all builds -— regardless of which tasks actually execute. While time savings per individual task may seem modest, -they compound quickly: **dd-trace-java has ~630 projects and ~33,000 tasks**. At that scale, even small -inefficiencies add up significantly. +Gradle configures participating projects before executing tasks, unless it can reuse the +*configuration cache*. Configuration work affects even short commands such as `help`. +With roughly 630 projects and 33,000 tasks in dd-trace-java, unnecessary task creation and +configuration can add substantial overhead. -The solution is Gradle's **lazy API**: make task creation and configuration as lazy as possible, so Gradle only realizes and configures objects it actually needs to execute. +Gradle's lazy APIs defer task creation, task configuration and value calculation until needed. ### Why Lazy Configuration Matters -When you use eager APIs, values are computed immediately during configuration—even if the task never runs. Lazy APIs defer this work to execution time, and Gradle can automatically track dependencies between producers and consumers. +The following methods `register`, `named` and `configureEach` avoid creating and +configuring unused tasks. Task configuration still belongs to the configuration phase. +The providers defer value calculation until they are queried, which can happen during +configuration or execution. Providers backed by task outputs also carry producer +dependencies. ### Eager vs Lazy API Comparison -| Eager (Don't ❌) | Lazy (Prefer ✅) | Notes | -|---------------------------------|----------------------------------------|------------------------------------------------------------------------| -| `configurations.getByName("x")` | `configurations.named("x")` | Returns a `NamedDomainObjectProvider` instead of resolving immediately | -| `tasks.getByName("x")` | `tasks.named("x")` | Avoids triggering task creation/configuration | -| `tasks.findByName("x")` | `tasks.named("x")` | Returns `null` if not found, but still realizes the task | -| `tasks.findByPath(":x")` | `tasks.named("x")` on target project | Realizes task eagerly; use project reference with `named()` instead | -| `tasks.create("x")` | `tasks.register("x")` | Task is only created when needed | -| `property.set(someValue)` | `property.set(provider { someValue })` | Defers computation of the value | -| `collection.all { }` | `collection.configureEach { }` | Configures lazily as elements are realized | -| `collection.forEach { }` | `collection.configureEach { }` | Avoids forcing realization of all elements | -| `file(path).exists()` | Use task inputs/outputs | Let Gradle track file dependencies | -| `exec { }.exitValue` | See Exec pattern below | Avoid running processes at configuration time | +| Eager (Don't ❌) | Lazy (Prefer ✅) | Notes | +| ------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------- | +| `configurations.getByName("x")` | `configurations.named("x")` | Defers realization of the configuration object, not dependency resolution | +| `tasks.getByName("x")` | `tasks.named("x")` | Avoids creating and configuring the task during lookup | +| `tasks.findByName("x")` | `tasks.named("x")` | `findByName` returns `null` if absent; `named` throws instead | +| `tasks.findByPath(":x")` | `tasks.named("x")` in the owning project | `findByPath` returns `null` if absent; `named` takes a local name and throws if absent | +| `tasks.create("x")` | `tasks.register("x")` | Registers the task without creating or configuring it | +| `property.set(computeValue())` | `property.set(provider { computeValue() })` | Defers the computation; wrapping an already computed value adds no laziness | +| `collection.all { }` | `collection.configureEach { }` | Runs configuration actions only for realized domain objects | +| `collection.forEach { }` | `collection.configureEach { }` | Applies to configuration actions, rather than collection transformations | +| `file(path).exists()` | Declared task inputs/outputs | Tracks file dependencies; configuration-time existence checks may still be necessary | +| `configuration.files` | `inputs.files(configuration)` | Passes the file collection through and preserves producer dependencies | +| `exec { }.exitValue` | `providers.exec { ... }` | `exec` runs immediately; `providers.exec` defers execution until its output is queried | +| `ProcessBuilder(...).start()` | `providers.exec { ... }` | Exposes process output lazily; see the example under Using Providers | > [!IMPORTANT] -> Any function that iterates over a Gradle collection (`forEach`, `map`, `filter`, `all`, `any`, -> `find`, `first`, etc.) will **eagerly realize all elements**. This defeats lazy configuration. -> Always use `configureEach`, or use `named`/`withType` to get lazy providers. +> Iterating a task or configuration container can realize its elements. +> **Any function that iterates over a Gradle collection** (`forEach`, `map`, `filter`, `all`, `any`, `find`, `first`, etc.) will **eagerly realize all elements**, +> defeating any other lazy API usage. +> +> Instead prefer, +> * `configureEach` registers actions for realized elements; +> * `withType` creates a filtered live collection. +> * Provider transformations such as `map` and `flatMap` remain lazy until queried. > [!WARNING] -> **Groovy DSL pitfall**: The shorthand syntax `name { }` is **eager** for both tasks and configurations. It calls `getByName()` under the hood, which realizes the element and its dependencies immediately. +> **Groovy DSL pitfall**: Dynamic task and configuration blocks (shorthand syntax +> `theTaskName { }`, or `theConfigurationName { }`) look up their target immediately +> (calling under the hood `getByName("theTaskName")`, or `getByName("theConfigurationName")`), realizing the object. +> +> Note that just looking up a configuration by name does not resolve its dependencies. > > ```Gradle > // ❌ Eager - realizes the task immediately @@ -1432,9 +1445,11 @@ When you use eager APIs, values are computed immediately during configuration— > ``` > > ```Gradle -> // ❌ Eager - resolves the configuration immediately -> runtimeClasspath { -> exclude group: 'org.slf4j' +> // ❌ Eager - realizes the configuration object +> configurations { +> runtimeClasspath { +> exclude group: 'org.slf4j' +> } > } > > // ✅ Lazy - configures only when needed @@ -1453,20 +1468,30 @@ tasks.create("processData") { // ✅ Lazy - task is created only when needed tasks.register("processData") { - // configuration runs only when this task is in the execution graph + // Task configuration normally runs when this task is needed for the execution graph or eager lookup. } ``` +Configuration actions may run immediately, later or never. Mutating another task from one +of these actions makes the result depend on realization order. Dependencies therefore belong +in the consuming task's own configuration action. + ### Configuration Access +`getByName` realizes the configuration object. Its dependencies remain unresolved until Gradle +needs their files. + ```Gradle Kotlin DSL -// ❌ Eager - resolves configuration immediately +// ❌ Eager - realizes the configuration object val runtimeClasspath = configurations.getByName("runtimeClasspath") // ✅ Lazy - returns a provider val runtimeClasspath = configurations.named("runtimeClasspath") ``` +Queries such as `.files`, `.singleFile` or `.resolve()` trigger dependency resolution. +Task inputs can accept a configuration as a file collection without resolving it in build logic. + ### Walking Collections ```Gradle Kotlin DSL @@ -1477,8 +1502,8 @@ tasks.all { } } -// ❌ Eager - forces all tasks to be created -tasks.withType() { +// ❌ Eager - realizes all JavaCompile tasks +tasks.withType { options.encoding = "UTF-8" } @@ -1514,69 +1539,208 @@ The lazy version also carries the producer dependency, so Gradle runs `jar` befo The eager version passes only a file path and loses that dependency information. > [!TIP] -> Kotlin DSL supports `=` for Gradle lazy properties, for example `archiveFileName = provider { "app.jar" }`. -> This has the same effect as `archiveFileName.set(provider { "app.jar" })`. +> Kotlin DSL supports `=` for Gradle lazy properties, for example `archiveFileName = "app.jar"`. +> This has the same effect as `archiveFileName.set("app.jar")`; both forms also accept providers. Provider support depends on the receiving API. Some APIs with `Any` or `Object` parameters convert a provider to a string instead of reading its value. JVM arguments are one example; see [Lazy JVM Arguments and System Properties](#lazy-jvm-arguments-and-system-properties). +Command output can also be exposed as a provider: + +```Gradle Kotlin DSL +val gitVersion = providers.exec { + commandLine("git", "--version") +}.standardOutput.asText.map { it.trim() } +``` + +The command runs when its output is queried. A configuration-time query makes that output +a configuration-cache input. + > [!TIP] > Existing `afterEvaluate` blocks can often be refactored away using providers, > lazy task configuration and plugin callbacks. -### Lazy JVM Arguments and System Properties +#### Custom Value Sources -A common pitfall is reading task outputs or other values during configuration. Here's how to pass values lazily to `Test` or `JavaExec` tasks using `CommandLineArgumentProvider`: +When a provider is queried during configuration, Gradle tracks the external data it reads +to decide whether the configuration cache can be reused. A custom +[`ValueSource`](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/ValueSource.html) +lets Gradle track the computed result instead. This is useful when only part of the data +affects configuration. -```Gradle -// ❌ Eager - task output is resolved at configuration time -tasks.withType(Test).configureEach { - def fooShadowJarTask = tasks.named('fooShadowJar', ShadowJar) - def barShadowJarTask = tasks.named('barShadowJarTask', ShadowJar) - dependsOn fooShadowJarTask, barShadowJarTask - - // This resolves the archive path immediately during configuration! - systemProperty "smoketest.foo.path", fooShadowJarTask.get().archiveFile.get() - environment "BAR_PATH", barShadowJarTask.get().archiveFile.get() +For example, reading the `version` entry from a `release.properties` file that also contains +other release metadata: + +```Gradle Kotlin DSL +import java.util.Properties + +val releaseFile = layout.projectDirectory.file("release.properties") + +// ❌ Any file content change invalidates the configuration cache +val releaseVersionProvider = providers.fileContents(releaseFile).asText.map { + Properties().apply { load(it.reader()) }.getProperty("version") } -// ✅ Lazy - use CommandLineArgumentProvider to defer resolution -tasks.withType(Test).configureEach { - def fooShadowJarTask = tasks.named('fooShadowJar', ShadowJar) - def barShadowJarTask = tasks.named('barShadowJarTask', ShadowJar) - dependsOn fooShadowJarTask, barShadowJarTask - - jvmArgumentProviders.add(new CommandLineArgumentProvider() { - @Override - Iterable asArguments() { - // This is only called at execution time - return fooShadowJarTask.map { ["-Dsmoketest.foo.path=${it.archiveFile.get()}"] }.get() +// ✅ Only a change to the returned version invalidates the configuration cache +abstract class ReleaseVersionValueSource : + ValueSource { + interface Parameters : ValueSourceParameters { + val releaseFile: RegularFileProperty + } + + override fun obtain(): String = + parameters.releaseFile.get().asFile.reader().use { + Properties().apply { load(it) }.getProperty("version") } - }) - - // Workaround: environment() calls toString() at execution time - environment("BAR_PATH", new Object() { - @Override - String toString() { - return barShadowJarTask.get().archiveFile.get().asFile.absolutePath +} + +val releaseVersion = providers.of(ReleaseVersionValueSource::class) { + parameters.releaseFile.set(releaseFile) +} +``` + +When queried during configuration, `obtain()` is rerun on subsequent builds to check its +result. Changes to other entries leave the returned version unchanged and allow cache reuse. +Built-in providers remain sufficient for simple environment, file and command reads. + +A value source is also a good fit to run a CLI through `ExecOperations`: + +```Gradle Kotlin DSL +import java.io.ByteArrayOutputStream + +abstract class GitVersionValueSource @Inject constructor( + private val execOperations: ExecOperations +) : ValueSource { + override fun obtain(): String { + val output = ByteArrayOutputStream() + execOperations.exec { + commandLine("git", "--version") + standardOutput = output } - }) + return output.toString().trim() + } } + +val gitVersion = providers.of(GitVersionValueSource::class) {} ``` -> [!TIP] -> `CommandLineArgumentProvider` is the recommended way to pass lazily-computed JVM arguments. -> It's configuration-cache compatible and properly tracks inputs for up-to-date checks. However, -> for older APIs like `environment()` that don't accept providers, use the `toString()` wrapper -> trick: pass an anonymous object whose `toString()` method computes the value—it will only be -> called at execution time. +For a simple command, `providers.exec` is shorter. A custom source is useful when combining +command output with other external data. The command runs again when Gradle checks cache +reuse, so slow commands add overhead even when the cached configuration is reused. + +### Lazy JVM Arguments and System Properties + +A common issue is passing arguments immediately by reading task outputs of other tasks or computing values during configuration. + +Instead, a `CommandLineArgumentProvider` supplies JVM arguments from tracked task inputs. +It is applicable to `Test`, `JavaExec`, `Exec`, and any others. +For example, this can be used to provide the output of `Jar` tasks (in the below example +`fooJar` and `barJar`) are needed to pass a system property and an environment var. + +```Gradle Kotlin DSL +// ❌ Eager - reads producer task properties while configuring Test +tasks.withType().configureEach { + val fooJarTask = tasks.named("fooJar") + val barJarTask = tasks.named("barJar") + dependsOn(fooJarTask, barJarTask) + + systemProperty("smoketest.foo.path", fooJarTask.get().archiveFile.get().asFile.absolutePath) + environment("BAR_PATH", barJarTask.get().archiveFile.get().asFile.absolutePath) +} + +abstract class SmokeTestJvmArguments : CommandLineArgumentProvider { + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val fooJar: RegularFileProperty + + override fun asArguments(): Iterable = + listOf("-Dsmoketest.foo.path=${fooJar.get().asFile.absolutePath}") +} + +// ✅ Lazy - tracks the artifacts and reads their paths when needed +tasks.withType().configureEach { + val fooJar = tasks.named("fooJar").flatMap { it.archiveFile } + val barJar = tasks.named("barJar").flatMap { it.archiveFile } + val arguments = objects.newInstance() + arguments.fooJar = fooJar + jvmArgumentProviders.add(arguments) + + // Workaround: environment() currently do not accept providers + inputs.file(barJar).withPropertyName("barJar").withPathSensitivity(PathSensitivity.NONE) + doFirst { + environment("BAR_PATH", barJar.get().asFile.absolutePath) + } +} +``` + +`@InputFile` on `SmokeTestJvmArguments.fooJar` makes the JAR's fingerprint of the +test task's up-to-date checks. Setting that property from the `fooJar` task's output also +schedules `fooJar` before the tests. `inputs.file(barJar)` provides the same tracking and +dependency for `barJar`; `doFirst` sets `BAR_PATH` before the test process starts. + +> [!CAUTION] +> Deferred evaluation alone does not guarantee input tracking or configuration-cache compatibility. +> Argument providers and task actions follow the same +> [configuration-cache requirements](https://docs.gradle.org/current/userguide/configuration_cache_requirements.html). +> +> * An incorrect input on an argument provider coud track path **string** without tracking the file's contents: +> +> ```Gradle Kotlin DSL +> // ❌ Tracks the path string, so changes to the JAR at that path are missed +> abstract class PathOnlyJvmArguments : CommandLineArgumentProvider { +> @get:Input +> abstract val fooJarPath: Property +> +> override fun asArguments(): Iterable = +> listOf("-Dsmoketest.foo.path=${fooJarPath.get()}") +> } +> ``` +> +> The earlier `SmokeTestJvmArguments` properly uses `@InputFile` on a `RegularFileProperty` to track the JAR's contents. +> +> * This assignment is deferred, but does not track the JAR's contents or schedule its producer: +> +> ```Gradle Kotlin DSL +> // ❌ Deferred assignment without a declared file input +> tasks.withType().configureEach { +> val barJar = tasks.named("barJar").flatMap { it.archiveFile } +> doFirst { +> environment("BAR_PATH", barJar.get().asFile.absolutePath) +> } +> } +> ``` +> +> The `inputs.file(barJar)` declaration in the lazy example above provides both. +> +> * Capturing another task in an action also prevents configuration-cache storage: +> +> ```Gradle Kotlin DSL +> // ❌ Deferred lookup that retains the producing task +> tasks.withType().configureEach { +> val fooJarTask = tasks.named("fooJar").get() +> dependsOn(fooJarTask) +> doFirst { +> systemProperty("smoketest.foo.path", fooJarTask.archiveFile.get().asFile.absolutePath) +> } +> } +> ``` +> +> `dependsOn` orders the tasks, but the captured `Jar` task cannot be stored in the +> configuration cache. The lazy example reads the declared `fooJar` file property instead. + +### Replacing `afterEvaluate` + +`afterEvaluate` runs after a project's build script, during configuration. Its callbacks +run in registration order, which makes interactions between plugins fragile. Existing +blocks can often be eliminated with provider wiring, lazy task configuration or +`pluginManager.withPlugin`, depending on what they are waiting for. ### Benefits of Lazy Configuration 1. **Faster configuration**: Only necessary work is performed 2. **Automatic dependency tracking**: Gradle knows which tasks produce values that others consume -3. **Configuration cache compatibility**: Lazy providers are serializable and can be cached +3. **Configuration cache compatibility**: Explicit inputs and compatible provider implementations support configuration cache reuse 4. **Correct ordering**: Task dependencies are inferred from provider relationships ## Gradle Daemon JVM From 2e7ce1fff9bcf1a0480ba33603e6df550cc7980b Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 21 Oct 2026 15:09:53 +0200 Subject: [PATCH 8/8] docs: Reword paragraph in Project COnvention plugin --- docs/how_to_work_with_gradle.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index bd35fbb2eff..0e8f8645999 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -1259,10 +1259,13 @@ Gradle schedules tasks for project dependencies as part of the main build. Files ending in `.gradle.kts` placed in `buildSrc/src/main/kotlin/` target `Project` and can configure tasks, dependencies, and extensions. The `buildSrc/` directory is automatically included by Gradle before the main build. +New convention plugins belong in `build-logic`. During the script-plugin migration, conventions +that need shared `buildSrc` classes still need to land in `buildSrc`; see [Build Logic Location](#build-logic-location). + > [!IMPORTANT] -> Prefer build logic that is independent of concrete project paths. Referring to repository projects -> from `build-logic` or `buildSrc` couples the plugin to the project layout; keep such dependencies -> deliberate and limited to conventions that need them. +> Hard-coded project paths such as `:dd-trace-api` tie a plugin to the repository layout, in other words it creates coupling. +> They belong in the consuming build script unless the dependency is part of the convention; +> see [Dependencies on Main Projects](#dependencies-on-main-projects). In this project, convention plugins use the `dd-trace-java.` prefix. For example, `dd-trace-java.configure-tests.gradle.kts` configures test tasks across all subprojects: