skip to content

How do script plugins differ from precompiled script plugins, and why would you migrate from one to the other?

level: middleimportance: must knowfreq 50%

answer

  1. script = interpreted, no ID, no accessors
  2. precompiled = compiled, has ID, accessors back
  3. buildSrc / build-logic + kotlin-dsl
  4. configure<>()/the<>() workaround
  5. migrate when logic grows / cross-repo

basics

~20 s

A script plugin is a loose .gradle.kts applied via apply(from = ...) with no compilation, no type-safe accessors, and weak reuse. A precompiled script plugin lives in buildSrc/build-logic, is compiled, gets an ID, applies via plugins {}, and keeps type-safe accessors.

solid answer

~50 s

**Script plugins** are loose files applied by path with `apply(from = ...)`. They are **not compiled**, get **no plugin ID**, can't be used in a `plugins {}` block, and — crucially — **lose type-safe accessors** generated from other applied plugins, so you must use `configure<>()`/`the<>()` and string-keyed lookups. **Precompiled script plugins** are `*.gradle.kts` files placed under `src/main/kotlin` in `buildSrc` or a `build-logic` included build. Gradle **compiles** them into real `Plugin<Project>` classes with an auto-derived ID (the filename, optional package prefix), applied via the `plugins {}` block. Inside them, the `plugins {}` block works and **type-safe accessors are available**. You migrate from script plugins to precompiled plugins when shared logic grows, when you want type safety and IDE support, when you need a stable ID, or when sharing across repositories (publish the build-logic as a real plugin). Script plugins remain fine for trivial, build-local snippets like version constants.

code

kotlin · 10 lines
kotlin
// Script plugin: no type-safe accessor, must use configure<>()
// shared.gradle.kts
configure<JavaApplication> {
    mainClass.set("com.acme.Main")
}

// Precompiled plugin: accessor works
// build-logic/src/main/kotlin/my.app-conventions.gradle.kts
plugins { application }
application { mainClass.set("com.acme.Main") }

go deeper

for a junior

Know the two are different: loose applied-by-path file vs compiled file in buildSrc/build-logic with an ID.

for a middle

Articulate the accessor loss, the configure<>()/the<>() workaround, and the apply(from) vs plugins{} difference.

for a senior

Drive the migration decision: when ceremony of build-logic pays off, testing, cross-repo sharing, and ID stability.

for a principal

Set org standards: convention plugins in a shared build-logic / published plugin, reserving script plugins for trivial local snippets.

## Two things with confusingly similar names | | Script plugin | Precompiled script plugin | |---|---|---| | File location | Anywhere, referenced by path/URL | `src/main/kotlin/*.gradle.kts` in `buildSrc` or a `build-logic` included build | | Compiled? | No — interpreted as a build script | Yes — compiled to a `Plugin<Project>` | | Applied via | `apply(from = "x.gradle.kts")` | `plugins { id("my-convention") }` | | Has a plugin ID? | No | Yes (derived from filename, e.g. `my.team.java-conventions.gradle.kts` → id `my.team.java-conventions`) | | Type-safe accessors | **No** | **Yes** | | `plugins {}` block inside it | Not really usable | Fully usable | ## Why type-safe accessors matter When you apply, say, the `application` plugin via the `plugins {}` block, Gradle generates a Kotlin accessor so you can write: ```kotlin application { mainClass = "com.acme.Main" } ``` Inside a **script plugin** those accessors don't exist because the script isn't compiled against the plugin classpath. You must fall back to: ```kotlin configure<JavaApplication> { mainClass.set("com.acme.Main") } // or the<JavaApplication>().mainClass.set("com.acme.Main") ``` A **precompiled** plugin is compiled with that classpath, so the nice accessors come back. ## Authoring a precompiled plugin ```kotlin // build-logic/build.gradle.kts plugins { `kotlin-dsl` } // build-logic/src/main/kotlin/my.team.java-conventions.gradle.kts plugins { java } java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } // consumer settings.gradle.kts includeBuild("build-logic") // consumer build.gradle.kts plugins { id("my.team.java-conventions") } ``` The `kotlin-dsl` plugin is what turns each `*.gradle.kts` under `src/main/kotlin` into a real plugin with an ID. ## Migration triggers - Logic grew beyond a couple of lines → want compilation, tests, IDE navigation. - You keep writing `configure<>()`/`the<>()` workarounds → want type-safe accessors. - You need a stable, referenceable ID and ordering via `plugins {}`. - You want to share across repositories → publish the `build-logic` as a binary plugin to a repo. ## When to stay with a script plugin Trivial, build-local sharing — a `versions.gradle.kts` with `extra` properties, or one shared task — where the ceremony of a `build-logic` module isn't worth it.

  • Where must a precompiled script plugin file live, and what enables compilation?
    Under src/main/kotlin (or groovy) of buildSrc or a build-logic included build, with the `kotlin-dsl` plugin applied to that build — that's what compiles each *.gradle.kts into a Plugin<Project> with an ID.
  • Why can't a plain script plugin use the plugins {} block effectively?
    The plugins {} block resolves plugins from the plugin classpath at compile time; a non-compiled script plugin isn't processed that way, so you don't get the resolution or the resulting type-safe accessors.

saying these in an interview costs you the question

  • Conflating 'script plugin' and 'precompiled script plugin' as the same thing.
  • Claiming you get type-safe accessors inside apply(from = ...) scripts.
  • Saying buildSrc and apply(from=) are equivalent ways to share logic.

context