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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@ project-dep type checks + content layout rules from live validators
**Canonical reference (code truth):** [`docs/DEPENDENCY-MATRIX.md`](docs/DEPENDENCY-MATRIX.md).

That document is generated from each target’s
`applyDependencies(validator = …)` call and content helpers. Summary:
`applyDependencies(validator = …)` call and content helpers. Forked-in vendor
modules (no Forma suffixes): optional project-global
`dependencyValidationExclusions` on `androidProjectConfiguration` only
([F-090](docs/DEPENDENCY-MATRIX.md#dependency-validation-exclusions-f-090--gh-97)) —
never a per-call-site escape hatch. Summary:

| Consumer | May depend on project suffixes… |
|----------|----------------------------------|
Expand Down
2 changes: 1 addition & 1 deletion TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Promoted from daily next-actions / historical GH. Workers pick top `todo` in ord
| F-087 | done | AndroidX / SDK ceiling after AGP 9 | compile/target **37**; core **1.19.0**, activity **1.13.0**, lifecycle **2.11.0**, compose **1.11.4**, room **2.8.4**, material **1.14.0**; JVM **11**; navigationevent on activity graph; paging **2.1.2** kept. Host+CI platform 37. |
| 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 | todo | Exclude modules from dependency validation | GH **#97** — allow-list / opt-out for forked-in libraries (ExoPlayer-class cases) without weakening default matrix. One global configuration path (not per-call-site escape hatch spam). Docs + sample or unit coverage. |
| 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-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. |
Expand Down
5 changes: 5 additions & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ buildscript {
agpVersion = "9.3.0",
// Navigation 2.9 / modern AndroidX ship JVM 11 bytecode — cannot inline into 1.8
javaVersionCompatibility = JavaVersion.VERSION_11,
// F-090 / GH #97: optional allow-list for forked-in third-party modules that do
// not use Forma suffixes. Exact Gradle path and/or project name. Default empty.
// dependencyValidationExclusions = setOf(
// ":third-party:exoplayer:library-core",
// ),
extraPlugins =
listOf(
libs.plugins.toolsFormaDemoDependencies,
Expand Down
45 changes: 45 additions & 0 deletions docs/DEPENDENCY-MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,51 @@ suffix (`ApiTargetTemplate("api")`).

---

## Dependency validation exclusions (F-090 / GH #97)

Sometimes a monorepo vendors a **forked third-party tree** (ExoPlayer-class
cases) whose module names do **not** follow Forma suffixes. Those modules
must be dependable from Forma targets without weakening the default matrix
for first-party code.

**One global configuration path only** — set once on root
`androidProjectConfiguration`:

```kotlin
buildscript {
androidProjectConfiguration(
project = rootProject,
// ...
dependencyValidationExclusions = setOf(
":third-party:exoplayer:library-core", // Gradle project path
"forked-media-engine", // or exact project name
),
)
}
```

| Skips | Does **not** skip |
|-------|-------------------|
| Project-dependency **type/suffix** validation when the **dependency** project is listed | Self-type validation on Forma DSL modules (`target.validate(...)`) |
| | The default allow-list matrix for any **non-listed** dependency |
| | Content layout rules (`disallowResources`, etc.) |

**Matching:** exact Gradle project **path** (e.g. `:third-party:exoplayer:library-core`)
and/or exact project **name**. No prefix/glob matching.

**When to use:** forked-in libraries that live in the composite build but are
not Forma targets.

**When not to use:** never to bypass `impl`↛`impl` (or any other matrix edge)
between first-party Forma modules. Fix the graph or re-slice targets instead.
There is **no** per-call-site `skipValidation` flag.

Implementation: `FormaSettingsStore.isExcludedFromDependencyValidation` gated
inside `applyDependencies` `projectAction`. Default exclusions = empty (strict
matrix unchanged). See [`PROJECT-CONFIGURATION.md`](PROJECT-CONFIGURATION.md).

---

## Target templates (suffixes)

From `plugins/android/.../AndroidTargets.kt`:
Expand Down
8 changes: 6 additions & 2 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,11 @@ Details: [COMPOSE.md](COMPOSE.md). Sample:
- Prefer small targets; keep feature folders consistent.
- Put shared non-feature code under `core/` or `common/` (see sample).
- When a validator fails, fix the dependency edge or re-slice the target —
don’t disable validation.
don’t disable validation. For **forked-in** third-party modules only, use
project-global `dependencyValidationExclusions` on `androidProjectConfiguration`
(F-090) — see [DEPENDENCY-MATRIX.md](DEPENDENCY-MATRIX.md) and
[PROJECT-CONFIGURATION.md](PROJECT-CONFIGURATION.md). Never use that list to
bypass first-party matrix rules (e.g. `impl`↛`impl`).

---

Expand All @@ -446,7 +450,7 @@ Details: [COMPOSE.md](COMPOSE.md). Sample:
| “Java not found” / old JDK | Use JDK 21 (or 17+ min); export `JAVA_HOME` ([ENV.md](ENV.md)) |
| SDK / `compileSdk` errors | Install platform **37** (`platforms;android-37.0` + `android-37` symlink); set `local.properties` `sdk.dir` |
| Project not included | Missing `build.gradle.kts`, or nested `settings.gradle.kts` blocked Includer |
| Illegal project dependency | Matrix violation — see [DEPENDENCY-MATRIX.md](DEPENDENCY-MATRIX.md) |
| Illegal project dependency | Matrix violation — see [DEPENDENCY-MATRIX.md](DEPENDENCY-MATRIX.md). Forked vendor modules: global `dependencyValidationExclusions` only (F-090) |
| Empty / wrong package | `packageName` ≠ directory under `src/main/java` — run `formaLayoutCheck` / `formaLayoutGenerate` |
| Compose compiler mismatch | Align `composeCompilerVersion` with Kotlin (sample: 2.3.21 ↔ 2.3.21) |
| AGP resolution conflicts | Align consumer `agpVersion` with plugin AGP line (**9.3.0** today) |
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-090: Exclude modules from dependency validation (GH #97)

- **Ticket:** F-090 → `done`
- **Branch:** `forma/F-090-validation-exclusions` (from origin/v2)
- **Actions:**
- API: `dependencyValidationExclusions: Set<String> = emptySet()` on
`AndroidProjectSettings` + root `androidProjectConfiguration(...)`
- Matcher: exact Gradle project path and/or name
(`matchesDependencyValidationExclusion`); store helpers null-safe when
settings never written (`FormaSettingsStore.isExcludedFromDependencyValidation`)
- `applyDependencies` `projectAction` skips suffix `validator.validate` only for
excluded dependency projects; still adds the edge. Self-type DSL validation
unchanged. Test/androidTest project deps still unvalidated (pre-existing).
- Unit tests: `:config` matcher/store; `:deps` apply-gate exclude vs non-exclude
- Docs: `DEPENDENCY-MATRIX.md` exclusions section; `PROJECT-CONFIGURATION.md`
parameter; GETTING-STARTED cross-links; sample `application/build.gradle.kts`
commented example
- **Verify (real tool output, `source scripts/env-mac.sh`):**
- `plugins/`: `./gradlew :core:test :validation:test :deps:test :config:test build --no-daemon` → **BUILD SUCCESSFUL** in 26s (80 tasks)
- `application/`: `./gradlew help --no-daemon` + `./gradlew assembleDebug --no-daemon` → **BUILD SUCCESSFUL** in 1m46s (896 tasks)
- **Blockers:** none
- **Next:** F-091 BuildFeatures configuration (GH #88)
- **Commits:** local on `forma/F-090-validation-exclusions` (orchestrator opens PR)

## 2026-07-22 — F-089: Navigation Safe Args task cache (GH #110)

- **Ticket:** F-089 → `done`
Expand Down
29 changes: 29 additions & 0 deletions 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-090: skip project-dep suffix checks only for listed forked-in modules
// dependencyValidationExclusions = setOf(
// ":third-party:exoplayer:library-core",
// "forked-media-engine",
// ),
extraPlugins = listOf(
// jars / Provider<PluginDependency> for the *buildscript classpath only*
libs.plugins.navigationSafeArgs,
Expand All @@ -35,6 +40,29 @@ This is the **only** supported way to configure an Android Forma project.

Downstream targets and features read global values (SDK versions, `compose` default, `kotlinVersion`, `agpVersion`, etc.) from `Forma.settings` (or the delegated store surfaces). The store is initialized exactly once from the root `buildscript` block.

### `dependencyValidationExclusions` (F-090 / GH #97)

| | |
|--|--|
| **Type** | `Set<String>` (default `emptySet()`) |
| **Set on** | Root `androidProjectConfiguration(...)` only |
| **Stored as** | `AndroidProjectSettings.dependencyValidationExclusions` |
| **Read by** | `applyDependencies` via `FormaSettingsStore.isExcludedFromDependencyValidation` |

Lists Gradle **project paths** and/or **project names** that skip **project-dependency
type/suffix validation** when they appear as a dependency of a Forma target.

**Intended use:** monorepo-vendored forks (ExoPlayer-class) that do not use Forma
name suffixes.

**Not for:** bypassing the matrix between first-party Forma modules (e.g. `impl`→`impl`).
There is no per-module / per-call-site `skipValidation` flag — one global allow-list only.

**Matching:** exact path (e.g. `:third-party:exoplayer:library-core`) or exact name.
Does **not** disable self-type validation on Forma DSL entry points.

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

## 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 @@ -84,6 +112,7 @@ Readers (feature wiring, dependency helpers, target DSLs) obtain values through
- [ ] No calls to any `Project.androidProjectConfiguration` form (removed)
- [ ] Child targets rely on `Forma.settings` / per-target overrides only
- [ ] Type-owned plugins used for external plugin application (no ad-hoc apply)
- [ ] Forked third-party modules (if any) listed once in `dependencyValidationExclusions` — never per-call-site skips

---

Expand Down
7 changes: 7 additions & 0 deletions plugins/android/src/main/java/androidProjectConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ import tools.forma.deps.fleet.ensureFormaLayoutRootTasks
* @param checkPackageLayoutAtConfiguration when true, fail configuration if a target's
* `packageName` source dir is missing (default false; see [AndroidProjectSettings] and
* docs/FLEET-TOOLING.md). Generate via `formaLayoutGenerate` / `formaLayoutGenerateAll`.
* @param dependencyValidationExclusions project paths and/or names that skip **project-
* dependency** suffix validation when depended on by a Forma target (F-090 / GH #97).
* 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 extraPlugins list of extra artifacts / plugin providers to add to the **buildscript classpath only**.
* See "Classpath vs apply" in TARGET-PLUGINS.md.
*/
Expand All @@ -85,6 +90,7 @@ fun ScriptHandlerScope.androidProjectConfiguration(
mandatoryOwners: Boolean = false,
vectorDrawablesUseSupportLibrary: Boolean = false,
checkPackageLayoutAtConfiguration: Boolean = false,
dependencyValidationExclusions: Set<String> = emptySet(),
extraPlugins: List<Any> = emptyList()
) {
buildScriptConfiguration(
Expand Down Expand Up @@ -121,6 +127,7 @@ fun ScriptHandlerScope.androidProjectConfiguration(
composeCompilerVersion = composeCompilerVersion,
vectorDrawablesUseSupportLibrary = vectorDrawablesUseSupportLibrary,
checkPackageLayoutAtConfiguration = checkPackageLayoutAtConfiguration,
dependencyValidationExclusions = dependencyValidationExclusions,
)

Forma.store(configuration)
Expand Down
7 changes: 7 additions & 0 deletions plugins/config/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ formaPublishedPlugin(name = "config")

dependencies {
implementation(gradleApi())

testImplementation(kotlin("test"))
}

tasks.test {
useJUnitPlatform()
}

Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,37 @@ data class AndroidProjectSettings(
* in CI or local strict mode only.
*/
val checkPackageLayoutAtConfiguration: Boolean = false,
/**
* Project paths or names that skip **project-dependency type/suffix validation**
* when they appear as a dependency of a Forma target (F-090 / GH #97).
*
* Use for forked-in third-party trees (e.g. ExoPlayer) that live in the monorepo
* but do not follow Forma target suffixes. Does **not** disable self-type validation
* on Forma DSL modules and does **not** change the default dependency matrix.
*
* Match by Gradle project path (e.g. `:third-party:exoplayer:library-core`) and/or
* project name. Exact match only (see [matchesDependencyValidationExclusion]).
* Default empty — strict matrix unchanged.
*/
val dependencyValidationExclusions: Set<String> = emptySet(),
)

/**
* Exact-match check for [AndroidProjectSettings.dependencyValidationExclusions].
*
* @param projectName Gradle [org.gradle.api.Project.getName]
* @param projectPath Gradle [org.gradle.api.Project.getPath] (e.g. `:foo:bar`)
* @param exclusions allow-list from project settings; empty ⇒ never excluded
*/
fun matchesDependencyValidationExclusion(
projectName: String,
projectPath: String,
exclusions: Set<String>,
): Boolean {
if (exclusions.isEmpty()) return false
return projectName in exclusions || projectPath in exclusions
}

/**
* Singleton project configuration store, used by internal plugins
*/
Expand All @@ -69,6 +98,32 @@ object FormaSettingsStore : SettingsStore<AndroidProjectSettings>, PluginInfoSto
_settings = configuration
}

/**
* Whether settings have been written by [store] (false on JVM-only paths that never
* call `androidProjectConfiguration`).
*/
val isSettingsStored: Boolean
get() = this::_settings.isInitialized

/**
* [AndroidProjectSettings.dependencyValidationExclusions] when settings exist;
* empty set otherwise (null-safe — does not throw).
*/
fun dependencyValidationExclusionsOrEmpty(): Set<String> =
if (isSettingsStored) _settings.dependencyValidationExclusions else emptySet()

/**
* True when [projectName] or [projectPath] is listed in
* [AndroidProjectSettings.dependencyValidationExclusions].
* Safe when settings were never stored (treats as no exclusions).
*/
fun isExcludedFromDependencyValidation(projectName: String, projectPath: String): Boolean =
matchesDependencyValidationExclusion(
projectName = projectName,
projectPath = projectPath,
exclusions = dependencyValidationExclusionsOrEmpty(),
)

override val plugins: MutableMap<Provider<PluginDependency>, PluginConfiguration> = mutableMapOf()
override val dependencyPlugins: MutableMap<String, PluginConfiguration> = mutableMapOf()
override val dependencies: MutableMap<String, DependencyConfiguration> = mutableMapOf()
Expand Down
Loading
Loading