diff --git a/README.md b/README.md
index 817a77f6..e85edbd6 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ systems like Buck and Bazel.
11. [Bazel adapter design](docs/BAZEL-ADAPTER.md) — target types to rules, restriction graph to visibility (F-040 design)
12. [Bazel adapter spike](bazel-adapter/README.md) — generate/check BUILD from Forma model (F-041; JVM-first)
13. [Bazel sample (experimental)](bazel-sample/) — minimal `kt_jvm_*` workspace exercising forma-core matrix + `impl` ↛ `impl` (F-042)
+14. [**Progressive examples + agent skills**](docs/PROGRESSIVE-EXAMPLES.md) — feature-by-feature ladders (`examples/`) + coding-agent skills (F-050)
Configuration made easy:
@@ -212,6 +213,7 @@ JVM getting-started tutorial: **F-032** done ([`docs/JVM-GETTING-STARTED.md`](do
Bazel adapter mapping design (targets ↔ rules, visibility ↔ deps): **F-040** done (see [`docs/BAZEL-ADAPTER.md`](docs/BAZEL-ADAPTER.md)).
Bazel adapter spike (generate/check BUILD from portable model, JVM matrix): **F-041** done ([`bazel-adapter/`](bazel-adapter/), spike results in BAZEL-ADAPTER.md).
Minimal Bazel sample workspace (two features + library/util + binary, real `bazelisk build` + `run`): **F-042** done (see [`bazel-sample/`](bazel-sample/) + BAZEL-ADAPTER.md).
+Progressive examples + agent skills (all supported features): **F-050** done ([`docs/PROGRESSIVE-EXAMPLES.md`](docs/PROGRESSIVE-EXAMPLES.md), [`examples/`](examples/)).
Icons made by Freepik
from www.flaticon.com
diff --git a/TICKETS.md b/TICKETS.md
index 64e011b5..5652f8fb 100644
--- a/TICKETS.md
+++ b/TICKETS.md
@@ -52,6 +52,12 @@ Update this file when picking or finishing work. Cron workers must pick the **hi
| F-041 | done | Spike: generate or check Bazel BUILD from forma declarations | `bazel-adapter/` generate+check via core RestrictionGraph; examples + tests green |
| F-042 | done | Minimal Bazel sample using forma-core concepts | `bazel-sample/` + real `bazelisk build`/`run` + docs; see PROGRESS |
+## P5 — Progressive examples + agent skills
+
+| ID | Status | Title | Notes |
+|----|--------|-------|-------|
+| F-050 | done | Progressive examples + agent skills for all supported features | `examples/{jvm,android}/*` + `examples/agent-skills/` + `docs/PROGRESSIVE-EXAMPLES.md`; JVM build+run and Android assembleDebug verified |
+
## Backlog (lower priority / historical GitHub)
Keep for reference; do not start unless higher tickets done or user prioritizes:
diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md
index 4d242dfb..d9ba50e0 100644
--- a/docs/GETTING-STARTED.md
+++ b/docs/GETTING-STARTED.md
@@ -429,6 +429,7 @@ Details: [COMPOSE.md](COMPOSE.md). Sample:
| [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) |
+| [PROGRESSIVE-EXAMPLES.md](PROGRESSIVE-EXAMPLES.md) | Feature-by-feature ladders + agent skills (F-050) |
---
diff --git a/docs/JVM-GETTING-STARTED.md b/docs/JVM-GETTING-STARTED.md
index b40b9fb8..f30497a3 100644
--- a/docs/JVM-GETTING-STARTED.md
+++ b/docs/JVM-GETTING-STARTED.md
@@ -390,6 +390,7 @@ See the Android parallel: [GETTING-STARTED.md](GETTING-STARTED.md).
| [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 |
+| [PROGRESSIVE-EXAMPLES.md](PROGRESSIVE-EXAMPLES.md) | Feature-by-feature ladders + agent skills (F-050) |
| [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 |
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
index 39991ea9..6af39e85 100644
--- a/docs/PROGRESS.md
+++ b/docs/PROGRESS.md
@@ -2,6 +2,25 @@
Newest entries first.
+## 2026-07-15 — F-050 Progressive examples + agent skills
+
+- **Ticket:** F-050 → `done`
+- **Branch:** `forma/F-050-progressive-examples-skills` (from `origin/v2`)
+- **Skills/modes:** Grok Build `--mode full` (design + partial implement, max-turns); Hermes finish path completed Android ladder, skills, docs, verify, PR
+- **Deliverable:**
+ - `examples/jvm/01-hello-binary` … `05-test-util` — full JVM target set progressive ladder
+ - `examples/android/01-hello-apk` … `09-test-utils` — full Android target set + catalogs + compose + test utils
+ - `examples/agent-skills/` — overview, android/jvm targets, matrix, catalogs, compose, includer, layout, bazel
+ - `docs/PROGRESSIVE-EXAMPLES.md` + `examples/README.md` curriculum + feature coverage matrix
+ - Cross-links: README, GETTING-STARTED, JVM-GETTING-STARTED
+- **Verify (real host, OpenJDK 17 + `scripts/env-mac.sh`):**
+ - All JVM steps: `./gradlew build` + `:binary:run` → **OK**
+ - All Android steps: `:binary:assembleDebug` → **OK**
+ - Android 09: `:feature-hello-impl:testDebugUnitTest` → **OK**
+- **Notes:** `deps()` is non-transitive — Compose/JUnit examples use `transitiveDeps(...)`. Teaching apps use framework `Activity` (not AppCompat) to keep transitive surface small.
+- **Blockers:** none
+- **Next step:** backlog / new tickets as prioritized
+
## 2026-07-15 — Ticket queue empty; close done GitHub issues
- **Ticket:** none open in P0–P4 (`TICKETS.md` all `done` through F-042)
diff --git a/docs/PROGRESSIVE-EXAMPLES.md b/docs/PROGRESSIVE-EXAMPLES.md
new file mode 100644
index 00000000..a6c02fc2
--- /dev/null
+++ b/docs/PROGRESSIVE-EXAMPLES.md
@@ -0,0 +1,43 @@
+# Progressive examples (F-050)
+
+Forma’s **feature-by-feature teaching ladder**. Start here after (or instead of) the
+narrative tutorials when you want a **minimal buildable project per concept**.
+
+| Audience | Start |
+|----------|-------|
+| Humans learning Android Forma | [examples/android/01-hello-apk](../examples/android/01-hello-apk) then climb |
+| Humans learning JVM Forma | [examples/jvm/01-hello-binary](../examples/jvm/01-hello-binary) |
+| Coding agents | [examples/agent-skills/](../examples/agent-skills/) |
+| Full product reference | [SAMPLE-APP.md](SAMPLE-APP.md) / [JVM-SAMPLE.md](JVM-SAMPLE.md) |
+| Narrative tutorials | [GETTING-STARTED.md](GETTING-STARTED.md) / [JVM-GETTING-STARTED.md](JVM-GETTING-STARTED.md) |
+
+## Feature coverage matrix
+
+| Feature | Android step | JVM step | Agent skill |
+|---------|--------------|----------|-------------|
+| Includer + composite plugins | 01 | 01 | forma-includer-settings |
+| `androidProjectConfiguration` | 01 | — | forma-android-targets |
+| `androidBinary` / `androidApp` | 01 | — | forma-android-targets |
+| `androidRes` | 01, 03 | — | forma-android-targets |
+| `binary` (JVM) | — | 01 | forma-jvm-targets |
+| `api` / `impl` | 02 | 02 | platform skills + matrix |
+| `viewBinding` | 03 | — | forma-android-targets |
+| `library` / `util` | 04 | 03 | platform skills |
+| `androidUtil` / `androidLibrary` | 04 | — | forma-android-targets |
+| `widget` / `uiLibrary` | 05 | — | forma-android-targets |
+| Compose + `composeWidget` | 06 | — | forma-compose |
+| Multi-feature composition | 07 | 04 | forma-project-layout |
+| `projectDependencies` catalogs | 08 | — | forma-deps-catalog |
+| `testUtil` | 09 | 05 | platform skills |
+| `androidTestUtil` | 09 | — | forma-android-targets |
+| `androidNative` | skill/docs | — | forma-android-targets |
+| Bazel adapter / sample | skill | skill | forma-bazel |
+
+## Verify
+
+See [examples/README.md](../examples/README.md).
+
+## Relationship to gold standards
+
+Progressive examples are **minimal**. Patterns for real apps (navigation, Dagger,
+network) live in `application/` and are documented in SAMPLE-APP / GETTING-STARTED.
diff --git a/examples/.gitignore b/examples/.gitignore
new file mode 100644
index 00000000..0069a760
--- /dev/null
+++ b/examples/.gitignore
@@ -0,0 +1,5 @@
+**/local.properties
+**/.gradle/
+**/build/
+**/.cxx/
+**/captures/
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..4bdec768
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,58 @@
+# Forma progressive examples (F-050)
+
+Hands-on ladders that introduce **every supported Forma feature** one step at a time.
+Gold-standard product apps remain [`application/`](../application/) (Android) and
+[`jvm-application/`](../jvm-application/) (JVM). These examples are **teaching-focused**.
+
+## Curriculum
+
+| Track | Path | Steps |
+|-------|------|-------|
+| JVM | [`jvm/`](jvm/) | 01 hello-binary → 05 test-util |
+| Android | [`android/`](android/) | 01 hello-apk → 09 test-utils |
+| Agent skills | [`agent-skills/`](agent-skills/) | SKILL docs for coding agents |
+| User index | [`docs/PROGRESSIVE-EXAMPLES.md`](../docs/PROGRESSIVE-EXAMPLES.md) | narrative + feature coverage matrix |
+
+### JVM (`tools.forma.jvm`)
+
+| Step | Directory | Features |
+|------|-----------|----------|
+| 01 | `jvm/01-hello-binary` | `binary`, includer, composite plugins |
+| 02 | `jvm/02-api-impl` | `api`, `impl`, composition-root listing |
+| 03 | `jvm/03-library-util` | `library`, `util` |
+| 04 | `jvm/04-multi-feature` | multi-feature; **impl ↛ impl** |
+| 05 | `jvm/05-test-util` | `testUtil` + unit tests |
+
+### Android (`tools.forma.android`)
+
+| Step | Directory | Features |
+|------|-----------|----------|
+| 01 | `android/01-hello-apk` | `androidProjectConfiguration`, `androidBinary`, `androidApp`, `androidRes` |
+| 02 | `android/02-feature-api-impl` | `api`, `impl` |
+| 03 | `android/03-res-viewbinding` | feature `androidRes`, `viewBinding` |
+| 04 | `android/04-shared-libs` | `library`, `util`, `androidUtil`, `androidLibrary` |
+| 05 | `android/05-widget-ui` | `widget`, `uiLibrary` |
+| 06 | `android/06-compose` | `compose` flags, `composeWidget` |
+| 07 | `android/07-multi-feature` | two features at composition root |
+| 08 | `android/08-deps-catalog` | `projectDependencies` / `library` / `bundle` / `plugin` |
+| 09 | `android/09-test-utils` | `testUtil`, `androidTestUtil` (+ `androidNative` docs) |
+
+Bazel is covered by agent skill + existing [`bazel-adapter/`](../bazel-adapter/) and [`bazel-sample/`](../bazel-sample/) (not duplicated here).
+
+## Verify
+
+```bash
+source scripts/env-mac.sh
+
+for d in examples/jvm/*/; do
+ (cd "$d" && ./gradlew build --quiet && ./gradlew :binary:run --quiet)
+done
+
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > /tmp/forma-ex-local.properties
+for d in examples/android/*/; do
+ cp /tmp/forma-ex-local.properties "$d/local.properties"
+ (cd "$d" && ./gradlew :binary:assembleDebug --quiet)
+done
+```
+
+Each step has its own `README.md` with the delta vs the previous step.
diff --git a/examples/agent-skills/README.md b/examples/agent-skills/README.md
new file mode 100644
index 00000000..5cc5e935
--- /dev/null
+++ b/examples/agent-skills/README.md
@@ -0,0 +1,24 @@
+# Forma agent skills
+
+Load these when scaffolding or reviewing Forma targets. They encode DSL, suffixes,
+matrix rules, and progressive example pointers.
+
+| Skill | When to load |
+|-------|----------------|
+| [forma-overview](forma-overview.md) | Any Forma work; entry map |
+| [forma-android-targets](forma-android-targets.md) | Creating/editing Android targets |
+| [forma-jvm-targets](forma-jvm-targets.md) | Pure JVM targets |
+| [forma-dependency-matrix](forma-dependency-matrix.md) | Dependency / visibility errors |
+| [forma-deps-catalog](forma-deps-catalog.md) | External deps / catalogs |
+| [forma-compose](forma-compose.md) | Jetpack Compose |
+| [forma-includer-settings](forma-includer-settings.md) | settings.gradle / includer / composite |
+| [forma-project-layout](forma-project-layout.md) | Feature folders, packages, composition roots |
+| [forma-bazel](forma-bazel.md) | Bazel adapter / sample |
+
+**Hard rules agents must never violate**
+
+1. `impl` must not depend on another `impl`
+2. Composition roots list feature `api` **and** `impl` explicitly
+3. Suffix of project name must match DSL (`…/impl` → `impl { }`)
+4. `packageName` must match source root path
+5. Do not weaken validators to “make it build”
diff --git a/examples/agent-skills/forma-android-targets.md b/examples/agent-skills/forma-android-targets.md
new file mode 100644
index 00000000..22c8fc5e
--- /dev/null
+++ b/examples/agent-skills/forma-android-targets.md
@@ -0,0 +1,55 @@
+---
+name: forma-android-targets
+description: All Android Forma DSL targets, suffixes, content rules, and example steps.
+---
+
+# Android targets (agent skill)
+
+Plugin: `tools.forma.android`. Register happens on first DSL use via `AndroidTargetRegistry`.
+
+| DSL | Suffix | Content rules | Progressive step |
+|-----|--------|---------------|------------------|
+| `androidBinary` | `binary` | no `res/`; composition root | 01+ |
+| `androidApp` | `app` | no `res/`; composition root | 01+ |
+| `api` | `api` | no `res/` | 02+ |
+| `impl` | `impl` | Android feature impl | 02+ |
+| `androidRes` | `res` | **only** `res/` under src/main | 01, 03 |
+| `viewBinding` | `viewbinding` | **only** `layout*` under res | 03 |
+| `library` | `library` | pure JVM library | 04 |
+| `util` | `util` | no res/ | 04 |
+| `androidUtil` | `android-util` | no res/ | 04 |
+| `androidLibrary` | `library` | Android lib; **no** `impl` deps | 04 |
+| `widget` | `widget` | Custom View | 05 |
+| `uiLibrary` | `ui-library` | shared UI blocks | 05 |
+| `composeWidget` | `compose-widget` | always Compose | 06 |
+| `testUtil` | `test-util` | no res/ | 09 |
+| `androidTestUtil` | `android-test-util` | Android test helpers | 09 |
+| `androidNative` | `native` | no res/; **no project-dep validation yet** | docs only |
+
+## Minimal skeletons
+
+```kotlin
+androidBinary(packageName = "…", versionCode = 1, versionName = "0.1.0", dependencies = deps(target(":root-app")))
+androidApp(packageName = "…", dependencies = deps("androidx.appcompat:appcompat:1.6.1") + deps(target(":root-res")))
+androidRes(packageName = "…")
+api(packageName = "…")
+impl(packageName = "…", dependencies = deps(target(":feature:x:api")))
+viewBinding(packageName = "…", dependencies = deps(target(":feature:x:res")))
+widget(packageName = "…")
+composeWidget(packageName = "…", dependencies = deps(/* compose GAVs */))
+androidLibrary(packageName = "…")
+androidUtil(packageName = "…")
+util(packageName = "…")
+library(packageName = "…")
+testUtil(packageName = "…")
+androidTestUtil(packageName = "…")
+// androidNative(packageName = "…") // NDK; validators incomplete
+```
+
+## Forbidden
+
+- `impl` → `impl`
+- `api` shipping `res/`
+- `androidLibrary` → `impl`
+- Wrong suffix for DSL
+- Relying on transitive feature wiring instead of listing api+impl on roots
diff --git a/examples/agent-skills/forma-bazel.md b/examples/agent-skills/forma-bazel.md
new file mode 100644
index 00000000..5412ce05
--- /dev/null
+++ b/examples/agent-skills/forma-bazel.md
@@ -0,0 +1,16 @@
+---
+name: forma-bazel
+description: Bazel adapter generate/check and sample workspace pointers.
+---
+
+# Bazel (agent skill)
+
+Not part of the progressive Gradle ladders. Use:
+
+| Artifact | Path |
+|----------|------|
+| Design | `docs/BAZEL-ADAPTER.md` |
+| Generate/check spike | `bazel-adapter/` (`./gradlew test`, `runSample`) |
+| Runnable sample | `bazel-sample/` (`bazelisk build //...`, `run //binary:binary`) |
+
+Concepts: target types → `kt_jvm_*`, restriction graph → visibility, tags `forma:type=…`, **impl ↛ impl**.
diff --git a/examples/agent-skills/forma-compose.md b/examples/agent-skills/forma-compose.md
new file mode 100644
index 00000000..5546766a
--- /dev/null
+++ b/examples/agent-skills/forma-compose.md
@@ -0,0 +1,22 @@
+---
+name: forma-compose
+description: Compose flags and composeWidget target.
+---
+
+# Compose (agent skill)
+
+Doc: `docs/COMPOSE.md`. Example: `examples/android/06-compose`.
+
+```kotlin
+// root
+androidProjectConfiguration(..., compose = true, composeCompilerVersion = "1.5.3")
+
+impl(..., compose = true, dependencies = transitiveDeps("androidx.compose.ui:ui:…", /* … */))
+composeWidget(packageName = "…", dependencies = transitiveDeps(/* compose libs */))
+androidBinary(..., compose = true, ...)
+```
+
+- Forma enables `buildFeatures.compose` + compiler extension version
+- **You** still add Compose Maven artifacts
+- Align compiler version with Kotlin (sample: 1.5.3 ↔ 1.9.10)
+- **`deps("gav")` is non-transitive** — use `transitiveDeps(...)` for Compose (same as `androidx.compose` in `build-dependencies/`)
diff --git a/examples/agent-skills/forma-dependency-matrix.md b/examples/agent-skills/forma-dependency-matrix.md
new file mode 100644
index 00000000..5a043ffb
--- /dev/null
+++ b/examples/agent-skills/forma-dependency-matrix.md
@@ -0,0 +1,28 @@
+---
+name: forma-dependency-matrix
+description: Project-dep allowlists and how to fix validator failures.
+---
+
+# Dependency matrix (agent skill)
+
+**Source of truth:** `docs/DEPENDENCY-MATRIX.md` (Android), `docs/JVM-TARGETS.md` (JVM).
+
+## Dagger-friendly rules
+
+| Consumer | May depend on (project suffixes) |
+|----------|-----------------------------------|
+| `api` | `api`, `library` |
+| `impl` | api, utils, library, ui-library, res, viewbinding, widget, compose-widget — **not** other `impl` |
+| `androidApp` / `androidBinary` | api, impl, shared UI/libs (composition roots) |
+| `androidLibrary` | library, utils, res, api — **not** impl |
+
+## Fixing failures
+
+1. Read error: consumer suffix, illegal dep, allowed set
+2. Prefer re-slice (move types to `api` / shared lib) over disabling validation
+3. Composition roots must list feature entrypoints explicitly
+
+## Progressive demos of illegal vs legal
+
+- Legal multi-feature: `examples/android/07-multi-feature`, `examples/jvm/04-multi-feature`
+- Shared libs without impl edges: `examples/android/04-shared-libs`
diff --git a/examples/agent-skills/forma-deps-catalog.md b/examples/agent-skills/forma-deps-catalog.md
new file mode 100644
index 00000000..8216be78
--- /dev/null
+++ b/examples/agent-skills/forma-deps-catalog.md
@@ -0,0 +1,29 @@
+---
+name: forma-deps-catalog
+description: projectDependencies catalogs — library, bundle, plugin, bare GAV.
+---
+
+# Deps catalog (agent skill)
+
+Doc: `docs/DEPS-CATALOG.md`. Example: `examples/android/08-deps-catalog`.
+
+```kotlin
+// settings.gradle.kts
+projectDependencies(
+ "libs",
+ "com.jakewharton.timber:timber:5.0.1",
+ library("androidx.appcompat:appcompat:1.6.1", name = "appcompat"),
+ bundle(name = "uiBasics", "com.google.android.material:material:1.10.0"),
+ plugin("androidx.navigation:navigation-safe-args-gradle-plugin", "2.7.4"),
+)
+
+// module
+dependencies = deps(libs.appcompat, libs.jakewhartonTimber, libs.bundles.uiBasics)
+```
+
+External GAV strings also work in `deps("group:artifact:version")` for tiny examples.
+Prefer catalogs in real apps.
+
+## Transitivity
+
+`deps("g:a:v")` is **non-transitive**. Use `transitiveDeps(...)` when Maven transitively is required (Compose UI, JUnit/hamcrest, AppCompat clusters).
diff --git a/examples/agent-skills/forma-includer-settings.md b/examples/agent-skills/forma-includer-settings.md
new file mode 100644
index 00000000..397546fe
--- /dev/null
+++ b/examples/agent-skills/forma-includer-settings.md
@@ -0,0 +1,28 @@
+---
+name: forma-includer-settings
+description: settings.gradle.kts, includer, composite includeBuild patterns.
+---
+
+# Includer & settings (agent skill)
+
+## Composite monorepo (examples / samples)
+
+```kotlin
+pluginManagement {
+ repositories { google(); gradlePluginPortal(); mavenCentral() }
+ includeBuild("../../../plugins") // depth depends on location
+ includeBuild("../../../includer")
+}
+plugins {
+ id("tools.forma.includer")
+ id("tools.forma.android") // or tools.forma.jvm
+}
+includer { arbitraryBuildScriptNames = true }
+```
+
+## Rules
+
+- Drop `build.gradle.kts` in a folder → project discovered (no manual `include`)
+- Nested `settings.gradle.kts` skips that tree
+- Path `feature/home/impl` → Gradle `:feature-home-impl`; Forma `target(":feature:home:impl")`
+- Android root needs `androidProjectConfiguration` in root `build.gradle.kts`
diff --git a/examples/agent-skills/forma-jvm-targets.md b/examples/agent-skills/forma-jvm-targets.md
new file mode 100644
index 00000000..cc90be08
--- /dev/null
+++ b/examples/agent-skills/forma-jvm-targets.md
@@ -0,0 +1,37 @@
+---
+name: forma-jvm-targets
+description: Pure JVM Forma DSL targets and progressive JVM ladder.
+---
+
+# JVM targets (agent skill)
+
+Plugin: `tools.forma.jvm`. Imports: `tools.forma.jvm.api`, `.impl`, `.library`, `.util`, `.testUtil`, `.binary`.
+
+| DSL | Suffix | Role | Step |
+|-----|--------|------|------|
+| `binary` | `binary` | composition root + `mainClass` | 01+ |
+| `api` | `api` | contracts | 02+ |
+| `impl` | `impl` | feature impl | 02+ |
+| `library` | `library` | shared JVM | 03 |
+| `util` | `util` | helpers | 03 |
+| `testUtil` | `test-util` | test helpers | 05 |
+
+## Matrix rules of thumb
+
+- `impl` ↛ `impl`
+- `api` → only `api`, `library`
+- `library` → `util`, `test-util`
+- `binary` may pull api + many impls + shared
+
+## Skeleton
+
+```kotlin
+import tools.forma.jvm.binary
+binary(
+ packageName = "com.example.binary",
+ mainClass = "com.example.binary.MainKt",
+ dependencies = deps(target(":feature:x:api"), target(":feature:x:impl"))
+)
+```
+
+Examples: `examples/jvm/01-hello-binary` … `05-test-util`. Full sample: `jvm-application/`.
diff --git a/examples/agent-skills/forma-overview.md b/examples/agent-skills/forma-overview.md
new file mode 100644
index 00000000..e1bf5ea7
--- /dev/null
+++ b/examples/agent-skills/forma-overview.md
@@ -0,0 +1,36 @@
+---
+name: forma-overview
+description: Entry map for Forma progressive examples and platform plugins.
+---
+
+# Forma overview (agent skill)
+
+## What Forma is
+
+Meta build system (Gradle plugins) that replaces ad-hoc AGP wiring with **typed targets**,
+shared configuration, and a **dependency restriction graph**.
+
+## Platforms
+
+| Plugin id | Module | Sample | Progressive ladder |
+|-----------|--------|--------|--------------------|
+| `tools.forma.android` | `plugins/android` | `application/` | `examples/android/01`…`09` |
+| `tools.forma.jvm` | `plugins/jvm` | `jvm-application/` | `examples/jvm/01`…`05` |
+| `tools.forma:core` | `plugins/core` | — | restriction engine |
+| Bazel adapter | `bazel-adapter/` | `bazel-sample/` | skill `forma-bazel` |
+
+## Learning order for agents
+
+1. This overview + `forma-includer-settings` + `forma-project-layout`
+2. Platform targets (`forma-android-targets` or `forma-jvm-targets`)
+3. `forma-dependency-matrix` before multi-module wiring
+4. `forma-deps-catalog` / `forma-compose` as needed
+5. Walk progressive examples in order; copy the **smallest** step that has the feature
+
+## Docs of record
+
+- `docs/DEPENDENCY-MATRIX.md` — live validator truth (Android)
+- `docs/JVM-TARGETS.md` — JVM matrix
+- `docs/GETTING-STARTED.md` / `docs/JVM-GETTING-STARTED.md`
+- `docs/PROGRESSIVE-EXAMPLES.md`
+- `docs/COMPOSE.md`, `docs/DEPS-CATALOG.md`
diff --git a/examples/agent-skills/forma-project-layout.md b/examples/agent-skills/forma-project-layout.md
new file mode 100644
index 00000000..436f6a62
--- /dev/null
+++ b/examples/agent-skills/forma-project-layout.md
@@ -0,0 +1,26 @@
+---
+name: forma-project-layout
+description: Feature folder conventions, packages, composition roots.
+---
+
+# Project layout (agent skill)
+
+## Feature slice
+
+```
+feature//{api,impl,res,viewbinding}/
+```
+
+## Composition roots
+
+- Android: `binary/` (`androidBinary`) + optional `root-app/` (`androidApp`)
+- JVM: `binary/` with `mainClass`
+
+Roots **must** depend on each feature’s `api` and `impl` explicitly.
+
+## Packages
+
+`packageName` ↔ `src/main/java|kotlin//`
+
+Examples gold standards: `docs/SAMPLE-APP.md`, `docs/JVM-SAMPLE.md`.
+Teaching ladders: `examples/**`.
diff --git a/examples/android/01-hello-apk/README.md b/examples/android/01-hello-apk/README.md
new file mode 100644
index 00000000..e1ceb6e1
--- /dev/null
+++ b/examples/android/01-hello-apk/README.md
@@ -0,0 +1,20 @@
+# 01 — hello-apk (Android)
+
+**Goal:** Smallest installable APK with Forma: composition roots + app resources.
+
+## Features introduced
+- `androidProjectConfiguration` (SDK + AGP once at root)
+- `androidBinary` — APK composition root
+- `androidApp` — application / activity shell
+- `androidRes` — resources-only target
+- Includer + composite `includeBuild` of plugins
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+```
+
+## Next
+02 adds feature `api` + `impl`.
diff --git a/examples/android/01-hello-apk/binary/build.gradle.kts b/examples/android/01-hello-apk/binary/build.gradle.kts
new file mode 100644
index 00000000..b2a2c107
--- /dev/null
+++ b/examples/android/01-hello-apk/binary/build.gradle.kts
@@ -0,0 +1,8 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.hello",
+ versionCode = 1,
+ versionName = "0.1.0",
+ dependencies = deps(
+ target(":root-app")
+ )
+)
diff --git a/examples/android/01-hello-apk/binary/src/main/AndroidManifest.xml b/examples/android/01-hello-apk/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..39da2469
--- /dev/null
+++ b/examples/android/01-hello-apk/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/01-hello-apk/build.gradle.kts b/examples/android/01-hello-apk/build.gradle.kts
new file mode 100644
index 00000000..5cfe0ce4
--- /dev/null
+++ b/examples/android/01-hello-apk/build.gradle.kts
@@ -0,0 +1,9 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2"
+ )
+}
diff --git a/examples/android/01-hello-apk/gradle.properties b/examples/android/01-hello-apk/gradle.properties
new file mode 100644
index 00000000..83fe8cce
--- /dev/null
+++ b/examples/android/01-hello-apk/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example 01
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/01-hello-apk/gradle/wrapper/gradle-wrapper.jar b/examples/android/01-hello-apk/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/01-hello-apk/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/01-hello-apk/gradle/wrapper/gradle-wrapper.properties b/examples/android/01-hello-apk/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/01-hello-apk/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/examples/android/01-hello-apk/gradlew b/examples/android/01-hello-apk/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/01-hello-apk/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/examples/android/01-hello-apk/gradlew.bat b/examples/android/01-hello-apk/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/01-hello-apk/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/examples/android/01-hello-apk/root-app/build.gradle.kts b/examples/android/01-hello-apk/root-app/build.gradle.kts
new file mode 100644
index 00000000..85ad46b3
--- /dev/null
+++ b/examples/android/01-hello-apk/root-app/build.gradle.kts
@@ -0,0 +1,6 @@
+androidApp(
+ packageName = "tools.forma.examples.android.hello.root",
+ dependencies = deps(
+ target(":root-res")
+ )
+)
diff --git a/examples/android/01-hello-apk/root-app/src/main/AndroidManifest.xml b/examples/android/01-hello-apk/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/01-hello-apk/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/01-hello-apk/root-app/src/main/kotlin/tools/forma/examples/android/hello/root/HelloActivity.kt b/examples/android/01-hello-apk/root-app/src/main/kotlin/tools/forma/examples/android/hello/root/HelloActivity.kt
new file mode 100644
index 00000000..e1aa0567
--- /dev/null
+++ b/examples/android/01-hello-apk/root-app/src/main/kotlin/tools/forma/examples/android/hello/root/HelloActivity.kt
@@ -0,0 +1,14 @@
+package tools.forma.examples.android.hello.root
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+
+class HelloActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val tv = TextView(this)
+ tv.text = "Forma Android 01 hello-apk"
+ setContentView(tv)
+ }
+}
diff --git a/examples/android/01-hello-apk/root-res/build.gradle.kts b/examples/android/01-hello-apk/root-res/build.gradle.kts
new file mode 100644
index 00000000..fb8790c0
--- /dev/null
+++ b/examples/android/01-hello-apk/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.hello.res"
+)
diff --git a/examples/android/01-hello-apk/root-res/src/main/res/values/strings.xml b/examples/android/01-hello-apk/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..19592ca9
--- /dev/null
+++ b/examples/android/01-hello-apk/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma Hello 01
+
diff --git a/examples/android/01-hello-apk/settings.gradle.kts b/examples/android/01-hello-apk/settings.gradle.kts
new file mode 100644
index 00000000..49f575c8
--- /dev/null
+++ b/examples/android/01-hello-apk/settings.gradle.kts
@@ -0,0 +1,21 @@
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "01-hello-apk"
diff --git a/examples/android/02-feature-api-impl/README.md b/examples/android/02-feature-api-impl/README.md
new file mode 100644
index 00000000..fcde22bf
--- /dev/null
+++ b/examples/android/02-feature-api-impl/README.md
@@ -0,0 +1,19 @@
+# 02 — feature-api-impl (Android)
+
+**Goal:** Feature slice with `api` + `impl`. Composition roots list both.
+
+## Features introduced
+- `api` (JVM contracts, no `res/`)
+- `impl` (Android implementation)
+- Explicit `api` + `impl` on `androidBinary` / `androidApp`
+- **Rule:** `impl` ↛ `impl`
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+```
+
+## Next
+03 adds feature `androidRes` + `viewBinding`.
diff --git a/examples/android/02-feature-api-impl/binary/build.gradle.kts b/examples/android/02-feature-api-impl/binary/build.gradle.kts
new file mode 100644
index 00000000..43ab74bb
--- /dev/null
+++ b/examples/android/02-feature-api-impl/binary/build.gradle.kts
@@ -0,0 +1,10 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.apiimpl",
+ versionCode = 1,
+ versionName = "0.1.0",
+ dependencies = deps(
+ target(":root-app"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/02-feature-api-impl/binary/src/main/AndroidManifest.xml b/examples/android/02-feature-api-impl/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..839c8a97
--- /dev/null
+++ b/examples/android/02-feature-api-impl/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/02-feature-api-impl/build.gradle.kts b/examples/android/02-feature-api-impl/build.gradle.kts
new file mode 100644
index 00000000..15759185
--- /dev/null
+++ b/examples/android/02-feature-api-impl/build.gradle.kts
@@ -0,0 +1,9 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2",
+ )
+}
diff --git a/examples/android/02-feature-api-impl/feature/hello/api/build.gradle.kts b/examples/android/02-feature-api-impl/feature/hello/api/build.gradle.kts
new file mode 100644
index 00000000..d0068035
--- /dev/null
+++ b/examples/android/02-feature-api-impl/feature/hello/api/build.gradle.kts
@@ -0,0 +1,3 @@
+api(
+ packageName = "tools.forma.examples.android.apiimpl.feature.hello.api"
+)
diff --git a/examples/android/02-feature-api-impl/feature/hello/api/src/main/kotlin/tools/forma/examples/android/apiimpl/feature/hello/api/HelloMessage.kt b/examples/android/02-feature-api-impl/feature/hello/api/src/main/kotlin/tools/forma/examples/android/apiimpl/feature/hello/api/HelloMessage.kt
new file mode 100644
index 00000000..5d1608c9
--- /dev/null
+++ b/examples/android/02-feature-api-impl/feature/hello/api/src/main/kotlin/tools/forma/examples/android/apiimpl/feature/hello/api/HelloMessage.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.apiimpl.feature.hello.api
+
+interface HelloMessage {
+ fun text(): String
+}
diff --git a/examples/android/02-feature-api-impl/feature/hello/impl/build.gradle.kts b/examples/android/02-feature-api-impl/feature/hello/impl/build.gradle.kts
new file mode 100644
index 00000000..e07705f0
--- /dev/null
+++ b/examples/android/02-feature-api-impl/feature/hello/impl/build.gradle.kts
@@ -0,0 +1,6 @@
+impl(
+ packageName = "tools.forma.examples.android.apiimpl.feature.hello.impl",
+ dependencies = deps(
+ target(":feature:hello:api")
+ )
+)
diff --git a/examples/android/02-feature-api-impl/feature/hello/impl/src/main/AndroidManifest.xml b/examples/android/02-feature-api-impl/feature/hello/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/02-feature-api-impl/feature/hello/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/02-feature-api-impl/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/apiimpl/feature/hello/impl/DefaultHelloMessage.kt b/examples/android/02-feature-api-impl/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/apiimpl/feature/hello/impl/DefaultHelloMessage.kt
new file mode 100644
index 00000000..85ef9bb2
--- /dev/null
+++ b/examples/android/02-feature-api-impl/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/apiimpl/feature/hello/impl/DefaultHelloMessage.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.android.apiimpl.feature.hello.impl
+
+import tools.forma.examples.android.apiimpl.feature.hello.api.HelloMessage
+
+class DefaultHelloMessage : HelloMessage {
+ override fun text(): String = "Forma Android 02 feature api+impl"
+}
diff --git a/examples/android/02-feature-api-impl/gradle.properties b/examples/android/02-feature-api-impl/gradle.properties
new file mode 100644
index 00000000..3672d5a4
--- /dev/null
+++ b/examples/android/02-feature-api-impl/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/02-feature-api-impl/gradle/wrapper/gradle-wrapper.jar b/examples/android/02-feature-api-impl/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/02-feature-api-impl/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/02-feature-api-impl/gradle/wrapper/gradle-wrapper.properties b/examples/android/02-feature-api-impl/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/02-feature-api-impl/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/examples/android/02-feature-api-impl/gradlew b/examples/android/02-feature-api-impl/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/02-feature-api-impl/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/examples/android/02-feature-api-impl/gradlew.bat b/examples/android/02-feature-api-impl/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/02-feature-api-impl/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/examples/android/02-feature-api-impl/root-app/build.gradle.kts b/examples/android/02-feature-api-impl/root-app/build.gradle.kts
new file mode 100644
index 00000000..da46a8af
--- /dev/null
+++ b/examples/android/02-feature-api-impl/root-app/build.gradle.kts
@@ -0,0 +1,8 @@
+androidApp(
+ packageName = "tools.forma.examples.android.apiimpl.root",
+ dependencies = deps(
+ target(":root-res"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/02-feature-api-impl/root-app/src/main/AndroidManifest.xml b/examples/android/02-feature-api-impl/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/02-feature-api-impl/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/02-feature-api-impl/root-app/src/main/kotlin/tools/forma/examples/android/apiimpl/root/HelloActivity.kt b/examples/android/02-feature-api-impl/root-app/src/main/kotlin/tools/forma/examples/android/apiimpl/root/HelloActivity.kt
new file mode 100644
index 00000000..8a1220a4
--- /dev/null
+++ b/examples/android/02-feature-api-impl/root-app/src/main/kotlin/tools/forma/examples/android/apiimpl/root/HelloActivity.kt
@@ -0,0 +1,17 @@
+package tools.forma.examples.android.apiimpl.root
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import tools.forma.examples.android.apiimpl.feature.hello.api.HelloMessage
+import tools.forma.examples.android.apiimpl.feature.hello.impl.DefaultHelloMessage
+
+class HelloActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val msg: HelloMessage = DefaultHelloMessage()
+ val tv = TextView(this)
+ tv.text = msg.text()
+ setContentView(tv)
+ }
+}
diff --git a/examples/android/02-feature-api-impl/root-res/build.gradle.kts b/examples/android/02-feature-api-impl/root-res/build.gradle.kts
new file mode 100644
index 00000000..c0781cc7
--- /dev/null
+++ b/examples/android/02-feature-api-impl/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.apiimpl.res"
+)
diff --git a/examples/android/02-feature-api-impl/root-res/src/main/res/values/strings.xml b/examples/android/02-feature-api-impl/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..478f5fbf
--- /dev/null
+++ b/examples/android/02-feature-api-impl/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma 02 api-impl
+
diff --git a/examples/android/02-feature-api-impl/settings.gradle.kts b/examples/android/02-feature-api-impl/settings.gradle.kts
new file mode 100644
index 00000000..0a3cd0d2
--- /dev/null
+++ b/examples/android/02-feature-api-impl/settings.gradle.kts
@@ -0,0 +1,21 @@
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "02-feature-api-impl"
diff --git a/examples/android/03-res-viewbinding/README.md b/examples/android/03-res-viewbinding/README.md
new file mode 100644
index 00000000..54c9fc44
--- /dev/null
+++ b/examples/android/03-res-viewbinding/README.md
@@ -0,0 +1,18 @@
+# 03 — res-viewbinding (Android)
+
+**Goal:** Feature resources + layout-only view binding modules.
+
+## Features introduced
+- Feature `androidRes` (only `res/` under src/main)
+- `viewBinding` (only `layout*` resource types)
+- `impl` depends on own `api` + `res` + `viewbinding`
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+```
+
+## Next
+04 adds shared `library` / `util` / `androidUtil` / `androidLibrary`.
diff --git a/examples/android/03-res-viewbinding/binary/build.gradle.kts b/examples/android/03-res-viewbinding/binary/build.gradle.kts
new file mode 100644
index 00000000..cf34a4b6
--- /dev/null
+++ b/examples/android/03-res-viewbinding/binary/build.gradle.kts
@@ -0,0 +1,10 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.resvb",
+ versionCode = 1,
+ versionName = "0.1.0",
+ dependencies = deps(
+ target(":root-app"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/03-res-viewbinding/binary/src/main/AndroidManifest.xml b/examples/android/03-res-viewbinding/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..839c8a97
--- /dev/null
+++ b/examples/android/03-res-viewbinding/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/03-res-viewbinding/build.gradle.kts b/examples/android/03-res-viewbinding/build.gradle.kts
new file mode 100644
index 00000000..15759185
--- /dev/null
+++ b/examples/android/03-res-viewbinding/build.gradle.kts
@@ -0,0 +1,9 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2",
+ )
+}
diff --git a/examples/android/03-res-viewbinding/feature/hello/api/build.gradle.kts b/examples/android/03-res-viewbinding/feature/hello/api/build.gradle.kts
new file mode 100644
index 00000000..c4c08d21
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/api/build.gradle.kts
@@ -0,0 +1,3 @@
+api(
+ packageName = "tools.forma.examples.android.resvb.feature.hello.api"
+)
diff --git a/examples/android/03-res-viewbinding/feature/hello/api/src/main/kotlin/tools/forma/examples/android/resvb/feature/hello/api/HelloTitle.kt b/examples/android/03-res-viewbinding/feature/hello/api/src/main/kotlin/tools/forma/examples/android/resvb/feature/hello/api/HelloTitle.kt
new file mode 100644
index 00000000..80bd56dc
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/api/src/main/kotlin/tools/forma/examples/android/resvb/feature/hello/api/HelloTitle.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.resvb.feature.hello.api
+
+interface HelloTitle {
+ fun titleRes(): Int
+}
diff --git a/examples/android/03-res-viewbinding/feature/hello/impl/build.gradle.kts b/examples/android/03-res-viewbinding/feature/hello/impl/build.gradle.kts
new file mode 100644
index 00000000..23ff31b8
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/impl/build.gradle.kts
@@ -0,0 +1,8 @@
+impl(
+ packageName = "tools.forma.examples.android.resvb.feature.hello.impl",
+ dependencies = deps(
+ target(":feature:hello:api"),
+ target(":feature:hello:res"),
+ target(":feature:hello:viewbinding")
+ )
+)
diff --git a/examples/android/03-res-viewbinding/feature/hello/impl/src/main/AndroidManifest.xml b/examples/android/03-res-viewbinding/feature/hello/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/03-res-viewbinding/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/resvb/feature/hello/impl/HelloScreen.kt b/examples/android/03-res-viewbinding/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/resvb/feature/hello/impl/HelloScreen.kt
new file mode 100644
index 00000000..49778f37
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/resvb/feature/hello/impl/HelloScreen.kt
@@ -0,0 +1,15 @@
+package tools.forma.examples.android.resvb.feature.hello.impl
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import tools.forma.examples.android.resvb.feature.hello.viewbinding.databinding.HelloFeatureBinding
+import tools.forma.examples.android.resvb.feature.hello.res.R as HelloR
+
+class HelloScreen(context: Context) {
+ val view: View = run {
+ val binding = HelloFeatureBinding.inflate(LayoutInflater.from(context))
+ binding.message.setText(HelloR.string.hello_title)
+ binding.root
+ }
+}
diff --git a/examples/android/03-res-viewbinding/feature/hello/res/build.gradle.kts b/examples/android/03-res-viewbinding/feature/hello/res/build.gradle.kts
new file mode 100644
index 00000000..3dcf31d5
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.resvb.feature.hello.res"
+)
diff --git a/examples/android/03-res-viewbinding/feature/hello/res/src/main/res/values/strings.xml b/examples/android/03-res-viewbinding/feature/hello/res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..6a11984f
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/res/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ unused
+ Hello from feature res
+
diff --git a/examples/android/03-res-viewbinding/feature/hello/viewbinding/build.gradle.kts b/examples/android/03-res-viewbinding/feature/hello/viewbinding/build.gradle.kts
new file mode 100644
index 00000000..0b3883a2
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/viewbinding/build.gradle.kts
@@ -0,0 +1,6 @@
+viewBinding(
+ packageName = "tools.forma.examples.android.resvb.feature.hello.viewbinding",
+ dependencies = deps(
+ target(":feature:hello:res")
+ )
+)
diff --git a/examples/android/03-res-viewbinding/feature/hello/viewbinding/src/main/AndroidManifest.xml b/examples/android/03-res-viewbinding/feature/hello/viewbinding/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/viewbinding/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/03-res-viewbinding/feature/hello/viewbinding/src/main/res/layout/hello_feature.xml b/examples/android/03-res-viewbinding/feature/hello/viewbinding/src/main/res/layout/hello_feature.xml
new file mode 100644
index 00000000..9200bcb3
--- /dev/null
+++ b/examples/android/03-res-viewbinding/feature/hello/viewbinding/src/main/res/layout/hello_feature.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/examples/android/03-res-viewbinding/gradle.properties b/examples/android/03-res-viewbinding/gradle.properties
new file mode 100644
index 00000000..3672d5a4
--- /dev/null
+++ b/examples/android/03-res-viewbinding/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/03-res-viewbinding/gradle/wrapper/gradle-wrapper.jar b/examples/android/03-res-viewbinding/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/03-res-viewbinding/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/03-res-viewbinding/gradle/wrapper/gradle-wrapper.properties b/examples/android/03-res-viewbinding/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/03-res-viewbinding/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/examples/android/03-res-viewbinding/gradlew b/examples/android/03-res-viewbinding/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/03-res-viewbinding/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/examples/android/03-res-viewbinding/gradlew.bat b/examples/android/03-res-viewbinding/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/03-res-viewbinding/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/examples/android/03-res-viewbinding/root-app/build.gradle.kts b/examples/android/03-res-viewbinding/root-app/build.gradle.kts
new file mode 100644
index 00000000..44bdf67f
--- /dev/null
+++ b/examples/android/03-res-viewbinding/root-app/build.gradle.kts
@@ -0,0 +1,8 @@
+androidApp(
+ packageName = "tools.forma.examples.android.resvb.root",
+ dependencies = deps(
+ target(":root-res"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/03-res-viewbinding/root-app/src/main/AndroidManifest.xml b/examples/android/03-res-viewbinding/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/03-res-viewbinding/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/03-res-viewbinding/root-app/src/main/kotlin/tools/forma/examples/android/resvb/root/HelloActivity.kt b/examples/android/03-res-viewbinding/root-app/src/main/kotlin/tools/forma/examples/android/resvb/root/HelloActivity.kt
new file mode 100644
index 00000000..5f15efe2
--- /dev/null
+++ b/examples/android/03-res-viewbinding/root-app/src/main/kotlin/tools/forma/examples/android/resvb/root/HelloActivity.kt
@@ -0,0 +1,12 @@
+package tools.forma.examples.android.resvb.root
+
+import android.app.Activity
+import android.os.Bundle
+import tools.forma.examples.android.resvb.feature.hello.impl.HelloScreen
+
+class HelloActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(HelloScreen(this).view)
+ }
+}
diff --git a/examples/android/03-res-viewbinding/root-res/build.gradle.kts b/examples/android/03-res-viewbinding/root-res/build.gradle.kts
new file mode 100644
index 00000000..1f9aaf98
--- /dev/null
+++ b/examples/android/03-res-viewbinding/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.resvb.res"
+)
diff --git a/examples/android/03-res-viewbinding/root-res/src/main/res/values/strings.xml b/examples/android/03-res-viewbinding/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..ef848190
--- /dev/null
+++ b/examples/android/03-res-viewbinding/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma 03 res-vb
+
diff --git a/examples/android/03-res-viewbinding/settings.gradle.kts b/examples/android/03-res-viewbinding/settings.gradle.kts
new file mode 100644
index 00000000..176f4eff
--- /dev/null
+++ b/examples/android/03-res-viewbinding/settings.gradle.kts
@@ -0,0 +1,21 @@
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "03-res-viewbinding"
diff --git a/examples/android/04-shared-libs/README.md b/examples/android/04-shared-libs/README.md
new file mode 100644
index 00000000..40147a38
--- /dev/null
+++ b/examples/android/04-shared-libs/README.md
@@ -0,0 +1,19 @@
+# 04 — shared-libs (Android)
+
+**Goal:** Shared pure-JVM and Android libraries outside features.
+
+## Features introduced
+- `library` (JVM)
+- `util` (JVM helpers)
+- `androidUtil` (Android helpers, no res/)
+- `androidLibrary` (shared Android library — **cannot** depend on `impl`)
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+```
+
+## Next
+05 adds `widget` + `uiLibrary`.
diff --git a/examples/android/04-shared-libs/binary/build.gradle.kts b/examples/android/04-shared-libs/binary/build.gradle.kts
new file mode 100644
index 00000000..a93079d0
--- /dev/null
+++ b/examples/android/04-shared-libs/binary/build.gradle.kts
@@ -0,0 +1,10 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.shared",
+ versionCode = 1,
+ versionName = "0.1.0",
+ dependencies = deps(
+ target(":root-app"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/04-shared-libs/binary/src/main/AndroidManifest.xml b/examples/android/04-shared-libs/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..839c8a97
--- /dev/null
+++ b/examples/android/04-shared-libs/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/04-shared-libs/build.gradle.kts b/examples/android/04-shared-libs/build.gradle.kts
new file mode 100644
index 00000000..15759185
--- /dev/null
+++ b/examples/android/04-shared-libs/build.gradle.kts
@@ -0,0 +1,9 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2",
+ )
+}
diff --git a/examples/android/04-shared-libs/common/android-util/build.gradle.kts b/examples/android/04-shared-libs/common/android-util/build.gradle.kts
new file mode 100644
index 00000000..b9c5bfb2
--- /dev/null
+++ b/examples/android/04-shared-libs/common/android-util/build.gradle.kts
@@ -0,0 +1,3 @@
+androidUtil(
+ packageName = "tools.forma.examples.android.shared.common.androidutil"
+)
diff --git a/examples/android/04-shared-libs/common/android-util/src/main/AndroidManifest.xml b/examples/android/04-shared-libs/common/android-util/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/04-shared-libs/common/android-util/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/04-shared-libs/common/android-util/src/main/kotlin/tools/forma/examples/android/shared/common/androidutil/Ctx.kt b/examples/android/04-shared-libs/common/android-util/src/main/kotlin/tools/forma/examples/android/shared/common/androidutil/Ctx.kt
new file mode 100644
index 00000000..56e68edb
--- /dev/null
+++ b/examples/android/04-shared-libs/common/android-util/src/main/kotlin/tools/forma/examples/android/shared/common/androidutil/Ctx.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.shared.common.androidutil
+
+import android.content.Context
+
+fun Context.appLabel(): String = applicationInfo.loadLabel(packageManager).toString()
diff --git a/examples/android/04-shared-libs/common/library/build.gradle.kts b/examples/android/04-shared-libs/common/library/build.gradle.kts
new file mode 100644
index 00000000..e45101aa
--- /dev/null
+++ b/examples/android/04-shared-libs/common/library/build.gradle.kts
@@ -0,0 +1,6 @@
+library(
+ packageName = "tools.forma.examples.android.shared.common.library",
+ dependencies = deps(
+ target(":common:util")
+ )
+)
diff --git a/examples/android/04-shared-libs/common/library/src/main/kotlin/tools/forma/examples/android/shared/common/library/Label.kt b/examples/android/04-shared-libs/common/library/src/main/kotlin/tools/forma/examples/android/shared/common/library/Label.kt
new file mode 100644
index 00000000..343b7fee
--- /dev/null
+++ b/examples/android/04-shared-libs/common/library/src/main/kotlin/tools/forma/examples/android/shared/common/library/Label.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.android.shared.common.library
+
+import tools.forma.examples.android.shared.common.util.exclaim
+
+data class Label(val value: String) {
+ fun loud(): String = value.exclaim()
+}
diff --git a/examples/android/04-shared-libs/common/util/build.gradle.kts b/examples/android/04-shared-libs/common/util/build.gradle.kts
new file mode 100644
index 00000000..650b612f
--- /dev/null
+++ b/examples/android/04-shared-libs/common/util/build.gradle.kts
@@ -0,0 +1,3 @@
+util(
+ packageName = "tools.forma.examples.android.shared.common.util"
+)
diff --git a/examples/android/04-shared-libs/common/util/src/main/kotlin/tools/forma/examples/android/shared/common/util/Strings.kt b/examples/android/04-shared-libs/common/util/src/main/kotlin/tools/forma/examples/android/shared/common/util/Strings.kt
new file mode 100644
index 00000000..0c13fde0
--- /dev/null
+++ b/examples/android/04-shared-libs/common/util/src/main/kotlin/tools/forma/examples/android/shared/common/util/Strings.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.android.shared.common.util
+
+fun String.exclaim(): String = "$this!"
diff --git a/examples/android/04-shared-libs/core/platform/library/build.gradle.kts b/examples/android/04-shared-libs/core/platform/library/build.gradle.kts
new file mode 100644
index 00000000..9e7a7fe5
--- /dev/null
+++ b/examples/android/04-shared-libs/core/platform/library/build.gradle.kts
@@ -0,0 +1,7 @@
+androidLibrary(
+ packageName = "tools.forma.examples.android.shared.core.platform.library",
+ dependencies = deps(
+ target(":common:library"),
+ target(":common:android-util")
+ )
+)
diff --git a/examples/android/04-shared-libs/core/platform/library/src/main/AndroidManifest.xml b/examples/android/04-shared-libs/core/platform/library/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/04-shared-libs/core/platform/library/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/04-shared-libs/core/platform/library/src/main/kotlin/tools/forma/examples/android/shared/core/platform/library/PlatformHello.kt b/examples/android/04-shared-libs/core/platform/library/src/main/kotlin/tools/forma/examples/android/shared/core/platform/library/PlatformHello.kt
new file mode 100644
index 00000000..fe20747d
--- /dev/null
+++ b/examples/android/04-shared-libs/core/platform/library/src/main/kotlin/tools/forma/examples/android/shared/core/platform/library/PlatformHello.kt
@@ -0,0 +1,10 @@
+package tools.forma.examples.android.shared.core.platform.library
+
+import android.content.Context
+import tools.forma.examples.android.shared.common.androidutil.appLabel
+import tools.forma.examples.android.shared.common.library.Label
+
+object PlatformHello {
+ fun text(context: Context): String =
+ Label("shared-libs " + context.appLabel()).loud()
+}
diff --git a/examples/android/04-shared-libs/feature/hello/api/build.gradle.kts b/examples/android/04-shared-libs/feature/hello/api/build.gradle.kts
new file mode 100644
index 00000000..1b8c73e3
--- /dev/null
+++ b/examples/android/04-shared-libs/feature/hello/api/build.gradle.kts
@@ -0,0 +1,6 @@
+api(
+ packageName = "tools.forma.examples.android.shared.feature.hello.api",
+ dependencies = deps(
+ target(":common:library")
+ )
+)
diff --git a/examples/android/04-shared-libs/feature/hello/api/src/main/kotlin/tools/forma/examples/android/shared/feature/hello/api/HelloApi.kt b/examples/android/04-shared-libs/feature/hello/api/src/main/kotlin/tools/forma/examples/android/shared/feature/hello/api/HelloApi.kt
new file mode 100644
index 00000000..01082e46
--- /dev/null
+++ b/examples/android/04-shared-libs/feature/hello/api/src/main/kotlin/tools/forma/examples/android/shared/feature/hello/api/HelloApi.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.android.shared.feature.hello.api
+
+import tools.forma.examples.android.shared.common.library.Label
+
+interface HelloApi {
+ fun label(): Label
+}
diff --git a/examples/android/04-shared-libs/feature/hello/impl/build.gradle.kts b/examples/android/04-shared-libs/feature/hello/impl/build.gradle.kts
new file mode 100644
index 00000000..81e5a45c
--- /dev/null
+++ b/examples/android/04-shared-libs/feature/hello/impl/build.gradle.kts
@@ -0,0 +1,10 @@
+impl(
+ packageName = "tools.forma.examples.android.shared.feature.hello.impl",
+ dependencies = deps(
+ target(":feature:hello:api"),
+ target(":common:library"),
+ target(":common:util"),
+ target(":common:android-util"),
+ target(":core:platform:library")
+ )
+)
diff --git a/examples/android/04-shared-libs/feature/hello/impl/src/main/AndroidManifest.xml b/examples/android/04-shared-libs/feature/hello/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/04-shared-libs/feature/hello/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/04-shared-libs/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/shared/feature/hello/impl/HelloFacade.kt b/examples/android/04-shared-libs/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/shared/feature/hello/impl/HelloFacade.kt
new file mode 100644
index 00000000..5dff4210
--- /dev/null
+++ b/examples/android/04-shared-libs/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/shared/feature/hello/impl/HelloFacade.kt
@@ -0,0 +1,12 @@
+package tools.forma.examples.android.shared.feature.hello.impl
+
+import android.content.Context
+import tools.forma.examples.android.shared.common.library.Label
+import tools.forma.examples.android.shared.core.platform.library.PlatformHello
+import tools.forma.examples.android.shared.feature.hello.api.HelloApi
+
+object HelloFacade : HelloApi {
+ override fun label(): Label = Label("feature")
+ fun message(context: Context): String =
+ label().loud() + " / " + PlatformHello.text(context)
+}
diff --git a/examples/android/04-shared-libs/gradle.properties b/examples/android/04-shared-libs/gradle.properties
new file mode 100644
index 00000000..3672d5a4
--- /dev/null
+++ b/examples/android/04-shared-libs/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/04-shared-libs/gradle/wrapper/gradle-wrapper.jar b/examples/android/04-shared-libs/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/04-shared-libs/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/04-shared-libs/gradle/wrapper/gradle-wrapper.properties b/examples/android/04-shared-libs/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/04-shared-libs/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/examples/android/04-shared-libs/gradlew b/examples/android/04-shared-libs/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/04-shared-libs/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/examples/android/04-shared-libs/gradlew.bat b/examples/android/04-shared-libs/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/04-shared-libs/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/examples/android/04-shared-libs/root-app/build.gradle.kts b/examples/android/04-shared-libs/root-app/build.gradle.kts
new file mode 100644
index 00000000..a499d3e5
--- /dev/null
+++ b/examples/android/04-shared-libs/root-app/build.gradle.kts
@@ -0,0 +1,10 @@
+androidApp(
+ packageName = "tools.forma.examples.android.shared.root",
+ dependencies = deps(
+ target(":root-res"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl"),
+ target(":common:library"),
+ target(":core:platform:library")
+ )
+)
diff --git a/examples/android/04-shared-libs/root-app/src/main/AndroidManifest.xml b/examples/android/04-shared-libs/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/04-shared-libs/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/04-shared-libs/root-app/src/main/kotlin/tools/forma/examples/android/shared/root/HelloActivity.kt b/examples/android/04-shared-libs/root-app/src/main/kotlin/tools/forma/examples/android/shared/root/HelloActivity.kt
new file mode 100644
index 00000000..8b104f74
--- /dev/null
+++ b/examples/android/04-shared-libs/root-app/src/main/kotlin/tools/forma/examples/android/shared/root/HelloActivity.kt
@@ -0,0 +1,15 @@
+package tools.forma.examples.android.shared.root
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import tools.forma.examples.android.shared.feature.hello.impl.HelloFacade
+
+class HelloActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val tv = TextView(this)
+ tv.text = HelloFacade.message(this)
+ setContentView(tv)
+ }
+}
diff --git a/examples/android/04-shared-libs/root-res/build.gradle.kts b/examples/android/04-shared-libs/root-res/build.gradle.kts
new file mode 100644
index 00000000..cddbd91b
--- /dev/null
+++ b/examples/android/04-shared-libs/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.shared.res"
+)
diff --git a/examples/android/04-shared-libs/root-res/src/main/res/values/strings.xml b/examples/android/04-shared-libs/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..7d8d791c
--- /dev/null
+++ b/examples/android/04-shared-libs/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma 04 shared
+
diff --git a/examples/android/04-shared-libs/settings.gradle.kts b/examples/android/04-shared-libs/settings.gradle.kts
new file mode 100644
index 00000000..13ce48c6
--- /dev/null
+++ b/examples/android/04-shared-libs/settings.gradle.kts
@@ -0,0 +1,21 @@
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "04-shared-libs"
diff --git a/examples/android/05-widget-ui/README.md b/examples/android/05-widget-ui/README.md
new file mode 100644
index 00000000..969743bf
--- /dev/null
+++ b/examples/android/05-widget-ui/README.md
@@ -0,0 +1,17 @@
+# 05 — widget-ui (Android)
+
+**Goal:** Custom View widgets and shared UI libraries.
+
+## Features introduced
+- `widget` (Custom View / UI component)
+- `uiLibrary` (shared UI building blocks for widgets)
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+```
+
+## Next
+06 adds Compose (`compose` flag + `composeWidget`).
diff --git a/examples/android/05-widget-ui/binary/build.gradle.kts b/examples/android/05-widget-ui/binary/build.gradle.kts
new file mode 100644
index 00000000..ef1a7625
--- /dev/null
+++ b/examples/android/05-widget-ui/binary/build.gradle.kts
@@ -0,0 +1,12 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.widgetui",
+ versionCode = 1,
+ versionName = "0.1.0",
+ dependencies = deps(
+ target(":root-app"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl"),
+ target(":common:banner:widget"),
+ target(":common:ui:ui-library")
+ )
+)
diff --git a/examples/android/05-widget-ui/binary/src/main/AndroidManifest.xml b/examples/android/05-widget-ui/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..839c8a97
--- /dev/null
+++ b/examples/android/05-widget-ui/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/05-widget-ui/build.gradle.kts b/examples/android/05-widget-ui/build.gradle.kts
new file mode 100644
index 00000000..15759185
--- /dev/null
+++ b/examples/android/05-widget-ui/build.gradle.kts
@@ -0,0 +1,9 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2",
+ )
+}
diff --git a/examples/android/05-widget-ui/common/banner/widget/build.gradle.kts b/examples/android/05-widget-ui/common/banner/widget/build.gradle.kts
new file mode 100644
index 00000000..25845d70
--- /dev/null
+++ b/examples/android/05-widget-ui/common/banner/widget/build.gradle.kts
@@ -0,0 +1,6 @@
+widget(
+ packageName = "tools.forma.examples.android.widgetui.common.banner.widget",
+ dependencies = deps(
+ target(":common:ui:ui-library")
+ )
+)
diff --git a/examples/android/05-widget-ui/common/banner/widget/src/main/AndroidManifest.xml b/examples/android/05-widget-ui/common/banner/widget/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/05-widget-ui/common/banner/widget/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/05-widget-ui/common/banner/widget/src/main/kotlin/tools/forma/examples/android/widgetui/common/banner/widget/BannerView.kt b/examples/android/05-widget-ui/common/banner/widget/src/main/kotlin/tools/forma/examples/android/widgetui/common/banner/widget/BannerView.kt
new file mode 100644
index 00000000..129edee4
--- /dev/null
+++ b/examples/android/05-widget-ui/common/banner/widget/src/main/kotlin/tools/forma/examples/android/widgetui/common/banner/widget/BannerView.kt
@@ -0,0 +1,26 @@
+package tools.forma.examples.android.widgetui.common.banner.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.widget.TextView
+import tools.forma.examples.android.widgetui.common.ui.library.UiTokens
+
+class BannerView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : TextView(context, attrs) {
+ init {
+ val pad = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ UiTokens.paddingDp.toFloat(),
+ resources.displayMetrics
+ ).toInt()
+ setPadding(pad, pad, pad, pad)
+ textSize = 18f
+ }
+
+ fun setMessage(value: String) {
+ text = value
+ }
+}
diff --git a/examples/android/05-widget-ui/common/ui/ui-library/build.gradle.kts b/examples/android/05-widget-ui/common/ui/ui-library/build.gradle.kts
new file mode 100644
index 00000000..8c2fc839
--- /dev/null
+++ b/examples/android/05-widget-ui/common/ui/ui-library/build.gradle.kts
@@ -0,0 +1,3 @@
+uiLibrary(
+ packageName = "tools.forma.examples.android.widgetui.common.ui.library"
+)
diff --git a/examples/android/05-widget-ui/common/ui/ui-library/src/main/AndroidManifest.xml b/examples/android/05-widget-ui/common/ui/ui-library/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/05-widget-ui/common/ui/ui-library/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/05-widget-ui/common/ui/ui-library/src/main/kotlin/tools/forma/examples/android/widgetui/common/ui/library/UiTokens.kt b/examples/android/05-widget-ui/common/ui/ui-library/src/main/kotlin/tools/forma/examples/android/widgetui/common/ui/library/UiTokens.kt
new file mode 100644
index 00000000..6f1be2a6
--- /dev/null
+++ b/examples/android/05-widget-ui/common/ui/ui-library/src/main/kotlin/tools/forma/examples/android/widgetui/common/ui/library/UiTokens.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.widgetui.common.ui.library
+
+object UiTokens {
+ const val paddingDp: Int = 16
+}
diff --git a/examples/android/05-widget-ui/feature/hello/api/build.gradle.kts b/examples/android/05-widget-ui/feature/hello/api/build.gradle.kts
new file mode 100644
index 00000000..41764529
--- /dev/null
+++ b/examples/android/05-widget-ui/feature/hello/api/build.gradle.kts
@@ -0,0 +1,3 @@
+api(
+ packageName = "tools.forma.examples.android.widgetui.feature.hello.api"
+)
diff --git a/examples/android/05-widget-ui/feature/hello/api/src/main/kotlin/tools/forma/examples/android/widgetui/feature/hello/api/HelloApi.kt b/examples/android/05-widget-ui/feature/hello/api/src/main/kotlin/tools/forma/examples/android/widgetui/feature/hello/api/HelloApi.kt
new file mode 100644
index 00000000..40f114ff
--- /dev/null
+++ b/examples/android/05-widget-ui/feature/hello/api/src/main/kotlin/tools/forma/examples/android/widgetui/feature/hello/api/HelloApi.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.android.widgetui.feature.hello.api
+
+interface HelloApi
diff --git a/examples/android/05-widget-ui/feature/hello/impl/build.gradle.kts b/examples/android/05-widget-ui/feature/hello/impl/build.gradle.kts
new file mode 100644
index 00000000..f7acf47c
--- /dev/null
+++ b/examples/android/05-widget-ui/feature/hello/impl/build.gradle.kts
@@ -0,0 +1,8 @@
+impl(
+ packageName = "tools.forma.examples.android.widgetui.feature.hello.impl",
+ dependencies = deps(
+ target(":feature:hello:api"),
+ target(":common:banner:widget"),
+ target(":common:ui:ui-library")
+ )
+)
diff --git a/examples/android/05-widget-ui/feature/hello/impl/src/main/AndroidManifest.xml b/examples/android/05-widget-ui/feature/hello/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/05-widget-ui/feature/hello/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/05-widget-ui/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/widgetui/feature/hello/impl/HelloMarker.kt b/examples/android/05-widget-ui/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/widgetui/feature/hello/impl/HelloMarker.kt
new file mode 100644
index 00000000..9ac40a23
--- /dev/null
+++ b/examples/android/05-widget-ui/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/widgetui/feature/hello/impl/HelloMarker.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.widgetui.feature.hello.impl
+
+import tools.forma.examples.android.widgetui.feature.hello.api.HelloApi
+
+object HelloMarker : HelloApi
diff --git a/examples/android/05-widget-ui/gradle.properties b/examples/android/05-widget-ui/gradle.properties
new file mode 100644
index 00000000..3672d5a4
--- /dev/null
+++ b/examples/android/05-widget-ui/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/05-widget-ui/gradle/wrapper/gradle-wrapper.jar b/examples/android/05-widget-ui/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/05-widget-ui/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/05-widget-ui/gradle/wrapper/gradle-wrapper.properties b/examples/android/05-widget-ui/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/05-widget-ui/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/examples/android/05-widget-ui/gradlew b/examples/android/05-widget-ui/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/05-widget-ui/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/examples/android/05-widget-ui/gradlew.bat b/examples/android/05-widget-ui/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/05-widget-ui/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/examples/android/05-widget-ui/root-app/build.gradle.kts b/examples/android/05-widget-ui/root-app/build.gradle.kts
new file mode 100644
index 00000000..55f1d5c7
--- /dev/null
+++ b/examples/android/05-widget-ui/root-app/build.gradle.kts
@@ -0,0 +1,9 @@
+androidApp(
+ packageName = "tools.forma.examples.android.widgetui.root",
+ dependencies = deps(
+ target(":root-res"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl"),
+ target(":common:banner:widget")
+ )
+)
diff --git a/examples/android/05-widget-ui/root-app/src/main/AndroidManifest.xml b/examples/android/05-widget-ui/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/05-widget-ui/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/05-widget-ui/root-app/src/main/kotlin/tools/forma/examples/android/widgetui/root/HelloActivity.kt b/examples/android/05-widget-ui/root-app/src/main/kotlin/tools/forma/examples/android/widgetui/root/HelloActivity.kt
new file mode 100644
index 00000000..4d719458
--- /dev/null
+++ b/examples/android/05-widget-ui/root-app/src/main/kotlin/tools/forma/examples/android/widgetui/root/HelloActivity.kt
@@ -0,0 +1,12 @@
+package tools.forma.examples.android.widgetui.root
+
+import android.app.Activity
+import android.os.Bundle
+import tools.forma.examples.android.widgetui.common.banner.widget.BannerView
+
+class HelloActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(BannerView(this).also { it.setMessage("Forma Android 05 widget + uiLibrary") })
+ }
+}
diff --git a/examples/android/05-widget-ui/root-res/build.gradle.kts b/examples/android/05-widget-ui/root-res/build.gradle.kts
new file mode 100644
index 00000000..2f9f77ce
--- /dev/null
+++ b/examples/android/05-widget-ui/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.widgetui.res"
+)
diff --git a/examples/android/05-widget-ui/root-res/src/main/res/values/strings.xml b/examples/android/05-widget-ui/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..0a22c510
--- /dev/null
+++ b/examples/android/05-widget-ui/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma 05 widget
+
diff --git a/examples/android/05-widget-ui/settings.gradle.kts b/examples/android/05-widget-ui/settings.gradle.kts
new file mode 100644
index 00000000..79bfacc8
--- /dev/null
+++ b/examples/android/05-widget-ui/settings.gradle.kts
@@ -0,0 +1,21 @@
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "05-widget-ui"
diff --git a/examples/android/06-compose/README.md b/examples/android/06-compose/README.md
new file mode 100644
index 00000000..82c6bc24
--- /dev/null
+++ b/examples/android/06-compose/README.md
@@ -0,0 +1,19 @@
+# 06 — compose (Android)
+
+**Goal:** Jetpack Compose via project default / per-target flags and `composeWidget`.
+
+## Features introduced
+- `androidProjectConfiguration(compose = true, composeCompilerVersion = …)`
+- Per-target `compose = true` on `impl` / `androidApp` / `androidBinary`
+- `composeWidget` (always enables Compose)
+- Compose libraries still declared via `deps(...)` (Forma does not inject artifacts)
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+```
+
+## Next
+07 wires two features without impl→impl.
diff --git a/examples/android/06-compose/binary/build.gradle.kts b/examples/android/06-compose/binary/build.gradle.kts
new file mode 100644
index 00000000..a13214c0
--- /dev/null
+++ b/examples/android/06-compose/binary/build.gradle.kts
@@ -0,0 +1,12 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.compose",
+ versionCode = 1,
+ versionName = "0.1.0",
+ compose = true,
+ dependencies = deps(
+ target(":root-app"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl"),
+ target(":common:greeting:compose-widget")
+ )
+)
diff --git a/examples/android/06-compose/binary/src/main/AndroidManifest.xml b/examples/android/06-compose/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..839c8a97
--- /dev/null
+++ b/examples/android/06-compose/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/06-compose/build.gradle.kts b/examples/android/06-compose/build.gradle.kts
new file mode 100644
index 00000000..2e559d3b
--- /dev/null
+++ b/examples/android/06-compose/build.gradle.kts
@@ -0,0 +1,11 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2",
+ compose = true,
+ composeCompilerVersion = "1.5.3",
+ )
+}
diff --git a/examples/android/06-compose/common/greeting/compose-widget/build.gradle.kts b/examples/android/06-compose/common/greeting/compose-widget/build.gradle.kts
new file mode 100644
index 00000000..10f6aebe
--- /dev/null
+++ b/examples/android/06-compose/common/greeting/compose-widget/build.gradle.kts
@@ -0,0 +1,10 @@
+composeWidget(
+ packageName = "tools.forma.examples.android.compose.common.greeting.compose.widget",
+ dependencies = transitiveDeps(
+ "androidx.compose.runtime:runtime:1.5.4",
+ "androidx.compose.ui:ui:1.5.4",
+ "androidx.compose.foundation:foundation:1.5.4",
+ "androidx.compose.material:material:1.5.4",
+ "androidx.compose.ui:ui-tooling-preview:1.5.4",
+ )
+)
diff --git a/examples/android/06-compose/common/greeting/compose-widget/src/main/AndroidManifest.xml b/examples/android/06-compose/common/greeting/compose-widget/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/06-compose/common/greeting/compose-widget/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/06-compose/common/greeting/compose-widget/src/main/kotlin/tools/forma/examples/android/compose/common/greeting/compose/widget/GreetingCard.kt b/examples/android/06-compose/common/greeting/compose-widget/src/main/kotlin/tools/forma/examples/android/compose/common/greeting/compose/widget/GreetingCard.kt
new file mode 100644
index 00000000..5d36e721
--- /dev/null
+++ b/examples/android/06-compose/common/greeting/compose-widget/src/main/kotlin/tools/forma/examples/android/compose/common/greeting/compose/widget/GreetingCard.kt
@@ -0,0 +1,16 @@
+package tools.forma.examples.android.compose.common.greeting.compose.widget
+
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+
+@Composable
+fun GreetingCard(text: String) {
+ Text(text = text)
+}
+
+@Preview
+@Composable
+private fun PreviewGreeting() {
+ GreetingCard("preview")
+}
diff --git a/examples/android/06-compose/feature/hello/api/build.gradle.kts b/examples/android/06-compose/feature/hello/api/build.gradle.kts
new file mode 100644
index 00000000..80f52324
--- /dev/null
+++ b/examples/android/06-compose/feature/hello/api/build.gradle.kts
@@ -0,0 +1,3 @@
+api(
+ packageName = "tools.forma.examples.android.compose.feature.hello.api"
+)
diff --git a/examples/android/06-compose/feature/hello/api/src/main/kotlin/tools/forma/examples/android/compose/feature/hello/api/HelloApi.kt b/examples/android/06-compose/feature/hello/api/src/main/kotlin/tools/forma/examples/android/compose/feature/hello/api/HelloApi.kt
new file mode 100644
index 00000000..d47111e9
--- /dev/null
+++ b/examples/android/06-compose/feature/hello/api/src/main/kotlin/tools/forma/examples/android/compose/feature/hello/api/HelloApi.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.android.compose.feature.hello.api
+
+interface HelloApi
diff --git a/examples/android/06-compose/feature/hello/impl/build.gradle.kts b/examples/android/06-compose/feature/hello/impl/build.gradle.kts
new file mode 100644
index 00000000..13f2dab6
--- /dev/null
+++ b/examples/android/06-compose/feature/hello/impl/build.gradle.kts
@@ -0,0 +1,14 @@
+impl(
+ packageName = "tools.forma.examples.android.compose.feature.hello.impl",
+ compose = true,
+ dependencies = transitiveDeps(
+ "androidx.compose.runtime:runtime:1.5.4",
+ "androidx.compose.ui:ui:1.5.4",
+ "androidx.compose.foundation:foundation:1.5.4",
+ "androidx.compose.material:material:1.5.4",
+ "androidx.compose.ui:ui-tooling-preview:1.5.4",
+ ) + deps(
+ target(":feature:hello:api"),
+ target(":common:greeting:compose-widget")
+ )
+)
diff --git a/examples/android/06-compose/feature/hello/impl/src/main/AndroidManifest.xml b/examples/android/06-compose/feature/hello/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/06-compose/feature/hello/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/06-compose/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/compose/feature/hello/impl/HelloMarker.kt b/examples/android/06-compose/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/compose/feature/hello/impl/HelloMarker.kt
new file mode 100644
index 00000000..c1951aa0
--- /dev/null
+++ b/examples/android/06-compose/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/compose/feature/hello/impl/HelloMarker.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.compose.feature.hello.impl
+
+import tools.forma.examples.android.compose.feature.hello.api.HelloApi
+
+object HelloMarker : HelloApi
diff --git a/examples/android/06-compose/gradle.properties b/examples/android/06-compose/gradle.properties
new file mode 100644
index 00000000..3672d5a4
--- /dev/null
+++ b/examples/android/06-compose/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/06-compose/gradle/wrapper/gradle-wrapper.jar b/examples/android/06-compose/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/06-compose/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/06-compose/gradle/wrapper/gradle-wrapper.properties b/examples/android/06-compose/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/06-compose/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/examples/android/06-compose/gradlew b/examples/android/06-compose/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/06-compose/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/examples/android/06-compose/gradlew.bat b/examples/android/06-compose/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/06-compose/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/examples/android/06-compose/root-app/build.gradle.kts b/examples/android/06-compose/root-app/build.gradle.kts
new file mode 100644
index 00000000..7620ad81
--- /dev/null
+++ b/examples/android/06-compose/root-app/build.gradle.kts
@@ -0,0 +1,19 @@
+androidApp(
+ packageName = "tools.forma.examples.android.compose.root",
+ compose = true,
+ dependencies = transitiveDeps(
+ "androidx.activity:activity-compose:1.8.0",
+ "androidx.activity:activity-ktx:1.8.0",
+ "androidx.activity:activity:1.8.0",
+ "androidx.compose.runtime:runtime:1.5.4",
+ "androidx.compose.ui:ui:1.5.4",
+ "androidx.compose.foundation:foundation:1.5.4",
+ "androidx.compose.material:material:1.5.4",
+ "androidx.compose.ui:ui-tooling-preview:1.5.4",
+ ) + deps(
+ target(":root-res"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl"),
+ target(":common:greeting:compose-widget")
+ )
+)
diff --git a/examples/android/06-compose/root-app/src/main/AndroidManifest.xml b/examples/android/06-compose/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/06-compose/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/06-compose/root-app/src/main/kotlin/tools/forma/examples/android/compose/root/HelloActivity.kt b/examples/android/06-compose/root-app/src/main/kotlin/tools/forma/examples/android/compose/root/HelloActivity.kt
new file mode 100644
index 00000000..4b2c1f91
--- /dev/null
+++ b/examples/android/06-compose/root-app/src/main/kotlin/tools/forma/examples/android/compose/root/HelloActivity.kt
@@ -0,0 +1,15 @@
+package tools.forma.examples.android.compose.root
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import tools.forma.examples.android.compose.common.greeting.compose.widget.GreetingCard
+
+class HelloActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ GreetingCard(text = "Forma Android 06 compose")
+ }
+ }
+}
diff --git a/examples/android/06-compose/root-res/build.gradle.kts b/examples/android/06-compose/root-res/build.gradle.kts
new file mode 100644
index 00000000..08357980
--- /dev/null
+++ b/examples/android/06-compose/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.compose.res"
+)
diff --git a/examples/android/06-compose/root-res/src/main/res/values/strings.xml b/examples/android/06-compose/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..1da85208
--- /dev/null
+++ b/examples/android/06-compose/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma 06 compose
+
diff --git a/examples/android/06-compose/settings.gradle.kts b/examples/android/06-compose/settings.gradle.kts
new file mode 100644
index 00000000..4f5fcdb3
--- /dev/null
+++ b/examples/android/06-compose/settings.gradle.kts
@@ -0,0 +1,21 @@
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "06-compose"
diff --git a/examples/android/07-multi-feature/README.md b/examples/android/07-multi-feature/README.md
new file mode 100644
index 00000000..7164c22f
--- /dev/null
+++ b/examples/android/07-multi-feature/README.md
@@ -0,0 +1,17 @@
+# 07 — multi-feature (Android)
+
+**Goal:** Two independent features composed at the root. No `impl` → `impl`.
+
+## Features introduced
+- Multi-feature wiring on composition roots
+- Cross-feature collaboration only via `api` (here: composed in `androidApp`)
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+```
+
+## Next
+08 introduces `projectDependencies` catalogs.
diff --git a/examples/android/07-multi-feature/binary/build.gradle.kts b/examples/android/07-multi-feature/binary/build.gradle.kts
new file mode 100644
index 00000000..a97b69e6
--- /dev/null
+++ b/examples/android/07-multi-feature/binary/build.gradle.kts
@@ -0,0 +1,12 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.multi",
+ versionCode = 1,
+ versionName = "0.1.0",
+ dependencies = deps(
+ target(":root-app"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl"),
+ target(":feature:settings:api"),
+ target(":feature:settings:impl")
+ )
+)
diff --git a/examples/android/07-multi-feature/binary/src/main/AndroidManifest.xml b/examples/android/07-multi-feature/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..839c8a97
--- /dev/null
+++ b/examples/android/07-multi-feature/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/07-multi-feature/build.gradle.kts b/examples/android/07-multi-feature/build.gradle.kts
new file mode 100644
index 00000000..15759185
--- /dev/null
+++ b/examples/android/07-multi-feature/build.gradle.kts
@@ -0,0 +1,9 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2",
+ )
+}
diff --git a/examples/android/07-multi-feature/feature/hello/api/build.gradle.kts b/examples/android/07-multi-feature/feature/hello/api/build.gradle.kts
new file mode 100644
index 00000000..45ade5ee
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/hello/api/build.gradle.kts
@@ -0,0 +1,3 @@
+api(
+ packageName = "tools.forma.examples.android.multi.feature.hello.api"
+)
diff --git a/examples/android/07-multi-feature/feature/hello/api/src/main/kotlin/tools/forma/examples/android/multi/feature/hello/api/HelloApi.kt b/examples/android/07-multi-feature/feature/hello/api/src/main/kotlin/tools/forma/examples/android/multi/feature/hello/api/HelloApi.kt
new file mode 100644
index 00000000..f0a73720
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/hello/api/src/main/kotlin/tools/forma/examples/android/multi/feature/hello/api/HelloApi.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.multi.feature.hello.api
+
+interface HelloApi {
+ fun greet(): String
+}
diff --git a/examples/android/07-multi-feature/feature/hello/impl/build.gradle.kts b/examples/android/07-multi-feature/feature/hello/impl/build.gradle.kts
new file mode 100644
index 00000000..53c4af89
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/hello/impl/build.gradle.kts
@@ -0,0 +1,6 @@
+impl(
+ packageName = "tools.forma.examples.android.multi.feature.hello.impl",
+ dependencies = deps(
+ target(":feature:hello:api")
+ )
+)
diff --git a/examples/android/07-multi-feature/feature/hello/impl/src/main/AndroidManifest.xml b/examples/android/07-multi-feature/feature/hello/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/hello/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/07-multi-feature/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/multi/feature/hello/impl/HelloImpl.kt b/examples/android/07-multi-feature/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/multi/feature/hello/impl/HelloImpl.kt
new file mode 100644
index 00000000..0cbd8fb7
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/multi/feature/hello/impl/HelloImpl.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.android.multi.feature.hello.impl
+
+import tools.forma.examples.android.multi.feature.hello.api.HelloApi
+
+class HelloImpl : HelloApi {
+ override fun greet() = "hello-feature"
+}
diff --git a/examples/android/07-multi-feature/feature/settings/api/build.gradle.kts b/examples/android/07-multi-feature/feature/settings/api/build.gradle.kts
new file mode 100644
index 00000000..af7f08f5
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/settings/api/build.gradle.kts
@@ -0,0 +1,3 @@
+api(
+ packageName = "tools.forma.examples.android.multi.feature.settings.api"
+)
diff --git a/examples/android/07-multi-feature/feature/settings/api/src/main/kotlin/tools/forma/examples/android/multi/feature/settings/api/SettingsApi.kt b/examples/android/07-multi-feature/feature/settings/api/src/main/kotlin/tools/forma/examples/android/multi/feature/settings/api/SettingsApi.kt
new file mode 100644
index 00000000..b3f5d262
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/settings/api/src/main/kotlin/tools/forma/examples/android/multi/feature/settings/api/SettingsApi.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.multi.feature.settings.api
+
+interface SettingsApi {
+ fun themeName(): String
+}
diff --git a/examples/android/07-multi-feature/feature/settings/impl/build.gradle.kts b/examples/android/07-multi-feature/feature/settings/impl/build.gradle.kts
new file mode 100644
index 00000000..bfaf23dc
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/settings/impl/build.gradle.kts
@@ -0,0 +1,6 @@
+impl(
+ packageName = "tools.forma.examples.android.multi.feature.settings.impl",
+ dependencies = deps(
+ target(":feature:settings:api")
+ )
+)
diff --git a/examples/android/07-multi-feature/feature/settings/impl/src/main/AndroidManifest.xml b/examples/android/07-multi-feature/feature/settings/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/settings/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/07-multi-feature/feature/settings/impl/src/main/kotlin/tools/forma/examples/android/multi/feature/settings/impl/SettingsImpl.kt b/examples/android/07-multi-feature/feature/settings/impl/src/main/kotlin/tools/forma/examples/android/multi/feature/settings/impl/SettingsImpl.kt
new file mode 100644
index 00000000..fb009f13
--- /dev/null
+++ b/examples/android/07-multi-feature/feature/settings/impl/src/main/kotlin/tools/forma/examples/android/multi/feature/settings/impl/SettingsImpl.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.android.multi.feature.settings.impl
+
+import tools.forma.examples.android.multi.feature.settings.api.SettingsApi
+
+class SettingsImpl : SettingsApi {
+ override fun themeName() = "settings-feature"
+}
diff --git a/examples/android/07-multi-feature/gradle.properties b/examples/android/07-multi-feature/gradle.properties
new file mode 100644
index 00000000..3672d5a4
--- /dev/null
+++ b/examples/android/07-multi-feature/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/07-multi-feature/gradle/wrapper/gradle-wrapper.jar b/examples/android/07-multi-feature/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/07-multi-feature/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/07-multi-feature/gradle/wrapper/gradle-wrapper.properties b/examples/android/07-multi-feature/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/07-multi-feature/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/examples/android/07-multi-feature/gradlew b/examples/android/07-multi-feature/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/07-multi-feature/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/examples/android/07-multi-feature/gradlew.bat b/examples/android/07-multi-feature/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/07-multi-feature/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/examples/android/07-multi-feature/root-app/build.gradle.kts b/examples/android/07-multi-feature/root-app/build.gradle.kts
new file mode 100644
index 00000000..499ac572
--- /dev/null
+++ b/examples/android/07-multi-feature/root-app/build.gradle.kts
@@ -0,0 +1,10 @@
+androidApp(
+ packageName = "tools.forma.examples.android.multi.root",
+ dependencies = deps(
+ target(":root-res"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl"),
+ target(":feature:settings:api"),
+ target(":feature:settings:impl")
+ )
+)
diff --git a/examples/android/07-multi-feature/root-app/src/main/AndroidManifest.xml b/examples/android/07-multi-feature/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/07-multi-feature/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/07-multi-feature/root-app/src/main/kotlin/tools/forma/examples/android/multi/root/HelloActivity.kt b/examples/android/07-multi-feature/root-app/src/main/kotlin/tools/forma/examples/android/multi/root/HelloActivity.kt
new file mode 100644
index 00000000..343dd2f2
--- /dev/null
+++ b/examples/android/07-multi-feature/root-app/src/main/kotlin/tools/forma/examples/android/multi/root/HelloActivity.kt
@@ -0,0 +1,20 @@
+package tools.forma.examples.android.multi.root
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import tools.forma.examples.android.multi.feature.hello.api.HelloApi
+import tools.forma.examples.android.multi.feature.hello.impl.HelloImpl
+import tools.forma.examples.android.multi.feature.settings.api.SettingsApi
+import tools.forma.examples.android.multi.feature.settings.impl.SettingsImpl
+
+class HelloActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val hello: HelloApi = HelloImpl()
+ val settings: SettingsApi = SettingsImpl()
+ val tv = TextView(this)
+ tv.text = hello.greet() + " | " + settings.themeName()
+ setContentView(tv)
+ }
+}
diff --git a/examples/android/07-multi-feature/root-res/build.gradle.kts b/examples/android/07-multi-feature/root-res/build.gradle.kts
new file mode 100644
index 00000000..89f78f70
--- /dev/null
+++ b/examples/android/07-multi-feature/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.multi.res"
+)
diff --git a/examples/android/07-multi-feature/root-res/src/main/res/values/strings.xml b/examples/android/07-multi-feature/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..a6be752a
--- /dev/null
+++ b/examples/android/07-multi-feature/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma 07 multi
+
diff --git a/examples/android/07-multi-feature/settings.gradle.kts b/examples/android/07-multi-feature/settings.gradle.kts
new file mode 100644
index 00000000..6d30ac95
--- /dev/null
+++ b/examples/android/07-multi-feature/settings.gradle.kts
@@ -0,0 +1,21 @@
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "07-multi-feature"
diff --git a/examples/android/08-deps-catalog/README.md b/examples/android/08-deps-catalog/README.md
new file mode 100644
index 00000000..9720285b
--- /dev/null
+++ b/examples/android/08-deps-catalog/README.md
@@ -0,0 +1,20 @@
+# 08 — deps-catalog (Android)
+
+**Goal:** External dependency catalogs via `projectDependencies`.
+
+## Features introduced
+- Bare GAV → auto-named accessor (`libs.jakewhartonTimber`)
+- `library(gav, name = …)` stable names
+- `bundle(name, …)` → `libs.bundles.*`
+- `plugin(id, version)` catalog entry (declared; optional apply)
+- Consuming catalogs with `deps(libs…)`
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+```
+
+## Next
+09 adds `testUtil` + `androidTestUtil` (and documents `androidNative`).
diff --git a/examples/android/08-deps-catalog/binary/build.gradle.kts b/examples/android/08-deps-catalog/binary/build.gradle.kts
new file mode 100644
index 00000000..a2716786
--- /dev/null
+++ b/examples/android/08-deps-catalog/binary/build.gradle.kts
@@ -0,0 +1,10 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.catalog",
+ versionCode = 1,
+ versionName = "0.1.0",
+ dependencies = deps(
+ target(":root-app"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/08-deps-catalog/binary/src/main/AndroidManifest.xml b/examples/android/08-deps-catalog/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..839c8a97
--- /dev/null
+++ b/examples/android/08-deps-catalog/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/08-deps-catalog/build.gradle.kts b/examples/android/08-deps-catalog/build.gradle.kts
new file mode 100644
index 00000000..15759185
--- /dev/null
+++ b/examples/android/08-deps-catalog/build.gradle.kts
@@ -0,0 +1,9 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2",
+ )
+}
diff --git a/examples/android/08-deps-catalog/feature/hello/api/build.gradle.kts b/examples/android/08-deps-catalog/feature/hello/api/build.gradle.kts
new file mode 100644
index 00000000..a2f29744
--- /dev/null
+++ b/examples/android/08-deps-catalog/feature/hello/api/build.gradle.kts
@@ -0,0 +1,3 @@
+api(
+ packageName = "tools.forma.examples.android.catalog.feature.hello.api"
+)
diff --git a/examples/android/08-deps-catalog/feature/hello/api/src/main/kotlin/tools/forma/examples/android/catalog/feature/hello/api/HelloApi.kt b/examples/android/08-deps-catalog/feature/hello/api/src/main/kotlin/tools/forma/examples/android/catalog/feature/hello/api/HelloApi.kt
new file mode 100644
index 00000000..a2d553cd
--- /dev/null
+++ b/examples/android/08-deps-catalog/feature/hello/api/src/main/kotlin/tools/forma/examples/android/catalog/feature/hello/api/HelloApi.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.android.catalog.feature.hello.api
+
+interface HelloApi
diff --git a/examples/android/08-deps-catalog/feature/hello/impl/build.gradle.kts b/examples/android/08-deps-catalog/feature/hello/impl/build.gradle.kts
new file mode 100644
index 00000000..11a74c4e
--- /dev/null
+++ b/examples/android/08-deps-catalog/feature/hello/impl/build.gradle.kts
@@ -0,0 +1,8 @@
+impl(
+ packageName = "tools.forma.examples.android.catalog.feature.hello.impl",
+ dependencies = deps(
+ libs.jakewhartonTimber,
+ ) + deps(
+ target(":feature:hello:api")
+ )
+)
diff --git a/examples/android/08-deps-catalog/feature/hello/impl/src/main/AndroidManifest.xml b/examples/android/08-deps-catalog/feature/hello/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/08-deps-catalog/feature/hello/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/08-deps-catalog/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/catalog/feature/hello/impl/HelloLogger.kt b/examples/android/08-deps-catalog/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/catalog/feature/hello/impl/HelloLogger.kt
new file mode 100644
index 00000000..32f51b4e
--- /dev/null
+++ b/examples/android/08-deps-catalog/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/catalog/feature/hello/impl/HelloLogger.kt
@@ -0,0 +1,10 @@
+package tools.forma.examples.android.catalog.feature.hello.impl
+
+import timber.log.Timber
+import tools.forma.examples.android.catalog.feature.hello.api.HelloApi
+
+object HelloLogger : HelloApi {
+ fun log(msg: String) {
+ Timber.d("hello: %s", msg)
+ }
+}
diff --git a/examples/android/08-deps-catalog/gradle.properties b/examples/android/08-deps-catalog/gradle.properties
new file mode 100644
index 00000000..3672d5a4
--- /dev/null
+++ b/examples/android/08-deps-catalog/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/08-deps-catalog/gradle/wrapper/gradle-wrapper.jar b/examples/android/08-deps-catalog/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/08-deps-catalog/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/08-deps-catalog/gradle/wrapper/gradle-wrapper.properties b/examples/android/08-deps-catalog/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/08-deps-catalog/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/examples/android/08-deps-catalog/gradlew b/examples/android/08-deps-catalog/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/08-deps-catalog/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/examples/android/08-deps-catalog/gradlew.bat b/examples/android/08-deps-catalog/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/08-deps-catalog/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/examples/android/08-deps-catalog/root-app/build.gradle.kts b/examples/android/08-deps-catalog/root-app/build.gradle.kts
new file mode 100644
index 00000000..8ce40b99
--- /dev/null
+++ b/examples/android/08-deps-catalog/root-app/build.gradle.kts
@@ -0,0 +1,12 @@
+androidApp(
+ packageName = "tools.forma.examples.android.catalog.root",
+ dependencies = deps(
+ libs.jakewhartonTimber,
+ libs.coroutines,
+ libs.bundles.logging,
+ ) + deps(
+ target(":root-res"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/08-deps-catalog/root-app/src/main/AndroidManifest.xml b/examples/android/08-deps-catalog/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/08-deps-catalog/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/08-deps-catalog/root-app/src/main/kotlin/tools/forma/examples/android/catalog/root/HelloActivity.kt b/examples/android/08-deps-catalog/root-app/src/main/kotlin/tools/forma/examples/android/catalog/root/HelloActivity.kt
new file mode 100644
index 00000000..2c344f62
--- /dev/null
+++ b/examples/android/08-deps-catalog/root-app/src/main/kotlin/tools/forma/examples/android/catalog/root/HelloActivity.kt
@@ -0,0 +1,18 @@
+package tools.forma.examples.android.catalog.root
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import timber.log.Timber
+import tools.forma.examples.android.catalog.feature.hello.impl.HelloLogger
+
+class HelloActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Timber.plant(Timber.DebugTree())
+ HelloLogger.log("catalog step")
+ val tv = TextView(this)
+ tv.text = "Forma Android 08 deps catalog"
+ setContentView(tv)
+ }
+}
diff --git a/examples/android/08-deps-catalog/root-res/build.gradle.kts b/examples/android/08-deps-catalog/root-res/build.gradle.kts
new file mode 100644
index 00000000..ab17d3f7
--- /dev/null
+++ b/examples/android/08-deps-catalog/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.catalog.res"
+)
diff --git a/examples/android/08-deps-catalog/root-res/src/main/res/values/strings.xml b/examples/android/08-deps-catalog/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..3ff9a5ed
--- /dev/null
+++ b/examples/android/08-deps-catalog/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma 08 catalog
+
diff --git a/examples/android/08-deps-catalog/settings.gradle.kts b/examples/android/08-deps-catalog/settings.gradle.kts
new file mode 100644
index 00000000..8882a5b6
--- /dev/null
+++ b/examples/android/08-deps-catalog/settings.gradle.kts
@@ -0,0 +1,37 @@
+import tools.forma.deps.catalog.bundle
+import tools.forma.deps.catalog.library
+import tools.forma.deps.catalog.plugin
+import tools.forma.deps.catalog.projectDependencies
+
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "08-deps-catalog"
+
+projectDependencies(
+ "libs",
+ "com.jakewharton.timber:timber:5.0.1",
+ library("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3", name = "coroutines"),
+ bundle(
+ name = "logging",
+ "com.jakewharton.timber:timber:5.0.1",
+ ),
+ plugin("androidx.navigation:navigation-safe-args-gradle-plugin", "2.7.4"),
+)
diff --git a/examples/android/09-test-utils/README.md b/examples/android/09-test-utils/README.md
new file mode 100644
index 00000000..d1cde8c5
--- /dev/null
+++ b/examples/android/09-test-utils/README.md
@@ -0,0 +1,20 @@
+# 09 — test-utils (Android)
+
+**Goal:** Shared unit / instrumentation test helpers.
+
+## Features introduced
+- `testUtil` (shared unit-test code, no res/)
+- `androidTestUtil` (shared Android test code)
+- `testDependencies` / `androidTestDependencies` on `impl`
+
+## androidNative (documented)
+`androidNative` exists for NDK modules (suffix `native`). It has **no project-dep validation yet**.
+Not required for this step; see agent skill `forma-android-targets`.
+
+## Build
+```bash
+source ../../../scripts/env-mac.sh
+printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
+./gradlew :binary:assembleDebug
+./gradlew :feature-hello-impl:testDebugUnitTest
+```
diff --git a/examples/android/09-test-utils/binary/build.gradle.kts b/examples/android/09-test-utils/binary/build.gradle.kts
new file mode 100644
index 00000000..86cd61a3
--- /dev/null
+++ b/examples/android/09-test-utils/binary/build.gradle.kts
@@ -0,0 +1,10 @@
+androidBinary(
+ packageName = "tools.forma.examples.android.testutils",
+ versionCode = 1,
+ versionName = "0.1.0",
+ dependencies = deps(
+ target(":root-app"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/09-test-utils/binary/src/main/AndroidManifest.xml b/examples/android/09-test-utils/binary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..839c8a97
--- /dev/null
+++ b/examples/android/09-test-utils/binary/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/android/09-test-utils/build.gradle.kts b/examples/android/09-test-utils/build.gradle.kts
new file mode 100644
index 00000000..15759185
--- /dev/null
+++ b/examples/android/09-test-utils/build.gradle.kts
@@ -0,0 +1,9 @@
+buildscript {
+ androidProjectConfiguration(
+ project = rootProject,
+ minSdk = 21,
+ targetSdk = 33,
+ compileSdk = 34,
+ agpVersion = "8.1.2",
+ )
+}
diff --git a/examples/android/09-test-utils/common/android-test-util/build.gradle.kts b/examples/android/09-test-utils/common/android-test-util/build.gradle.kts
new file mode 100644
index 00000000..df6d0949
--- /dev/null
+++ b/examples/android/09-test-utils/common/android-test-util/build.gradle.kts
@@ -0,0 +1,6 @@
+androidTestUtil(
+ packageName = "tools.forma.examples.android.testutils.common.androidtestutil",
+ dependencies = deps(
+ target(":common:test-util")
+ )
+)
diff --git a/examples/android/09-test-utils/common/android-test-util/src/main/AndroidManifest.xml b/examples/android/09-test-utils/common/android-test-util/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/09-test-utils/common/android-test-util/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/09-test-utils/common/android-test-util/src/main/kotlin/tools/forma/examples/android/testutils/common/androidtestutil/AndroidChecks.kt b/examples/android/09-test-utils/common/android-test-util/src/main/kotlin/tools/forma/examples/android/testutils/common/androidtestutil/AndroidChecks.kt
new file mode 100644
index 00000000..47679b6b
--- /dev/null
+++ b/examples/android/09-test-utils/common/android-test-util/src/main/kotlin/tools/forma/examples/android/testutils/common/androidtestutil/AndroidChecks.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.android.testutils.common.androidtestutil
+
+import tools.forma.examples.android.testutils.common.testutil.assertPositive
+
+object AndroidChecks {
+ fun touchPositive(v: Int) = assertPositive(v)
+}
diff --git a/examples/android/09-test-utils/common/test-util/build.gradle.kts b/examples/android/09-test-utils/common/test-util/build.gradle.kts
new file mode 100644
index 00000000..ea21332d
--- /dev/null
+++ b/examples/android/09-test-utils/common/test-util/build.gradle.kts
@@ -0,0 +1,3 @@
+testUtil(
+ packageName = "tools.forma.examples.android.testutils.common.testutil"
+)
diff --git a/examples/android/09-test-utils/common/test-util/src/main/kotlin/tools/forma/examples/android/testutils/common/testutil/AssertHelpers.kt b/examples/android/09-test-utils/common/test-util/src/main/kotlin/tools/forma/examples/android/testutils/common/testutil/AssertHelpers.kt
new file mode 100644
index 00000000..fe153c66
--- /dev/null
+++ b/examples/android/09-test-utils/common/test-util/src/main/kotlin/tools/forma/examples/android/testutils/common/testutil/AssertHelpers.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.android.testutils.common.testutil
+
+fun assertPositive(value: Int) {
+ check(value > 0) { "expected positive, got $value" }
+}
diff --git a/examples/android/09-test-utils/feature/hello/api/build.gradle.kts b/examples/android/09-test-utils/feature/hello/api/build.gradle.kts
new file mode 100644
index 00000000..bcffcabb
--- /dev/null
+++ b/examples/android/09-test-utils/feature/hello/api/build.gradle.kts
@@ -0,0 +1,3 @@
+api(
+ packageName = "tools.forma.examples.android.testutils.feature.hello.api"
+)
diff --git a/examples/android/09-test-utils/feature/hello/api/src/main/kotlin/tools/forma/examples/android/testutils/feature/hello/api/HelloApi.kt b/examples/android/09-test-utils/feature/hello/api/src/main/kotlin/tools/forma/examples/android/testutils/feature/hello/api/HelloApi.kt
new file mode 100644
index 00000000..2b11d0c4
--- /dev/null
+++ b/examples/android/09-test-utils/feature/hello/api/src/main/kotlin/tools/forma/examples/android/testutils/feature/hello/api/HelloApi.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.android.testutils.feature.hello.api
+
+interface HelloApi
diff --git a/examples/android/09-test-utils/feature/hello/impl/build.gradle.kts b/examples/android/09-test-utils/feature/hello/impl/build.gradle.kts
new file mode 100644
index 00000000..9444c988
--- /dev/null
+++ b/examples/android/09-test-utils/feature/hello/impl/build.gradle.kts
@@ -0,0 +1,12 @@
+impl(
+ packageName = "tools.forma.examples.android.testutils.feature.hello.impl",
+ dependencies = deps(
+ target(":feature:hello:api"),
+ // Allowed by matrix: impl may depend on test-util (shared helpers visible to unit tests).
+ target(":common:test-util")
+ ),
+ // deps() is non-transitive; junit needs hamcrest → use transitiveDeps
+ testDependencies = transitiveDeps(
+ "junit:junit:4.13.2"
+ )
+)
diff --git a/examples/android/09-test-utils/feature/hello/impl/src/main/AndroidManifest.xml b/examples/android/09-test-utils/feature/hello/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/examples/android/09-test-utils/feature/hello/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/android/09-test-utils/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/testutils/feature/hello/impl/Adder.kt b/examples/android/09-test-utils/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/testutils/feature/hello/impl/Adder.kt
new file mode 100644
index 00000000..5762e9f6
--- /dev/null
+++ b/examples/android/09-test-utils/feature/hello/impl/src/main/kotlin/tools/forma/examples/android/testutils/feature/hello/impl/Adder.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.android.testutils.feature.hello.impl
+
+import tools.forma.examples.android.testutils.feature.hello.api.HelloApi
+
+object Adder : HelloApi {
+ fun add(a: Int, b: Int): Int = a + b
+}
diff --git a/examples/android/09-test-utils/feature/hello/impl/src/test/kotlin/tools/forma/examples/android/testutils/feature/hello/impl/AdderTest.kt b/examples/android/09-test-utils/feature/hello/impl/src/test/kotlin/tools/forma/examples/android/testutils/feature/hello/impl/AdderTest.kt
new file mode 100644
index 00000000..fc09194a
--- /dev/null
+++ b/examples/android/09-test-utils/feature/hello/impl/src/test/kotlin/tools/forma/examples/android/testutils/feature/hello/impl/AdderTest.kt
@@ -0,0 +1,14 @@
+package tools.forma.examples.android.testutils.feature.hello.impl
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import tools.forma.examples.android.testutils.common.testutil.assertPositive
+
+class AdderTest {
+ @Test
+ fun adds() {
+ val sum = Adder.add(2, 3)
+ assertPositive(sum)
+ assertEquals(5, sum)
+ }
+}
diff --git a/examples/android/09-test-utils/gradle.properties b/examples/android/09-test-utils/gradle.properties
new file mode 100644
index 00000000..3672d5a4
--- /dev/null
+++ b/examples/android/09-test-utils/gradle.properties
@@ -0,0 +1,10 @@
+# Progressive Android example
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+android.useAndroidX=true
+android.enableJetifier=false
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false
diff --git a/examples/android/09-test-utils/gradle/wrapper/gradle-wrapper.jar b/examples/android/09-test-utils/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/android/09-test-utils/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/09-test-utils/gradle/wrapper/gradle-wrapper.properties b/examples/android/09-test-utils/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/android/09-test-utils/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/examples/android/09-test-utils/gradlew b/examples/android/09-test-utils/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/android/09-test-utils/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/examples/android/09-test-utils/gradlew.bat b/examples/android/09-test-utils/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/android/09-test-utils/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/examples/android/09-test-utils/root-app/build.gradle.kts b/examples/android/09-test-utils/root-app/build.gradle.kts
new file mode 100644
index 00000000..515ec94d
--- /dev/null
+++ b/examples/android/09-test-utils/root-app/build.gradle.kts
@@ -0,0 +1,8 @@
+androidApp(
+ packageName = "tools.forma.examples.android.testutils.root",
+ dependencies = deps(
+ target(":root-res"),
+ target(":feature:hello:api"),
+ target(":feature:hello:impl")
+ )
+)
diff --git a/examples/android/09-test-utils/root-app/src/main/AndroidManifest.xml b/examples/android/09-test-utils/root-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fff2a820
--- /dev/null
+++ b/examples/android/09-test-utils/root-app/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/09-test-utils/root-app/src/main/kotlin/tools/forma/examples/android/testutils/root/HelloActivity.kt b/examples/android/09-test-utils/root-app/src/main/kotlin/tools/forma/examples/android/testutils/root/HelloActivity.kt
new file mode 100644
index 00000000..f037a1de
--- /dev/null
+++ b/examples/android/09-test-utils/root-app/src/main/kotlin/tools/forma/examples/android/testutils/root/HelloActivity.kt
@@ -0,0 +1,15 @@
+package tools.forma.examples.android.testutils.root
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import tools.forma.examples.android.testutils.feature.hello.impl.Adder
+
+class HelloActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val tv = TextView(this)
+ tv.text = "Forma Android 09 test utils sum=" + Adder.add(2, 3)
+ setContentView(tv)
+ }
+}
diff --git a/examples/android/09-test-utils/root-res/build.gradle.kts b/examples/android/09-test-utils/root-res/build.gradle.kts
new file mode 100644
index 00000000..7dbdfe9b
--- /dev/null
+++ b/examples/android/09-test-utils/root-res/build.gradle.kts
@@ -0,0 +1,3 @@
+androidRes(
+ packageName = "tools.forma.examples.android.testutils.res"
+)
diff --git a/examples/android/09-test-utils/root-res/src/main/res/values/strings.xml b/examples/android/09-test-utils/root-res/src/main/res/values/strings.xml
new file mode 100644
index 00000000..716c4d3f
--- /dev/null
+++ b/examples/android/09-test-utils/root-res/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Forma 09 tests
+
diff --git a/examples/android/09-test-utils/settings.gradle.kts b/examples/android/09-test-utils/settings.gradle.kts
new file mode 100644
index 00000000..9a4c120c
--- /dev/null
+++ b/examples/android/09-test-utils/settings.gradle.kts
@@ -0,0 +1,21 @@
+pluginManagement {
+ repositories {
+ google()
+ 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.android")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "09-test-utils"
diff --git a/examples/jvm/01-hello-binary/README.md b/examples/jvm/01-hello-binary/README.md
new file mode 100644
index 00000000..3859b16c
--- /dev/null
+++ b/examples/jvm/01-hello-binary/README.md
@@ -0,0 +1,41 @@
+# 01 — hello-binary (JVM)
+
+**Goal:** Minimal runnable JVM binary using Forma. Introduces `binary` target, includer, composite `includeBuild`.
+
+## Features introduced
+- `binary` (composition root + `application` plugin → `:binary:run`)
+- `tools.forma.jvm` plugin via settings
+- Includer with `arbitraryBuildScriptNames`
+- Composite includeBuild to repo `plugins` + `includer` + `build-settings`
+
+## Project tree
+```
+01-hello-binary/
+├── settings.gradle.kts
+├── build.gradle.kts
+├── gradle.properties
+├── gradlew, gradle/wrapper/
+└── binary/
+ ├── build.gradle.kts
+ └── src/main/kotlin/tools/forma/examples/jvm/hello/binary/Main.kt
+```
+
+## Build & run (from this dir)
+```bash
+source ../../../scripts/env-mac.sh
+./gradlew build
+./gradlew :binary:run
+```
+
+Expected:
+```
+Hello from Forma JVM progressive example 01 (binary only)
+
+BUILD SUCCESSFUL
+```
+
+## What changed vs nothing
+First use of `binary(...)` DSL. No feature slices yet. No deps. Package name matches directory structure exactly.
+
+## Next
+See `02-api-impl` for first feature split (`api` + `impl`).
diff --git a/examples/jvm/01-hello-binary/binary/build.gradle.kts b/examples/jvm/01-hello-binary/binary/build.gradle.kts
new file mode 100644
index 00000000..808638ee
--- /dev/null
+++ b/examples/jvm/01-hello-binary/binary/build.gradle.kts
@@ -0,0 +1,6 @@
+import tools.forma.jvm.binary
+
+binary(
+ packageName = "tools.forma.examples.jvm.hello.binary",
+ mainClass = "tools.forma.examples.jvm.hello.binary.MainKt"
+)
diff --git a/examples/jvm/01-hello-binary/binary/src/main/kotlin/tools/forma/examples/jvm/hello/binary/Main.kt b/examples/jvm/01-hello-binary/binary/src/main/kotlin/tools/forma/examples/jvm/hello/binary/Main.kt
new file mode 100644
index 00000000..0a502e2d
--- /dev/null
+++ b/examples/jvm/01-hello-binary/binary/src/main/kotlin/tools/forma/examples/jvm/hello/binary/Main.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.jvm.hello.binary
+
+fun main() {
+ println("Hello from Forma JVM progressive example 01 (binary only)")
+}
diff --git a/examples/jvm/01-hello-binary/build.gradle.kts b/examples/jvm/01-hello-binary/build.gradle.kts
new file mode 100644
index 00000000..64ae811b
--- /dev/null
+++ b/examples/jvm/01-hello-binary/build.gradle.kts
@@ -0,0 +1,2 @@
+// Root for progressive JVM example 01: minimal binary only.
+// tools.forma.jvm plugin + includer discover sub-targets.
diff --git a/examples/jvm/01-hello-binary/gradle.properties b/examples/jvm/01-hello-binary/gradle.properties
new file mode 100644
index 00000000..578b5c7a
--- /dev/null
+++ b/examples/jvm/01-hello-binary/gradle.properties
@@ -0,0 +1,5 @@
+# Progressive JVM example 01
+org.gradle.jvmargs=-Xmx1024m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+kotlin.code.style=official
diff --git a/examples/jvm/01-hello-binary/gradle/wrapper/gradle-wrapper.jar b/examples/jvm/01-hello-binary/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/jvm/01-hello-binary/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/jvm/01-hello-binary/gradle/wrapper/gradle-wrapper.properties b/examples/jvm/01-hello-binary/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/jvm/01-hello-binary/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/examples/jvm/01-hello-binary/gradlew b/examples/jvm/01-hello-binary/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/jvm/01-hello-binary/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/examples/jvm/01-hello-binary/gradlew.bat b/examples/jvm/01-hello-binary/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/jvm/01-hello-binary/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/examples/jvm/01-hello-binary/settings.gradle.kts b/examples/jvm/01-hello-binary/settings.gradle.kts
new file mode 100644
index 00000000..683b2bf0
--- /dev/null
+++ b/examples/jvm/01-hello-binary/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 = "01-hello-binary"
diff --git a/examples/jvm/02-api-impl/README.md b/examples/jvm/02-api-impl/README.md
new file mode 100644
index 00000000..67302fca
--- /dev/null
+++ b/examples/jvm/02-api-impl/README.md
@@ -0,0 +1,41 @@
+# 02 — api-impl (JVM)
+
+**Goal:** Split a feature into `api` (contract) + `impl`. Binary is the composition root that lists both.
+
+## Features introduced
+- `api` target (public contracts, no resources)
+- `impl` target (depends on its `api`)
+- `binary` lists **api + impl** explicitly
+- **Key rule:** impls never depend on other impls (enforced by validator)
+
+## Project tree
+```
+02-api-impl/
+├── binary/
+│ ├── build.gradle.kts
+│ └── src/.../Main.kt
+├── feature/greeter/
+│ ├── api/
+│ │ ├── build.gradle.kts
+│ │ └── src/.../Greeter.kt
+│ └── impl/
+│ ├── build.gradle.kts
+│ └── src/.../RealGreeter.kt
+└── ...
+```
+
+## Build & run
+```bash
+source ../../../scripts/env-mac.sh
+./gradlew build
+./gradlew :binary:run
+```
+
+## Changes vs 01
+- Introduced feature slice under `feature/`
+- `target(":feature:greeter:api")` colon paths (includer + Forma)
+- Composition root explicitly wires api+impl
+- Package names match source roots
+
+## Next
+03 adds `library` + `util`.
diff --git a/examples/jvm/02-api-impl/binary/build.gradle.kts b/examples/jvm/02-api-impl/binary/build.gradle.kts
new file mode 100644
index 00000000..7f9b52e1
--- /dev/null
+++ b/examples/jvm/02-api-impl/binary/build.gradle.kts
@@ -0,0 +1,10 @@
+import tools.forma.jvm.binary
+
+binary(
+ packageName = "tools.forma.examples.jvm.apiimpl.binary",
+ mainClass = "tools.forma.examples.jvm.apiimpl.binary.MainKt",
+ dependencies = deps(
+ target(":feature:greeter:api"),
+ target(":feature:greeter:impl")
+ )
+)
diff --git a/examples/jvm/02-api-impl/binary/src/main/kotlin/tools/forma/examples/jvm/apiimpl/binary/Main.kt b/examples/jvm/02-api-impl/binary/src/main/kotlin/tools/forma/examples/jvm/apiimpl/binary/Main.kt
new file mode 100644
index 00000000..7d04ce6f
--- /dev/null
+++ b/examples/jvm/02-api-impl/binary/src/main/kotlin/tools/forma/examples/jvm/apiimpl/binary/Main.kt
@@ -0,0 +1,10 @@
+package tools.forma.examples.jvm.apiimpl.binary
+
+import tools.forma.examples.jvm.apiimpl.feature.greeter.api.Greeter
+import tools.forma.examples.jvm.apiimpl.feature.greeter.impl.RealGreeter
+
+fun main() {
+ val greeter: Greeter = RealGreeter()
+ println(greeter.greet("World"))
+ println("JVM 02 (api+impl) build + run successful.")
+}
diff --git a/examples/jvm/02-api-impl/build.gradle.kts b/examples/jvm/02-api-impl/build.gradle.kts
new file mode 100644
index 00000000..617d50b3
--- /dev/null
+++ b/examples/jvm/02-api-impl/build.gradle.kts
@@ -0,0 +1,2 @@
+// Progressive JVM example 02: api + impl feature slice + binary composition root.
+// Composition root (binary) explicitly lists api + impl.
diff --git a/examples/jvm/02-api-impl/feature/greeter/api/build.gradle.kts b/examples/jvm/02-api-impl/feature/greeter/api/build.gradle.kts
new file mode 100644
index 00000000..665d2f25
--- /dev/null
+++ b/examples/jvm/02-api-impl/feature/greeter/api/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.api
+
+api(packageName = "tools.forma.examples.jvm.apiimpl.feature.greeter.api")
diff --git a/examples/jvm/02-api-impl/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/apiimpl/feature/greeter/api/Greeter.kt b/examples/jvm/02-api-impl/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/apiimpl/feature/greeter/api/Greeter.kt
new file mode 100644
index 00000000..6a5e0f38
--- /dev/null
+++ b/examples/jvm/02-api-impl/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/apiimpl/feature/greeter/api/Greeter.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.jvm.apiimpl.feature.greeter.api
+
+interface Greeter {
+ fun greet(name: String): String
+}
diff --git a/examples/jvm/02-api-impl/feature/greeter/impl/build.gradle.kts b/examples/jvm/02-api-impl/feature/greeter/impl/build.gradle.kts
new file mode 100644
index 00000000..6233b032
--- /dev/null
+++ b/examples/jvm/02-api-impl/feature/greeter/impl/build.gradle.kts
@@ -0,0 +1,8 @@
+import tools.forma.jvm.impl
+
+impl(
+ packageName = "tools.forma.examples.jvm.apiimpl.feature.greeter.impl",
+ dependencies = deps(
+ target(":feature:greeter:api")
+ )
+)
diff --git a/examples/jvm/02-api-impl/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/apiimpl/feature/greeter/impl/RealGreeter.kt b/examples/jvm/02-api-impl/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/apiimpl/feature/greeter/impl/RealGreeter.kt
new file mode 100644
index 00000000..9e9b1781
--- /dev/null
+++ b/examples/jvm/02-api-impl/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/apiimpl/feature/greeter/impl/RealGreeter.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.jvm.apiimpl.feature.greeter.impl
+
+import tools.forma.examples.jvm.apiimpl.feature.greeter.api.Greeter
+
+class RealGreeter : Greeter {
+ override fun greet(name: String): String = "Hello, $name (from impl)"
+}
diff --git a/examples/jvm/02-api-impl/gradle.properties b/examples/jvm/02-api-impl/gradle.properties
new file mode 100644
index 00000000..ee8dcbc6
--- /dev/null
+++ b/examples/jvm/02-api-impl/gradle.properties
@@ -0,0 +1,5 @@
+# Progressive JVM example 02
+org.gradle.jvmargs=-Xmx1024m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+kotlin.code.style=official
diff --git a/examples/jvm/02-api-impl/gradle/wrapper/gradle-wrapper.jar b/examples/jvm/02-api-impl/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/jvm/02-api-impl/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/jvm/02-api-impl/gradle/wrapper/gradle-wrapper.properties b/examples/jvm/02-api-impl/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/jvm/02-api-impl/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/examples/jvm/02-api-impl/gradlew b/examples/jvm/02-api-impl/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/jvm/02-api-impl/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/examples/jvm/02-api-impl/gradlew.bat b/examples/jvm/02-api-impl/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/jvm/02-api-impl/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/examples/jvm/02-api-impl/settings.gradle.kts b/examples/jvm/02-api-impl/settings.gradle.kts
new file mode 100644
index 00000000..35b3e11f
--- /dev/null
+++ b/examples/jvm/02-api-impl/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 = "02-api-impl"
diff --git a/examples/jvm/03-library-util/README.md b/examples/jvm/03-library-util/README.md
new file mode 100644
index 00000000..0b381637
--- /dev/null
+++ b/examples/jvm/03-library-util/README.md
@@ -0,0 +1,33 @@
+# 03 — library-util (JVM)
+
+**Goal:** Introduce shared `library` (contracts/helpers) and `util` (utilities).
+
+## Features introduced
+- `library` target
+- `util` target
+- `api` may depend on `library`
+- `impl` may depend on `api` + `util` + `library`
+
+## Tree highlights
+```
+common/
+ library/ # pure shared (Ids)
+ util/ # string helpers
+feature/greeter/{api,impl}
+binary/
+```
+
+## Build & run
+```bash
+source ../../../scripts/env-mac.sh
+./gradlew build
+./gradlew :binary:run
+```
+
+## Key wiring
+- impl → api, util, library (allowed per matrix)
+- No impl→impl
+- api → library (allowed)
+
+## Next
+04 demonstrates two features with impl→impl forbidden.
diff --git a/examples/jvm/03-library-util/binary/build.gradle.kts b/examples/jvm/03-library-util/binary/build.gradle.kts
new file mode 100644
index 00000000..569308f3
--- /dev/null
+++ b/examples/jvm/03-library-util/binary/build.gradle.kts
@@ -0,0 +1,12 @@
+import tools.forma.jvm.binary
+
+binary(
+ packageName = "tools.forma.examples.jvm.libutil.binary",
+ mainClass = "tools.forma.examples.jvm.libutil.binary.MainKt",
+ dependencies = deps(
+ target(":feature:greeter:api"),
+ target(":feature:greeter:impl"),
+ target(":common:util"),
+ target(":common:library")
+ )
+)
diff --git a/examples/jvm/03-library-util/binary/src/main/kotlin/tools/forma/examples/jvm/libutil/binary/Main.kt b/examples/jvm/03-library-util/binary/src/main/kotlin/tools/forma/examples/jvm/libutil/binary/Main.kt
new file mode 100644
index 00000000..070c6829
--- /dev/null
+++ b/examples/jvm/03-library-util/binary/src/main/kotlin/tools/forma/examples/jvm/libutil/binary/Main.kt
@@ -0,0 +1,10 @@
+package tools.forma.examples.jvm.libutil.binary
+
+import tools.forma.examples.jvm.libutil.feature.greeter.api.Greeter
+import tools.forma.examples.jvm.libutil.feature.greeter.impl.RealGreeter
+
+fun main() {
+ val greeter: Greeter = RealGreeter()
+ println(greeter.greet("World"))
+ println("JVM 03 (library+util) build + run successful.")
+}
diff --git a/examples/jvm/03-library-util/build.gradle.kts b/examples/jvm/03-library-util/build.gradle.kts
new file mode 100644
index 00000000..fa61fd6a
--- /dev/null
+++ b/examples/jvm/03-library-util/build.gradle.kts
@@ -0,0 +1 @@
+// Progressive JVM example 03: library + util shared + api/impl feature.
diff --git a/examples/jvm/03-library-util/common/library/build.gradle.kts b/examples/jvm/03-library-util/common/library/build.gradle.kts
new file mode 100644
index 00000000..0868941f
--- /dev/null
+++ b/examples/jvm/03-library-util/common/library/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.library
+
+library(packageName = "tools.forma.examples.jvm.libutil.common.library")
diff --git a/examples/jvm/03-library-util/common/library/src/main/kotlin/tools/forma/examples/jvm/libutil/common/library/Id.kt b/examples/jvm/03-library-util/common/library/src/main/kotlin/tools/forma/examples/jvm/libutil/common/library/Id.kt
new file mode 100644
index 00000000..8662279e
--- /dev/null
+++ b/examples/jvm/03-library-util/common/library/src/main/kotlin/tools/forma/examples/jvm/libutil/common/library/Id.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.jvm.libutil.common.library
+
+object Ids {
+ fun next(prefix: String): String = "$prefix-${(0..999).random()}"
+}
diff --git a/examples/jvm/03-library-util/common/util/build.gradle.kts b/examples/jvm/03-library-util/common/util/build.gradle.kts
new file mode 100644
index 00000000..e9bd4e5a
--- /dev/null
+++ b/examples/jvm/03-library-util/common/util/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.util
+
+util(packageName = "tools.forma.examples.jvm.libutil.common.util")
diff --git a/examples/jvm/03-library-util/common/util/src/main/kotlin/tools/forma/examples/jvm/libutil/common/util/Strings.kt b/examples/jvm/03-library-util/common/util/src/main/kotlin/tools/forma/examples/jvm/libutil/common/util/Strings.kt
new file mode 100644
index 00000000..8433e87b
--- /dev/null
+++ b/examples/jvm/03-library-util/common/util/src/main/kotlin/tools/forma/examples/jvm/libutil/common/util/Strings.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.jvm.libutil.common.util
+
+fun greetPrefix(name: String): String = "Hi $name"
diff --git a/examples/jvm/03-library-util/feature/greeter/api/build.gradle.kts b/examples/jvm/03-library-util/feature/greeter/api/build.gradle.kts
new file mode 100644
index 00000000..b508cd9e
--- /dev/null
+++ b/examples/jvm/03-library-util/feature/greeter/api/build.gradle.kts
@@ -0,0 +1,8 @@
+import tools.forma.jvm.api
+
+api(
+ packageName = "tools.forma.examples.jvm.libutil.feature.greeter.api",
+ dependencies = deps(
+ target(":common:library")
+ )
+)
diff --git a/examples/jvm/03-library-util/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/libutil/feature/greeter/api/Greeter.kt b/examples/jvm/03-library-util/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/libutil/feature/greeter/api/Greeter.kt
new file mode 100644
index 00000000..f48974e6
--- /dev/null
+++ b/examples/jvm/03-library-util/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/libutil/feature/greeter/api/Greeter.kt
@@ -0,0 +1,5 @@
+package tools.forma.examples.jvm.libutil.feature.greeter.api
+
+interface Greeter {
+ fun greet(name: String): String
+}
diff --git a/examples/jvm/03-library-util/feature/greeter/impl/build.gradle.kts b/examples/jvm/03-library-util/feature/greeter/impl/build.gradle.kts
new file mode 100644
index 00000000..f1ff9c0f
--- /dev/null
+++ b/examples/jvm/03-library-util/feature/greeter/impl/build.gradle.kts
@@ -0,0 +1,10 @@
+import tools.forma.jvm.impl
+
+impl(
+ packageName = "tools.forma.examples.jvm.libutil.feature.greeter.impl",
+ dependencies = deps(
+ target(":feature:greeter:api"),
+ target(":common:util"),
+ target(":common:library")
+ )
+)
diff --git a/examples/jvm/03-library-util/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/libutil/feature/greeter/impl/RealGreeter.kt b/examples/jvm/03-library-util/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/libutil/feature/greeter/impl/RealGreeter.kt
new file mode 100644
index 00000000..03fa3974
--- /dev/null
+++ b/examples/jvm/03-library-util/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/libutil/feature/greeter/impl/RealGreeter.kt
@@ -0,0 +1,10 @@
+package tools.forma.examples.jvm.libutil.feature.greeter.impl
+
+import tools.forma.examples.jvm.libutil.common.util.greetPrefix
+import tools.forma.examples.jvm.libutil.common.library.Ids
+import tools.forma.examples.jvm.libutil.feature.greeter.api.Greeter
+
+class RealGreeter : Greeter {
+ override fun greet(name: String): String =
+ "${greetPrefix(name)} [${Ids.next("g")}] (from impl+util+lib)"
+}
diff --git a/examples/jvm/03-library-util/gradle.properties b/examples/jvm/03-library-util/gradle.properties
new file mode 100644
index 00000000..1e0ada11
--- /dev/null
+++ b/examples/jvm/03-library-util/gradle.properties
@@ -0,0 +1,5 @@
+# Progressive JVM example 03
+org.gradle.jvmargs=-Xmx1024m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+kotlin.code.style=official
diff --git a/examples/jvm/03-library-util/gradle/wrapper/gradle-wrapper.jar b/examples/jvm/03-library-util/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/jvm/03-library-util/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/jvm/03-library-util/gradle/wrapper/gradle-wrapper.properties b/examples/jvm/03-library-util/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/jvm/03-library-util/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/examples/jvm/03-library-util/gradlew b/examples/jvm/03-library-util/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/jvm/03-library-util/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/examples/jvm/03-library-util/gradlew.bat b/examples/jvm/03-library-util/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/jvm/03-library-util/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/examples/jvm/03-library-util/settings.gradle.kts b/examples/jvm/03-library-util/settings.gradle.kts
new file mode 100644
index 00000000..b9ed863e
--- /dev/null
+++ b/examples/jvm/03-library-util/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 = "03-library-util"
diff --git a/examples/jvm/04-multi-feature/README.md b/examples/jvm/04-multi-feature/README.md
new file mode 100644
index 00000000..a0b54ff0
--- /dev/null
+++ b/examples/jvm/04-multi-feature/README.md
@@ -0,0 +1,29 @@
+# 04 — multi-feature (JVM)
+
+**Goal:** Two independent features; binary is single composition root. Proves `impl` does not (and may not) depend on other `impl`.
+
+## Features introduced
+- Second feature slice
+- Explicit composition at `binary` of multiple (api+impl) pairs
+- Dependency matrix enforcement: impl ↛ impl (would fail validation at config time)
+
+## Tree
+```
+feature/
+ greeter/{api,impl}
+ calc/{api,impl}
+binary/
+```
+
+## Build & run
+```bash
+source ../../../scripts/env-mac.sh
+./gradlew build
+./gradlew :binary:run
+```
+
+## Rule reminder
+From DEPENDENCY-MATRIX: `impl` validator allows only `api`, `library`, `util`, `test-util` (JVM). No `impl`.
+
+## Next
+05 adds `testUtil`.
diff --git a/examples/jvm/04-multi-feature/binary/build.gradle.kts b/examples/jvm/04-multi-feature/binary/build.gradle.kts
new file mode 100644
index 00000000..de78729a
--- /dev/null
+++ b/examples/jvm/04-multi-feature/binary/build.gradle.kts
@@ -0,0 +1,12 @@
+import tools.forma.jvm.binary
+
+binary(
+ packageName = "tools.forma.examples.jvm.multifeature.binary",
+ mainClass = "tools.forma.examples.jvm.multifeature.binary.MainKt",
+ dependencies = deps(
+ target(":feature:greeter:api"),
+ target(":feature:greeter:impl"),
+ target(":feature:calc:api"),
+ target(":feature:calc:impl")
+ )
+)
diff --git a/examples/jvm/04-multi-feature/binary/src/main/kotlin/tools/forma/examples/jvm/multifeature/binary/Main.kt b/examples/jvm/04-multi-feature/binary/src/main/kotlin/tools/forma/examples/jvm/multifeature/binary/Main.kt
new file mode 100644
index 00000000..e5c3586a
--- /dev/null
+++ b/examples/jvm/04-multi-feature/binary/src/main/kotlin/tools/forma/examples/jvm/multifeature/binary/Main.kt
@@ -0,0 +1,14 @@
+package tools.forma.examples.jvm.multifeature.binary
+
+import tools.forma.examples.jvm.multifeature.feature.greeter.api.Greeter
+import tools.forma.examples.jvm.multifeature.feature.greeter.impl.RealGreeter
+import tools.forma.examples.jvm.multifeature.feature.calc.api.Calculator
+import tools.forma.examples.jvm.multifeature.feature.calc.impl.RealCalc
+
+fun main() {
+ val g: Greeter = RealGreeter()
+ val c: Calculator = RealCalc()
+ println(g.greet("World"))
+ println("2+3=${c.add(2,3)}")
+ println("JVM 04 (multi-feature, no impl->impl) successful.")
+}
diff --git a/examples/jvm/04-multi-feature/build.gradle.kts b/examples/jvm/04-multi-feature/build.gradle.kts
new file mode 100644
index 00000000..c65a4e6a
--- /dev/null
+++ b/examples/jvm/04-multi-feature/build.gradle.kts
@@ -0,0 +1,2 @@
+// Progressive JVM 04: two features; composition root wires both api+impl pairs.
+// Demonstrates impl cannot (and does not) depend on another impl.
diff --git a/examples/jvm/04-multi-feature/feature/calc/api/build.gradle.kts b/examples/jvm/04-multi-feature/feature/calc/api/build.gradle.kts
new file mode 100644
index 00000000..44467528
--- /dev/null
+++ b/examples/jvm/04-multi-feature/feature/calc/api/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.api
+
+api(packageName = "tools.forma.examples.jvm.multifeature.feature.calc.api")
diff --git a/examples/jvm/04-multi-feature/feature/calc/api/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/calc/api/Calculator.kt b/examples/jvm/04-multi-feature/feature/calc/api/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/calc/api/Calculator.kt
new file mode 100644
index 00000000..9dac4d7a
--- /dev/null
+++ b/examples/jvm/04-multi-feature/feature/calc/api/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/calc/api/Calculator.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.jvm.multifeature.feature.calc.api
+
+fun interface Calculator { fun add(a: Int, b: Int): Int }
diff --git a/examples/jvm/04-multi-feature/feature/calc/impl/build.gradle.kts b/examples/jvm/04-multi-feature/feature/calc/impl/build.gradle.kts
new file mode 100644
index 00000000..4ef10ef5
--- /dev/null
+++ b/examples/jvm/04-multi-feature/feature/calc/impl/build.gradle.kts
@@ -0,0 +1,6 @@
+import tools.forma.jvm.impl
+
+impl(
+ packageName = "tools.forma.examples.jvm.multifeature.feature.calc.impl",
+ dependencies = deps(target(":feature:calc:api"))
+)
diff --git a/examples/jvm/04-multi-feature/feature/calc/impl/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/calc/impl/RealCalc.kt b/examples/jvm/04-multi-feature/feature/calc/impl/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/calc/impl/RealCalc.kt
new file mode 100644
index 00000000..b3988d39
--- /dev/null
+++ b/examples/jvm/04-multi-feature/feature/calc/impl/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/calc/impl/RealCalc.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.jvm.multifeature.feature.calc.impl
+
+import tools.forma.examples.jvm.multifeature.feature.calc.api.Calculator
+
+class RealCalc : Calculator {
+ override fun add(a: Int, b: Int) = a + b
+}
diff --git a/examples/jvm/04-multi-feature/feature/greeter/api/build.gradle.kts b/examples/jvm/04-multi-feature/feature/greeter/api/build.gradle.kts
new file mode 100644
index 00000000..98f80d11
--- /dev/null
+++ b/examples/jvm/04-multi-feature/feature/greeter/api/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.api
+
+api(packageName = "tools.forma.examples.jvm.multifeature.feature.greeter.api")
diff --git a/examples/jvm/04-multi-feature/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/greeter/api/Greeter.kt b/examples/jvm/04-multi-feature/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/greeter/api/Greeter.kt
new file mode 100644
index 00000000..8262ed18
--- /dev/null
+++ b/examples/jvm/04-multi-feature/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/greeter/api/Greeter.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.jvm.multifeature.feature.greeter.api
+
+fun interface Greeter { fun greet(name: String): String }
diff --git a/examples/jvm/04-multi-feature/feature/greeter/impl/build.gradle.kts b/examples/jvm/04-multi-feature/feature/greeter/impl/build.gradle.kts
new file mode 100644
index 00000000..5801b45e
--- /dev/null
+++ b/examples/jvm/04-multi-feature/feature/greeter/impl/build.gradle.kts
@@ -0,0 +1,6 @@
+import tools.forma.jvm.impl
+
+impl(
+ packageName = "tools.forma.examples.jvm.multifeature.feature.greeter.impl",
+ dependencies = deps(target(":feature:greeter:api"))
+)
diff --git a/examples/jvm/04-multi-feature/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/greeter/impl/RealGreeter.kt b/examples/jvm/04-multi-feature/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/greeter/impl/RealGreeter.kt
new file mode 100644
index 00000000..a206b052
--- /dev/null
+++ b/examples/jvm/04-multi-feature/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/multifeature/feature/greeter/impl/RealGreeter.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.jvm.multifeature.feature.greeter.impl
+
+import tools.forma.examples.jvm.multifeature.feature.greeter.api.Greeter
+
+class RealGreeter : Greeter {
+ override fun greet(name: String) = "Hello $name (greeter)"
+}
diff --git a/examples/jvm/04-multi-feature/gradle.properties b/examples/jvm/04-multi-feature/gradle.properties
new file mode 100644
index 00000000..ba35fd8e
--- /dev/null
+++ b/examples/jvm/04-multi-feature/gradle.properties
@@ -0,0 +1,5 @@
+# Progressive JVM example 04
+org.gradle.jvmargs=-Xmx1024m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+kotlin.code.style=official
diff --git a/examples/jvm/04-multi-feature/gradle/wrapper/gradle-wrapper.jar b/examples/jvm/04-multi-feature/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/jvm/04-multi-feature/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/jvm/04-multi-feature/gradle/wrapper/gradle-wrapper.properties b/examples/jvm/04-multi-feature/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/jvm/04-multi-feature/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/examples/jvm/04-multi-feature/gradlew b/examples/jvm/04-multi-feature/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/jvm/04-multi-feature/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/examples/jvm/04-multi-feature/gradlew.bat b/examples/jvm/04-multi-feature/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/jvm/04-multi-feature/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/examples/jvm/04-multi-feature/settings.gradle.kts b/examples/jvm/04-multi-feature/settings.gradle.kts
new file mode 100644
index 00000000..20daeb88
--- /dev/null
+++ b/examples/jvm/04-multi-feature/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 = "04-multi-feature"
diff --git a/examples/jvm/05-test-util/README.md b/examples/jvm/05-test-util/README.md
new file mode 100644
index 00000000..631fc92c
--- /dev/null
+++ b/examples/jvm/05-test-util/README.md
@@ -0,0 +1,27 @@
+# 05 — test-util (JVM)
+
+**Goal:** Share test helpers via `testUtil` target consumed only in `testDependencies`.
+
+## Features introduced
+- `testUtil` target
+- `impl(..., testDependencies = deps(...))`
+- Test code lives in `src/test/...` of consumer; testUtil sources are main of their target but only visible to tests.
+
+## Tree
+```
+common/test-util/ # test helpers (fake impls etc)
+feature/greeter/impl/src/test/... # uses the test util
+```
+
+## Build & run
+```bash
+source ../../../scripts/env-mac.sh
+./gradlew build # includes test compilation + run (0 tests in this teaching slice)
+./gradlew :binary:run
+```
+
+## Rule
+`testUtil` allowed from impl tests (and other testUtils).
+
+## Next
+JVM ladder complete. Move to Android progressive examples.
diff --git a/examples/jvm/05-test-util/binary/build.gradle.kts b/examples/jvm/05-test-util/binary/build.gradle.kts
new file mode 100644
index 00000000..66d43539
--- /dev/null
+++ b/examples/jvm/05-test-util/binary/build.gradle.kts
@@ -0,0 +1,10 @@
+import tools.forma.jvm.binary
+
+binary(
+ packageName = "tools.forma.examples.jvm.testutil.binary",
+ mainClass = "tools.forma.examples.jvm.testutil.binary.MainKt",
+ dependencies = deps(
+ target(":feature:greeter:api"),
+ target(":feature:greeter:impl")
+ )
+)
diff --git a/examples/jvm/05-test-util/binary/src/main/kotlin/tools/forma/examples/jvm/testutil/binary/Main.kt b/examples/jvm/05-test-util/binary/src/main/kotlin/tools/forma/examples/jvm/testutil/binary/Main.kt
new file mode 100644
index 00000000..aae58d99
--- /dev/null
+++ b/examples/jvm/05-test-util/binary/src/main/kotlin/tools/forma/examples/jvm/testutil/binary/Main.kt
@@ -0,0 +1,10 @@
+package tools.forma.examples.jvm.testutil.binary
+
+import tools.forma.examples.jvm.testutil.feature.greeter.api.Greeter
+import tools.forma.examples.jvm.testutil.feature.greeter.impl.RealGreeter
+
+fun main() {
+ val g: Greeter = RealGreeter()
+ println(g.greet("World"))
+ println("JVM 05 (test-util) build + run successful.")
+}
diff --git a/examples/jvm/05-test-util/build.gradle.kts b/examples/jvm/05-test-util/build.gradle.kts
new file mode 100644
index 00000000..e9104198
--- /dev/null
+++ b/examples/jvm/05-test-util/build.gradle.kts
@@ -0,0 +1 @@
+// Progressive JVM 05: testUtil shared test helpers wired via testDependencies on impl.
diff --git a/examples/jvm/05-test-util/common/test-util/build.gradle.kts b/examples/jvm/05-test-util/common/test-util/build.gradle.kts
new file mode 100644
index 00000000..070b15cf
--- /dev/null
+++ b/examples/jvm/05-test-util/common/test-util/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.testUtil
+
+testUtil(packageName = "tools.forma.examples.jvm.testutil.common.testutil")
diff --git a/examples/jvm/05-test-util/common/test-util/src/main/kotlin/tools/forma/examples/jvm/testutil/common/testutil/TestGreeter.kt b/examples/jvm/05-test-util/common/test-util/src/main/kotlin/tools/forma/examples/jvm/testutil/common/testutil/TestGreeter.kt
new file mode 100644
index 00000000..758c5c89
--- /dev/null
+++ b/examples/jvm/05-test-util/common/test-util/src/main/kotlin/tools/forma/examples/jvm/testutil/common/testutil/TestGreeter.kt
@@ -0,0 +1,8 @@
+package tools.forma.examples.jvm.testutil.common.testutil
+
+// Test helper (pure, no production api dep — testUtil may only depend on util/test-util per matrix).
+// Consumed from impl's testDependencies.
+object TestSupport {
+ fun greetingFor(name: String): String = "TEST:$name"
+}
+
diff --git a/examples/jvm/05-test-util/feature/greeter/api/build.gradle.kts b/examples/jvm/05-test-util/feature/greeter/api/build.gradle.kts
new file mode 100644
index 00000000..61bc33e8
--- /dev/null
+++ b/examples/jvm/05-test-util/feature/greeter/api/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.api
+
+api(packageName = "tools.forma.examples.jvm.testutil.feature.greeter.api")
diff --git a/examples/jvm/05-test-util/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/api/Greeter.kt b/examples/jvm/05-test-util/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/api/Greeter.kt
new file mode 100644
index 00000000..bf28fb8c
--- /dev/null
+++ b/examples/jvm/05-test-util/feature/greeter/api/src/main/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/api/Greeter.kt
@@ -0,0 +1,3 @@
+package tools.forma.examples.jvm.testutil.feature.greeter.api
+
+interface Greeter { fun greet(name: String): String }
diff --git a/examples/jvm/05-test-util/feature/greeter/impl/build.gradle.kts b/examples/jvm/05-test-util/feature/greeter/impl/build.gradle.kts
new file mode 100644
index 00000000..8436b836
--- /dev/null
+++ b/examples/jvm/05-test-util/feature/greeter/impl/build.gradle.kts
@@ -0,0 +1,11 @@
+import tools.forma.jvm.impl
+
+impl(
+ packageName = "tools.forma.examples.jvm.testutil.feature.greeter.impl",
+ dependencies = deps(
+ target(":feature:greeter:api")
+ ),
+ testDependencies = deps(
+ target(":common:test-util")
+ )
+)
diff --git a/examples/jvm/05-test-util/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/impl/RealGreeter.kt b/examples/jvm/05-test-util/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/impl/RealGreeter.kt
new file mode 100644
index 00000000..ffd62d0f
--- /dev/null
+++ b/examples/jvm/05-test-util/feature/greeter/impl/src/main/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/impl/RealGreeter.kt
@@ -0,0 +1,7 @@
+package tools.forma.examples.jvm.testutil.feature.greeter.impl
+
+import tools.forma.examples.jvm.testutil.feature.greeter.api.Greeter
+
+class RealGreeter : Greeter {
+ override fun greet(name: String) = "Hello $name (real)"
+}
diff --git a/examples/jvm/05-test-util/feature/greeter/impl/src/test/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/impl/RealGreeterTest.kt b/examples/jvm/05-test-util/feature/greeter/impl/src/test/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/impl/RealGreeterTest.kt
new file mode 100644
index 00000000..c475c2ac
--- /dev/null
+++ b/examples/jvm/05-test-util/feature/greeter/impl/src/test/kotlin/tools/forma/examples/jvm/testutil/feature/greeter/impl/RealGreeterTest.kt
@@ -0,0 +1,15 @@
+package tools.forma.examples.jvm.testutil.feature.greeter.impl
+
+import tools.forma.examples.jvm.testutil.common.testutil.TestSupport
+
+// Compile-only test sources to exercise testDependencies + testUtil target.
+// No external test framework declared in this minimal teaching example;
+// Gradle test task passes with 0 discovered tests.
+class GreeterSmoke {
+ fun smokeReal() {
+ check(RealGreeter().greet("World") == "Hello World (real)")
+ }
+ fun smokeTestUtil() {
+ check(TestSupport.greetingFor("World") == "TEST:World")
+ }
+}
diff --git a/examples/jvm/05-test-util/gradle.properties b/examples/jvm/05-test-util/gradle.properties
new file mode 100644
index 00000000..8cddb234
--- /dev/null
+++ b/examples/jvm/05-test-util/gradle.properties
@@ -0,0 +1,5 @@
+# Progressive JVM example 05
+org.gradle.jvmargs=-Xmx1024m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+kotlin.code.style=official
diff --git a/examples/jvm/05-test-util/gradle/wrapper/gradle-wrapper.jar b/examples/jvm/05-test-util/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/examples/jvm/05-test-util/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/jvm/05-test-util/gradle/wrapper/gradle-wrapper.properties b/examples/jvm/05-test-util/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/examples/jvm/05-test-util/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/examples/jvm/05-test-util/gradlew b/examples/jvm/05-test-util/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/examples/jvm/05-test-util/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/examples/jvm/05-test-util/gradlew.bat b/examples/jvm/05-test-util/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/examples/jvm/05-test-util/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/examples/jvm/05-test-util/settings.gradle.kts b/examples/jvm/05-test-util/settings.gradle.kts
new file mode 100644
index 00000000..5f2ecab9
--- /dev/null
+++ b/examples/jvm/05-test-util/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 = "05-test-util"