Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ 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

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

```shell
Expand Down
25 changes: 25 additions & 0 deletions build-logic/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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.
Comment thread
bric3 marked this conversation as resolved.

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 across modules, `./gradlew :build-logic:check` runs the included build's checks
from the repository root.
1 change: 1 addition & 0 deletions build-logic/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
23 changes: 23 additions & 0 deletions buildSrc/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Working in buildSrc

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.

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.

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`.

## Tests

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.

Changes to `call-site-instrumentation-plugin` or `modifiable-config-agent` also need their
own subproject tests. Run `./gradlew -p buildSrc spotlessCheck` for formatting.
1 change: 1 addition & 0 deletions buildSrc/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
201 changes: 171 additions & 30 deletions docs/how_to_work_with_gradle.md
Original file line number Diff line number Diff line change
@@ -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 best practices, how we apply them in this repository, and the
constraints that currently require exceptions. Understanding these concepts will help
you navigate the build system and contribute effectively.

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

**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
Expand All @@ -25,16 +32,30 @@ 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. 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.

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.

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 {
Expand Down Expand Up @@ -93,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
```

include("module-a")
include("module-b")
include("module-c:submodule")
```
This invocation uses cached dependencies and fails if a required dependency is missing.


- **`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

Expand Down Expand Up @@ -161,19 +202,60 @@ 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/` | 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, 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.

`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

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).

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.

Expand Down Expand Up @@ -253,6 +335,11 @@ 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).

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

For ad-hoc tasks or when you can't use annotations, declare inputs and outputs programmatically via the `inputs` and `outputs` properties:
Expand Down Expand Up @@ -333,6 +420,19 @@ 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

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. `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

- **Prefer `Sync` over `Copy`**: `Sync` mirrors the source to the destination by removing files that no longer exist
Expand Down Expand Up @@ -1125,6 +1225,28 @@ 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.

### Plugin Boundaries

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.

See Gradle's [plugin development guide](https://docs.gradle.org/current/userguide/implementing_gradle_plugins_binary.html).

### Dependencies on Main Projects

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.

Concrete project paths usually belong in the consuming build, keeping reusable plugins
independent of the repository layout. Exceptions have a documented reason.

[`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

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.
Expand Down Expand Up @@ -1235,7 +1357,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
Expand Down Expand Up @@ -1357,21 +1479,40 @@ tasks.withType<JavaCompile>().configureEach {

### Using Providers

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, these examples show eager and lazy access to its JAR:

```Gradle Kotlin DSL
// ❌ Eager - version is read immediately
tasks.register<Jar>("myJar") {
archiveVersion.set(project.version.toString())
// ❌ Eager - reads the archive path while configuring this task
tasks.register<Copy>("stageJarEager") {
from(tasks.named<Jar>("jar").get().archiveFile.get())
into(layout.buildDirectory.dir("staged-eager"))
}

// ✅ Lazy - version is read when the jar task runs
tasks.register<Jar>("myJar") {
archiveVersion.set(project.provider { project.version.toString() })
// ✅ Lazy - preserves the output provider and its dependency on the jar task
tasks.register<Copy>("stageJar") {
from(tasks.named<Jar>("jar").flatMap { it.archiveFile })
into(layout.buildDirectory.dir("staged"))
}
```

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]
> In Gradle Kotlin DSL (and Groovy DSL) you can use `=` instead of `set(...)`, e.g.
> `archiveVersion = project.version.toString()`
> Existing `afterEvaluate` blocks can often be refactored away using providers,
> lazy task configuration and plugin callbacks.

### Lazy JVM Arguments and System Properties

Expand Down
Loading