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"