From 468f7451d1919fa7c9b9de0ec8fcb593801fc6c7 Mon Sep 17 00:00:00 2001 From: Claw Date: Mon, 13 Jul 2026 18:06:46 -0700 Subject: [PATCH] F-030: pure JVM target set on forma-core (tools.forma.jvm) Add plugins/jvm platform: JvmTargetTypes + JvmTargetRegistry matrix, Kotlin-JVM DSL (api/impl/library/util/testUtil), unit tests, and docs. --- README.md | 2 + TICKETS.md | 2 +- docs/ARCHITECTURE.md | 22 ++-- docs/JVM-TARGETS.md | 115 +++++++++++++++++ docs/PLUGIN-PUBLISH.md | 18 ++- docs/PROGRESS.md | 23 ++++ docs/forma-core-api.md | 6 +- plugins/jvm/build.gradle.kts | 39 ++++++ .../jvm/src/main/java/tools/forma/jvm/api.kt | 39 ++++++ .../tools/forma/jvm/feature/JvmFeature.kt | 37 ++++++ .../jvm/src/main/java/tools/forma/jvm/impl.kt | 40 ++++++ .../src/main/java/tools/forma/jvm/library.kt | 40 ++++++ .../tools/forma/jvm/plugin/FormaPlugin.kt | 17 +++ .../forma/jvm/target/JvmTargetRegistry.kt | 69 ++++++++++ .../tools/forma/jvm/target/JvmTargetTypes.kt | 29 +++++ .../src/main/java/tools/forma/jvm/testUtil.kt | 37 ++++++ .../jvm/src/main/java/tools/forma/jvm/util.kt | 39 ++++++ .../forma/jvm/target/JvmTargetRegistryTest.kt | 118 ++++++++++++++++++ 18 files changed, 677 insertions(+), 15 deletions(-) create mode 100644 docs/JVM-TARGETS.md create mode 100644 plugins/jvm/build.gradle.kts create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/api.kt create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/feature/JvmFeature.kt create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/impl.kt create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/library.kt create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/plugin/FormaPlugin.kt create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/testUtil.kt create mode 100644 plugins/jvm/src/main/java/tools/forma/jvm/util.kt create mode 100644 plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt diff --git a/README.md b/README.md index f4f7e8d..5f3ba41 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ systems like Buck and Bazel. 5. [Plugin publish path](docs/PLUGIN-PUBLISH.md) — Portal metadata DSL + release notes (F-016) 6. [Configuration performance](docs/CONFIGURATION-PERFORMANCE.md) — measure + hot-path guidance (F-017 / GH #106) 7. [forma-core public API design](docs/forma-core-api.md) — extraction contract for types / restrictions / registry (F-020) +8. [JVM targets](docs/JVM-TARGETS.md) — pure JVM platform plugin `tools.forma.jvm` (F-030) Configuration made easy: @@ -200,6 +201,7 @@ forma-core extraction (restriction engine + TargetType + Android matrix kit): ** forma-core validation SPI + content rules (Gradle facade in `:validation`): **F-022** done. forma-core TargetRegistry + Android DSL consumers: **F-023** done. Publish coordinates for `tools.forma:core` (library) + facade deprecation policy: **F-024** done (see [`docs/PLUGIN-PUBLISH.md`](docs/PLUGIN-PUBLISH.md)). +Pure JVM target set (`tools.forma.jvm`, `JvmTargetRegistry`): **F-030** done (see [`docs/JVM-TARGETS.md`](docs/JVM-TARGETS.md)). Icons made by Freepik from www.flaticon.com diff --git a/TICKETS.md b/TICKETS.md index 8cad5bb..6702198 100644 --- a/TICKETS.md +++ b/TICKETS.md @@ -40,7 +40,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi | ID | Status | Title | Notes | |----|--------|-------|-------| -| F-030 | todo | JVM target set on forma-core (`library`, `api`, `impl`, `utils`, tests) | | +| F-030 | done | JVM target set on forma-core (`library`, `api`, `impl`, `utils`, tests) | `plugins/jvm` + `tools.forma.jvm`; `docs/JVM-TARGETS.md` | | F-031 | todo | JVM sample application | | | F-032 | todo | Docs: JVM getting started | | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index da3a850..210aa59 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -60,7 +60,7 @@ that compose via `includeBuild`: - `pluginManagement { includeBuild("../build-settings") }` - `tools.forma.includer` **0.2.0** from Portal (not local includer composite) -- Includer discovers `:android`, `:config`, `:core`, `:deps`, `:owners`, `:target`, `:validation` (F-021 added `:core`) +- Includer discovers `:android`, `:config`, `:core`, `:deps`, `:jvm`, `:owners`, `:target`, `:validation` (F-021 `:core`; F-030 `:jvm`) --- @@ -90,9 +90,12 @@ Group/version (root `plugins/build.gradle.kts`): **`tools.forma` / `0.1.3`**. │ │ │ └─────┬─────┴────────────┘ │ - ┌─────┴─────┐ - │ android │ DSL + AGP + AndroidTargetTypes + matrix + content helpers - └───────────┘ + ┌───────────┴───────────┐ + │ │ + ┌─────┴─────┐ ┌─────┴─────┐ + │ android │ │ jvm │ F-030 pure JVM DSL + JvmTargetRegistry + │ DSL+AGP │ │ no AGP │ (parallel platform consumer of core) + └───────────┘ └───────────┘ ``` | Module | Plugin id | Depends on | Responsibility | @@ -104,6 +107,7 @@ Group/version (root `plugins/build.gradle.kts`): **`tools.forma` / `0.1.3`**. | `:config` | `tools.forma.config` | `gradleApi` | `AndroidProjectSettings`, `FormaSettingsStore`, plugin/dep registration maps | | `:deps` | `tools.forma.deps` | `:core`, `:validation`, `:target`, `:config`, kotlin-dsl | `FormaDependency` model, `applyDependencies`, version-catalog generators | | `:android` | `tools.forma.android` | `:core` + all of the above + **AGP** + Kotlin GP | Target DSL; `AndroidTargetTypes` + `AndroidRestrictionKit` (F-021); content helpers call core `ContentRule` (F-022) | +| `:jvm` | `tools.forma.jvm` | `:core`, `:deps`, `:validation`, `:target`, `:owners` + Kotlin GP (**no AGP**) | Pure JVM DSL (`api`/`impl`/`library`/`util`/`testUtil` in package `tools.forma.jvm`); `JvmTargetTypes` + `JvmTargetRegistry` (F-030) | `:android` compiles against **AGP 8.1.2** (aligned with sample runtime force in `application/settings.gradle.kts` as of F-003). Keep `plugins/android` AGP @@ -315,7 +319,7 @@ Aligned with `docs/VISION.md`: core must not assume Android/AGP/Dagger. | Settings store | `:config` | **Split** | Generic `SettingsStore` / plugin registry → core; `AndroidProjectSettings` → android | | Owners | `:owners` | **Optional / yes** | Platform-agnostic metadata | | Feature definitions (AGP library/binary/native) | `:android` | **No** | Stay platform | -| DSL entrypoints (`api`/`impl`/…) | `:android` | **No** (register *onto* core) | First consumer of core — **F-023 done** (`AndroidTargetRegistry`) | +| DSL entrypoints (`api`/`impl`/…) | `:android` + `:jvm` | **No** (register *onto* core) | Android first consumer — **F-023**; pure JVM plugin — **F-030** (`JvmTargetRegistry`) | | Includer / depgen | separate builds | **No** | Adjacent tooling | | build-dependencies catalogs | sample support | **No** | Demo-only; pattern informs F-012 | @@ -329,6 +333,9 @@ Suggested extraction order (tickets F-020…F-024): 4. Wire Android DSL / registry as first consumer of core — **done (F-023)** (`TargetRegistry` + `AndroidTargetRegistry`; sample green). 5. Coordinates + publish finalization: `tools.forma:core` (library) vs `tools.forma.android` + facades (F-024 done — see `docs/PLUGIN-PUBLISH.md`). +6. JVM platform consumer of core — **done (F-030):** `plugins/jvm`, plugin id + `tools.forma.jvm`, `JvmTargetRegistry` + matrix ([`JVM-TARGETS.md`](JVM-TARGETS.md)). + Sample JVM app is **F-031**. --- @@ -354,8 +361,9 @@ Suggested extraction order (tickets F-020…F-024): |------|------------| | New user / first project | [`docs/GETTING-STARTED.md`](GETTING-STARTED.md) | | Configuration-time performance | [`docs/CONFIGURATION-PERFORMANCE.md`](CONFIGURATION-PERFORMANCE.md) | -| New target type | `AndroidTargets.kt` + new DSL file under `plugins/android/src/main/java/` + validator list | -| Tighten dep rules | `validator(...)` in that DSL file; update this doc §2.2 | +| New Android target type | `AndroidTargetTypes` + DSL under `plugins/android/` + `registerAndroidDefaults` | +| New JVM target type | `JvmTargetTypes` + DSL under `plugins/jvm/` + `registerJvmDefaults` ([`JVM-TARGETS.md`](JVM-TARGETS.md)) | +| Tighten dep rules | platform registry allow-lists; update DEPENDENCY-MATRIX / JVM-TARGETS | | Global SDK/AGP defaults | `androidProjectConfiguration` + sample `application/build.gradle.kts` | | External deps UX | `plugins/deps` catalog + `build-dependencies` | | Auto module discovery | `includer/` | diff --git a/docs/JVM-TARGETS.md b/docs/JVM-TARGETS.md new file mode 100644 index 0000000..1467daa --- /dev/null +++ b/docs/JVM-TARGETS.md @@ -0,0 +1,115 @@ +# JVM targets (F-030) + +Pure **JVM** platform for Forma, built on **forma-core** (`tools.forma:core`) +without AGP. Parallel to the Android product (`tools.forma.android`), not a +fork of the restriction engine. + +Plugin id: **`tools.forma.jvm`** (module `plugins/:jvm`). + +## Types + +| DSL (package `tools.forma.jvm`) | Type id | Name suffix | Role | +|---------------------------------|---------|-------------|------| +| `api` | `jvm.api` | `api` | Public contracts (Dagger-friendly) | +| `impl` | `jvm.impl` | `impl` | Feature implementation | +| `library` | `jvm.library` | `library` | Shared pure-JVM library | +| `util` | `jvm.util` | `util` | Utilities / extensions | +| `testUtil` | `jvm.test-util` | `test-util` | Shared test helpers | + +Name matching is the same as Android / core: project name equals the suffix or +ends with `-$suffix` (see `SuffixNameMatcher`). + +Composition roots (`binary` / app) are **out of scope for F-030** (sample app +is F-031). Until then, feature graphs should avoid needing multi-`impl` +composition at a root. + +## Dependency matrix (project edges only) + +External / catalog GAV deps are not filtered by type (same as Android). + +| Consumer ↓ \ Dep → | api | impl | library | util | test-util | +|--------------------|-----|------|---------|------|-----------| +| **api** | Y | — | Y | — | — | +| **impl** | Y | — | Y | Y | Y | +| **library** | — | — | — | Y | Y | +| **util** | — | — | Y | Y | — | +| **test-util** | — | — | Y | Y | Y | + +### Rules of thumb + +- **`impl` ↛ `impl`** — implementations stay independent; compose later at a + binary/app root (F-031). +- **`api` only sees contracts** — `api` + `library` (no util/impl leakage into + the public surface). +- **`library` stays leaf-ish** — only `util` / `test-util` (parity with the + historical JVM `library()` row inside the Android matrix). +- **`util` cannot depend on `api` / `impl`**. +- **Content rules** — pure JVM has no Android `res/`; no content rules are + attached for F-030. + +Source of truth in code: `plugins/jvm/.../JvmTargetRegistry.kt` +(`registerJvmDefaults`). + +## Relation to Android + +- Android already exposes pure-Kotlin entrypoints (`api`, `library`, `util`, + `testUtil`) **inside** `tools.forma.android`, registered on + `AndroidTargetRegistry` with some shared **id strings** (`jvm.library`, + `jvm.util`). +- F-030 introduces an **authoritative JVM platform registry** + (`JvmTargetRegistry`) and plugin so pure-JVM products do not need AGP. +- Registries are **separate singletons**; shared id strings are intentional. +- Full Android matrix remains [`DEPENDENCY-MATRIX.md`](DEPENDENCY-MATRIX.md). + +## Consumer sketch + +```kotlin +// settings.gradle.kts +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} +plugins { + id("tools.forma.jvm") version "0.1.3" +} + +// some-module/build.gradle.kts +import tools.forma.jvm.api +import tools.forma.jvm.library +// … + +api( + packageName = "com.example.feature.api", + dependencies = deps( + project(":shared-library") + ) +) +``` + +`registerJvmDefaults()` runs on first DSL entry so the matrix is available +without a separate configuration call (a dedicated `jvmProjectConfiguration` +can land later if settings need grow). + +Kotlin JVM is applied by the DSL (`org.jetbrains.kotlin.jvm`); default +compatibility is Java **11** (`JvmDefaults`). + +## Tests + +- `plugins/jvm` unit tests: `JvmTargetRegistryTest` (matrix allow/deny, + self-suffix, validator identity cache). +- Engine coverage remains in `plugins/core` tests. + +## Next + +- **F-031** — multi-module JVM sample application (gold standard like + `application/`). +- **F-032** — JVM getting-started tutorial. + +## See also + +- [`forma-core-api.md`](forma-core-api.md) — core types / registry SPI +- [`ARCHITECTURE.md`](ARCHITECTURE.md) — module graph +- [`PLUGIN-PUBLISH.md`](PLUGIN-PUBLISH.md) — coordinates +- [`VISION.md`](VISION.md) — product sequencing diff --git a/docs/PLUGIN-PUBLISH.md b/docs/PLUGIN-PUBLISH.md index c89bfa6..5c8f7a1 100644 --- a/docs/PLUGIN-PUBLISH.md +++ b/docs/PLUGIN-PUBLISH.md @@ -9,7 +9,8 @@ expected ownership model and credential flow so workers do not invent secrets. | Project | Plugin id | Notes | |---------|-----------|--------| -| `plugins/:android` | `tools.forma.android` | Main product entrypoint | +| `plugins/:android` | `tools.forma.android` | Main Android product entrypoint | +| `plugins/:jvm` | `tools.forma.jvm` | Pure JVM platform (F-030); no AGP | | `plugins/:target` | `tools.forma.target` | Target identity | | `plugins/:validation` | `tools.forma.validation` | Validators | | `plugins/:owners` | `tools.forma.owners` | Ownership metadata | @@ -37,7 +38,8 @@ Android consumers continue to use the unchanged plugin id `tools.forma.android`. | Artifact | Coordinates | Type | Notes | |----------|-------------|------|-------| | forma-core | `tools.forma:core:0.1.3` (+ `-sources`) | Maven library | Single jar. Contains `tools.forma.core.target`, `.restriction`, `.validation`. | -| Android | `tools.forma.android` (Plugin Portal) | Gradle plugin | Primary entrypoint. Depends on core (transitive). | +| Android | `tools.forma.android` (Plugin Portal) | Gradle plugin | Primary Android entrypoint. Depends on core (transitive). | +| JVM | `tools.forma.jvm` (Plugin Portal) | Gradle plugin | Pure JVM targets (F-030). Depends on core (transitive); **no AGP**. See [`JVM-TARGETS.md`](JVM-TARGETS.md). | | Facades (compat) | `tools.forma.target`, `.validation`, `.deps`, `.config`, `.owners` | Gradle plugins (thin) | Remain published; delegate/re-export core. | Group `tools.forma`, version `0.1.3` (shared source of truth in root `formaPluginConfiguration`). @@ -50,10 +52,16 @@ Group `tools.forma`, version `0.1.3` (shared source of truth in root `formaPlugi id("tools.forma.android") version "0.1.3" } ``` -- **Pure engine / future JVM or Bazel** (F-030+): depend directly on the library - when writing adapters that do not need the Android DSL: +- **Pure JVM apps** (F-030): use the JVM plugin (no AGP): + ```kotlin + plugins { + id("tools.forma.jvm") version "0.1.3" + } + ``` + DSL lives in package `tools.forma.jvm` — see [`JVM-TARGETS.md`](JVM-TARGETS.md). +- **Pure engine / Bazel adapters**: depend directly on the library when writing + adapters that do not need a platform DSL: `implementation("tools.forma:core:0.1.3")`. - (A future `tools.forma.jvm` plugin would also pull core.) ### How plugin POMs declare the core dependency diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 6b8272d..9989344 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,6 +2,29 @@ Newest entries first. +## 2026-07-13 — F-030 JVM target set on forma-core + +- **Ticket:** F-030 → `done` +- **Branch:** `forma/F-030-jvm-targets` (from `origin/v2`) +- **Skills/modes:** Grok Build `--mode full` (design plan completed; implement hit max-turns with partial tree); Hermes finished tests, docs, verify, commit/PR +- **Platform (`plugins/jvm`):** + - Plugin id **`tools.forma.jvm`** via `formaPublishedPlugin(name = "jvm")` + empty Settings `FormaPlugin` + - **No AGP**; depends on `:core`, `:deps`, `:validation`, `:target`, `:owners` + embedded Kotlin GP + - `JvmTargetTypes`: `jvm.api`, `jvm.impl`, `jvm.library`, `jvm.util`, `jvm.test-util` + - `JvmTargetRegistry` + `registerJvmDefaults()` matrix (impl↛impl; api→api/library; library→util/test-util; util→util/library; test-util→test-util/util/library) + - DSL under package `tools.forma.jvm`: `api` / `impl` / `library` / `util` / `testUtil` + - Minimal `applyKotlinJvm()` (Java 11) without `AndroidProjectSettings` +- **Tests:** `JvmTargetRegistryTest` (matrix, self-suffix, identity cache) +- **Docs:** `docs/JVM-TARGETS.md`; ARCHITECTURE module graph; PLUGIN-PUBLISH table; README Progress; forma-core-api status +- **Verify (OpenJDK 17 + env-mac.sh):** + - `plugins/`: `./gradlew :core:test :jvm:test build` → **BUILD SUCCESSFUL** (78 tasks; `JvmTargetRegistryTest` 5/5) + - Plugin descriptor `tools.forma.jvm` → `tools.forma.jvm.plugin.FormaPlugin` + - `application/`: `./gradlew :binary:assembleDebug` → **BUILD SUCCESSFUL** (578 tasks) +- **Grounded:** all commands executed; no invented green builds. +- **Commits/PRs:** this run — push + PR base `v2` +- **Blockers:** none (Grok implement max-turns; completed under Hermes) +- **Next step:** F-031 JVM sample application + ## 2026-07-13 — F-024 Publish/coordinate coordinates: `tools.forma:core` vs android plugins - **Ticket:** F-024 → `done` diff --git a/docs/forma-core-api.md b/docs/forma-core-api.md index b4769e4..6242acb 100644 --- a/docs/forma-core-api.md +++ b/docs/forma-core-api.md @@ -7,8 +7,10 @@ plugins under `plugins/` and in [`ARCHITECTURE.md`](ARCHITECTURE.md) §6 + **Status:** accepted design. **Implemented so far:** F-021 (types + restriction graph + Android kit), **F-022** (validation SPI + content predicates in `plugins/core`; `:validation` is a Gradle facade), **F-023** -(`TargetRegistry` + Android DSL consumers via `AndroidTargetRegistry`). -Publishing coordinates remain **F-024**. +(`TargetRegistry` + Android DSL consumers via `AndroidTargetRegistry`), +**F-024** (publish coordinates for `tools.forma:core`), **F-030** (pure JVM +platform plugin `tools.forma.jvm` + `JvmTargetRegistry` — see +[`JVM-TARGETS.md`](JVM-TARGETS.md)). --- diff --git a/plugins/jvm/build.gradle.kts b/plugins/jvm/build.gradle.kts new file mode 100644 index 0000000..50acbb0 --- /dev/null +++ b/plugins/jvm/build.gradle.kts @@ -0,0 +1,39 @@ +@file:Suppress("UnstableApiUsage") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +plugins { + id("org.gradle.kotlin.kotlin-dsl") + id("com.gradle.plugin-publish") +} + +formaPublishedPlugin(name = "jvm") + +tasks.named("compileKotlin", KotlinCompilationTask::class.java) { + compilerOptions { + // Keep parity options with sibling plugins if useful; none strictly required for pure JVM. + } +} + +dependencies { + // Pure JVM platform plugin — NO AGP. Kotlin JVM only. + implementation(embeddedKotlin("gradle-plugin")) + + // forma-core + supporting facades (deps/validation own the apply + validator facades) + implementation(project(":core")) + implementation(project(":deps")) + implementation(project(":validation")) + implementation(project(":target")) + implementation(project(":owners")) + + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} + +tasks.named("publishPlugins") { + // If/when wiring a root publish that chains jvm too, list deps here. + // For F-030 slice, standalone :jvm:publishPlugins works for validation. +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/api.kt b/plugins/jvm/src/main/java/tools/forma/jvm/api.kt new file mode 100644 index 0000000..dc55f2e --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/api.kt @@ -0,0 +1,39 @@ +package tools.forma.jvm + +import org.gradle.api.Project +import tools.forma.deps.core.EmptyDependency +import tools.forma.deps.core.FormaDependency +import tools.forma.deps.core.applyDependencies +import tools.forma.jvm.feature.applyKotlinJvm +import tools.forma.jvm.target.JvmTargetRegistry +import tools.forma.jvm.target.JvmTargetTypes +import tools.forma.jvm.target.registerJvmDefaults +import tools.forma.owners.NoOwner +import tools.forma.owners.Owner +import tools.forma.target.FormaTarget +import tools.forma.validation.asValidator + +/** + * Feature **API** surface (pure JVM). + * + * Dagger2-friendly public contracts only. + * May depend on other `api` and pure `jvm.library` modules. + * Must not depend on `impl`, `util` (except through library contracts if modeled that way). + */ +fun Project.api( + packageName: String, + owner: Owner = NoOwner, + dependencies: FormaDependency = EmptyDependency +) { + registerJvmDefaults() + + JvmTargetRegistry.selfValidator(JvmTargetTypes.api).asValidator() + .validate(FormaTarget(this)) + + applyKotlinJvm() + + applyDependencies( + validator = JvmTargetRegistry.validatorFor(JvmTargetTypes.api).asValidator(), + dependencies = dependencies + ) +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/feature/JvmFeature.kt b/plugins/jvm/src/main/java/tools/forma/jvm/feature/JvmFeature.kt new file mode 100644 index 0000000..3e9216e --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/feature/JvmFeature.kt @@ -0,0 +1,37 @@ +package tools.forma.jvm.feature + +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.apply +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +/** + * Minimal Kotlin JVM feature applicator for the pure JVM plugin (F-030). + * + * Applies `org.jetbrains.kotlin.jvm` and configures source/target compatibility + jvmTarget. + * Does not depend on AndroidProjectSettings / Forma.settings or any Android bits. + * + * Default JVM target: 11 (modern default for pure JVM targets; override in future + * jvmProjectConfiguration if added). + */ +object JvmDefaults { + val javaVersionCompatibility: JavaVersion = JavaVersion.VERSION_11 + const val jvmTarget: String = "11" +} + +fun Project.applyKotlinJvm() { + apply(plugin = "org.jetbrains.kotlin.jvm") + + val jvmTarget = JvmDefaults.jvmTarget + val javaVersion = JvmDefaults.javaVersionCompatibility.toString() + + tasks.withType(JavaCompile::class.java).configureEach { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + } + + tasks.withType(KotlinCompile::class.java).configureEach { + kotlinOptions.jvmTarget = jvmTarget + } +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt b/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt new file mode 100644 index 0000000..4410962 --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt @@ -0,0 +1,40 @@ +package tools.forma.jvm + +import org.gradle.api.Project +import tools.forma.deps.core.EmptyDependency +import tools.forma.deps.core.FormaDependency +import tools.forma.deps.core.NamedDependency +import tools.forma.deps.core.applyDependencies +import tools.forma.jvm.feature.applyKotlinJvm +import tools.forma.jvm.target.JvmTargetRegistry +import tools.forma.jvm.target.JvmTargetTypes +import tools.forma.jvm.target.registerJvmDefaults +import tools.forma.target.FormaTarget +import tools.forma.validation.asValidator + +/** + * Feature **implementation** (pure JVM). + * + * Dagger2-friendly boundaries: + * - May depend on `api`, `library`, `util`, `test-util`. + * - **Must not** depend on other `impl` modules. + * Composition of impls happens at a binary / app root (later F-031). + */ +fun Project.impl( + packageName: String, + dependencies: FormaDependency = EmptyDependency, + testDependencies: NamedDependency = NamedDependency() +) { + registerJvmDefaults() + + JvmTargetRegistry.selfValidator(JvmTargetTypes.impl).asValidator() + .validate(FormaTarget(this)) + + applyKotlinJvm() + + applyDependencies( + validator = JvmTargetRegistry.validatorFor(JvmTargetTypes.impl).asValidator(), + dependencies = dependencies, + testDependencies = testDependencies + ) +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/library.kt b/plugins/jvm/src/main/java/tools/forma/jvm/library.kt new file mode 100644 index 0000000..0bb9ac7 --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/library.kt @@ -0,0 +1,40 @@ +package tools.forma.jvm + +import org.gradle.api.Project +import tools.forma.deps.core.EmptyDependency +import tools.forma.deps.core.FormaDependency +import tools.forma.deps.core.NamedDependency +import tools.forma.deps.core.applyDependencies +import tools.forma.jvm.feature.applyKotlinJvm +import tools.forma.jvm.target.JvmTargetRegistry +import tools.forma.jvm.target.JvmTargetTypes +import tools.forma.jvm.target.registerJvmDefaults +import tools.forma.owners.NoOwner +import tools.forma.owners.Owner +import tools.forma.target.FormaTarget +import tools.forma.validation.asValidator + +/** + * Pure JVM **library** (shared code, no Android). + * + * May only depend on `util` + `test-util` (parity with prior JVM library row). + */ +fun Project.library( + packageName: String, + dependencies: FormaDependency = EmptyDependency, + owner: Owner = NoOwner, + testDependencies: NamedDependency = NamedDependency() +) { + registerJvmDefaults() + + JvmTargetRegistry.selfValidator(JvmTargetTypes.library).asValidator() + .validate(FormaTarget(this)) + + applyKotlinJvm() + + applyDependencies( + validator = JvmTargetRegistry.validatorFor(JvmTargetTypes.library).asValidator(), + dependencies = dependencies, + testDependencies = testDependencies + ) +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/plugin/FormaPlugin.kt b/plugins/jvm/src/main/java/tools/forma/jvm/plugin/FormaPlugin.kt new file mode 100644 index 0000000..8ab6f17 --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/plugin/FormaPlugin.kt @@ -0,0 +1,17 @@ +@file:Suppress("unused") + +package tools.forma.jvm.plugin + +import org.gradle.api.Plugin +import org.gradle.api.initialization.Settings + +/** + * Empty Settings plugin implementation to enable publishing `tools.forma.jvm` + * to the Gradle Plugin Portal (same pattern as :android and sibling facades). + */ +class FormaPlugin : Plugin { + + override fun apply(settings: Settings) { + return + } +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt new file mode 100644 index 0000000..a46fec8 --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt @@ -0,0 +1,69 @@ +package tools.forma.jvm.target + +import tools.forma.core.target.DefaultTargetRegistry +import tools.forma.core.target.TargetRegistration +import tools.forma.core.target.TargetRegistry + +/** + * Singleton registry for pure JVM target types (F-030). + * Separate from AndroidTargetRegistry so platforms can evolve matrices independently. + * Use [registerJvmDefaults] to populate the restriction matrix. + */ +object JvmTargetRegistry : TargetRegistry by DefaultTargetRegistry() + +/** + * Register JVM target types + restriction matrix. + * + * Matrix (Dagger-friendly, pure JVM, no Android types): + * - api → api, library + * - impl → api, library, util, test-util (NO impl — composition only at binary/app roots later) + * - library → util, test-util + * - util → util, library + * - testUtil → test-util, util (library allowed for test helpers that need prod contracts) + * + * No ContentRules attached for pure JVM (no Android resources concept). + * Safe to call multiple times (re-register replaces). + */ +fun registerJvmDefaults(registry: TargetRegistry = JvmTargetRegistry) { + val t = JvmTargetTypes + + // api: contracts only — api + (pure jvm) library + registry.register( + TargetRegistration( + type = t.api, + allowedDependencies = setOf(t.api, t.library) + ) + ) + + // impl: impl may use contracts + shared + utils; **no other impl** + registry.register( + TargetRegistration( + type = t.impl, + allowedDependencies = setOf(t.api, t.library, t.util, t.testUtil) + ) + ) + + // library (pure JVM): only util + test-util (parity with existing android jvm.library row) + registry.register( + TargetRegistration( + type = t.library, + allowedDependencies = setOf(t.util, t.testUtil) + ) + ) + + // util: utilities may depend on other utils + libraries + registry.register( + TargetRegistration( + type = t.util, + allowedDependencies = setOf(t.util, t.library) + ) + ) + + // test-util: test helpers use other test utils + util (+ library for test code needing contracts) + registry.register( + TargetRegistration( + type = t.testUtil, + allowedDependencies = setOf(t.testUtil, t.util, t.library) + ) + ) +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt new file mode 100644 index 0000000..27133fc --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt @@ -0,0 +1,29 @@ +package tools.forma.jvm.target + +import tools.forma.core.target.TargetType +import tools.forma.core.target.targetType + +/** + * Stable TargetType instances for the pure JVM platform (F-030). + * + * These are the authoritative definitions for standalone JVM usage. + * Ids are distinct where needed; "jvm.library" / "jvm.util" intentionally reuse the + * ids that AndroidTargetTypes also declares for its pure-JVM rows (shared id strings ok; + * each platform uses its own *TargetRegistry singleton). + */ +object JvmTargetTypes { + /** Public API contracts (Dagger friendly). */ + val api: TargetType = targetType("jvm.api", "api") + + /** Feature implementation (pure JVM). Must not depend on other impl. */ + val impl: TargetType = targetType("jvm.impl", "impl") + + /** Shared pure JVM library code. */ + val library: TargetType = targetType("jvm.library", "library") + + /** Utility / extension modules (JVM). */ + val util: TargetType = targetType("jvm.util", "util") + + /** Test helpers (JVM). */ + val testUtil: TargetType = targetType("jvm.test-util", "test-util") +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/testUtil.kt b/plugins/jvm/src/main/java/tools/forma/jvm/testUtil.kt new file mode 100644 index 0000000..6788617 --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/testUtil.kt @@ -0,0 +1,37 @@ +package tools.forma.jvm + +import org.gradle.api.Project +import tools.forma.deps.core.EmptyDependency +import tools.forma.deps.core.FormaDependency +import tools.forma.deps.core.applyDependencies +import tools.forma.jvm.feature.applyKotlinJvm +import tools.forma.jvm.target.JvmTargetRegistry +import tools.forma.jvm.target.JvmTargetTypes +import tools.forma.jvm.target.registerJvmDefaults +import tools.forma.owners.NoOwner +import tools.forma.owners.Owner +import tools.forma.target.FormaTarget +import tools.forma.validation.asValidator + +/** + * JVM **test-util** helpers. + * + * Intended for test code shared across modules. Depends on `test-util` + `util` (+ library). + */ +fun Project.testUtil( + packageName: String, + owner: Owner = NoOwner, + dependencies: FormaDependency = EmptyDependency +) { + registerJvmDefaults() + + JvmTargetRegistry.selfValidator(JvmTargetTypes.testUtil).asValidator() + .validate(FormaTarget(this)) + + applyKotlinJvm() + + applyDependencies( + validator = JvmTargetRegistry.validatorFor(JvmTargetTypes.testUtil).asValidator(), + dependencies = dependencies + ) +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/util.kt b/plugins/jvm/src/main/java/tools/forma/jvm/util.kt new file mode 100644 index 0000000..365be39 --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/util.kt @@ -0,0 +1,39 @@ +package tools.forma.jvm + +import org.gradle.api.Project +import tools.forma.deps.core.EmptyDependency +import tools.forma.deps.core.FormaDependency +import tools.forma.deps.core.applyDependencies +import tools.forma.jvm.feature.applyKotlinJvm +import tools.forma.jvm.target.JvmTargetRegistry +import tools.forma.jvm.target.JvmTargetTypes +import tools.forma.jvm.target.registerJvmDefaults +import tools.forma.owners.NoOwner +import tools.forma.owners.Owner +import tools.forma.target.FormaTarget +import tools.forma.validation.asValidator + +/** + * JVM **util** / extensions module. + * + * May depend on other `util` + `library`. + */ +fun Project.util( + packageName: String, + owner: Owner = NoOwner, + dependencies: FormaDependency = EmptyDependency, + testDependencies: FormaDependency = EmptyDependency +) { + registerJvmDefaults() + + JvmTargetRegistry.selfValidator(JvmTargetTypes.util).asValidator() + .validate(FormaTarget(this)) + + applyKotlinJvm() + + applyDependencies( + validator = JvmTargetRegistry.validatorFor(JvmTargetTypes.util).asValidator(), + dependencies = dependencies, + testDependencies = testDependencies + ) +} diff --git a/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt b/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt new file mode 100644 index 0000000..2533a0c --- /dev/null +++ b/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt @@ -0,0 +1,118 @@ +package tools.forma.jvm.target + +import tools.forma.core.target.TargetRef +import tools.forma.core.validation.FormaValidationException +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue + +/** + * F-030: pure JVM matrix + registry behavior (no AGP). + */ +class JvmTargetRegistryTest { + + private data class TestRef(override val name: String) : TargetRef + + @BeforeTest + fun setup() { + // Fresh defaults each test (replace semantics) + registerJvmDefaults() + } + + @Test + fun `all five jvm types are registered by id`() { + val t = JvmTargetTypes + assertEquals(t.api, JvmTargetRegistry.get("jvm.api")) + assertEquals(t.impl, JvmTargetRegistry.get("jvm.impl")) + assertEquals(t.library, JvmTargetRegistry.get("jvm.library")) + assertEquals(t.util, JvmTargetRegistry.get("jvm.util")) + assertEquals(t.testUtil, JvmTargetRegistry.get("jvm.test-util")) + assertEquals(5, JvmTargetRegistry.all().size) + } + + @Test + fun `restriction graph encodes dagger friendly matrix`() { + val t = JvmTargetTypes + val g = JvmTargetRegistry.restrictionGraph() + + // api + assertTrue(g.isAllowed(t.api, t.api)) + assertTrue(g.isAllowed(t.api, t.library)) + assertFalse(g.isAllowed(t.api, t.impl)) + assertFalse(g.isAllowed(t.api, t.util)) + + // impl: no impl→impl + assertTrue(g.isAllowed(t.impl, t.api)) + assertTrue(g.isAllowed(t.impl, t.library)) + assertTrue(g.isAllowed(t.impl, t.util)) + assertTrue(g.isAllowed(t.impl, t.testUtil)) + assertFalse(g.isAllowed(t.impl, t.impl)) + + // library + assertTrue(g.isAllowed(t.library, t.util)) + assertTrue(g.isAllowed(t.library, t.testUtil)) + assertFalse(g.isAllowed(t.library, t.api)) + assertFalse(g.isAllowed(t.library, t.impl)) + assertFalse(g.isAllowed(t.library, t.library)) + + // util + assertTrue(g.isAllowed(t.util, t.util)) + assertTrue(g.isAllowed(t.util, t.library)) + assertFalse(g.isAllowed(t.util, t.api)) + assertFalse(g.isAllowed(t.util, t.impl)) + + // test-util + assertTrue(g.isAllowed(t.testUtil, t.testUtil)) + assertTrue(g.isAllowed(t.testUtil, t.util)) + assertTrue(g.isAllowed(t.testUtil, t.library)) + assertFalse(g.isAllowed(t.testUtil, t.api)) + assertFalse(g.isAllowed(t.testUtil, t.impl)) + } + + @Test + fun `validatorFor rejects disallowed project suffixes`() { + val t = JvmTargetTypes + val apiV = JvmTargetRegistry.validatorFor(t.api) + apiV.validate(TestRef("feature-api")) + apiV.validate(TestRef("core-library")) + assertFailsWith { apiV.validate(TestRef("feature-impl")) } + assertFailsWith { apiV.validate(TestRef("shared-util")) } + + val implV = JvmTargetRegistry.validatorFor(t.impl) + implV.validate(TestRef("feature-api")) + assertFailsWith { implV.validate(TestRef("other-impl")) } + + val libV = JvmTargetRegistry.validatorFor(t.library) + libV.validate(TestRef("shared-util")) + assertFailsWith { libV.validate(TestRef("feature-api")) } + } + + @Test + fun `selfValidator accepts matching suffix`() { + val t = JvmTargetTypes + JvmTargetRegistry.selfValidator(t.api).validate(TestRef("api")) + JvmTargetRegistry.selfValidator(t.api).validate(TestRef("feature-api")) + assertFailsWith { + JvmTargetRegistry.selfValidator(t.api).validate(TestRef("feature-impl")) + } + JvmTargetRegistry.selfValidator(t.impl).validate(TestRef("home-impl")) + JvmTargetRegistry.selfValidator(t.library).validate(TestRef("common-library")) + JvmTargetRegistry.selfValidator(t.util).validate(TestRef("network-util")) + JvmTargetRegistry.selfValidator(t.testUtil).validate(TestRef("shared-test-util")) + } + + @Test + fun `validatorFor is identity cached for same type`() { + val t = JvmTargetTypes + val v1 = JvmTargetRegistry.validatorFor(t.api) + val v2 = JvmTargetRegistry.validatorFor(t.api) + assertSame(v1, v2) + val s1 = JvmTargetRegistry.selfValidator(t.impl) + val s2 = JvmTargetRegistry.selfValidator(t.impl) + assertSame(s1, s2) + } +}