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 @@ -139,7 +139,7 @@ worker (ops); `v2`→`master` (explicit git promote only). **F-094** stays `bloc
| F-096 | done | `String.transitiveDep` catalog parity | GH **#77** — `val String.transitiveDep` → `transitiveDeps(this)` next to `String.dep` / `String.ksp`; unit tests + `docs/DEPS-CATALOG.md` API/tip. |
| F-097 | done | Finish build types: signing configs | GH **#51** — `FormaSigningConfig` + `androidBinary(signingConfigs, buildTypeSigning)` wired to AGP application signing only; dummy `application/binary/demo-release.keystore`; CALL-SITE-SURFACE § APK signing; unit tests on `:android`. |
| F-098 | done | Core library desugaring (Java 8+ APIs) | GH **#103** — project-global `coreLibraryDesugaring` + optional dep pin on `androidProjectConfiguration` / `AndroidProjectSettings`; `CompileOptions.applyFrom` + `applyCoreLibraryDesugaring` on library/binary/native; default off; docs PROJECT-CONFIGURATION + CALL-SITE-SURFACE; unit tests `:config` + `:android`. |
| F-099 | todo | Target-feature configuration options | GH **#126** — design+thin slice: conditional / flagged deps and feature toggles (e.g. AP vs reflect DI) without dual happy paths. Prefer **type/rule or single global configuration** over call-site plugin shopping. Outcome may be design doc + smallest useful API; reject free-form per-module plugin lists. |
| F-099 | done | Target-feature configuration options | GH **#126** — **done:** design `docs/TARGET-FEATURE-OPTIONS.md` + project-global `FormaFeatureFlags` on `androidProjectConfiguration` / `AndroidProjectSettings`; `depsIf`/`depsUnless`/`whenFlag` resolve at `applyDependencies` via pure `resolveFeatureFlags`; unit tests `:config`+`:deps`; docs PROJECT-CONFIGURATION / CALL-SITE / DEPS-CATALOG. Rejects call-site plugin shopping + binary-only flags. |
| F-100 | todo | Gradle project on buildscript classpath | GH **#111** — investigate allowing a Gradle **project** (composite/included build) as `extraPlugins` / buildscript classpath source alongside Maven coords. Design note first if API shape unclear; keep `extraPlugins` = classpath only (F-082). Implement only if clean with AGP 9 / Gradle 9. |
| F-101 | todo | Close `target(...)` deps API (audit) | GH **#56** — **likely largely done:** `Project.target` / `target(name)` / `target(ProjectDependency)` + `deps(vararg FormaTarget)` used across sample. Audit docs/DEPS-CATALOG/CALL-SITE; close issue or ship any missing notation/docs; do not reintroduce raw `project()` as happy path. |
| F-102 | todo | Navigation abstraction (sample + optional targets) | GH **#46** — reduce Jetpack Navigation codegen bleed across features. **Design first** (doc): presentation-layer nav ports vs Navigation Component; optional Forma nav targets only if type=rule fits. Prefer progressive example / sample refactor slice over new forever DSL. Large — split follow-ups if needed. |
Expand Down
6 changes: 6 additions & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ buildscript {
// Project-global only — not a per-module call-site flag. Default off (sample minSdk 23).
// coreLibraryDesugaring = true,
// coreLibraryDesugaringDependency = "com.android.tools:desugar_jdk_libs:2.1.5", // optional override
// F-099 / GH #126: project-global product feature flags (not AGP BuildFeatures).
// Conditional deps: depsIf("daggerReflect", …) / depsUnless("daggerReflect", …).
// See docs/TARGET-FEATURE-OPTIONS.md. Sample keeps default empty (all unknown = false).
// featureFlags = tools.forma.config.FormaFeatureFlags(
// "daggerReflect" to 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,15 +12,16 @@ 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`, `Forma.settings.buildFeatures`, `coreLibraryDesugaring`, SDK, etc.) |
| Call site (`build.gradle.kts`) | Instance attrs only: `packageName`, deps, version, optional rule flags |
| Project configuration | Global defaults (`Forma.settings.compose`, `Forma.settings.buildFeatures`, `Forma.settings.featureFlags`, `coreLibraryDesugaring`, SDK, etc.) |
| Call site (`build.gradle.kts`) | Instance attrs only: `packageName`, deps (incl. conditional), version, optional rule flags |

