How do precompiled convention plugins differ from legacy `apply from:` script plugins, and why are they preferred?
answer
- apply from: = uncompiled, untyped
- convention = compiled binary plugin
- type-safe accessors generated
- id + plugins {} block
- config-cache friendly
basics
~20 sapply from: includes a raw .gradle.kts file at configuration time — uncompiled, untyped, no plugin id. Precompiled convention plugins are compiled by kotlin-dsl into binary plugins with type-safe accessors, applied via the plugins {} block by id.
solid answer
~40 sLegacy **script plugins** are applied with `apply(from = "other.gradle.kts")`. The file is parsed and executed inline during configuration, so you get **no compilation, no type-safe accessors, no plugin id**, and poor IDE support; they also can't declare plugin dependencies in a `plugins {}` block. **Precompiled convention plugins** live in a `kotlin-dsl` project's source set; Gradle **compiles** them into binary `Plugin<Project>` implementations, generates **type-safe accessors** (so `java {}`, `kotlin {}`, custom extensions resolve with completion), assigns a **plugin id from the filename**, and lets you apply other plugins via a real `plugins {}` block. They participate in the build cache and configuration cache as compiled code. The result is faster, safer, and far more maintainable — which is why Gradle steers everyone toward convention plugins and treats `apply from:` as legacy.
code
kotlin · 7 lines// LEGACY: no id, no type-safe accessors
apply(from = "../gradle/java.gradle.kts")
// MODERN: compiled, typed, id-addressable
plugins {
id("com.acme.java-conventions")
}go deeper
Know that apply from: is the old way and convention plugins are the recommended replacement.
List the concrete differences: compilation, type-safe accessors, plugin id, plugins {} block.
Discuss configuration-cache and maintainability implications and when migrating legacy builds.
Define a migration strategy off apply from: across many repos and the standards that make convention plugins the default.
## Two ways to factor out shared build logic ### 1. Legacy script plugins — `apply from:` ```kotlin // build.gradle.kts apply(from = "../gradle/quality.gradle.kts") ``` Gradle reads `quality.gradle.kts` and executes it against the current `Project` at **configuration time**. Characteristics: - **Not compiled ahead of time** — re-evaluated each run, no separate compilation unit. - **No type-safe accessors** — inside the script you often can't use `java { }` blocks for plugins applied elsewhere; you fall back to `configure<JavaPluginExtension> { }` or `extensions.getByType(...)`. - **No plugin id**, so it can't appear in a `plugins { }` block of consumers. - **No `plugins { }` block of its own** — it can only `apply(plugin = ...)` imperatively, which loses type-safe accessors for those plugins. - **Weak IDE support** — limited completion and navigation. ### 2. Precompiled convention plugins Put the same logic in `build-logic/src/main/kotlin/com.acme.quality.gradle.kts` in a project that applies `kotlin-dsl`. Now: - Gradle **compiles** it into a binary `Plugin<Project>` class. - It gets a **plugin id** = filename (`com.acme.quality`). - It can use a **real `plugins { }` block**, and the `kotlin-dsl` plugin **generates type-safe accessors** so `java { }`, `checkstyle { }`, your own extension, etc. are all available with IDE completion *inside the convention plugin*. - Consumers apply it the idiomatic way: ```kotlin plugins { id("com.acme.quality") } ``` ## Why precompiled wins | Aspect | apply from: | Precompiled convention | |---|---|---| | Compiled | No | Yes | | Type-safe accessors | No | Yes | | Plugin id / `plugins {}` apply | No | Yes | | Own `plugins {}` block | No | Yes | | IDE completion | Poor | Full | | Config-cache friendliness | Weaker | Strong | ## When script plugins still linger You may still see `apply from:` for tiny one-off includes or in very old builds. Gradle's guidance is to migrate to convention plugins. The only real cost of convention plugins is the up-front `build-logic`/`buildSrc` scaffolding, which pays for itself the moment more than one module shares logic.
- Why can't a legacy `apply from:` script use a `plugins {}` block?The `plugins {}` block is only valid at the top of a real build/settings script or a precompiled script plugin. An applied-from script runs as an inline fragment, so it must use imperative `apply(plugin = ...)` instead, losing type-safe accessors.
- What gives a precompiled convention plugin its type-safe accessors?The `kotlin-dsl` plugin generates accessors for the extensions/configurations contributed by any plugins requested in the convention plugin's own `plugins {}` block, so `java {}`, the test task, and custom extensions resolve with completion.
saying these in an interview costs you the question
- Saying `apply from:` and convention plugins are interchangeable.
- Claiming script plugins have plugin ids.
- Thinking type-safe accessors work automatically inside an `apply from:` script.