- name
- gradle-developer
- description
- Expert guide for Gradle 9 builds in apache/grails-core on the 8.0.x line - multi-project topology, convention plugins, BOM platforms, dependency rules, task configuration hygiene, and repo-specific patterns that override generic Gradle docs
- license
- Apache-2.0
<!--
SPDX-License-Identifier: Apache-2.0
Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0.
-->
## What I Do
- Write and change Gradle build scripts the way **this repository** already does them on `8.0.x` (Gradle **9.8.x**).
- Keep agents off generic Gradle "best practice" when it conflicts with established monorepo patterns.
- Cover composite builds (`build-logic`, `grails-gradle`, `grails-forge`, `end-to-end`), convention plugins, BOM/`platform()` dependency management, test wiring, publishing hooks, and Gradle 9 task-configuration traps learned from recent PRs.
- Make Gradle changes boring, copy-paste consistent, and correct on the first try.
## When to Use Me
**MANDATORY** before any of the following:
- Editing `build.gradle`, `settings.gradle`, `gradle.properties`, `dependencies.gradle`, or anything under `gradle/`, `build-logic/`, `grails-gradle/`
- Adding or renaming a module / subproject
- Bumping a dependency version or the Gradle wrapper
- Changing test, publish, SBOM, code-style, or JaCoCo Gradle wiring
- Touching Grails Gradle plugins used by apps (`org.apache.grails.gradle.*`)
- Diagnosing configuration-cache, resolution, `afterEvaluate`, or task-graph failures
Also load when a change *looks* like application code but requires build script updates (new module, new published artifact, CLI companion jar, functional test app).
Related skills (do not substitute this one):
| Need | Skill |
|------|-------|
| CodeNarc / Checkstyle / PMD / SpotBugs reports | `violation-fixer` |
| Failing tests / aggregate reports | `test-fixer` |
| Merging an external plugin repo into the monorepo | `mono-repo-integration` |
| App-facing Grails 8 upgrade guidance | `grails-8-upgrade` |
---
## Prime Directive: This Repo Wins
1. **Match neighboring modules.** Before inventing structure, open 2-3 similar `build.gradle` files and copy their plugin block, dependency style, and `apply { from ... }` scripts.
2. **Prefer existing convention plugins** over inline configuration. If `CompilePlugin` already sets encoding, release, jars, and reproducibility, do not re-declare those in the module script.
3. **Gradle docs are secondary.** Official Gradle 9 docs are useful for APIs and deprecations, but this monorepo intentionally diverges (no Spring DM plugin, heavy `projectDir` remapping, presence-based `-P` flags, custom BOM validator). When docs and this repo disagree, **follow this repo** unless you are deliberately fixing a known issue with a tracked reason.
4. **Do not use** `io.spring.dependency-management` / Spring Dependency Management plugin in core modules or applications. The intentional regression fixture at `grails-test-examples/spring-dependency-management` is the only exception. Grails 8 otherwise uses native `platform()` / `enforcedPlatform()` plus `org.apache.grails.gradle.bom-property-overrides` (see PR #15467).
5. **Scope Gradle invocations** to the touched subproject (`:module:test`, not root `test`) unless the change is cross-cutting.
### Change workflow
1. Inspect 2-3 sibling modules and the relevant convention plugin or shared script.
2. Confirm the Gradle project path in `settings.gradle`, including any `projectDir` mapping.
3. Edit the smallest appropriate build file or convention plugin.
4. Run scoped compile, test, and `validateDependencyVersions` tasks for the changed project.
5. For `grails-gradle` changes, run the relevant plugin TestKit tests from `grails-gradle/` with its wrapper.
---
## Topology (Know Where You Are)
This git repo is **several independent Gradle builds**, not one flat multiproject:
| Build | Path | Role | How to run |
|-------|------|------|------------|
| **Root framework** | repo root | 60+ published modules, BOMs, test-examples, profiles, docs | `./gradlew …` from root |
| **build-logic** | `build-logic/` | Shared **convention plugins** via `includeBuild` | `cd build-logic && ./gradlew …` (or root pluginManagement includeBuild) |
| **grails-gradle** | `grails-gradle/` | **Published** Grails Gradle plugins for apps | `cd grails-gradle && ./gradlew …` |
| **grails-forge** | `grails-forge/` | App generator (own wrapper, own deps) | `cd grails-forge && ./gradlew …` |
| **end-to-end** | `end-to-end/` | Tests against **published** artifacts in `build/local-maven` | Full 3-step flow below (see `end-to-end/README.md`) |
| **gradle-bootstrap** | `gradle-bootstrap/` | Regenerates shared wrappers from `.sdkmanrc` | `gradle -p gradle-bootstrap` (see wrapper section) |
**end-to-end is not a composite consumer of the root build.** It resolves real published coordinates from **`<repo>/build/local-maven`** (the TestCaseMavenRepo), **not** `~/.m2` and **not** via `publishAllToMavenLocal`. Do not add `includeBuild('..')` substitution - that defeats validating consumer metadata, CLI companions, and POM/BOM shape.
Full local run (three steps, in order):
```bash
# 1) Publish grails-gradle + root into build/local-maven (both required)
(cd grails-gradle && ./gradlew publishAllPublicationsToTestCaseMavenRepoRepository)
./gradlew publishAllPublicationsToTestCaseMavenRepoRepository
# 2) Build the standalone Grails 7 fixture jar (JDK 17 / Gradle 8.x via its .sdkmanrc)
cd end-to-end/legacy-g7-command-plugin
sdk env
./gradlew jar
cd ../..
# 3) Run the suite on the root JDK 21 environment
sdk env # from repository root (.sdkmanrc)
cd end-to-end
./gradlew check
```
Leave `legacy-g7-command-plugin`'s wrapper on its pinned Grails 7 Gradle version when bumping the main line.
Root `settings.gradle` wires:
```groovy
pluginManagement {
includeBuild('./grails-gradle') { name = 'grails-gradle' }
includeBuild('./build-logic') { name = 'build-logic-root' }
// ...
}
```
`build-logic` exists because composite builds do **not** share `buildSrc` plugins. Internal conventions live there so root, grails-gradle, and forge can consume them.
### Project path != directory name
Root settings heavily remaps `projectDir` (100+ entries). Examples:
| Gradle path | Directory |
|-------------|-----------|
| `:grails-bom` | `grails-bom/default` |
| `:grails-base-bom` | `grails-bom/base` |
| `:grails-hibernate7-bom` | `grails-bom/hibernate7` |
| `:grails-controllers` | `grails-controllers` (often 1:1) |
| `:grails-data-hibernate7-core` | `grails-data-hibernate7/core` |
| `:grails-test-examples-app1` | `grails-test-examples/app1` |
Always use the **Gradle project path** in task names (`./gradlew :grails-data-hibernate7-core:test`). Confirm with `settings.gradle` `include` + `projectDir` when unsure. Do not invent paths from folder names alone.
### Micronaut "island"
`grails-micronaut*`, micronaut BOMs, and related test-examples are gated in `settings.gradle`:
- Auto-**excluded** on JDK < 25 (Micronaut 5 targets JVM 25 bytecode)
- Auto-**included** on JDK 25+
- `-PskipMicronautProjects` forces exclude (used by groovy-joint CI)
- `-PincludeMicronautProjects` forces include on older JDKs (still may not compile)
Presence-based flags (property **present**, value optional) match `skipFunctionalTests` / `skipCodeStyle` style elsewhere.
---
## Gradle Version Sync (Hard Rule)
Current line: **Gradle 9.8.0** (`distributionUrl` + `gradleToolingApiVersion=9.8.0`). Upstream may already ship a newer 9.8.x patch - this repo rides close to latest **only after** a deliberate multi-location bump PR. Do not "helpfully" jump one wrapper ahead of the rest.
**Two Groovy stacks:** Gradle itself embeds **Groovy 4** for build logic. Application/runtime code on 8.0.x is **Groovy 5**. That is why `dependencies.gradle` keeps separate maps:
- `gradleBomDependencyVersions` / `gradle-groovy.version` / `gradle-spock.version` → build tooling (Groovy 4 / Spock groovy-4)
- `bomDependencyVersions` / `groovy.version` / `spock.version` → apps and framework modules (Groovy 5 / Spock groovy-5.0)
Never unify those casually.
### Preferred bump workflow
1. Set the new Gradle version in **`.sdkmanrc`** (`gradle=…`).
2. Run the bootstrap project (uses a system `gradle` to regenerate shared wrappers from `.sdkmanrc`):
```bash
gradle -p gradle-bootstrap
```
Bootstrap generates the wrapper under `gradle-bootstrap/`, **copies** it to **grails-forge**, **grails-gradle**, and **end-to-end**, then **moves** the generated wrapper into the **repository root**. It also runs `legacyG7Wrapper` so **`end-to-end/legacy-g7-command-plugin` stays on its pinned Grails 7 Gradle version** (currently 8.x - do **not** force it to 9). Root is therefore bootstrap-covered; do not re-list it as a manual step.
3. Manually refresh the locations bootstrap does **not** cover. For each tree, keep the **full** wrapper set in sync (`gradle-wrapper.properties`, `gradle-wrapper.jar`, `gradlew`, `gradlew.bat`) - not properties alone:
- `build-logic/` - run its wrapper task or copy the complete set from root after bootstrap
- `grails-profiles/base/skeleton/` and `grails-profiles/profile/skeleton/`
- `grails-shell-cli/src/test/resources/gradle-sample/` (and `bin/test` copy if present)
- Forge **generated-app** wrapper assets (all of these - properties alone is not enough):
- `grails-forge/grails-forge-core/.../gradleWrapperProperties.rocker.raw` (properties template)
- `grails-forge/grails-forge-core/src/main/resources/gradle/gradlew`
- `grails-forge/grails-forge-core/src/main/resources/gradle/gradlew.bat`
- `grails-forge/grails-forge-core/src/main/resources/gradle/wrapper/gradle-wrapper.jar`
- `gradle.properties` → `gradleToolingApiVersion` (must match the new Gradle version)
4. Verify every **main-line** tree matches on properties **and** scripts/jars. The only intentional holdout is `end-to-end/legacy-g7-command-plugin` (Gradle 8.x).
Also keep `gradlew.bat` LF line endings on this line (PR #15709). Comment at top of root `gradle-wrapper.properties` remains a human checklist.
---
## Root `gradle.properties` Flags (Do Not "Fix" Blindly)
| Property | Value / note |
|----------|----------------|
| `org.gradle.caching` | `true` |
| `org.gradle.parallel` | `true` |
| `org.gradle.daemon` | `true` |
| `org.gradle.configuration-cache` | **`true`** in every build (root, `build-logic`, `grails-gradle`, `grails-forge`) - build logic must stay compatible, see [Configuration Cache](#configuration-cache) |
| `org.gradle.configureondemand` | **commented off** - Gradle issue #9489 |
| `org.gradle.jvmargs` | `-Xmx3G` (daemon only; groovydoc and the guide run in their own JVMs) |
| `javaVersion` | `21` (CompilePlugin reads this for `--release`) |
| `projectVersion` | framework version |
| `slf4jPreventExclusion` | `true` - Grails Gradle plugin POM behavior |
CI vs local behavior is branched on `System.getenv('CI')` and `SOURCE_DATE_EPOCH` (reproducible builds disable remote cache).
`groovydocMaxHeapSize` and `guideMaxHeapSize` are **not** keys in `gradle.properties` - do not add
them. Each documentation JVM carries its own default (1g per groovydoc task, raised to 3g/2g by the
two aggregates; 1500m for the guide), and these exist only as `-P` overrides that beat the build
script when a documentation run will not fit.
---
## Standard Published Library Module
Canonical pattern (see `grails-core/build.gradle`, `grails-controllers/build.gradle`, `grails-services/build.gradle`):
```groovy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* ... Apache header ...
*/
plugins {
id 'groovy'
id 'java-library'
id 'project-report' // optional but common
id 'org.apache.grails.buildsrc.properties'
id 'org.apache.grails.buildsrc.dependency-validator'
id 'org.apache.grails.buildsrc.compile'
id 'org.apache.grails.buildsrc.publish'
id 'org.apache.grails.buildsrc.sbom'
id 'org.apache.grails.buildsrc.vulnerability-scan' // when appropriate
id 'org.apache.grails.gradle.grails-code-style'
id 'org.apache.grails.gradle.grails-jacoco'
}
version = projectVersion
group = 'org.apache.grails' // or org.apache.grails.web / .data / etc. - match siblings
dependencies {
implementation platform(project(':grails-bom')) // or :grails-hibernate7-bom, etc.
api project(':grails-core')
api 'org.apache.groovy:groovy'
// versions come from the platform - do NOT hardcode versions here
compileOnly 'jakarta.servlet:jakarta.servlet-api'
testImplementation 'org.spockframework:spock-core'
testImplementation 'org.apache.groovy:groovy-test-junit5'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
// junit-platform-launcher is added by gradle/test-config.gradle
}
apply {
from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle')
from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
}
```
### Rules for module scripts
- **Apache license header** on every new `.gradle` / `.gradle.kts` file.
Ver en GitHub