**Rejected at call sites**

- Builder returns / `.withPlugin` / `.withPlugins`
- Free-form `plugins = plugins(plugin("id"))` lists
- Re-selecting plugin bindings per module
- Per-module `coreLibraryDesugaring` / desugar dependency shopping (F-098 — project-global only)
- Per-module product-flag Booleans for every toggle (F-099 — declare once in `featureFlags`)

## Android target DSLs (`tools.forma.android`)

Expand Down Expand Up @@ -190,6 +191,35 @@ configuration. Default **off** (sample stays green without the extra artifact).

See [`PROJECT-CONFIGURATION.md`](PROJECT-CONFIGURATION.md) § `coreLibraryDesugaring`.

## Product feature flags + conditional deps (F-099 / GH #126)

Named **product** toggles (DI mode, optional stacks) are **project-global only**.
They are **not** AGP `BuildFeatures` and **not** a second path to apply plugins.

| Concern | Owner | Notes |
|---------|--------|--------|
| Flag declarations | **Project only** | `androidProjectConfiguration(featureFlags = FormaFeatureFlags(...))` |
| Read | `Forma.settings.featureFlags["name"]` | Unknown names → **false** |
| Conditional named deps | Call-site helpers | `depsIf` / `depsUnless` / `NamedDependency.whenFlag` |
| Plugin identity shopping gated by flags | **Rejected** | Type-owned plugins stay type-owned ([TARGET-PLUGINS.md](TARGET-PLUGINS.md)) |
| Per-`impl` Boolean for each product flag | **Rejected** | Fat call sites; dual path |

```kotlin
// root — declare once
featureFlags = FormaFeatureFlags("daggerReflect" to true)

// target — same graph shape everywhere; resolution at apply time
dependencies = deps(
"com.google.dagger:dagger:…".dep,
depsIf("daggerReflect", "com.jakewharton.dagger:dagger-reflect:…".dep),
depsUnless("daggerReflect", "com.google.dagger:dagger-compiler:…".ksp),
)
```

Helpers **tag** specs; `applyDependencies` resolves against the store. Do not teach
raw `if (project.hasProperty)` Gradle as the happy path. Full design + rejected
alternatives: [`TARGET-FEATURE-OPTIONS.md`](TARGET-FEATURE-OPTIONS.md).

## Removed (F-081)

| Removed | Replacement |
Expand Down
9 changes: 8 additions & 1 deletion docs/DEPS-CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ step 08 introduces catalogs.
| `parseGroupArtifactVersion` | same | GAV validation |
| `deps` / `String.dep` / `Provider.dep` | root (`dependencies.kt`) | Bridge into `FormaDependency` (named deps default **non-transitive**) |
| `transitiveDeps` / `String.transitiveDep` | root (`dependencies.kt`) | Same bridge with **transitive** named deps (`String.transitiveDep` = single-string parity with `String.dep`) |
| `applyDependencies` | `tools.forma.deps.core` | Wire deps + plugin side effects |
| `depsIf` / `depsUnless` / `NamedDependency.whenFlag` | root (`dependencies.kt`) | **F-099** — gate named deps on project-global `FormaFeatureFlags`; resolved at `applyDependencies` time (not construction). Unknown flag = false |
| `resolveFeatureFlags` | `tools.forma.deps.core` | Pure filter of flag-gated `NameSpec`s (unit-tested) |
| `applyDependencies` | `tools.forma.deps.core` | Wire deps + plugin side effects (+ F-099 flag resolution) |

---

Expand All @@ -194,6 +196,10 @@ step 08 introduces catalogs.
6. **Plugins vs apply** — catalog `plugin(...)` + `extraPlugins` put jars on the
**buildscript classpath only**. Type-owned apply is separate
([TARGET-PLUGINS.md](TARGET-PLUGINS.md)).
7. **Conditional deps (F-099)** — declare flags once on
`androidProjectConfiguration(featureFlags = …)`; use `depsIf` / `depsUnless` at
call sites. Do **not** shop plugins with flags or add per-module Booleans for
every product toggle. See [`TARGET-FEATURE-OPTIONS.md`](TARGET-FEATURE-OPTIONS.md).

