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 @@ -137,7 +137,7 @@ worker (ops); `v2`→`master` (explicit git promote only). **F-094** stays `bloc
| ID | Status | Title | Notes |
|----|--------|-------|-------|
| 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 | todo | Finish build types: signing configs | GH **#51** — checklist mostly done (DSL/`BuildConfiguration`/`BuildType` lambdas, proguard, build fields). **Remaining:** first-class **signing configs** on `androidBinary` (one global way; attrs on binary only — not per-library shopping). Docs + sample/example if safe dummy keystore pattern; no secrets in repo. |
| 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 | todo | Core library desugaring (Java 8+ APIs) | GH **#103** — project-global desugar path for lower `minSdk` that needs `coreLibraryDesugaring` (Android Java 8+ library desugaring). One way on `androidProjectConfiguration` / settings; wire AGP `compileOptions.isCoreLibraryDesugaringEnabled` + desugar dep; docs ENV/GETTING-STARTED; verify on sample or progressive example. |
| 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-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. |
Expand Down
38 changes: 38 additions & 0 deletions application/binary/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
import tools.forma.android.utils.BuildConfiguration
import tools.forma.android.utils.FormaSigningConfig

androidBinary(
packageName = "tools.forma.sample.app",
owner = Teams.core,
versionCode = 1,
versionName = "0.0.1",
// Compose enabled so binary can host Compose content from compose-widget deps.
compose = true,
// F-097 / GH #51: first-class signing on the APK root only.
// demo-release.keystore is a committed *dummy* store (password "android") for docs/CI —
// never commit production keystores or secrets. Real apps: read path/passwords from
// env or project properties with empty defaults so assembleDebug stays green.
signingConfigs = mapOf(
"demoRelease" to FormaSigningConfig(
storeFile = file("demo-release.keystore"),
storePassword = "android",
keyAlias = "androiddebugkey",
keyPassword = "android",
),
// Production sketch (not active — empty passwords would fail release signing):
// "release" to FormaSigningConfig(
// storeFile = file(
// findProperty("forma.storeFile") as String?
// ?: System.getenv("FORMA_STORE_FILE")
// ?: "missing-release.keystore"
// ),
// storePassword = System.getenv("FORMA_STORE_PASSWORD") ?: "",
// keyAlias = System.getenv("FORMA_KEY_ALIAS") ?: "upload",
// keyPassword = System.getenv("FORMA_KEY_PASSWORD") ?: "",
// ),
),
// Wire release → dummy store so assembleRelease can succeed without secrets.
// debug keeps AGP's built-in debug signing (assembleDebug needs no custom config).
buildTypeSigning = mapOf(
"release" to "demoRelease",
),
buildConfiguration = BuildConfiguration(
buildTypes = mapOf(
"release" to {
isMinifyEnabled = false
},
),
),
dependencies = deps(
target(":root-app"),

Expand Down
Binary file added application/binary/demo-release.keystore
Binary file not shown.
55 changes: 54 additions & 1 deletion docs/CALL-SITE-SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ All return **`Unit`**.
| `api` | `api` | `packageName`, `dependencies` | JVM contracts; no Android UI |
| `impl` | `impl` | deps, **`viewBinding`**, **`compose`**, test runners, `buildConfiguration` | Feature impl; no → other `impl` |
| `androidApp` | `app` | deps, **`compose`**, `buildConfiguration`, … | Composition **library** (not APK) — **no** `versionCode`/`Name` |
| `androidBinary` | `binary` | **`versionCode`/`versionName` (required)**, deps, **`compose`**, … | APK composition root; version identity is **per-binary only** (F-092) |
| `androidBinary` | `binary` | **`versionCode`/`versionName` (required)**, deps, **`compose`**, **`signingConfigs` / `buildTypeSigning`**, … | APK composition root; version + signing are **per-binary only** (F-092 / F-097) |
| `library` | `library` | deps (pure JVM inside Android plugin) | Distinct from removed `androidLibrary` |
| `util` | `util` | deps | JVM helpers |
| `androidUtil` | `android-util` | deps, **`compose`** | Android helpers, no res content |
Expand Down Expand Up @@ -92,6 +92,59 @@ override ladder — each binary declares both attrs explicitly.
**Gradle limit:** `versionCode`/`versionName` apply to application modules. Putting
them on library-shaped `androidApp` would not produce a second APK version surface.

## APK signing (F-097 / GH #51)

| Concern | Owner | Notes |
|---------|--------|--------|
| Named signing configs | **`androidBinary` call site only** (`signingConfigs: Map<String, FormaSigningConfig>`) | Wired to AGP `ApplicationExtension.signingConfigs` |
| Build type → signing | **`androidBinary` `buildTypeSigning`** (build type name → config name) | Applied after both containers exist |
| Library / `androidApp` signing API | **None** | No per-`impl` / `uiLibrary` shopping; APK signing is composition-root only |
| Raw `android { signingConfigs { … } }` | **Rejected as happy path** | Escape hatch only; do not teach dual paths |

**Why binary-only:** same rationale as version identity — only `com.android.application`
materializes installable APK signing. Library shells (`androidApp`, `impl`, …) stay
free of release-keystore concerns.

**Call-site shape:**

```kotlin
import tools.forma.android.utils.BuildConfiguration
import tools.forma.android.utils.FormaSigningConfig

