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
4 changes: 2 additions & 2 deletions TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,11 @@ External multimodule OSS validation of meta-build axioms (structure over configu
type-owned plugins, attrs-only call sites, closed matrix). **Not** another in-repo sample.

Open coding: **F-115** (`in_progress`, **`priority: now`**, **cron may continue**). **F-094** Portal remains `blocked` (human).
4h workers: pick F-115 phase C remainder (**F27 library flavors** / Phase D) while gated; if no ticket has `priority: now` / `cron may continue`, respond `[SILENT]` (audit 2026-07-29 gate — interactive “Go ahead” alone is not enough).
4h workers: pick F-115 **Phase D** case study (or residual dogfood polish) while gated; if no ticket has `priority: now` / `cron may continue`, respond `[SILENT]` (audit 2026-07-29 gate — interactive “Go ahead” alone is not enough).

| ID | Status | Title | Notes |
|----|--------|-------|-------|
| F-115 | in_progress · **priority: now** · **cron may continue** | Dogfood Now in Android under Forma | Primary OSS target [`android/nowinandroid`](https://github.com/android/nowinandroid). Design: [`docs/DOGFOOD-NIA.md`](docs/DOGFOOD-NIA.md). **Phase A+B+C through analytics+notifications done:** Hilt/Room/Firebase/Proto DS + features + WM + flavors + designsystem + core:ui + network + **`core:analytics`** + **`core:notifications`{res,util}** (F31). Findings F16–F31. **Next phase C:** F27 library flavors / Phase D case study. No `androidLibrary`. |
| F-115 | in_progress · **priority: now** · **cron may continue** | Dogfood Now in Android under Forma | Primary OSS target [`android/nowinandroid`](https://github.com/android/nowinandroid). Design: [`docs/DOGFOOD-NIA.md`](docs/DOGFOOD-NIA.md). **Phase A+B+C done through F27 library flavors:** Hilt/Room/Firebase/Proto DS + features + WM + binary flavors + designsystem + core:ui + network + analytics/notifications + **library `productFlavors` + `.forProductFlavor` deps** (F27). Findings F16–F31 (F27 closed). **Next:** Phase D case study. No `androidLibrary`. |

## Backlog (lower priority / historical GitHub)

Expand Down
42 changes: 37 additions & 5 deletions docs/CALL-SITE-SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,19 @@ default).

**Not in scope:** Play App Signing backend integration, library AAR signing APIs.

## Product flavors (F-115 / NiA F7)
## Product flavors (F-115 / NiA F7 + F27)

| Concern | Owner | Notes |
|---------|--------|--------|
| Flavor dimensions + product flavors | **`androidBinary` call site only** (`productFlavors: List<FormaProductFlavor>`) | Wired to AGP `ApplicationExtension.flavorDimensions` + `productFlavors` |
| Flavor model | **`FormaProductFlavor`** (one global way) | Same data class for binary + library; libraries ignore APK-only fields (`applicationIdSuffix` / `versionNameSuffix`) |
| Binary flavor dimensions | **`androidBinary(productFlavors=…)`** | Wired to AGP `ApplicationExtension.flavorDimensions` + `productFlavors` |
| Library flavor dimensions | **`androidUtil` / `impl` / `androidApp` / `composeWidget` (+ thin Hilt wrappers)** (F27) | Wired via `AndroidLibraryFeatureConfiguration` → `LibraryExtension.applyProductFlavors`; registers `src/<flavor>/kotlin`. Intermediate consumers of flavored libraries must declare the **same** dimension list (AGP variant matching). |
| Build types | **`BuildConfiguration`** (unchanged) | Flavors are **not** stuffed into build-type map |
| Library / `impl` / `androidApp` flavor API | **None (v1)** | Unflavored libraries resolve against flavored APKs; multi-module `demoImplementation` is a documented gap |
| Flavor-scoped external deps | **`NamedDependency.forProductFlavor` / `PlatformDependency.forProductFlavor`** | Maps onto AGP `{flavor}Implementation` via `ConfigurationType.CustomConfiguration`; preserves transitive + feature-flag gates |
| Raw `android { productFlavors { … } }` | **Rejected as happy path** | Escape hatch only |
| Free-form config name strings at call sites | **Rejected** | Prefer `.forProductFlavor("prod")` over hand-written `"prodImplementation"` |

**Call-site shape (NiA `demo` / `prod`):**
**Binary call-site shape (NiA `demo` / `prod`):**

```kotlin
import tools.forma.android.utils.FormaProductFlavor
Expand All @@ -183,11 +186,40 @@ androidBinary(
// Tasks: assembleDemoDebug / assembleProdDebug (not bare assembleDebug)
```

**Empty list (default)** keeps a single unflavored APK so existing samples stay
**Library + flavor-scoped deps (NiA analytics / notifications):**

```kotlin
import tools.forma.android.utils.FormaProductFlavor

val contentTypeFlavors = listOf(
FormaProductFlavor(name = "demo", dimension = "contentType"),
FormaProductFlavor(name = "prod", dimension = "contentType"),
)

androidUtil(
packageName = "com.example.analytics",
productFlavors = contentTypeFlavors,
dependencies = transitiveDeps(
"androidx.compose.runtime:runtime:…",
) +
transitivePlatform("com.google.firebase:firebase-bom:33.16.0")
.forProductFlavor("prod") +
transitiveDeps("com.google.firebase:firebase-analytics")
.forProductFlavor("prod"),
)
// Sources: src/main/kotlin + src/demo/kotlin + src/prod/kotlin
// prodImplementation(firebase-bom platform) + prodImplementation(firebase-analytics)
```

**Empty list (default)** keeps unflavored targets so existing samples stay
`assembleDebug`. Optional attrs on [FormaProductFlavor]: `versionNameSuffix`,
`matchingFallbacks`, `manifestPlaceholders`, `buildConfigFields` (BuildConfig
fields apply only when project-global `buildFeatures.buildConfig` is already on).

Other library DSLs (`impl`, `uiLibrary`, …) can gain `productFlavors` later by
forwarding into `AndroidLibraryFeatureConfiguration` — **androidUtil** is the
dogfood path for F27; do not invent per-module raw AGP blocks.

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

AGP `BuildFeatures` flags are **project-global by default** (all **off**), with
Expand Down
33 changes: 25 additions & 8 deletions docs/DOGFOOD-NIA.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dogfood: Now in Android → Forma (F-115)

**Status:** `in_progress` — phase C through **analytics + notifications** green (F27 library flavors next)
**Status:** `in_progress` — phase C through **library product flavors (F27)** green; **Phase D** case study next
**Upstream:** [android/nowinandroid](https://github.com/android/nowinandroid) (Apache-2.0)
**Pinned checkout (local):** `/Users/claw/work/nowinandroid` @ `7d45eae` (main tip when cloned 2026-07-29)
**Dogfood fork:** `/Users/claw/work/nowinandroid-forma`
Expand Down Expand Up @@ -157,8 +157,8 @@ These are the **value** of dogfooding — expected friction, not blockers to ign
### F7 — Product flavors `demo` / `prod`

- Affects source sets and dependency variants (`prodImplementation` FCM on sync).
- **Resolved (binary v1, 2026-07-31):** `FormaProductFlavor` + `androidBinary(productFlavors=…)` — binary-only; `BuildConfiguration` stays build-types. NiA spike: `demo`/`prod` on `contentType`; `assembleDemoDebug` / `assembleProdDebug`.
- **Still gap (F27):** library-level productFlavors + `demoImplementation`/`prodImplementation` edges (e.g. sync FCM) — not in v1; unflavored libraries resolve against flavored APK.
- **Resolved (binary v1, 2026-07-31):** `FormaProductFlavor` + `androidBinary(productFlavors=…)` — binary; `BuildConfiguration` stays build-types. NiA spike: `demo`/`prod` on `contentType`; `assembleDemoDebug` / `assembleProdDebug`.
- **Resolved (library F27, 2026-08-01):** same `FormaProductFlavor` on `androidUtil` / library feature config + `LibraryExtension.applyProductFlavors` + `src/<flavor>/kotlin`; flavor-scoped deps via `.forProductFlavor("prod")` → `prodImplementation`.

## Phased execution plan

Expand Down Expand Up @@ -352,7 +352,7 @@ Workspace: same **`forma-spike`** external tree.
| Spike binary | `hiltFirebaseBinary(productFlavors = demo+prod contentType)`; demo `applicationIdSuffix = ".demo"` |
| Tasks | `:binary:assembleDemoDebug` + `:binary:assembleProdDebug` (bare `assembleDebug` gone once flavored) |
| **F7** | closed for APK root |
| **F27** | multi-module library flavors / variant deps (e.g. sync FCM) — not in v1; unflavored libraries resolve against flavored APK. |
| **F27** | closed in library flavors section below |
| Verify | `forma-spike` assembleDemoDebug + assembleProdDebug green after `publish-local 0.1.3-NIA` |

**Still open in phase C (pre-designsystem):** full designsystem / core.ui port.
Expand Down Expand Up @@ -405,16 +405,33 @@ Workspace: same **`forma-spike`** external tree.
|------|--------|
| Module | `core-analytics-android-util` — `AnalyticsHelper` / `AnalyticsEvent` / Stub + NoOp + `LocalAnalyticsHelper` |
| Type | plain **`hiltAndroidUtil`** + Compose **runtime** library stack for CompositionLocal (**F18** sibling; `compose=false` target — F11) |
| Bind | **StubAnalyticsHelper** for **all** product flavors (F27 prod `FirebaseAnalyticsHelper` + flavor source sets deferred) |
| Bind (pre-F27) | **StubAnalyticsHelper** for all flavors until library flavors section |
| Module | `core-notifications-res` **`androidRes`** + `core-notifications-android-util` **`hiltAndroidUtil`** |
| **F31 / F23** | Tray strings + vector icon on **res** module (unique namespace `…notifications.res`); util imports `R as NotificationsR` — androidUtil still no `res/` |
| Bind | **SystemTrayNotifier** for all flavors (validates res split + data edge; demo NoOp deferred with F27) |
| Bind (pre-F27) | **SystemTrayNotifier** for all flavors until library flavors section |
| Data | UserData toggles → analytics events; News `sync()` → notifier for new followed-topic items after onboard |
| UI | `core-ui` → analytics; NewsFeed logs `news_resource_opened`; root `CompositionLocalProvider` + `TrackScreenViewEvent` |
| Verify | `forma-spike` `:binary:assembleDemoDebug` + `:binary:assembleProdDebug` → **BUILD SUCCESSFUL** (645 tasks); APKs ~22 MB |
| Version | `0.16.0-nia-forma-analytics-notifications` |

**Still open in phase C:** F27 library flavors / prod Firebase analytics + demo NoOp notifier source sets; optional case-study Phase D.
### Phase C — Library product flavors (F27) ✅ (2026-08-01)

| Step | Result |
|------|--------|
| Engine | `AndroidLibraryFeatureConfiguration.productFlavors` + `LibraryExtension.applyProductFlavors(plan)` (no-op empty; `src/<flavor>/kotlin`; APK-only fields ignored) |
| Model | Same **`FormaProductFlavor`** as binary — one global way; KDoc no longer binary-only |
| DSL | `androidUtil` / `androidUtilTarget(productFlavors=…)` + thin `hiltAndroidUtil` forward |
| Flavor-scoped deps | `NamedDependency.forProductFlavor` / `PlatformDependency.forProductFlavor` → `CustomConfiguration("${flavor}Implementation")`; preserves transitive + feature flags |
| Unit tests | `FormaProductFlavorTest` library plan reuse; `DepsModelAndPluginHappyPathTest` flavor config mapping |
| Spike analytics | `hiltAndroidUtil(productFlavors=demo+prod)`; Hilt modules in `src/demo` (Stub) / `src/prod` (Firebase + `FirebaseAnalyticsHelper`); `prod` BOM + analytics via `.forProductFlavor("prod")` |
| Spike notifications | same flavors; `src/demo` NoOpNotifier bind / `src/prod` SystemTrayNotifier bind; main keeps implementations |
| Spike sync | optional `prodImplementation` FCM via `.forProductFlavor("prod")` (deps-only; no new FCM source) |
| Docs | `CALL-SITE-SURFACE.md` library flavors + flavor-scoped deps |
| Verify | `plugins` `:android:test` + `:deps:test`; `publish-local 0.1.3-NIA`; spike `:binary:assembleDemoDebug` + `:binary:assembleProdDebug` |
| Version | `0.17.0-nia-forma-library-flavors` |
| **F27** | **closed** |

**Still open:** Phase D case-study write-up (optional remaining Retrofit/prod network out of scope).

### Phase D — Case study write-up

Expand Down Expand Up @@ -453,7 +470,7 @@ Workspace: same **`forma-spike`** external tree.
| F24 | 2026-07-31 | `deps()` overload mix | Cannot pass `.ksp` NamedDependency and `target()` FormaTarget in one `deps(...)` call (distinct overloads). Compose `transitiveDeps + deps(ksp) + deps(target)`. |
| F25 | 2026-07-31 | Protobuf needs type-owned apply on JVM library | NiA `datastore-proto` is pure JVM + `com.google.protobuf`. Engine needed `libraryTarget` (like `androidUtilTarget`) so Path B `protobufLibrary` can pass derived type. Call sites stay attrs-only. |
| F26 | 2026-07-31 | Proto lite supers on consumers | `UserPreferences` / enums extend protobuf lite; androidUtil consumer must `transitiveDeps(protobuf-kotlin-lite)` even when proto module already depends on it (project dep does not re-export non-api runtime the same way as NiA `api(libs.protobuf…)`). |
| F27 | 2026-07-31 | Library product flavors / variant deps | Binary-only `FormaProductFlavor` closes F7 for APK. NiA also flavors libraries + `prodImplementation(FCM)` on sync + **analytics demo/prod binds** + **notifications demo NoOp / prod tray**. Spike binds Stub analytics + SystemTray notifier for **all** flavors until library flavor attrs exist. Future: optional library flavor attrs or type-owned missingDimensionStrategy — do **not** put free-form `android { productFlavors }` on every module. |
| F27 | 2026-07-31 | Library product flavors / variant deps | **Closed 2026-08-01:** same `FormaProductFlavor` on `androidUtil` + `LibraryExtension.applyProductFlavors` + `src/<flavor>/kotlin`; flavor-scoped deps via `.forProductFlavor` → `prodImplementation`. Spike analytics demo Stub / prod Firebase; notifications demo NoOp / prod SystemTray; optional sync FCM prod dep. No raw `android { productFlavors }` at call sites. |

## Local reference commands

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-08-01 — F-115: NiA dogfood library product flavors (F27)

- **Ticket:** F-115 still `in_progress` · `priority: now` · `cron may continue` (Phase D case study next)
- **Skills/modes:** Grok Build CLI `--mode full` (design/plan + implement + check) model `grok-4.5`; Hermes finish path (publish-local + spike verify + PROGRESS/PR)
- **Engine (`plugins/`):**
- `LibraryExtension.applyProductFlavors` + `AndroidLibraryFeatureConfiguration.productFlavors` + flavor `src/<name>/kotlin` dirs
- DSL attr `productFlavors` on `androidUtil` / `androidUtilTarget` / `impl` / `androidApp` / `composeWidget` (empty default)
- `NamedDependency.forProductFlavor` / `PlatformDependency.forProductFlavor` → AGP `prodImplementation` via `CustomConfiguration` (no free-form config strings)
- Unit tests: `:android:test` + `:deps:test` **BUILD SUCCESSFUL**
- **Publish:** `bash scripts/publish-local.sh 0.1.3-NIA` → mavenLocal
- **External spike** `/Users/claw/work/nowinandroid-forma/forma-spike`:
- `niaContentTypeFlavors` shared list in forma-defs; forward on hilt wrappers
- analytics: demo Stub / prod FirebaseAnalyticsHelper + flavor-scoped BOM+analytics
- notifications: demo NoOp / prod SystemTray binds via flavor source sets
- intermediate consumers (data/domain/ui/features/root/sync) share library flavors for AGP variant match
- binary `0.17.0-nia-forma-library-flavors`
- **Verify (real host):**
- `forma-spike` `./gradlew :binary:assembleDemoDebug :binary:assembleProdDebug` → **BUILD SUCCESSFUL** (945 tasks)
- APKs: `binary-demo-debug.apk` ~22 MB, `binary-prod-debug.apk` ~22 MB
- **Docs:** CALL-SITE-SURFACE flavors F7+F27; DOGFOOD-NIA F27 closed; TICKETS F-115 Phase D next
- **Commits/PRs:** this branch → PR base `v2`
- **Blockers:** none product; F-094 Portal still human-blocked
- **Next:** Phase D case study write-up (before/after LOC, config notes)

## 2026-08-01 — F-115: NiA dogfood core:analytics + core:notifications

- **Ticket:** F-115 still `in_progress` · `priority: now` · `cron may continue` (F27 library flavors / Phase D next)
Expand Down
4 changes: 4 additions & 0 deletions plugins/android/src/main/java/androidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import tools.forma.android.feature.kotlinAndroidFeatureDefinition
import tools.forma.android.target.AndroidTargetRegistry
import tools.forma.android.target.AndroidTargetTypes
import tools.forma.android.utils.BuildConfiguration
import tools.forma.android.utils.FormaProductFlavor
import tools.forma.android.validation.disallowResources
import tools.forma.android.visibility.Public
import tools.forma.android.visibility.Visibility
Expand Down Expand Up @@ -43,6 +44,8 @@ fun Project.androidApp(
manifestPlaceholders: Map<String, Any> = emptyMap(),
/** Enable Jetpack Compose; defaults to project-wide `compose` setting. */
compose: Boolean = Forma.settings.compose,
/** Library product flavors (F-115 / NiA F27); empty = unflavored. */
productFlavors: List<FormaProductFlavor> = emptyList(),
) {

disallowResources()
Expand All @@ -58,6 +61,7 @@ fun Project.androidApp(
manifestPlaceholders,
selfValidator = selfV,
compose = compose,
productFlavors = productFlavors,
)
applyFeatures(
androidLibraryFeatureDefinition(libraryFeatureConfiguration),
Expand Down
8 changes: 8 additions & 0 deletions plugins/android/src/main/java/androidUtil.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import tools.forma.android.target.AndroidTargetTypes
import tools.forma.android.utils.FormaProductFlavor
import tools.forma.owners.NoOwner
import tools.forma.owners.Owner
import tools.forma.android.visibility.Public
Expand Down Expand Up @@ -31,6 +32,12 @@ fun Project.androidUtil(
testDependencies: FormaDependency = emptyDependency(),
/** Enable Jetpack Compose; defaults to project-wide `compose` setting. */
compose: Boolean = Forma.settings.compose,
/**
* Library product flavors (F-115 / NiA F27). Same [FormaProductFlavor] model as
* `androidBinary`; empty = unflavored. Pair with `NamedDependency.forProductFlavor` /
* `PlatformDependency.forProductFlavor` for AGP `prodImplementation` edges.
*/
productFlavors: List<FormaProductFlavor> = emptyList(),
) {
androidUtilTarget(
type = AndroidTargetTypes.androidUtil,
Expand All @@ -40,5 +47,6 @@ fun Project.androidUtil(
dependencies = dependencies,
testDependencies = testDependencies,
compose = compose,
productFlavors = productFlavors,
)
}
6 changes: 6 additions & 0 deletions plugins/android/src/main/java/androidUtilTarget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import tools.forma.android.feature.applyFeatures
import tools.forma.android.feature.kotlinAndroidFeatureDefinition
import tools.forma.android.feature.processorConfigurationFeatures
import tools.forma.android.target.AndroidTargetRegistry
import tools.forma.android.utils.FormaProductFlavor
import tools.forma.android.validation.disallowResources
import tools.forma.android.visibility.Public
import tools.forma.android.visibility.Visibility
Expand All @@ -25,6 +26,9 @@ import tools.forma.validation.validate
* (e.g. dogfood `roomAndroidUtil`) so plugin identity stays on the type while the
* AGP/util wiring is shared. Call sites of derived DSLs remain attributes-only —
* never pass plugin ids here.
*
* [productFlavors] (F-115 / NiA F27): optional library product flavors using the
* same [FormaProductFlavor] model as `androidBinary`. Empty = unflavored.
*/
fun Project.androidUtilTarget(
type: TargetType,
Expand All @@ -35,6 +39,7 @@ fun Project.androidUtilTarget(
testDependencies: FormaDependency = emptyDependency(),
/** Enable Jetpack Compose; defaults to project-wide `compose` setting. */
compose: Boolean = Forma.settings.compose,
productFlavors: List<FormaProductFlavor> = emptyList(),
) {
disallowResources()

Expand All @@ -44,6 +49,7 @@ fun Project.androidUtilTarget(
val androidFeatureConfig = AndroidLibraryFeatureConfiguration(
packageName = packageName,
compose = compose,
productFlavors = productFlavors,
)

applyFeatures(
Expand Down
Loading
Loading