diff --git a/README.md b/README.md index 5f3ba41d..c7d5b530 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ systems like Buck and Bazel. 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) +9. [JVM sample application](docs/JVM-SAMPLE.md) — multi-module pure-JVM gold standard (`jvm-application/`, F-031) Configuration made easy: @@ -202,6 +203,7 @@ forma-core validation SPI + content rules (Gradle facade in `:validation`): **F- 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)). +JVM sample application + `binary` composition root: **F-031** done ([`docs/JVM-SAMPLE.md`](docs/JVM-SAMPLE.md), `jvm-application/`). Icons made by Freepik from www.flaticon.com diff --git a/TICKETS.md b/TICKETS.md index 67021982..b80aa680 100644 --- a/TICKETS.md +++ b/TICKETS.md @@ -41,7 +41,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi | ID | Status | Title | Notes | |----|--------|-------|-------| | 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-031 | done | JVM sample application | `jvm-application/` + `binary` DSL; docs/JVM-SAMPLE.md | | F-032 | todo | Docs: JVM getting started | | ## P4 — Bazel diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 210aa598..8ec14ba4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -18,6 +18,7 @@ that compose via `includeBuild`: |------|------|----------------|-------------------------| | `plugins/` | Forma Gradle plugins (main product) | 8.3 | Composite + Plugin Portal (`tools.forma.*`) | | `application/` | Sample Android product (gold standard) | 8.4 | Consumer only | +| `jvm-application/` | Sample pure-JVM product (`tools.forma.jvm`) | 8.4 | Consumer only | | `includer/` | Settings plugin: auto-`include` subprojects | (own wrapper) | `tools.forma.includer` | | `depgen/` | Transitive-deps generation plugin | (own wrapper) | `tools.forma.depgen` (standalone) | | `build-settings/` | Shared settings conventions (repos) | — | `includeBuild` / convention plugins | @@ -335,7 +336,7 @@ Suggested extraction order (tickets F-020…F-024): 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**. + Sample JVM app is **F-031** done (`jvm-application/`, `binary` composition root). --- @@ -368,7 +369,7 @@ Suggested extraction order (tickets F-020…F-024): | External deps UX | `plugins/deps` catalog + `build-dependencies` | | Auto module discovery | `includer/` | | CI | `.github/workflows/main.yml` | -| Sample structure | `application/feature/**`, `binary/` · [SAMPLE-APP.md](SAMPLE-APP.md) | +| Sample structure | `application/feature/**`, `binary/` · [SAMPLE-APP.md](SAMPLE-APP.md); JVM: `jvm-application/` · [JVM-SAMPLE.md](JVM-SAMPLE.md) | --- diff --git a/docs/JVM-SAMPLE.md b/docs/JVM-SAMPLE.md new file mode 100644 index 00000000..b097bfc2 --- /dev/null +++ b/docs/JVM-SAMPLE.md @@ -0,0 +1,111 @@ +# JVM sample application (F-031) + +Pure-JVM product reference for **`tools.forma.jvm`**, parallel to the Android +[`application/`](../application/) gold standard. + +Location: **`jvm-application/`** at the repo root. + +## Layout + +``` +jvm-application/ +├── binary/ binary (composition root + run) +├── common/ +│ ├── library/ library +│ ├── util/ util +│ └── test-util/ testUtil +└── feature/ + ├── greeter/{api,impl} + └── calculator/{api,impl} +``` + +Includer discovers every `build.gradle.kts` (`arbitraryBuildScriptNames = true`). +Project path uses `-` instead of `/` for Gradle names (e.g. +`feature/greeter/impl` → `:feature-greeter-impl`). Forma `target(":feature:greeter:impl")` +uses colon path form. + +## What it exercises + +| Target | Role in sample | +|--------|----------------| +| `api` / `impl` | Two features (greeter, calculator); **impl ↛ impl** | +| `library` | Shared math helpers | +| `util` | String helpers used by greeter impl | +| `testUtil` | Shared test helpers (wired as `testDependencies` on greeter impl) | +| `binary` | Composition root: wires both feature **api + impl** + shared modules; Gradle `application` plugin provides `:binary:run` | + +Cross-feature code only through `api`. Multiple `impl`s meet only at `binary`. + +## Package naming + +| Module path | `packageName` | +|-------------|----------------| +| `feature/greeter/api` | `tools.forma.jvm.sample.feature.greeter.api` | +| `feature/greeter/impl` | `tools.forma.jvm.sample.feature.greeter.impl` | +| `feature/calculator/api` | `tools.forma.jvm.sample.feature.calculator.api` | +| `feature/calculator/impl` | `tools.forma.jvm.sample.feature.calculator.impl` | +| `common/library` | `tools.forma.jvm.sample.common.library` | +| `common/util` | `tools.forma.jvm.sample.common.util` | +| `common/test-util` | `tools.forma.jvm.sample.common.testutil` | +| `binary` | `tools.forma.jvm.sample.binary` | + +Sources live under `src/main/kotlin//…`. + +## Build / run + +Requires JDK **17+** (same host bootstrap as Android: `source scripts/env-mac.sh`). +**No Android SDK.** + +```bash +source scripts/env-mac.sh +cd jvm-application +./gradlew build +./gradlew :binary:run +``` + +Expected run output: + +``` +Hello, World! (2 + 3 = 5) +JVM sample (tools.forma.jvm) build + run successful. +``` + +Composite includes: `../plugins` (`tools.forma.jvm`), `../includer`, +`../build-settings` (`convention-dependencies` for `mavenCentral` / +`google` / Plugin Portal on project resolution). + +## Consumer DSL notes + +JVM entrypoints live in package **`tools.forma.jvm`** (unlike Android DSL files +in the default package). Sample scripts import them explicitly: + +```kotlin +import tools.forma.jvm.api +import tools.forma.jvm.impl +import tools.forma.jvm.binary +// … + +binary( + packageName = "tools.forma.jvm.sample.binary", + mainClass = "tools.forma.jvm.sample.binary.MainKt", + dependencies = deps( + target(":feature:greeter:api"), + target(":feature:greeter:impl"), + // … + ) +) +``` + +For a top-level `fun main()` in `Main.kt`, `mainClass` is `…MainKt`. + +## Matrix + +Full pure-JVM dependency matrix (including `binary`): +[`JVM-TARGETS.md`](JVM-TARGETS.md). + +## See also + +- [`JVM-TARGETS.md`](JVM-TARGETS.md) — types + matrix +- [`SAMPLE-APP.md`](SAMPLE-APP.md) — Android multi-feature analog +- [`VISION.md`](VISION.md) — product sequencing (JVM after Android + core) +- F-032 — fuller JVM getting-started tutorial (next) diff --git a/docs/JVM-TARGETS.md b/docs/JVM-TARGETS.md index 1467daa7..4b6bd4b3 100644 --- a/docs/JVM-TARGETS.md +++ b/docs/JVM-TARGETS.md @@ -1,4 +1,4 @@ -# JVM targets (F-030) +# JVM targets (F-030 / F-031) Pure **JVM** platform for Forma, built on **forma-core** (`tools.forma:core`) without AGP. Parallel to the Android product (`tools.forma.android`), not a @@ -15,37 +15,38 @@ Plugin id: **`tools.forma.jvm`** (module `plugins/:jvm`). | `library` | `jvm.library` | `library` | Shared pure-JVM library | | `util` | `jvm.util` | `util` | Utilities / extensions | | `testUtil` | `jvm.test-util` | `test-util` | Shared test helpers | +| `binary` | `jvm.binary` | `binary` | Composition root + runnable app | 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. +`binary(...)` applies `org.jetbrains.kotlin.jvm` + Gradle `application`, sets +`mainClass`, and may depend on multiple `impl` modules (composition 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 | +| Consumer ↓ \ Dep → | api | impl | library | util | test-util | binary | +|--------------------|-----|------|---------|------|-----------|--------| +| **api** | Y | — | Y | — | — | — | +| **impl** | Y | — | Y | Y | Y | — | +| **library** | — | — | — | Y | Y | — | +| **util** | — | — | Y | Y | — | — | +| **test-util** | — | — | Y | Y | Y | — | +| **binary** | Y | Y | Y | Y | Y | — | ### Rules of thumb -- **`impl` ↛ `impl`** — implementations stay independent; compose later at a - binary/app root (F-031). +- **`impl` ↛ `impl`** — implementations stay independent; compose at `binary`. - **`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`**. +- **`binary` is the composition root** — may pull api + multiple impls + shared. - **Content rules** — pure JVM has no Android `res/`; no content rules are - attached for F-030. + attached. Source of truth in code: `plugins/jvm/.../JvmTargetRegistry.kt` (`registerJvmDefaults`). @@ -56,8 +57,9 @@ Source of truth in code: `plugins/jvm/.../JvmTargetRegistry.kt` `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** +- F-030 introduced an **authoritative JVM platform registry** (`JvmTargetRegistry`) and plugin so pure-JVM products do not need AGP. +- F-031 added **`binary`** + the multi-module sample `jvm-application/`. - Registries are **separate singletons**; shared id strings are intentional. - Full Android matrix remains [`DEPENDENCY-MATRIX.md`](DEPENDENCY-MATRIX.md). @@ -73,12 +75,12 @@ pluginManagement { } plugins { id("tools.forma.jvm") version "0.1.3" + // or includeBuild("../plugins") in a composite } -// some-module/build.gradle.kts +// module build.gradle.kts import tools.forma.jvm.api import tools.forma.jvm.library -// … api( packageName = "com.example.feature.api", @@ -98,17 +100,18 @@ compatibility is Java **11** (`JvmDefaults`). ## Tests - `plugins/jvm` unit tests: `JvmTargetRegistryTest` (matrix allow/deny, - self-suffix, validator identity cache). + self-suffix, validator identity cache, binary composition-root row). - Engine coverage remains in `plugins/core` tests. +- Integration: [`jvm-application/`](../jvm-application/) (`./gradlew build` / + `:binary:run`). ## Next -- **F-031** — multi-module JVM sample application (gold standard like - `application/`). -- **F-032** — JVM getting-started tutorial. +- **F-032** — JVM getting-started tutorial (point at this doc + sample). ## See also +- [`JVM-SAMPLE.md`](JVM-SAMPLE.md) — multi-module sample layout + run - [`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 diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 99893443..1826af4f 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,6 +2,31 @@ Newest entries first. +## 2026-07-13 — F-031 JVM sample application + +- **Ticket:** F-031 → `done` +- **Branch:** `forma/F-031-jvm-sample` (from `origin/v2`) +- **Skills/modes:** Grok Build `--mode full` (design plan completed; implement hit max-turns with partial tree on disk); Hermes finished wiring, verify, docs, commit/PR +- **Platform (`plugins/jvm`):** + - New type **`jvm.binary`** + `binary(...)` DSL (Kotlin JVM + Gradle `application` + `mainClass`) + - `registerJvmDefaults` composition-root row: binary → api/impl/library/util/test-util (no binary→binary) + - `impl` / `library` `testDependencies` widened to `FormaDependency` (project test-util) + - `JvmTargetRegistryTest` updated for six types + binary matrix/self-suffix +- **Sample (`jvm-application/`):** + - Multi-module pure-JVM product: greeter + calculator features (api/impl), common library/util/test-util, binary composition root + - Composite: `includeBuild` plugins + includer + build-settings; `convention-dependencies` for project repos + - Explicit `import tools.forma.jvm.*` in module scripts (JVM DSL is packaged, not default package) + - Runnable: `./gradlew :binary:run` → `Hello, World! (2 + 3 = 5)` +- **Docs:** `docs/JVM-SAMPLE.md`; JVM-TARGETS matrix + binary row; ARCHITECTURE layout; README Progress + getting-started link; TICKETS +- **Verify (OpenJDK 17 + env-mac.sh):** + - `plugins/`: `./gradlew :core:test :jvm:test build` → **BUILD SUCCESSFUL** (earlier full build; re-run `:jvm:test` green) + - `jvm-application/`: `./gradlew build :binary:run` → **BUILD SUCCESSFUL** (run output as above) + - `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-032 Docs: JVM getting started + ## 2026-07-13 — F-030 JVM target set on forma-core - **Ticket:** F-030 → `done` diff --git a/jvm-application/binary/build.gradle.kts b/jvm-application/binary/build.gradle.kts new file mode 100644 index 00000000..41cdae9e --- /dev/null +++ b/jvm-application/binary/build.gradle.kts @@ -0,0 +1,14 @@ +import tools.forma.jvm.binary + +binary( + packageName = "tools.forma.jvm.sample.binary", + mainClass = "tools.forma.jvm.sample.binary.MainKt", + dependencies = deps( + target(":feature:greeter:api"), + target(":feature:greeter:impl"), + target(":feature:calculator:api"), + target(":feature:calculator:impl"), + target(":common:util"), + target(":common:library") + ) +) diff --git a/jvm-application/binary/src/main/kotlin/tools/forma/jvm/sample/binary/Main.kt b/jvm-application/binary/src/main/kotlin/tools/forma/jvm/sample/binary/Main.kt new file mode 100644 index 00000000..87601a85 --- /dev/null +++ b/jvm-application/binary/src/main/kotlin/tools/forma/jvm/sample/binary/Main.kt @@ -0,0 +1,22 @@ +package tools.forma.jvm.sample.binary + +import tools.forma.jvm.sample.feature.greeter.api.Greeter +import tools.forma.jvm.sample.feature.greeter.impl.GreeterImpl +import tools.forma.jvm.sample.feature.calculator.api.Calculator +import tools.forma.jvm.sample.feature.calculator.impl.CalculatorImpl + +/** + * Composition root demo for F-031. + * Wires impls (greeter + calculator) + shared library/util only at the binary. + * Cross-feature communication happens exclusively through api modules. + */ +fun main() { + val greeter: Greeter = GreeterImpl() + val calculator: Calculator = CalculatorImpl() + + val greeting = greeter.greet("world") + val sum = calculator.add(2, 3) + + println("$greeting (2 + 3 = $sum)") + println("JVM sample (tools.forma.jvm) build + run successful.") +} diff --git a/jvm-application/build.gradle.kts b/jvm-application/build.gradle.kts new file mode 100644 index 00000000..55a2a9df --- /dev/null +++ b/jvm-application/build.gradle.kts @@ -0,0 +1,4 @@ +// Root build for the pure-JVM Forma sample (F-031). +// No androidProjectConfiguration or jvmProjectConfiguration hook is required yet. +// The tools.forma.jvm settings plugin + DSL entrypoints (api/impl/library/util/testUtil/binary) +// configure each module when their build.gradle.kts are evaluated. diff --git a/jvm-application/common/library/build.gradle.kts b/jvm-application/common/library/build.gradle.kts new file mode 100644 index 00000000..812f4b21 --- /dev/null +++ b/jvm-application/common/library/build.gradle.kts @@ -0,0 +1,3 @@ +import tools.forma.jvm.library + +library(packageName = "tools.forma.jvm.sample.common.library") diff --git a/jvm-application/common/library/src/main/kotlin/tools/forma/jvm/sample/common/library/Math.kt b/jvm-application/common/library/src/main/kotlin/tools/forma/jvm/sample/common/library/Math.kt new file mode 100644 index 00000000..d9d4da55 --- /dev/null +++ b/jvm-application/common/library/src/main/kotlin/tools/forma/jvm/sample/common/library/Math.kt @@ -0,0 +1,6 @@ +package tools.forma.jvm.sample.common.library + +object Math { + fun add(a: Int, b: Int): Int = a + b + fun multiply(a: Int, b: Int): Int = a * b +} diff --git a/jvm-application/common/test-util/build.gradle.kts b/jvm-application/common/test-util/build.gradle.kts new file mode 100644 index 00000000..a3c4d5a2 --- /dev/null +++ b/jvm-application/common/test-util/build.gradle.kts @@ -0,0 +1,3 @@ +import tools.forma.jvm.testUtil + +testUtil(packageName = "tools.forma.jvm.sample.common.testutil") diff --git a/jvm-application/common/test-util/src/main/kotlin/tools/forma/jvm/sample/common/testutil/TestHelpers.kt b/jvm-application/common/test-util/src/main/kotlin/tools/forma/jvm/sample/common/testutil/TestHelpers.kt new file mode 100644 index 00000000..371aa333 --- /dev/null +++ b/jvm-application/common/test-util/src/main/kotlin/tools/forma/jvm/sample/common/testutil/TestHelpers.kt @@ -0,0 +1,8 @@ +package tools.forma.jvm.sample.common.testutil + +// Shared test helpers (exercises jvm.test-util target + matrix). +// No src/test sources in this minimal sample; the module is declared via testDependencies +// on impls if needed in future. Presence proves registration and build. +object TestHelpers { + fun echo(s: String): String = s +} diff --git a/jvm-application/common/util/build.gradle.kts b/jvm-application/common/util/build.gradle.kts new file mode 100644 index 00000000..f700d354 --- /dev/null +++ b/jvm-application/common/util/build.gradle.kts @@ -0,0 +1,3 @@ +import tools.forma.jvm.util + +util(packageName = "tools.forma.jvm.sample.common.util") diff --git a/jvm-application/common/util/src/main/kotlin/tools/forma/jvm/sample/common/util/Strings.kt b/jvm-application/common/util/src/main/kotlin/tools/forma/jvm/sample/common/util/Strings.kt new file mode 100644 index 00000000..c4be1980 --- /dev/null +++ b/jvm-application/common/util/src/main/kotlin/tools/forma/jvm/sample/common/util/Strings.kt @@ -0,0 +1,3 @@ +package tools.forma.jvm.sample.common.util + +fun greet(name: String): String = "Hello, ${name.lowercase().replaceFirstChar { it.uppercase() }}!" diff --git a/jvm-application/feature/calculator/api/build.gradle.kts b/jvm-application/feature/calculator/api/build.gradle.kts new file mode 100644 index 00000000..398577f9 --- /dev/null +++ b/jvm-application/feature/calculator/api/build.gradle.kts @@ -0,0 +1,3 @@ +import tools.forma.jvm.api + +api(packageName = "tools.forma.jvm.sample.feature.calculator.api") diff --git a/jvm-application/feature/calculator/api/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/api/Calculator.kt b/jvm-application/feature/calculator/api/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/api/Calculator.kt new file mode 100644 index 00000000..cd1282b4 --- /dev/null +++ b/jvm-application/feature/calculator/api/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/api/Calculator.kt @@ -0,0 +1,5 @@ +package tools.forma.jvm.sample.feature.calculator.api + +interface Calculator { + fun add(a: Int, b: Int): Int +} diff --git a/jvm-application/feature/calculator/impl/build.gradle.kts b/jvm-application/feature/calculator/impl/build.gradle.kts new file mode 100644 index 00000000..410a3437 --- /dev/null +++ b/jvm-application/feature/calculator/impl/build.gradle.kts @@ -0,0 +1,10 @@ +import tools.forma.jvm.impl + +impl( + packageName = "tools.forma.jvm.sample.feature.calculator.impl", + dependencies = deps( + target(":feature:calculator:api"), + target(":common:library"), + target(":common:util") + ) +) diff --git a/jvm-application/feature/calculator/impl/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/impl/CalculatorImpl.kt b/jvm-application/feature/calculator/impl/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/impl/CalculatorImpl.kt new file mode 100644 index 00000000..aef5e66c --- /dev/null +++ b/jvm-application/feature/calculator/impl/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/impl/CalculatorImpl.kt @@ -0,0 +1,8 @@ +package tools.forma.jvm.sample.feature.calculator.impl + +import tools.forma.jvm.sample.common.library.Math +import tools.forma.jvm.sample.feature.calculator.api.Calculator + +class CalculatorImpl : Calculator { + override fun add(a: Int, b: Int): Int = Math.add(a, b) +} diff --git a/jvm-application/feature/greeter/api/build.gradle.kts b/jvm-application/feature/greeter/api/build.gradle.kts new file mode 100644 index 00000000..a5c37dfb --- /dev/null +++ b/jvm-application/feature/greeter/api/build.gradle.kts @@ -0,0 +1,3 @@ +import tools.forma.jvm.api + +api(packageName = "tools.forma.jvm.sample.feature.greeter.api") diff --git a/jvm-application/feature/greeter/api/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/api/Greeter.kt b/jvm-application/feature/greeter/api/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/api/Greeter.kt new file mode 100644 index 00000000..be30467a --- /dev/null +++ b/jvm-application/feature/greeter/api/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/api/Greeter.kt @@ -0,0 +1,5 @@ +package tools.forma.jvm.sample.feature.greeter.api + +interface Greeter { + fun greet(name: String): String +} diff --git a/jvm-application/feature/greeter/impl/build.gradle.kts b/jvm-application/feature/greeter/impl/build.gradle.kts new file mode 100644 index 00000000..30f09556 --- /dev/null +++ b/jvm-application/feature/greeter/impl/build.gradle.kts @@ -0,0 +1,13 @@ +import tools.forma.jvm.impl + +impl( + packageName = "tools.forma.jvm.sample.feature.greeter.impl", + dependencies = deps( + target(":feature:greeter:api"), + target(":common:util"), + target(":common:library") + ), + testDependencies = deps( + target(":common:test-util") + ) +) diff --git a/jvm-application/feature/greeter/impl/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/impl/GreeterImpl.kt b/jvm-application/feature/greeter/impl/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/impl/GreeterImpl.kt new file mode 100644 index 00000000..1e400ae3 --- /dev/null +++ b/jvm-application/feature/greeter/impl/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/impl/GreeterImpl.kt @@ -0,0 +1,8 @@ +package tools.forma.jvm.sample.feature.greeter.impl + +import tools.forma.jvm.sample.common.util.greet as formatGreeting +import tools.forma.jvm.sample.feature.greeter.api.Greeter + +class GreeterImpl : Greeter { + override fun greet(name: String): String = formatGreeting(name) +} diff --git a/jvm-application/gradle.properties b/jvm-application/gradle.properties new file mode 100644 index 00000000..dfe21152 --- /dev/null +++ b/jvm-application/gradle.properties @@ -0,0 +1,6 @@ +# Pure-JVM Forma sample (F-031) — no Android keys. +org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true +kotlin.code.style=official diff --git a/jvm-application/gradle/wrapper/gradle-wrapper.jar b/jvm-application/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7f93135c Binary files /dev/null and b/jvm-application/gradle/wrapper/gradle-wrapper.jar differ diff --git a/jvm-application/gradle/wrapper/gradle-wrapper.properties b/jvm-application/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3fa8f862 --- /dev/null +++ b/jvm-application/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/jvm-application/gradlew b/jvm-application/gradlew new file mode 100755 index 00000000..0adc8e1a --- /dev/null +++ b/jvm-application/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/jvm-application/gradlew.bat b/jvm-application/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/jvm-application/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jvm-application/settings.gradle.kts b/jvm-application/settings.gradle.kts new file mode 100644 index 00000000..10dc4e6a --- /dev/null +++ b/jvm-application/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } + apply(from = "../build-settings/conventions/src/main/kotlin/convention-plugins.settings.gradle.kts") + includeBuild("../build-settings") + includeBuild("../plugins") + includeBuild("../includer") +} + +plugins { + id("convention-dependencies") + id("tools.forma.includer") + id("tools.forma.jvm") +} + +includer { arbitraryBuildScriptNames = true } + +rootProject.name = "jvm-application" diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/binary.kt b/plugins/jvm/src/main/java/tools/forma/jvm/binary.kt new file mode 100644 index 00000000..2354c917 --- /dev/null +++ b/plugins/jvm/src/main/java/tools/forma/jvm/binary.kt @@ -0,0 +1,50 @@ +package tools.forma.jvm + +import org.gradle.api.Project +import org.gradle.api.plugins.JavaApplication +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +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 + +/** + * Pure JVM **binary** (composition root + runnable application). + * + * - Wires multiple `impl` modules (and api/library/util) at a single root. + * - Applies `org.jetbrains.kotlin.jvm` + `application` (enables `./gradlew :binary:run`). + * - `mainClass` must be the Kotlin `...MainKt` for a top-level `fun main()` in `Main.kt`. + * + * Matrix: binary may depend on api, impl, library, util, test-util. + */ +fun Project.binary( + packageName: String, + mainClass: String, + owner: Owner = NoOwner, + dependencies: FormaDependency = EmptyDependency +) { + registerJvmDefaults() + + JvmTargetRegistry.selfValidator(JvmTargetTypes.binary).asValidator() + .validate(FormaTarget(this)) + + applyKotlinJvm() + apply(plugin = "application") + + configure { + this.mainClass.set(mainClass) + } + + applyDependencies( + validator = JvmTargetRegistry.validatorFor(JvmTargetTypes.binary).asValidator(), + dependencies = dependencies + ) +} diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt b/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt index 4410962b..34430bd3 100644 --- a/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt +++ b/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt @@ -3,7 +3,6 @@ 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 @@ -18,12 +17,12 @@ import tools.forma.validation.asValidator * 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). + * Composition of impls happens at a binary root (`binary(...)`, F-031). */ fun Project.impl( packageName: String, dependencies: FormaDependency = EmptyDependency, - testDependencies: NamedDependency = NamedDependency() + testDependencies: FormaDependency = EmptyDependency ) { registerJvmDefaults() diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/library.kt b/plugins/jvm/src/main/java/tools/forma/jvm/library.kt index 0bb9ac76..8f8ad6be 100644 --- a/plugins/jvm/src/main/java/tools/forma/jvm/library.kt +++ b/plugins/jvm/src/main/java/tools/forma/jvm/library.kt @@ -3,7 +3,6 @@ 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 @@ -23,7 +22,7 @@ fun Project.library( packageName: String, dependencies: FormaDependency = EmptyDependency, owner: Owner = NoOwner, - testDependencies: NamedDependency = NamedDependency() + testDependencies: FormaDependency = EmptyDependency ) { registerJvmDefaults() 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 index a46fec83..d91ae05d 100644 --- a/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt +++ b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt @@ -20,6 +20,7 @@ object JvmTargetRegistry : TargetRegistry by DefaultTargetRegistry() * - library → util, test-util * - util → util, library * - testUtil → test-util, util (library allowed for test helpers that need prod contracts) + * - binary → api, impl, library, util, test-util (composition root for wiring multiple impls) * * No ContentRules attached for pure JVM (no Android resources concept). * Safe to call multiple times (re-register replaces). @@ -66,4 +67,12 @@ fun registerJvmDefaults(registry: TargetRegistry = JvmTargetRegistry) { allowedDependencies = setOf(t.testUtil, t.util, t.library) ) ) + + // binary (composition root): wires api + multiple impls + shared library/util for a runnable JVM app + registry.register( + TargetRegistration( + type = t.binary, + allowedDependencies = setOf(t.api, t.impl, t.library, t.util, t.testUtil) + ) + ) } 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 index 27133fca..1fef230c 100644 --- a/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt +++ b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt @@ -26,4 +26,7 @@ object JvmTargetTypes { /** Test helpers (JVM). */ val testUtil: TargetType = targetType("jvm.test-util", "test-util") + + /** JVM binary / composition root (runnable app entrypoint). */ + val binary: TargetType = targetType("jvm.binary", "binary") } 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 index 2533a0ca..95d44bf4 100644 --- a/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt +++ b/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt @@ -24,14 +24,15 @@ class JvmTargetRegistryTest { } @Test - fun `all five jvm types are registered by id`() { + fun `all six 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) + assertEquals(t.binary, JvmTargetRegistry.get("jvm.binary")) + assertEquals(6, JvmTargetRegistry.all().size) } @Test @@ -71,6 +72,14 @@ class JvmTargetRegistryTest { assertTrue(g.isAllowed(t.testUtil, t.library)) assertFalse(g.isAllowed(t.testUtil, t.api)) assertFalse(g.isAllowed(t.testUtil, t.impl)) + + // binary (composition root): may consume impls + api + shared; no binary→binary + assertTrue(g.isAllowed(t.binary, t.api)) + assertTrue(g.isAllowed(t.binary, t.impl)) + assertTrue(g.isAllowed(t.binary, t.library)) + assertTrue(g.isAllowed(t.binary, t.util)) + assertTrue(g.isAllowed(t.binary, t.testUtil)) + assertFalse(g.isAllowed(t.binary, t.binary)) } @Test @@ -103,6 +112,8 @@ class JvmTargetRegistryTest { 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")) + JvmTargetRegistry.selfValidator(t.binary).validate(TestRef("binary")) + JvmTargetRegistry.selfValidator(t.binary).validate(TestRef("app-binary")) } @Test