Skip to content

Build Setup

TheMeinerLP edited this page Aug 1, 2026 · 3 revisions

Build setup

How Falco's Gradle build is put together: what the root project is for, which plugin each module gets and from where, and why falco-bom needs an exception in nearly every block. This page covers the build wiring only — version and release mechanics are in Versioning and Releases, the publishing configuration in Publishing, and dependency resolution in Dependency Management. It is written for someone about to change a build file, not for someone consuming the library.

Everything below is checkable in four files: build.gradle.kts, settings.gradle.kts, gradle.properties and the seven module scripts. Gradle itself is pinned by the wrapper at 9.6.1, in gradle/wrapper/gradle-wrapper.properties, so ./gradlew is the only supported way to run any of it.

The root project has no sources

build.gradle.kts at the repository root has no plugins { } block at all and carries no sources. This is deliberate: the root project exists only to hold the shared version and the configuration that every module inherits. Applying java-library there would create an empty, publishable falco artefact next to the real ones, which nobody wants to consume.

Modules

Module Published Plugins, and where they are applied
falco-anvil yes java-library, jacoco, maven-publish — all from the root
falco-light yes java-library, jacoco, maven-publish — all from the root
falco-instance yes java-library, jacoco, maven-publish — all from the root
falco-bom yes java-platform, maven-publish — both from the root
falco-benchmarks no java-library, jacoco from the root; me.champeau.jmh from its own script
falco-demo no java-library, jacoco from the root
falco-archunit no java-library, jacoco from the root

falco-anvil, falco-light and falco-instance are the three library modules the project actually ships. falco-bom is a Maven BOM pinning those three to one version (see Publishing). falco-benchmarks and falco-demo are tooling modules that are never published — see Benchmarks and Demo. falco-archunit is never published either and is the odd one out: it has no main source set at all, only tests, and it exists to hold the architecture rules over the other modules — see Architecture Rules.

falco-benchmarks/build.gradle.kts holds the only plugins { } block in the repository, a single alias(libs.plugins.jmh). Every other plugin in the table is applied imperatively from the root, so the seven module scripts contain dependencies, a description, and — in falco-benchmarks and falco-demo — task wiring, and nothing else.

falco-archunit gets java-library and jacoco from the root like every non-BOM module, and both are largely inert there: with no main sources the javadoc and jar tasks have nothing to do, and the JaCoCo report covers a source set that does not exist. That is wiring the module inherits rather than wiring it needs, and it costs nothing to leave in place.

One toolchain, one release target

extensions.configure<JavaPluginExtension> {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

tasks.withType<JavaCompile>().configureEach {
    options.encoding = "UTF-8"
    options.release.set(25)
}

Both settings are declared in the root build and reach every module except falco-bom. They answer two different questions: the toolchain decides which JDK compiles, tests and runs, while options.release decides which class file version and which JDK API surface the compiler will accept. Stating both keeps the two decoupled — raising the toolchain to a newer JDK later still produces artefacts a Java 25 runtime can load, and a call into an API added after 25 fails at compile time rather than at a consumer's startup. Continuous integration asks for the same version explicitly: .github/workflows/build-pr.yml passes java-version: "25" and java-distribution: "temurin" to the shared build workflow.

Applying plugins: why falco-bom is special-cased

The root build's subprojects block applies java-library and jacoco to every subproject except falco-bom:

configure(subprojects - project(":falco-bom")) {
    apply(plugin = "java-library")
    apply(plugin = "jacoco")
    ...
}

java-library and java-platform are mutually exclusive Gradle plugins — applying both to one project fails the build — and a java-platform project has no sources, so there is nothing for a toolchain, a compiler, or a test task to do there anyway. Subtracting falco-bom from the subproject list here, rather than guarding every apply() call inside the block with an if (name != "falco-bom"), keeps the block itself unaware that an exception exists: a plugin added to it later inherits the exclusion for free, and the diff against the pre-BOM version of the file is a one-line change of scope rather than a scattering of conditionals through the body.

java-platform itself is applied to falco-bom from the root build, in the same place java-library is applied to every other subproject:

project(":falco-bom") {
    apply(plugin = "java-platform")
}

This is not a style choice but an evaluation-order requirement. A project(":falco-bom") { } block in the root script runs while the root project is being configured, which is before falco-bom/build.gradle.kts is evaluated at all. If java-platform were applied in the module's own script instead, the javaPlatform software component it registers would not exist yet by the time the root script's publishing block — further down the same file, and therefore still part of the root project's configuration — calls from(components["javaPlatform"]).

Group and version apply to every subproject, including falco-bom

subprojects {
    group = "net.onelitefeather"
    version = rootProject.version
}

This assignment sits outside the java-library configuration block precisely so it also reaches falco-bom. The BOM pins its siblings by project reference rather than by version string (see Publishing), and that only produces the right numbers in the generated POM if falco-bom carries the same version its siblings do.

No repositories are declared per-project

None of the seven module build files declares a repositories { } block. Repositories come exclusively from dependencyResolutionManagement in settings.gradle.kts, and that file does not set repositoriesMode, so Gradle's default RepositoriesMode.PREFER_PROJECT applies: the moment a module declared its own block, that block would win and the settings-level declaration would be ignored for that module — silently dropping the OneLiteFeather repository that Minestom, Cyano and the mycelium BOM are resolved from. See Dependency Management for what lives in that block.

Build-wide Gradle settings

gradle.properties configures how a build runs rather than what it produces:

org.gradle.caching=false
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8

org.gradle.parallel=true lets Gradle execute independent modules at the same time, so one ./gradlew build runs several test JVMs alongside each other — which is why the test heap is pinned rather than left to the default. The jvmargs line sizes the Gradle daemon, not those test JVMs; each test JVM is a separate process and gets its heap from the Test task configuration. The build cache is off, so a task's result is never restored from a previous build's output. How to reproduce a test run, what the -Werror doclint policy enforces, and where the test heap is set are in Testing and Javadoc.

Falco


Start here

The measured record

Why it is built this way

  • Rationale — the index for the five rationale pages
  • Research — the index for the five investigations
  • Research: Fluent API — the investigation behind the builders; a record, not a reference

Working on the build

Clone this wiki locally