How do you obtain ProjectLayout inside a plugin or non-Project class without referencing project, and what convention defaults would you set for output locations?
answer
- @Inject ProjectLayout
- avoid project.layout (CC-safe)
- convention() = overridable default
- default under buildDirectory
- finalizeValueOnRead / disallowChanges
basics
~10 sInject ProjectLayout via @Inject in a task or plugin-instantiated class instead of calling project.layout. Then set conventions like outputFile.convention(layout.buildDirectory.file("...")) so outputs default under build/ but stay overridable.
solid answer
~40 s`ProjectLayout` is a Gradle-managed service, so the idiomatic way to get it outside `Project` is dependency injection: declare an `@Inject` constructor parameter (or `@Inject` getter) of type `ProjectLayout` on a task, extension, or other Gradle-instantiated type. This avoids capturing `project`, which keeps the code configuration-cache compatible. With the injected `layout` you set **convention defaults** for output properties — `convention()` provides a value used unless the user overrides it. A good pattern: in `Plugin.apply`, create an extension whose `DirectoryProperty`/`RegularFileProperty` defaults to `layout.buildDirectory.dir("<plugin>")`, then have tasks derive children from the extension's base. This makes outputs land under `build/` by default, follow a relocated build dir, and remain user-overridable — all lazily. You can also pair `convention()` with `finalizeValueOnRead()`/`disallowChanges()` when you want to lock values after configuration.
code
kotlin · 7 linesabstract class WriteTask @Inject constructor(
layout: ProjectLayout
) : DefaultTask() {
@get:OutputFile abstract val out: RegularFileProperty
init { out.convention(layout.buildDirectory.file("write/out.txt")) }
@TaskAction fun run() = out.get().asFile.writeText("x")
}go deeper
Know you can default an output to layout.buildDirectory.file(...) via convention().
Use @Inject ProjectLayout and explain convention() as an overridable default.
Design extension+task wiring with convention defaults under buildDirectory and value-locking; keep it configuration-cache-safe.
Set org-wide conventions for plugin output locations (relocatable, overridable, CC-safe) so caching and build hygiene are uniform.
## Getting ProjectLayout without project Gradle instantiates tasks, extensions, and many helper objects through its object factory, so it can satisfy `@Inject` parameters for known services. `ProjectLayout` is such a service: ```kotlin abstract class WriteTask @Inject constructor( private val layout: ProjectLayout ) : DefaultTask() { @get:OutputFile abstract val out: RegularFileProperty init { out.convention(layout.buildDirectory.file("write/out.txt")) } @TaskAction fun run() = out.get().asFile.writeText("x") } ``` Injecting (rather than `project.layout`) means the task never touches `Project` at execution time — required for the configuration cache. The same `@Inject` works for `ObjectFactory`, `ProviderFactory`, etc., but here we focus on `ProjectLayout`. ## Convention defaults for output locations `Property.convention(value)` sets a *default* that applies only if no explicit value is set. This is the right tool for plugin defaults: ```kotlin abstract class ReportExtension { abstract val outputDir: DirectoryProperty } class ReportPlugin : Plugin<Project> { override fun apply(project: Project) { val layout = project.layout val ext = project.extensions.create("report", ReportExtension::class.java) // default under build/, still overridable ext.outputDir.convention(layout.buildDirectory.dir("reports")) project.tasks.register("report") { val target = ext.outputDir.file("report.html") // derives from base it.outputs.file(target) it.doLast { target.get().asFile.writeText("<html/>") } } } } ``` Key properties of this design: - **Default-but-overridable:** users can do `report { outputDir.set(layout.projectDirectory.dir("site")) }` and every derived child follows. - **Relocatable:** because the default is a `layout.buildDirectory` Provider, relocating the build dir moves the reports. - **Lazy:** nothing resolves until execution. ## Locking values When you want to prevent later mutation: - `property.finalizeValueOnRead()` — value is computed and frozen on first read. - `property.disallowChanges()` — any further `set()` throws. - `property.finalizeValue()` — explicitly freeze now. These guard plugin invariants without sacrificing the convention default. ## Why this matters at scale Standardizing on injected `ProjectLayout` + `convention(layout.buildDirectory...)` gives a plugin suite consistent, relocatable, configuration-cache-safe output locations — the foundation for reliable caching and clean `build/` semantics across an org.
- Why inject ProjectLayout instead of using project.layout in a task action?Injecting avoids referencing Project at execution time, which the configuration cache forbids. project.layout inside an action would fail.
- What's the difference between convention() and set() for a default output location?convention() is a fallback applied only when no value is set, so users can still override; set() imposes a value that user set() calls would override or conflict with, depending on order.
- How do you stop users from changing a property after the plugin configures it?Call finalizeValueOnRead(), disallowChanges(), or finalizeValue() to lock the property.
saying these in an interview costs you the question
- Calling project.layout inside @TaskAction/doLast instead of injecting it.
- Using set() where convention() is meant, making defaults non-overridable or order-dependent.
- Hardcoding absolute output paths instead of deriving from layout.buildDirectory.