androidBinary(
packageName = "com.example.app",
versionCode = 1,
versionName = "0.1.0",
signingConfigs = mapOf(
"demoRelease" to FormaSigningConfig(
storeFile = file("demo-release.keystore"),
storePassword = "android", // dummy / CI only — never real secrets in git
keyAlias = "androiddebugkey",
keyPassword = "android",
),
),
buildTypeSigning = mapOf(
"release" to "demoRelease",
// debug → AGP default debug signing when omitted
),
buildConfiguration = BuildConfiguration(
buildTypes = mapOf(
"release" to { isMinifyEnabled = false },
),
),
dependencies = deps(/* … */),
)
```

**Secrets:** commit only clearly-fake demo keystores (see sample
`application/binary/demo-release.keystore`). Production paths/passwords come from
env or `gradle.properties` (local, gitignored) — never from the repo. `assembleDebug`
must stay green without secrets (omit `buildTypeSigning` for `debug` or leave AGP
default).

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

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

AGP `BuildFeatures` flags are **project-global by default** (all **off**), with
Expand Down
5 changes: 5 additions & 0 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ androidBinary(
// F-092: version identity is per-binary (not androidProjectConfiguration)
versionCode = 1,
versionName = "0.1.0",
// F-097: optional APK signing — binary only; see CALL-SITE-SURFACE § APK signing
// signingConfigs = mapOf("demoRelease" to FormaSigningConfig(...)),
// buildTypeSigning = mapOf("release" to "demoRelease"),
dependencies = deps(
target(":root-app"),
target(":feature:hello:api"),
Expand All @@ -246,6 +249,8 @@ androidBinary(

Add another `androidBinary` module if you ship a second APK — give it its own
`versionCode` / `versionName`. Do not put version attrs on `androidApp` (library shell).
Signing configs are likewise **binary-only** (F-097); debug builds need no custom
signing. Full example: sample `application/binary/`.

**`root-app/build.gradle.kts`**:

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

Newest entries first.

## 2026-07-24 — F-097: signing configs on androidBinary (GH #51)

- **Ticket:** F-097 → `done` (GH #51 remaining checkbox)
- **Branch:** `forma/F-097-signing-configs` (from `origin/v2`)
- **Skills/modes:** Grok Build `--mode full` (design/plan + implement); Hermes finish path after CLI timeout on app assemble
- **Design:** binary-only attrs (mirror F-092). Keep `BuildConfiguration` = build-types only. New `FormaSigningConfig` + `signingConfigs` / `buildTypeSigning` on `androidBinary` → AGP application containers only. Name→name bridge because `BuildType.() -> Unit` cannot see application `signingConfig`.
- **Code:**
- `plugins/android/.../FormaSigningConfig.kt` — model, `toAppliedFields`, `resolveBuildTypeSigningPairs`, `applySigningConfigs`, `applyBuildTypeSigning`
- `AndroidBinaryFeatureConfiguration` + `androidBinary` public attrs; apply order in `androidBinaryFeatureDefinition`
- `:android` unit tests (7) via `kotlin("test")` + JUnit Platform
- Sample: committed dummy `application/binary/demo-release.keystore` (password `android`); `release` → `demoRelease`; minify off on release
- **Docs:** CALL-SITE-SURFACE § APK signing; PROJECT-CONFIGURATION “does not”; GETTING-STARTED pointer; KDoc on binary
- **Verify (real host, `source scripts/env-mac.sh`):**
- `plugins/`: `./gradlew :android:cleanTest :android:test` → **BUILD SUCCESSFUL** (7 tests)
- `plugins/`: `./gradlew test jacocoHappyPathCoverageVerification` → **BUILD SUCCESSFUL**
- `application/`: `./gradlew :binary:clean :binary:assembleDebug :binary:assembleRelease` → **BUILD SUCCESSFUL** (2m8s)
- **Commits/PRs:** this branch; Hermes PR/merge; close GH #51 when merged
- **Blockers:** none (F-094 Portal still human-blocked)
- **Next step:** F-098 (core library desugaring)

## 2026-07-24 — F-096: String.transitiveDep catalog parity

- **Ticket:** F-096 → `done` (GH #77)
Expand Down
3 changes: 3 additions & 0 deletions docs/PROJECT-CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ resolved values so AGP platform defaults cannot silently turn features on.
**required** attrs on each `androidBinary { … }` call site. `androidApp` is a library
composition shell and does not take version attrs. See
[`CALL-SITE-SURFACE.md`](CALL-SITE-SURFACE.md) § APK version identity.
- **APK signing configs** — not project-global (F-097 / GH #51). Named
`FormaSigningConfig` entries and `buildTypeSigning` live on each `androidBinary`
only. See [`CALL-SITE-SURFACE.md`](CALL-SITE-SURFACE.md) § APK signing.

There is no per-module `withPlugin`, no free-form `plugins =` lists, and no second configuration path.

Expand Down
7 changes: 7 additions & 0 deletions plugins/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ dependencies {
implementation(project(":deps"))
// F-021: core restriction + target types (pure engine); Android owns concrete type instances + matrix
implementation(project(":core"))

// F-097: pure model tests (FormaSigningConfig / buildTypeSigning resolver)
testImplementation(kotlin("test"))
}

tasks.test {
useJUnitPlatform()
}

tasks.named<Task>("publishPlugins") {
Expand Down
30 changes: 22 additions & 8 deletions plugins/android/src/main/java/androidBinary.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import tools.forma.android.feature.applyFeatures
import tools.forma.android.target.AndroidTargetRegistry
import tools.forma.android.target.AndroidTargetTypes
import tools.forma.android.utils.BuildConfiguration
import tools.forma.android.utils.FormaSigningConfig
import tools.forma.android.validation.disallowResources
import tools.forma.deps.core.FormaDependency
import tools.forma.deps.core.applyDependencies
Expand All @@ -30,12 +31,21 @@ import tools.forma.validation.validate
* only materialize `defaultConfig.versionCode`/`versionName` on
* `com.android.application`).
*
* **APK signing (F-097 / GH #51):** [signingConfigs] and [buildTypeSigning] are
* **binary-only** attrs. Forma registers named configs on AGP
* `ApplicationExtension.signingConfigs`, then assigns each build type via
* [buildTypeSigning] (build type name → signing config name). Libraries do not
* take a signing API. Do not use raw module-level `android { signingConfigs }` as
* the supported path.
*
* @param packageName Application package name / `applicationId` + namespace
* @param owner owner of the target, team responsible for maintenance
* @param versionCode Android `versionCode` for this APK (required; per-binary)
* @param versionName Android `versionName` for this APK (required; per-binary)
* @param dependencies list of external and project dependencies for the target
* @param buildConfiguration Android Gradle Plugin configuration DSL
* @param buildConfiguration Android Gradle Plugin build-type configuration DSL
* @param signingConfigs named [FormaSigningConfig] entries for this APK (optional)
* @param buildTypeSigning map of build type name → signing config name (optional)
* @param testInstrumentationRunner class name used for instrumentation tests execution
* @param consumerMinificationFiles Proguard/R8 minification files list
* @param manifestPlaceholders placeholders to be injected in manifest
Expand All @@ -47,6 +57,8 @@ fun Project.androidBinary(
versionName: String,
dependencies: FormaDependency = emptyDependency(),
buildConfiguration: BuildConfiguration = BuildConfiguration(),
signingConfigs: Map<String, FormaSigningConfig> = emptyMap(),
buildTypeSigning: Map<String, String> = emptyMap(),
testInstrumentationRunner: String = androidJunitRunner,
consumerMinificationFiles: Set<String> = emptySet(),
manifestPlaceholders: Map<String, Any> = emptyMap(),
Expand All @@ -61,13 +73,15 @@ fun Project.androidBinary(
// applicationId / namespace; composition root often has only a manifest.
registerFormaLayout(packageName, requirePackageSourceDir = false)
val binaryFeatureConfiguration = AndroidBinaryFeatureConfiguration(
packageName,
versionCode,
versionName,
buildConfiguration,
testInstrumentationRunner,
consumerMinificationFiles,
manifestPlaceholders,
packageName = packageName,
versionCode = versionCode,
versionName = versionName,
buildConfiguration = buildConfiguration,
testInstrumentationRunnerClass = testInstrumentationRunner,
consumerMinificationFiles = consumerMinificationFiles,
manifestPlaceholders = manifestPlaceholders,
signingConfigs = signingConfigs,
buildTypeSigning = buildTypeSigning,
selfValidator = selfV,
compose = compose,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ package tools.forma.android.feature
import com.android.build.api.dsl.ApplicationExtension
import tools.forma.android.target.BinaryTargetTemplate
import tools.forma.android.utils.BuildConfiguration
import tools.forma.android.utils.FormaSigningConfig
import tools.forma.android.utils.applyBuildTypeSigning
import tools.forma.android.utils.applyFrom
import tools.forma.android.utils.applySigningConfigs
import tools.forma.validation.Validator
import tools.forma.validation.validator

Expand All @@ -14,6 +17,9 @@ import tools.forma.validation.validator
*
* [versionCode] / [versionName] are **per-binary** (F-092) — never read from
* project-global [tools.forma.config.AndroidProjectSettings].
*
* [signingConfigs] / [buildTypeSigning] are **per-binary** (F-097 / GH #51) —
* APK signing identity lives only on the composition root, not on library shells.
*/
data class AndroidBinaryFeatureConfiguration(
val packageName: String,
Expand All @@ -23,6 +29,13 @@ data class AndroidBinaryFeatureConfiguration(
val testInstrumentationRunnerClass: String,
val consumerMinificationFiles: Set<String>,
val manifestPlaceholders: Map<String, Any> = emptyMap(),
/** Named signing configs applied to AGP `ApplicationExtension.signingConfigs`. */
val signingConfigs: Map<String, FormaSigningConfig> = emptyMap(),
/**
* Build type name → signing config name. Applied after both containers exist
* so call sites do not need AGP `SigningConfig` references inside build-type lambdas.
*/
val buildTypeSigning: Map<String, String> = emptyMap(),
/** Enable Jetpack Compose for the application (APK) target. */
val compose: Boolean = false,
val selfValidator: Validator = validator(BinaryTargetTemplate)
Expand Down Expand Up @@ -50,7 +63,15 @@ fun androidBinaryFeatureDefinition(
configuration.manifestPlaceholders
)

// Signing first so build types can rely on registered names; bridge map
// assigns ApplicationBuildType.signingConfig after buildTypes.applyFrom.
signingConfigs.applySigningConfigs(configuration.signingConfigs)
buildTypes.applyFrom(configuration.buildConfiguration)
applyBuildTypeSigning(
buildTypes = buildTypes,
signingConfigs = signingConfigs,
buildTypeSigning = configuration.buildTypeSigning,
)
compileOptions.applyFrom(formaConfiguration)

applyFormaBuildFeatures(
Expand Down
Loading
Loading