Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Promoted from daily next-actions / historical GH. Workers pick top `todo` in ord
| F-088 | done | Fleet tooling phase 2 (F-084 follow-ups) | Gradle `formaLayoutCheck`/`formaLayoutGenerate` + root `*All` via `tools.forma.deps.fleet.registerFormaLayout` on all packageName DSLs; opt-in `checkPackageLayoutAtConfiguration` (default false); res/viewBinding/binary skip source-dir requirement; docs/skill updated. GH **#54** generate path user-complete (Hermes may close). AST migrate / depgen still deferred. |
| F-089 | done | Navigation task cache broken | GH **#110** — upstream Safe Args **2.9.8** already `@PathSensitive(RELATIVE)` on `navigationFiles`; two-location build-cache hit verified on sample `navigationRes`; regression note in `docs/CONFIGURATION-PERFORMANCE.md`. |
| F-090 | done | Exclude modules from dependency validation | GH **#97** — `dependencyValidationExclusions` on root `androidProjectConfiguration` / `AndroidProjectSettings`; `applyDependencies` skips project-dep suffix validation for exact path/name matches only. Self-type validation unchanged. Unit tests in `:config` + `:deps`; matrix + PROJECT-CONFIGURATION docs. |
| F-091 | todo | BuildFeatures under Forma | GH **#88** — map AGP `BuildFeatures` into Forma (default off where safe); single configuration path aligned with type=rule + project-global defaults; document in CALL-SITE / PROJECT-CONFIGURATION. Do not reintroduce fat call-site plugin shopping. |
| F-091 | done | BuildFeatures under Forma | GH **#88** — `FormaBuildFeatures` project-global (defaults **off**); central apply on library+binary; type-owned viewBinding/composeWidget + minimal `impl`/`compose` attrs; docs CALL-SITE + PROJECT-CONFIGURATION. |
| F-092 | todo | versionCode / versionName on binary (and app) | GH **#82** — move version identity off global-only `FormaConfiguration`; set on `androidBinary` / `androidApp` as call-site attrs (Gradle limits acknowledged). Sample uses new API; docs. |
| F-093 | todo | Remove legacy `.kapt` once unused | After F-086 sample is KSP-only: hard-remove or hard-deprecate `String.kapt` / `kapt()` / `Kapt` configuration path if no in-repo consumers; keep only if a documented external-compat shim is still required. Grep tree + tests + DEPS docs. |
| F-094 | blocked | Gradle Plugin Portal Forma org/user | GH **#133** — human/admin: create shared Portal user/org and credentials for team publish. Worker cannot finish without Stepan Portal access. Track only; see `docs/PLUGIN-PUBLISH.md`. |
Expand Down
4 changes: 4 additions & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ buildscript {
// dependencyValidationExclusions = setOf(
// ":third-party:exoplayer:library-core",
// ),
// F-091 / GH #88: AGP BuildFeatures — all default off. Opt in intentionally, e.g.:
// buildFeatures = tools.forma.config.FormaBuildFeatures(
// buildConfig = true,
// ),
extraPlugins =
listOf(
libs.plugins.toolsFormaDemoDependencies,
Expand Down
34 changes: 32 additions & 2 deletions docs/CALL-SITE-SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ settings), not on a builder chain after the call.
| Layer | Owns |
|-------|------|
| Target **type** / rule | Plugins, content rules, matrix row, always-on features for that role |
| Project configuration | Global defaults (`Forma.settings.compose`, SDK, etc.) |
| Project configuration | Global defaults (`Forma.settings.compose`, `Forma.settings.buildFeatures`, SDK, etc.) |
| Call site (`build.gradle.kts`) | Instance attrs only: `packageName`, deps, version, optional rule flags |

**Rejected at call sites**
Expand Down Expand Up @@ -67,7 +67,7 @@ a second way to apply plugins.
| Flag | Where | Default | Policy |
|------|--------|---------|--------|
| `compose` | `impl`, `androidApp`, `androidBinary`, `uiLibrary`, `androidUtil` | `Forma.settings.compose` (project-global) | Prefer global on; override only when a module must differ. Dedicated Compose UI → `composeWidget` type. |
| `viewBinding` | `impl` only (Boolean) | `false` | Layouts-only modules use the **`viewBinding` target type** (always on). Do not invent dual structure via flags. |
| `viewBinding` | `impl` only (Boolean) | `Forma.settings.buildFeatures.viewBinding` (default **false**) | Layouts-only modules use the **`viewBinding` target type** (always on). Do not invent dual structure via flags. |
| *(type-owned)* | `composeWidget` | Compose always on | Prefer dedicated type over `widget(compose=true)`. |
| *(type-owned)* | `viewBinding` target | View binding always on | Prefer type over `impl(viewBinding=true)` when the module is layouts-only. |
| External plugins | never a call-site flag | n/a | `targetPlugin` + `deriveTargetType` / `registerTargetPlugin` |
Expand All @@ -77,6 +77,36 @@ Other common attrs (`testInstrumentationRunner`, `buildConfiguration`,
instance knobs with empty/public defaults — keep them; do not grow plugin-id
parameters beside them.

## AGP BuildFeatures (F-091 / GH #88)

AGP `BuildFeatures` flags are **project-global by default** (all **off**), with
type/call-site ownership only where already established. There is **no** per-flag
Boolean shopping list on every DSL.

| Concern | Owner | How to enable |
|---------|--------|----------------|
| `aidl`, `buildConfig`, `dataBinding`, `prefab`, `resValues`, `shaders` | Project only | `androidProjectConfiguration(buildFeatures = FormaBuildFeatures(...))` |
| `viewBinding` (project default for `impl` + binary) | Project + `impl` attr | `buildFeatures.viewBinding`; `impl` may override; binary has **no** call-site attr (fleet default only) |
| `viewBinding` always on | **`viewBinding` target type** | Use `viewBinding { … }` (type forces on) |
| `compose` project default | Top-level `compose =` on `androidProjectConfiguration` | **Not** nested under `buildFeatures` (one happy path) |
| `compose` per target | Call-site `compose` on listed DSLs | Defaults to `Forma.settings.compose` |
| `compose` always on | **`composeWidget` target type** | Use `composeWidget { … }` |

**Apply path:** library + binary feature definitions call a central helper that
starts from `Forma.settings.buildFeatures`, applies type/call-site overrides for
viewBinding/compose, and **explicitly sets every supported flag** so AGP cannot
silently enable features. Compose still runs `enableCompose` (compiler plugin),
not only the boolean.

**Do not** add call-site Booleans for `aidl` / `buildConfig` / `dataBinding` / etc.
Fleet-wide BuildConfig → `buildFeatures.buildConfig = true`. Rare per-module
BuildConfig (if ever needed) should be a dedicated derived type or a single
documented attr later — not free-form plugin/feature shopping.

**dataBinding** often pairs with view binding in app code; Forma does **not**
auto-enable one when the other is on — set both explicitly if both are required.
`renderScript` is not mapped (removed/deprecated in modern AGP).

## Removed (F-081)

| Removed | Replacement |
Expand Down
6 changes: 6 additions & 0 deletions docs/COMPOSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ androidProjectConfiguration(
`composeWidget` modules **always** enable Compose; they ignore the project default
(they do not need `compose = true`).

**F-091 note:** Compose stays a **top-level** `androidProjectConfiguration` parameter.
Other AGP `BuildFeatures` flags (`buildConfig`, `aidl`, …) live under nested
`buildFeatures = FormaBuildFeatures(...)`. Do not set Compose via that nested
object — one happy path only. See [`PROJECT-CONFIGURATION.md`](PROJECT-CONFIGURATION.md)
§ `buildFeatures`.

## Per-target `compose` flag

```kotlin
Expand Down
24 changes: 24 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

Newest entries first.

## 2026-07-22 — F-091: BuildFeatures under Forma (GH #88)

- **Ticket:** F-091 → `done`
- **Branch:** `forma/F-091-build-features` (from origin/v2 @ cb65049)
- **Skills/modes:** Grok Build `--mode full` (design/plan + implement); Hermes finish path after implement timeout on app verify
- **Design (shipped):**
- Layer ownership: type owns always-on (`viewBinding` / `composeWidget`); project owns fleet AGP flags; minimal call-site attrs only (`compose`, `impl.viewBinding`)
- Nested `FormaBuildFeatures` for non-Compose flags (all default **false**); Compose stays top-level `compose` / `composeCompilerVersion` (one happy path)
- No per-flag call-site shopping; no `renderScript`; dataBinding not auto-coupled to viewBinding
- **Code:**
- `plugins/config` `FormaBuildFeatures` + `resolveWith` → `ResolvedFormaBuildFeatures`; unit tests
- `AndroidProjectSettings.buildFeatures`; removed “No BuildConfig Support” limitation
- `androidProjectConfiguration(buildFeatures = …)` store wiring
- `BuildFeaturesSupport.applyFormaBuildFeatures` — explicit write of every supported flag; `dataBinding` via `LibraryBuildFeatures` / `ApplicationBuildFeatures` (AGP 9); compose via `enableCompose` or forced false
- Wired into library + binary feature definitions; binary viewBinding = project default only
- `impl(viewBinding=)` default now `Forma.settings.buildFeatures.viewBinding`
- **Docs:** `CALL-SITE-SURFACE.md` inventory; `PROJECT-CONFIGURATION.md` § buildFeatures; `COMPOSE.md` cross-link; sample commented example
- **Verify (real host, `source scripts/env-mac.sh`):**
- `plugins/`: `./gradlew :config:test :android:compileKotlin build --no-daemon` → **BUILD SUCCESSFUL** (80 tasks)
- `application/`: `./gradlew help` + `assembleDebug --no-daemon` → **BUILD SUCCESSFUL** in 2m4s (896 tasks)
- **Blockers:** none
- **Next:** F-092 versionCode / versionName on binary (and app) (GH #82)
- **GH #88:** close when this PR merges

## 2026-07-22 — F-090: Exclude modules from dependency validation (GH #97)

- **Ticket:** F-090 → `done`
Expand Down
39 changes: 38 additions & 1 deletion docs/PROJECT-CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ buildscript {
agpVersion = "9.3.0",
// compose = false,
// composeCompilerVersion = "2.3.21",
// F-091 / GH #88: AGP BuildFeatures — all default off; opt in intentionally
// buildFeatures = FormaBuildFeatures(
// buildConfig = true,
// // viewBinding = true, // default for impl(viewBinding=…) only
// ),
// F-090: skip project-dep suffix checks only for listed forked-in modules
// dependencyValidationExclusions = setOf(
// ":third-party:exoplayer:library-core",
Expand Down Expand Up @@ -63,6 +68,37 @@ Does **not** disable self-type validation on Forma DSL entry points.

See [`DEPENDENCY-MATRIX.md`](DEPENDENCY-MATRIX.md) § Dependency validation exclusions.

### `buildFeatures` (F-091 / GH #88)

| | |
|--|--|
| **Type** | `FormaBuildFeatures` (default `FormaBuildFeatures()` — **all flags false**) |
| **Set on** | Root `androidProjectConfiguration(...)` only |
| **Stored as** | `AndroidProjectSettings.buildFeatures` |
| **Read by** | Android library + binary feature definitions (central apply helper) |

Maps AGP public `BuildFeatures` flags into Forma with **defaults off** where safe:

| Flag | In `FormaBuildFeatures` | Notes |
|------|-------------------------|--------|
| `aidl` | yes | Project-global only |
| `buildConfig` | yes | Project-global; enables `BuildConfig` generation fleet-wide when true |
| `dataBinding` | yes | Project-global; AGP 9 exposes this on library/app feature subtypes; does not auto-enable `viewBinding` |
| `prefab` | yes | Project-global |
| `resValues` | yes | Project-global |
| `shaders` | yes | Project-global |
| `viewBinding` | yes | Default for `impl(viewBinding=…)`; **`viewBinding` target type always on** |
| `compose` | **no** — use top-level `compose` | One happy path for Compose project default |
| `renderScript` | not mapped | Deprecated/removed in modern AGP |

Every Android library/app target **explicitly sets** each supported flag from the
resolved values so AGP platform defaults cannot silently turn features on.

**Compose relationship:** keep using top-level `compose = true/false` and
`composeCompilerVersion`. Do not put a second Compose default inside
`FormaBuildFeatures`. See [`COMPOSE.md`](COMPOSE.md) and
[`CALL-SITE-SURFACE.md`](CALL-SITE-SURFACE.md) § BuildFeatures.

## What it does **not** do

- `extraPlugins` (and catalog `plugin(...)` entries) are **classpath only**. They put Gradle plugin jars on the buildscript classpath. They do **not** apply any plugin to your modules.
Expand All @@ -86,7 +122,8 @@ The deprecated `Project.androidProjectConfiguration(...)` receiver overload has
| Piece | Location | Role |
|-------|----------|------|
| `androidProjectConfiguration(...)` (ScriptHandlerScope) | `plugins/android/.../androidProjectConfiguration.kt` | The one public API; called from root `buildscript` |
| `AndroidProjectSettings` | `plugins/config/.../AndroidProjectSettings.kt` | Immutable data class holding SDKs, versions, compose default, repos, etc. |
| `AndroidProjectSettings` | `plugins/config/.../AndroidProjectSettings.kt` | Immutable data class holding SDKs, versions, compose default, `buildFeatures`, repos, etc. |
| `FormaBuildFeatures` | `plugins/config/.../FormaBuildFeatures.kt` | Nested AGP BuildFeatures defaults (all off); pure data + resolve helper |
| `FormaSettingsStore` | `plugins/config/.../AndroidProjectSettings.kt` (as `object`) | The backing singleton store (`SettingsStore<T>` + `PluginInfoStore`) |
| `Forma` (singleton accessor) | `plugins/android/.../androidProjectConfiguration.kt` | `object Forma : SettingsStore<...> by FormaSettingsStore, ...` — primary reader surface (`Forma.settings`) |
| `registerAndroidDefaults()` | `plugins/android/.../AndroidTargetRegistry.kt` | Populates `AndroidTargetRegistry` (target types + matrix) |
Expand Down
9 changes: 8 additions & 1 deletion plugins/android/src/main/java/androidProjectConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.gradle.plugin.use.PluginDependency
import tools.forma.android.utils.register
import tools.forma.android.target.registerAndroidDefaults
import tools.forma.config.AndroidProjectSettings
import tools.forma.config.FormaBuildFeatures
import tools.forma.config.FormaSettingsStore
import tools.forma.config.PluginInfoStore
import tools.forma.config.SettingsStore
Expand Down Expand Up @@ -59,7 +60,8 @@ import tools.forma.deps.fleet.ensureFormaLayoutRootTasks
* @param kotlinVersion Kotlin version (defaults to Gradle's embeddedKotlinVersion)
* @param agpVersion Android Gradle Plugin version to place on the buildscript classpath
* @param repositories optional block to configure repositories (applied in buildscript context)
* @param compose project-wide default for per-target Compose flags (see [AndroidProjectSettings.compose])
* @param compose project-wide default for per-target Compose flags (see [AndroidProjectSettings.compose]).
* Kept top-level — do **not** nest Compose under [buildFeatures] (one happy path for Compose default).
* @param composeCompilerVersion Compose compiler extension version for AGP `composeOptions`
* (must match the Kotlin version used)
* @param javaVersionCompatibility Java language level for source/target compatibility
Expand All @@ -73,6 +75,9 @@ import tools.forma.deps.fleet.ensureFormaLayoutRootTasks
* For forked-in third-party trees that do not use Forma suffixes. Does not weaken the
* default matrix for non-listed modules and does not skip self-type validation on DSL
* targets. Default empty. See [AndroidProjectSettings.dependencyValidationExclusions].
* @param buildFeatures project-global AGP BuildFeatures defaults (F-091 / GH #88). All flags
* default **false**. Nested object for non-Compose flags (+ optional `viewBinding` default
* for `impl`). Compose stays on [compose] above. See [FormaBuildFeatures].
* @param extraPlugins list of extra artifacts / plugin providers to add to the **buildscript classpath only**.
* See "Classpath vs apply" in TARGET-PLUGINS.md.
*/
Expand All @@ -91,6 +96,7 @@ fun ScriptHandlerScope.androidProjectConfiguration(
vectorDrawablesUseSupportLibrary: Boolean = false,
checkPackageLayoutAtConfiguration: Boolean = false,
dependencyValidationExclusions: Set<String> = emptySet(),
buildFeatures: FormaBuildFeatures = FormaBuildFeatures(),
extraPlugins: List<Any> = emptyList()
) {
buildScriptConfiguration(
Expand Down Expand Up @@ -128,6 +134,7 @@ fun ScriptHandlerScope.androidProjectConfiguration(
vectorDrawablesUseSupportLibrary = vectorDrawablesUseSupportLibrary,
checkPackageLayoutAtConfiguration = checkPackageLayoutAtConfiguration,
dependencyValidationExclusions = dependencyValidationExclusions,
buildFeatures = buildFeatures,
)

Forma.store(configuration)
Expand Down
5 changes: 4 additions & 1 deletion plugins/android/src/main/java/impl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import tools.forma.validation.validate
* - **Must not** depend on other `impl` modules — feature graphs compose only
* at [androidApp] / [androidBinary] so implementations stay independent.
*
* @param viewBinding enable View Binding for this feature impl; defaults to project-wide
* [androidProjectConfiguration] `buildFeatures.viewBinding`. Layouts-only modules should
* use the dedicated `viewBinding` target type instead (always on).
* @param compose enable Jetpack Compose for this target; defaults to project-wide
* [androidProjectConfiguration] `compose` flag.
*/
Expand All @@ -33,7 +36,7 @@ fun Project.impl(
dependencies: FormaDependency = emptyDependency(),
testDependencies: NamedDependency = emptyDependency(),
androidTestDependencies: NamedDependency = emptyDependency(),
viewBinding: Boolean = false,
viewBinding: Boolean = Forma.settings.buildFeatures.viewBinding,
compose: Boolean = Forma.settings.compose,
testInstrumentationRunner: String = androidJunitRunner,
buildConfiguration: BuildConfiguration = BuildConfiguration(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ fun androidBinaryFeatureDefinition(
buildTypes.applyFrom(configuration.buildConfiguration)
compileOptions.applyFrom(formaConfiguration)

if (configuration.compose) {
enableCompose(project, formaConfiguration.composeCompilerVersion)
}
applyFormaBuildFeatures(
project = project,
defaults = formaConfiguration.buildFeatures,
composeCompilerVersion = formaConfiguration.composeCompilerVersion,
// No binary call-site attr — fleet-wide project default only (F-091).
viewBinding = formaConfiguration.buildFeatures.viewBinding,
compose = configuration.compose,
)
}
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ fun androidLibraryFeatureDefinition(
buildTypes.applyFrom(feature.buildConfiguration)
compileOptions.applyFrom(formaConfiguration)

buildFeatures.viewBinding = feature.viewBinding
if (feature.compose) {
enableCompose(project, formaConfiguration.composeCompilerVersion)
}
applyFormaBuildFeatures(
project = project,
defaults = formaConfiguration.buildFeatures,
composeCompilerVersion = formaConfiguration.composeCompilerVersion,
viewBinding = feature.viewBinding,
compose = feature.compose,
)
}
// AGP library `verify*Resources` is overly strict with Navigation safe-args
// graphs in pure `androidRes` modules (debug APK still packages graphs correctly).
Expand Down
Loading
Loading