diff --git a/README.md b/README.md index c7d5b53..6444802 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,15 @@ systems like Buck and Bazel. **New to Forma?** Start here: 1. [**Android project tutorial**](docs/GETTING-STARTED.md) — mental model, run the sample, greenfield skeleton, target cheat sheet (F-015 / GH #53) -2. [Sample app gold standard](docs/SAMPLE-APP.md) — multi-feature layout to copy -3. [Dependency matrix](docs/DEPENDENCY-MATRIX.md) — what may depend on what -4. [External deps catalogs](docs/DEPS-CATALOG.md) · [Compose](docs/COMPOSE.md) · [Environment](docs/ENV.md) -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) -9. [JVM sample application](docs/JVM-SAMPLE.md) — multi-module pure-JVM gold standard (`jvm-application/`, F-031) +2. [**JVM getting started**](docs/JVM-GETTING-STARTED.md) — pure JVM tutorial, run the sample, greenfield skeleton with `binary`, `api`/`impl`, target cheat sheet (F-032) +3. [Sample app gold standard](docs/SAMPLE-APP.md) — multi-feature layout to copy +4. [Dependency matrix](docs/DEPENDENCY-MATRIX.md) — what may depend on what +5. [External deps catalogs](docs/DEPS-CATALOG.md) · [Compose](docs/COMPOSE.md) · [Environment](docs/ENV.md) +6. [Plugin publish path](docs/PLUGIN-PUBLISH.md) — Portal metadata DSL + release notes (F-016) +7. [Configuration performance](docs/CONFIGURATION-PERFORMANCE.md) — measure + hot-path guidance (F-017 / GH #106) +8. [forma-core public API design](docs/forma-core-api.md) — extraction contract for types / restrictions / registry (F-020) +9. [JVM targets](docs/JVM-TARGETS.md) — pure JVM platform plugin `tools.forma.jvm` (F-030) +10. [JVM sample application](docs/JVM-SAMPLE.md) — multi-module pure-JVM gold standard (`jvm-application/`, F-031) Configuration made easy: @@ -204,6 +205,7 @@ 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/`). +JVM getting-started tutorial: **F-032** done ([`docs/JVM-GETTING-STARTED.md`](docs/JVM-GETTING-STARTED.md)). Icons made by Freepik from www.flaticon.com diff --git a/TICKETS.md b/TICKETS.md index b80aa68..82c7394 100644 --- a/TICKETS.md +++ b/TICKETS.md @@ -42,7 +42,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi |----|--------|-------|-------| | 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 | done | JVM sample application | `jvm-application/` + `binary` DSL; docs/JVM-SAMPLE.md | -| F-032 | todo | Docs: JVM getting started | | +| F-032 | done | Docs: JVM getting started | `docs/JVM-GETTING-STARTED.md` + cross-links; see PROGRESS | ## P4 — Bazel diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index dbb685e..4d242df 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -9,6 +9,8 @@ visibility validation** for you. This tutorial gets you from zero to a working multi-target Android app. For the full multi-feature reference layout, see [SAMPLE-APP.md](SAMPLE-APP.md). +**Looking for pure JVM?** See the parallel [JVM getting-started tutorial](JVM-GETTING-STARTED.md) (no Android SDK required). + --- ## 1. Mental model (5 minutes) @@ -426,6 +428,7 @@ Details: [COMPOSE.md](COMPOSE.md). Sample: | [ENV.md](ENV.md) | JDK / SDK bootstrap | | [ARCHITECTURE.md](ARCHITECTURE.md) | Monorepo + plugin module graph | | [VISION.md](VISION.md) | forma-core → JVM → Bazel roadmap | +| [JVM-GETTING-STARTED.md](JVM-GETTING-STARTED.md) | Pure JVM parallel tutorial (F-032) | --- diff --git a/docs/JVM-GETTING-STARTED.md b/docs/JVM-GETTING-STARTED.md new file mode 100644 index 0000000..b40b9fb --- /dev/null +++ b/docs/JVM-GETTING-STARTED.md @@ -0,0 +1,410 @@ +# Getting started — JVM project with Forma (F-032) + +Forma is a **meta build system** for pure JVM (Gradle plugin). You declare +**targets** with typed, single-method builders (`api`, `impl`, `binary`, …) +instead of hand-wiring Kotlin JVM plugins, application configuration, and +ad-hoc dependency rules. Forma applies plugins and **dependency visibility +validation** for you. + +This tutorial gets you from zero to a working multi-target pure-JVM app. For +the full multi-feature reference layout, see [JVM-SAMPLE.md](JVM-SAMPLE.md). + +--- + +## 1. Mental model (5 minutes) + +| Concept | Meaning | +|---------|---------| +| **Target** | One Gradle project / module with a Forma type (`impl`, `api`, …). Prefer “target” over “module” in Forma docs. | +| **Suffix** | Folder / project name ending that encodes type (`…-impl`, `…-api`, `binary`, …). Validators use suffixes. | +| **Composition root** | `binary` — where feature graphs are wired (entry point for `main`). | +| **Feature slice** | Typical feature folder: `api` + `impl`. | +| **Catalogs** | External deps declared once (`projectDependencies` / typed catalogs), consumed via `deps(...)`. | + +**What Forma does for each target** + +1. Validates the project name matches the expected suffix. +2. Applies the right Kotlin JVM plugins (and Gradle `application` for `binary`). +3. Enforces **project** dependencies against the [JVM dependency matrix](JVM-TARGETS.md). + +You still write Kotlin and choose library coordinates — Forma owns +structure and boundaries, not business logic. + +--- + +## 2. Prerequisites + +| Tool | Version / notes | +|------|-----------------| +| JDK | **17+** (Temurin / OpenJDK recommended) | +| Android SDK | **Not required** (pure JVM; the Android sample still needs it) | +| Build tools | Gradle wrapper (sample uses 8.x) | + +Worker / macOS install steps: [ENV.md](ENV.md) (`source scripts/env-mac.sh` — it exports `JAVA_HOME`; Android-related warnings are harmless for JVM-only work). + +```bash +# After JDK is installed: +source scripts/env-mac.sh # if using this repo’s helper +``` + +No `local.properties` or SDK setup is needed for pure-JVM projects. + +--- + +## 3. Path A — run the sample first (recommended) + +The `jvm-application/` directory is the **gold-standard pure-JVM product**. +Build and run it before starting a greenfield project so you see real targets +and wiring. + +```bash +git clone https://github.com/formatools/forma.git +cd forma +source scripts/env-mac.sh # or export JAVA_HOME yourself + +cd jvm-application +./gradlew :binary:run +# or full verification: +./gradlew build +``` + +Expected output: + +``` +Hello, World! (2 + 3 = 5) +JVM sample (tools.forma.jvm) build + run successful. +``` + +Open `jvm-application/` in your IDE (import the `jvm-application` Gradle project). +Explore: + +| Path | Target | Role | +|------|--------|------| +| `binary/` | `binary` | Composition root + runnable entry (`:binary:run`) | +| `feature/greeter/{api,impl}` | feature slice | Public contracts + impl | +| `feature/calculator/{api,impl}` | feature slice | Second feature (cross-feature via api only) | +| `common/library/` | `library` | Shared pure JVM code | +| `common/util/` | `util` | Small helpers | +| `common/test-util/` | `testUtil` | Shared test helpers (used via `testDependencies`) | +| `settings.gradle.kts` | — | Plugin management + composite includes | + +Deep dive: [JVM-SAMPLE.md](JVM-SAMPLE.md). + +--- + +## 4. Path B — greenfield project skeleton + +You can consume Forma from the **Gradle Plugin Portal** or via **composite +`includeBuild`** (as this monorepo does for development). + +Published plugin id: `tools.forma.jvm` (badge version on the [README](../README.md); +sample monorepo develops **0.1.3** line). Companion settings plugin: +`tools.forma.includer` (auto-`include` subprojects that contain `build.gradle.kts`). + +### 4.1 `settings.gradle.kts` + +Minimal shape (Portal publish path — adjust version to the badge): + +```kotlin +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +plugins { + id("tools.forma.includer") version "0.2.0" + id("tools.forma.jvm") version "0.1.3" +} + +// Discover every build.gradle.kts under this root (skip nested settings.gradle*). +includer { + // true when folder names are not forced to end in the suffix only + // (sample uses nested paths like feature/greeter/impl). + arbitraryBuildScriptNames = true +} + +rootProject.name = "my-jvm-app" + +// Optional: settings-level version catalog — see docs/DEPS-CATALOG.md +// projectDependencies("libs", "com.squareup.okhttp3:okhttp:4.12.0", …) +``` + +**Monorepo / composite** (how `jvm-application/` is wired today): + +```kotlin +pluginManagement { + repositories { gradlePluginPortal(); mavenCentral() } + includeBuild("../plugins") + includeBuild("../includer") + includeBuild("../build-settings") + apply(from = "../build-settings/conventions/src/main/kotlin/convention-plugins.settings.gradle.kts") +} +plugins { + id("convention-dependencies") + id("tools.forma.includer") + id("tools.forma.jvm") +} +includer { arbitraryBuildScriptNames = true } +``` + +Do **not** list every module with `include(...)` when using Includer — drop a +`build.gradle.kts` in a directory and it becomes a project. Nested projects that +have their own `settings.gradle.kts` are skipped. + +Project path mapping (Includer): `feature/greeter/impl` → Gradle name +`:feature-greeter-impl` (type-safe accessors). Forma `target(":feature:greeter:impl")` +uses colon path form. + +### 4.2 Root `build.gradle.kts` + +**JVM does not require (and does not have) a `jvmProjectConfiguration` or +`androidProjectConfiguration` call.** The matrix and plugin application are +registered automatically the first time any JVM DSL entrypoint is used. + +A minimal root file can be empty or contain only comments (as in the sample): + +```kotlin +// Root build for a pure-JVM Forma project. +// No androidProjectConfiguration or jvmProjectConfiguration hook is required. +// The tools.forma.jvm settings plugin + DSL entrypoints (api/impl/library/.../binary) +// configure each module when their build.gradle.kts are evaluated. +``` + +Useful `gradle.properties` (sample-aligned): + +```properties +org.gradle.parallel=true +org.gradle.caching=true +``` + +### 4.3 First targets — “hello JVM app” + +Create folders + one-line builders. Suggested minimal tree: + +``` +my-jvm-app/ +├── settings.gradle.kts +├── build.gradle.kts +├── binary/build.gradle.kts # binary (composition root + main) +├── feature/hello/ +│ ├── api/build.gradle.kts +│ └── impl/build.gradle.kts +└── common/library/build.gradle.kts # optional shared library +``` + +**`binary/build.gradle.kts`** — composition root + runnable app: + +```kotlin +import tools.forma.jvm.binary + +binary( + packageName = "com.example.myapp.binary", + mainClass = "com.example.myapp.binary.MainKt", + dependencies = deps( + target(":feature:hello:api"), + target(":feature:hello:impl"), + // target(":common:library"), + ), +) +``` + +For a top-level `fun main()` inside `Main.kt`, the `mainClass` value ends in `MainKt`. + +**`feature/hello/api/build.gradle.kts`** — public contracts only: + +```kotlin +import tools.forma.jvm.api + +api(packageName = "com.example.myapp.feature.hello.api") +``` + +**`feature/hello/impl/build.gradle.kts`**: + +```kotlin +import tools.forma.jvm.impl + +impl( + packageName = "com.example.myapp.feature.hello.impl", + dependencies = deps( + target(":feature:hello:api"), + // target(":common:library"), + ), +) +``` + +**`common/library/build.gradle.kts`** (shared pure JVM): + +```kotlin +import tools.forma.jvm.library + +library(packageName = "com.example.myapp.common.library") +``` + +Put Kotlin under `src/main/kotlin//…` matching `packageName`. + +### 4.4 Build / run + +```bash +cd my-jvm-app +./gradlew :binary:run +``` + +If validation fails, the error names the **consumer suffix**, the **illegal +dependency**, and the allowed set — fix the edge, don’t weaken the graph. + +--- + +## 5. Target cheat sheet (when to use what) + +| DSL | Suffix | Use for | +|-----|--------|---------| +| `binary` | `binary` | Runnable application; top composition root (`mainClass` required) | +| `api` | `api` | Feature public contracts (JVM); no impls | +| `impl` | `impl` | Feature implementation (depends on its own `api` + shared) | +| `library` | `library` | Shared pure JVM library (leaf-ish) | +| `util` | `util` | Small JVM helpers / extensions | +| `testUtil` | `test-util` | Shared test helpers (wired via `testDependencies`) | + +Full matrix and rules: [JVM-TARGETS.md](JVM-TARGETS.md). + +--- + +## 6. Declaring dependencies + +### External + +```kotlin +dependencies = deps( + libs.okhttp, // from projectDependencies catalog + libs.bundles.testing, + // or bare coordinates when not using catalogs +) +``` + +Combine groups with `+`: + +```kotlin +dependencies = deps(libs.okhttp) + + deps( + target(":feature:hello:api"), + ) +``` + +### Project / internal + +```kotlin +target(":feature:hello:api") // Forma path (colons) +// Gradle project path is often :feature-hello-api (Includer dashes) +``` + +**Composition roots (`binary`) must list feature `api` + `impl` explicitly** — do not rely +on transitive feature wiring alone. That keeps graphs honest and matches +validator intent. + +--- + +## 7. Rules that bite on day one + +1. **`impl` → `impl` is forbidden.** Cross-feature collaboration goes through + `api` (or shared `library` / `util` modules). +2. **`api` only sees contracts** — `api` + `library`. +3. **`library` stays leaf-ish** — only `util` / `test-util`. +4. **`util` cannot depend on `api` / `impl`**. +5. **`binary` is the composition root** — may pull `api` + multiple `impl`s + shared. +6. **Name your folder / project so the suffix matches the DSL** (`…/impl` → + `impl { }`). +7. **Align `packageName` with source roots** under `src/main/kotlin`. + +If you fight the matrix, usually the **type** is wrong — not the tool. + +--- + +## 8. Adding a second feature + +Copy the `feature/hello` slice to `feature/settings`. Then: + +1. Implement `api` contracts the other features may need. +2. Implement code in `impl` depending only on `api` + shared libs. +3. Register **both** `api` and `impl` on `binary`. + +```kotlin +// binary/build.gradle.kts (excerpt) +dependencies = deps( + target(":feature:hello:api"), + target(":feature:hello:impl"), + target(":feature:settings:api"), + target(":feature:settings:impl"), +) +``` + +--- + +## 9. JVM-only considerations + +- No Android SDK, no AGP, no `androidProjectConfiguration`. +- No `res/`, `viewbinding`, Compose widgets, or Android-specific targets. +- The registry (`registerJvmDefaults`) activates on first use of any `tools.forma.jvm.*` DSL entrypoint. +- Default Kotlin JVM target is Java 11 (see `JvmDefaults`). +- Sources go under `src/main/kotlin` (matching `packageName`). +- For mixed Android + JVM in one repo, the Android tutorial still applies to the Android side; the JVM plugin is independent. + +See the Android parallel: [GETTING-STARTED.md](GETTING-STARTED.md). + +--- + +## 10. Day-to-day workflow + +```bash +# From the app root (jvm-application/ in this repo) +./gradlew :binary:run +./gradlew build +./gradlew :feature-greeter-impl:compileKotlin +``` + +- Prefer small targets; keep feature folders consistent. +- Put shared non-feature code under `common/` or `core/`. +- When a validator fails, fix the dependency edge or re-slice the target — + don’t disable validation. + +--- + +## 11. Common pitfalls + +| Symptom | Likely cause | +|---------|----------------| +| “Java not found” / old JDK | Use JDK 17+; export `JAVA_HOME` ([ENV.md](ENV.md)) | +| `mainClass` not found / wrong name | For top-level `fun main()` in `Main.kt` use `...MainKt` | +| Illegal project dependency | Matrix violation — see [JVM-TARGETS.md](JVM-TARGETS.md) | +| Project not included | Missing `build.gradle.kts`, or nested `settings.gradle.kts` blocked Includer | +| Empty / wrong package | `packageName` ≠ directory under `src/main/kotlin` | +| Missing import | JVM DSL lives in `tools.forma.jvm` — add `import tools.forma.jvm.*` | + +--- + +## 12. Next reading + +| Doc | Topic | +|-----|--------| +| [JVM-SAMPLE.md](JVM-SAMPLE.md) | Multi-feature pure-JVM gold standard layout + run | +| [JVM-TARGETS.md](JVM-TARGETS.md) | All JVM target types + full dependency matrix | +| [GETTING-STARTED.md](GETTING-STARTED.md) | Android parallel tutorial | +| [DEPENDENCY-MATRIX.md](DEPENDENCY-MATRIX.md) | Full matrix (includes JVM rows) | +| [DEPS-CATALOG.md](DEPS-CATALOG.md) | External catalogs (`library`, `bundle`, `plugin`) | +| [ENV.md](ENV.md) | JDK bootstrap | +| [ARCHITECTURE.md](ARCHITECTURE.md) | Monorepo + plugin module graph | +| [VISION.md](VISION.md) | forma-core → JVM → Bazel roadmap | +| [forma-core-api.md](forma-core-api.md) | Core registry & validation SPI | + +--- + +## 13. Checklist — “I’m using Forma correctly” + +- [ ] Includer (or explicit includes) discovers every target +- [ ] Features use `api` / `impl` (binary wires both explicitly) +- [ ] No `impl` → `impl` edges +- [ ] `binary` supplies `mainClass` and acts as composition root +- [ ] Explicit `import tools.forma.jvm.*` (or per-entry imports) in target scripts +- [ ] `./gradlew :binary:run` succeeds on a clean machine with JDK 17+ +- [ ] External versions live in a catalog (when scaling) diff --git a/docs/JVM-SAMPLE.md b/docs/JVM-SAMPLE.md index b097bfc..a57cbc4 100644 --- a/docs/JVM-SAMPLE.md +++ b/docs/JVM-SAMPLE.md @@ -108,4 +108,4 @@ Full pure-JVM dependency matrix (including `binary`): - [`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) +- [JVM getting-started tutorial](JVM-GETTING-STARTED.md) (F-032) diff --git a/docs/JVM-TARGETS.md b/docs/JVM-TARGETS.md index 4b6bd4b..d5d1bcb 100644 --- a/docs/JVM-TARGETS.md +++ b/docs/JVM-TARGETS.md @@ -107,7 +107,7 @@ compatibility is Java **11** (`JvmDefaults`). ## Next -- **F-032** — JVM getting-started tutorial (point at this doc + sample). +- [JVM getting-started tutorial](JVM-GETTING-STARTED.md) (F-032) — start here for a new pure-JVM project. ## See also diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 1826af4..1768c65 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,6 +2,39 @@ Newest entries first. +## 2026-07-14 — F-032 JVM getting started tutorial + +- **Ticket:** F-032 → `done` +- **Branch:** `forma/F-032-jvm-getting-started` (from `origin/v2`) +- **Skills/modes:** /goal + todo_write + plan subagent + implement docs + self-verify via spawned verifier subagent +- **Docs (primary):** + - New **`docs/JVM-GETTING-STARTED.md`** (410 lines, content-rich but focused) — polished tutorial so a developer with zero Forma experience reaches a working multi-module pure-JVM app. Mirrors structure/quality of Android [GETTING-STARTED.md](GETTING-STARTED.md): + - Mental model (targets, suffixes, `binary` composition root, `api`/`impl` features, catalogs) + - Prereqs: JDK 17+ only (**no Android SDK**), Gradle wrapper + - Path A: run `jvm-application/` sample (`./gradlew :binary:run`) + - Path B: greenfield — Portal + monorepo composite `settings.gradle.kts` (includer + `tools.forma.jvm`), root build note (no `androidProjectConfiguration`; matrix on first DSL use), minimal tree with `binary(..., mainClass = "...MainKt")` + one feature (`api`/`impl`) + `common/library`, **exact** `import tools.forma.jvm.*` snippets, build/run commands + - Target cheat sheet (JVM types only) + - Dependency rules of thumb + link to JVM-TARGETS + - Troubleshooting (validation names consumer/dep, missing import, mainClass naming) + - See also table + - Cross-links wired: + - `README.md` Getting started list (new JVM entry near top of JVM docs) + Progress section + - `docs/JVM-SAMPLE.md`: “F-032 next” → link to tutorial + - `docs/JVM-TARGETS.md`: “Next F-032” → link + context + - `docs/GETTING-STARTED.md`: brief “Looking for pure JVM?” pointer (top + See also table) + - `TICKETS.md`: F-032 `done` +- **Verify (real output, OpenJDK 17 + env-mac.sh — docs-only change):** + - `jvm-application/`: `source scripts/env-mac.sh && ./gradlew :binary:run` → **success** (exit 0); captured: + ``` + Hello, World! (2 + 3 = 5) + JVM sample (tools.forma.jvm) build + run successful. + ``` + - `git status` clean before run; only doc files touched. +- **Grounded:** all commands executed and outputs captured in session; no invented green builds or claims. +- **Commits/PRs:** `e2126e5` on `forma/F-032-jvm-getting-started`; PR base `v2` (this run) +- **Blockers:** none +- **Next step:** F-040 (Bazel design) per TICKETS priority + ## 2026-07-13 — F-031 JVM sample application - **Ticket:** F-031 → `done`