---

Expand All @@ -207,3 +213,4 @@ step 08 introduces catalogs.
- **Target external plugins** — type owns plugin, call sites auto-apply:
[`TARGET-PLUGINS.md`](TARGET-PLUGINS.md)
- Call-site surface: [`CALL-SITE-SURFACE.md`](CALL-SITE-SURFACE.md)
- Product feature flags + conditional deps: [`TARGET-FEATURE-OPTIONS.md`](TARGET-FEATURE-OPTIONS.md)
4 changes: 3 additions & 1 deletion docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ buildscript {
// compose = false, // project default for per-target compose flags
// composeCompilerVersion = "2.3.21", // match your Kotlin (2.3.21 → 2.3.21)
// coreLibraryDesugaring = true, // F-098: Java 8+ library APIs on lower minSdk (project-global)
// featureFlags = FormaFeatureFlags("daggerReflect" to true), // F-099: product flags + depsIf/depsUnless
// Classpath only — does NOT apply plugins to modules. See TARGET-PLUGINS.md.
extraPlugins = listOf(
// e.g. libs.plugins.navigationSafeArgs (jar on buildscript classpath)
Expand All @@ -183,7 +184,8 @@ puts AGP + optional plugin jars on the **buildscript classpath only**, and regis
`clean` task. Child targets read values from `Forma.settings`.

See [`PROJECT-CONFIGURATION.md`](PROJECT-CONFIGURATION.md) for the single-path + store story
(including optional **core library desugaring** for Java 8+ APIs on lower `minSdk` — F-098).
(including optional **core library desugaring** for Java 8+ APIs on lower `minSdk` — F-098,
and **feature flags** + conditional deps — F-099 / [`TARGET-FEATURE-OPTIONS.md`](TARGET-FEATURE-OPTIONS.md)).

### External Gradle plugins (safe-args, Firebase, …)

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

Newest entries first.

## 2026-07-25 — F-099: project-global feature flags + conditional deps (GH #126)

- **Ticket:** F-099 → `done` (GH #126)
- **Branch:** `forma/F-099-target-feature-options` (from `origin/v2`)
- **Design:** `docs/TARGET-FEATURE-OPTIONS.md` — project-global named boolean flags only; flags select deps/behavior not plugin identity; binary-linked config deferred; rejects `.withPlugin` / per-module flag shopping / free-form Gradle as happy path.
- **Code:**
- `FormaFeatureFlags` (pure) + `AndroidProjectSettings.featureFlags` + `FormaSettingsStore.featureFlagsOrEmpty()`
- `androidProjectConfiguration(featureFlags = …)`
- `NameSpec.featureFlag` / `featureFlagExpected`; `depsIf` / `depsUnless` / `NamedDependency.whenFlag`
- Pure `resolveFeatureFlags` in `ConditionalDependency.kt`; wired in `applyDependencies` at apply time
- Sample: commented `featureFlags` example in `application/build.gradle.kts` (default empty)
- Jacoco happy-path includes `FormaFeatureFlags*` + `ConditionalDependency*`
- **Tests:** `:config` `FormaFeatureFlagsTest`; `:deps` `ConditionalDependencyTest` (DI swap recipe on/off/unknown)
- **Docs:** TARGET-FEATURE-OPTIONS; PROJECT-CONFIGURATION § featureFlags; CALL-SITE-SURFACE § product flags; DEPS-CATALOG API rows; GETTING-STARTED pointer
- **Skills/modes:** Grok Build `--mode full` (design/plan + implement); Hermes finish path after CLI timeout on app assemble
- **Verify (real host, `source scripts/env-mac.sh`):**
- `plugins/`: `./gradlew :config:test :deps:test test jacocoHappyPathCoverageVerification` → **BUILD SUCCESSFUL** in 9s
- `application/`: `./gradlew :binary:assembleDebug` → **BUILD SUCCESSFUL** in 10s (599 tasks, up-to-date)
- **Commits/PRs:** this branch; Hermes PR/merge; close GH #126 when merged
- **Blockers:** none (F-094 Portal still human-blocked)
- **Next step:** F-100 (Gradle project on buildscript classpath)

## 2026-07-25 — F-098: project-global core library desugaring (GH #103)

- **Ticket:** F-098 → `done` (GH #103)
Expand Down
48 changes: 47 additions & 1 deletion docs/PROJECT-CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ buildscript {
// F-098 / GH #103: core library desugaring (Java 8+ APIs on lower minSdk)
// coreLibraryDesugaring = true,
// coreLibraryDesugaringDependency = "com.android.tools:desugar_jdk_libs:2.1.5", // optional
// F-099 / GH #126: project-global product feature flags (not AGP BuildFeatures)
// featureFlags = tools.forma.config.FormaFeatureFlags(
// "daggerReflect" to true,
// ),
// F-090: skip project-dep suffix checks only for listed forked-in modules
// dependencyValidationExclusions = setOf(
// ":third-party:exoplayer:library-core",
Expand Down Expand Up @@ -139,6 +143,46 @@ androidProjectConfiguration(
)
```

### `featureFlags` (F-099 / GH #126)

| | |
|--|--|
| **Type** | `FormaFeatureFlags` (default empty — unknown names read as **false**) |
| **Set on** | Root `androidProjectConfiguration(...)` only |
| **Stored as** | `AndroidProjectSettings.featureFlags` |
| **Read by** | `applyDependencies` (conditional deps); optional `Forma.settings.featureFlags[…]` |

Project-global **named boolean product flags** for conditional dependencies and
shared behavior toggles (e.g. AP/KSP Dagger vs dagger-reflect). **Not** AGP
`BuildFeatures` ([FormaBuildFeatures](#buildfeatures-f-091--gh-88) is a different
concern). **Not** per-module plugin shopping.

```kotlin
androidProjectConfiguration(
// ...
featureFlags = FormaFeatureFlags(
"daggerReflect" to true,
),
)
```

Conditional deps at target call sites:

```kotlin
dependencies = deps(
"com.google.dagger:dagger:2.48".dep,
depsIf("daggerReflect", "com.jakewharton.dagger:dagger-reflect:…".dep),
depsUnless("daggerReflect", "com.google.dagger:dagger-compiler:…".ksp),
)
```

Resolution runs at **apply** time against the store (not when the helper is built).
Unknown flag names are **false** (`get` / `isEnabled`); use `require(name)` only when
a missing declaration must fail configuration.

Full design, rejected alternatives, and DI recipe:
[`TARGET-FEATURE-OPTIONS.md`](TARGET-FEATURE-OPTIONS.md).

## 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 Down Expand Up @@ -169,8 +213,9 @@ 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, `buildFeatures`, core library desugaring, repos, etc. |
| `AndroidProjectSettings` | `plugins/config/.../AndroidProjectSettings.kt` | Immutable data class holding SDKs, versions, compose default, `buildFeatures`, `featureFlags`, core library desugaring, repos, etc. |
| `FormaBuildFeatures` | `plugins/config/.../FormaBuildFeatures.kt` | Nested AGP BuildFeatures defaults (all off); pure data + resolve helper |
| `FormaFeatureFlags` | `plugins/config/.../FormaFeatureFlags.kt` | Project-global named product flags (F-099); empty ⇒ unknown = false |
| `DEFAULT_CORE_LIBRARY_DESUGARING_DEPENDENCY` | `plugins/config/.../AndroidProjectSettings.kt` | Default `desugar_jdk_libs` GAV pin (F-098) |
| `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`) |
Expand All @@ -186,6 +231,7 @@ Readers (feature wiring, dependency helpers, target DSLs) obtain values through
- [`COMPOSE.md`](COMPOSE.md) — `compose` + `composeCompilerVersion` details
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — plugin module layout and store
- [`VISION.md`](VISION.md) — root principles (one global way)
- [`TARGET-FEATURE-OPTIONS.md`](TARGET-FEATURE-OPTIONS.md) — product feature flags + conditional deps (F-099)
- Sample: `application/build.gradle.kts`
- Progressive example 10: `examples/android/10-target-plugins`

Expand Down
Loading
Loading