Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/plugin-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
"ulite/hello": {
"versions": {
"0.5.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "hello_plugin.wasm"
}
}
Expand Down Expand Up @@ -207,7 +207,7 @@ jobs:
"ulite/jvm": {
"versions": {
"0.6.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "jvm_plugin.wasm"
}
}
Expand Down Expand Up @@ -266,7 +266,7 @@ jobs:
"ulite/jvm": {
"versions": {
"0.6.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "jvm_plugin.wasm"
}
}
Expand Down Expand Up @@ -365,7 +365,7 @@ jobs:
"ulite/jvm": {
"versions": {
"0.6.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "jvm_plugin.wasm"
}
}
Expand Down Expand Up @@ -487,7 +487,7 @@ jobs:
"ulite/jvm": {
"versions": {
"0.6.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "jvm_plugin.wasm"
}
}
Expand Down Expand Up @@ -616,7 +616,7 @@ jobs:
"ulite/jvm": {
"versions": {
"0.6.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "jvm_plugin.wasm"
}
}
Expand Down Expand Up @@ -740,7 +740,7 @@ jobs:
"ulite/kmp": {
"versions": {
"0.3.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "kmp_plugin.wasm"
}
}
Expand Down Expand Up @@ -927,7 +927,7 @@ jobs:
"ulite/android": {
"versions": {
"0.3.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "android_plugin.wasm"
}
}
Expand Down Expand Up @@ -1042,7 +1042,7 @@ jobs:
"ulite/android": {
"versions": {
"0.3.0": {
"abi": { "min": "0.7", "max": "0.7" },
"abi": { "min": "0.7", "max": "0.8" },
"artifact_url": "android_plugin.wasm"
}
}
Expand Down
9 changes: 7 additions & 2 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,22 @@
- [x] `ulite/android` — Compose compiler plugin support (2026-08-22): the android-plugin crate reads a `compose = true` key inside the `android {}` block. When enabled, the compose compiler plugin JAR is located on the compile classpath via `find_compose_compiler_jar` and loaded via `-Xplugin=<path>` — the standard Kotlin CLI contract for compiler plugins. The kmp plugin parses `android.compose` from its config and applies the same mechanism. The `d8_merge_args` function gains an `extra_jars` parameter for kotlin-stdlib inclusion in the kmp mergeDex task, and the extra JARs are declared as task inputs for proper fingerprinting. 7 unit tests added for compose flag parsing, JAR discovery, and kotlinc arg generation (`bool_value` helper for optional boolean keys). 39 android + 14 jvm + 24 kmp = 77 tests, clippy/fmt clean.
- [x] `ulite/android` — BuildConfig generation (2026-08-25): the android-plugin crate (ABI 0.7) generates a `BuildConfig.java` source file per variant during configure. Nine default fields (APPLICATION_ID, BUILD_TYPE, DEBUG, FLAVOR, VERSION_CODE, VERSION_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, COMPILE_SDK_VERSION) are populated from the `android {}` block and variant's effective values. User-defined fields are declared via `buildConfigField ["type", "name", "initializer"]` list triples inside the `android {}` block. The generated file lives at `<build>/<variant>/generated/buildconfig/<namespace>/BuildConfig.java` and is added to `javac`'s `-sourcepath` so it resolves at compile time. Task `generateBuildConfig<V>` depends on `prepareBuildDir` and is a dependency of `compileJava<V>`. Also fixed `merge_variant_sources` to resolve base paths before deduplication. 50 android plugin tests, clippy/fmt clean.
- [x] Phase 16B — migrate all four plugins to `#[derive(UlbConfig)]` (2026-08-26): each plugin now embeds its typed config schema in the wasm custom section via `embed_schema!`. hello-plugin: `PluginConfig` unit struct (schema structurally correct, no fields). jvm-plugin: `PluginConfig`/`Classpath`/`JvmConfig` structs with full typed deserialization; removed `classpath_bucket`/`optional_string_list` helpers. android-plugin: `AndroidPluginConfig`/`AndroidBlock`/`SigningBlock`/`ClasspathBlock` structs for schema generation; `configure()` keeps `serde_json::Value` reads because `compute_variants` deeply integrates with the raw value for dynamic `buildTypes`/`productFlavors` maps. kmp-plugin: `KmpPluginConfig` struct with `serde_json::Value` for dynamic `kmp`/`classpathSourceSets`/`android`/`buildTypes`/`productFlavors`/`signing` fields. Requires Uliab >= 0.8 (PR #33). 90+ tests pass, clippy/fmt clean. PR #12.
- [x] `ulite/jvm` — desktop Compose path + compose error rewording (2026-08-29): `JvmConfig` gains `compose` (optional bool). When `jvm.compose = true`, the compile-kotlin task locates `kotlin-compose-compiler-plugin` on the compile classpath (host now injects it alongside the BOM-managed `runtime`/`ui`/`material3` deps) and passes it via `-Xplugin=<jar>`. The compose logic lives inside the Kotlin-compile branch, so a module with `jvm.compose` but no `.kt` sources simply gets no `-Xplugin` (nothing to annotate) rather than a configure error — consistent with docs/jvm-plugin.md. New helpers `kotlinc_args`/`find_compose_compiler_jar`/`map_compose_error`; 6 new jvm-plugin tests. The android/kmp "compose = true but no compiler jar on the classpath" configure errors no longer tell the author to hand-declare the artifact — the host injects it, so the message now describes host-injection failure. docs/jvm-plugin.md documents the `compose` key and the `-Xplugin` invocation. Requires Uliab compose-compiler injection (see Uliab PROGRESS "Turnkey Compose"). 18 jvm / 53 android / 24 kmp tests, clippy/fmt clean.
- [x] ABI 0.8 re-sync (2026-08-29): Uliab `main` moved the SDK to ABI 0.8 (PR #40), but every CI E2E job still seeded a local registry index whose plugin rows declared `{min: 0.7, max: 0.7}`, so Uliab's ABI-0.8 host refused them (`no published build of 'ulite/*' supports plugin-ABI '0.8'`). Widened the eight E2E-seeded index rows in `.github/workflows/plugin-build.yml` (hello/jvm/kmp/android, `max: 0.7` → `max: 0.8`) and the four newest rows in `registry/index.json` (`hello@0.5.0`, `jvm@0.6.0`, `android@0.3.0`, `kmp@0.3.0`) so each covers host ABI 0.8 and the manifest-reported `ABI_VERSION` 0.8. Kept `min: 0.7` (plugins still require the 0.7+ host that carried the `UlbConfig` migration) and left older rows at their original caps. docs/registry.md and docs/architecture.md (kmp crate version 0.2.0→0.3.0) synced to match.
- [x] `ulite/jvm` — typed config serde-key fix (2026-08-29): the jvm-plugin type-deserializes `PluginConfig`, but `#[ulb(rename = "...")]` only drives the emitted schema, not serde's field lookup, so fields whose DSL key is camelCase and Rust name is snake_case (`projectDir`/`project_dir`, `classesDir`/`classes_dir`, `jarFile`/`jar_file`) failed to deserialize once the host re-tested it at ABI 0.8 (`missing field 'classes_dir'`). Added the matching `#[serde(rename = ...)]` to those three fields. Unmasking: this predates the compose work — introduced by the #12 UlbConfig migration, but CI was red at ABI resolution since then, so it was only observed after the ABI 0.8 re-sync. The android/kmp plugins deserialize to `serde_json::Value` and read DSL keys via `.get(...)`, so they are unaffected. Added `typed_config_deserializes_from_host_shaped_keys` regression test (19 jvm tests). 19 jvm / 53 android / 24 kmp tests, clippy/fmt clean.
- [x] `ulite/jvm` — KSP output fix (2026-08-29): the `ksp` task declared both `build/generated/ksp/{kotlin,java}` as outputs, but KSP2 creates only the Kotlin dir; a processor that emits no `.java` never creates the java dir, and the host rejected the declared-but-unproduced output (`task 'ksp' declared output '.../build/generated/ksp/java' but it does not exist after the action ran`). The task now declares only the Kotlin output dir; `-java-output-dir` is still passed so KSP generates into it when it does emit java, but it is no longer a required task output. The cannot-compile-generated-Java gap from the observed-issues list is unchanged. No unit test on the output list (register_task is a wit host call; the CI `jvm-ksp` E2E job is the regression test). 19 jvm / 53 android / 24 kmp tests, clippy/fmt clean.

## Next up (priority order)
1. `ulite/kmp` — per-target `implementation`/`api` deps scopes: source-set `deps {}` currently resolve every declaration onto the source set's single classpath; scoping by target is future work
2. `ulite/android` — Compose runtime dependency wiring and BOM support
2. ~~`ulite/android` — Compose runtime dependency wiring and BOM support~~ **Resolved host-side** (Uliab 2026-08-23): the host injects the BOM + `runtime`/`ui`/`material3`, and since 2026-08-29 also the `kotlin-compose-compiler-plugin` jar, so android/kmp/jvm modules no longer hand-declare the compiler artifact.
3. `ulite/android` — per-variant source-set layering (androidMain vs debug vs release source-set overrides)

## Observed issues (not fixed, noted for later)
- The `jvm` plugin writes KSP's `-java-output-dir` but has no way to compile the generated Java: `kotlinc` (2.2.0, on this JDK) emits no classes for `.java` sources in any invocation shape tested, and `javac` requires an explicit file list that a task's static inputs cannot enumerate after KSP has run. Documented in REFERENCE.md; a mixed module's javac task could adopt `-sourcepath <generated java dir>` so referenced generated Java compiles — not wired yet.
- The `jvm` plugin writes KSP's `-java-output-dir` but has no way to compile generated Java: a processor emitting `.java` produces sources under `build/generated/ksp/java` that no javac task compiles, because javac requires an explicit file list that a task's static inputs cannot enumerate after KSP has run. `kotlinc` (2.2.0, on this JDK) emits no classes for `.java` sources in any invocation shape tested. Documented in REFERENCE.md; a mixed module's javac task could adopt `-sourcepath <generated java dir>` so referenced generated Java compiles — not wired yet.
- Deleting a task's output dir out from under a fingerprint does not invalidate it (host hashes inputs, not output existence) — re-confirmed during ksp E2E: an earlier zero-output ksp run left a fingerprint that made the host skip ksp on later builds until the project state was cleared.
- Core `uliab`'s default registry URL points at `raw.githubusercontent.com/Ulite-Team/ulb-plugins/main/registry/index.json` — the repo is public now, so the default resolves and a custom `--registry` is only needed for local testing.
- AGENTS.md §2 suggests `wit_bindgen::generate!({ inline: ulb_plugin_sdk::WIT, ... })`, but wit-bindgen 0.51's `inline` option parses a string literal only — a const expression fails to compile. Plugins generate via `path:` into the sdk crate's `plugin.wit`, which keeps the single-source-of-truth intent; the AGENTS.md wording should be corrected when the sdk bumps to a wit-bindgen that accepts it.
- The Compose compiler jar must match the Kotlin compiler in use; the host injects it at a pinned version (`composeCompilerVersion`, default `2.3.10`, coordinate `org.jetbrains.kotlin:kotlin-compose-compiler-plugin`). Until the host parses `kotlinc -version` and coordinates the artifact, a compose module's effective compiler version is the pinned default unless the module sets `composeCompilerVersion` — a mismatch between kotlinc and the plugin jar fails at kotlinc runtime with an "unsupported plugin version" style error. No end-to-end `@Composable` compile test exists yet (needs a kotlinc on the CI host). An end-to-end compile is additionally blocked by a classpath gap: the injected Compose `runtime`/`ui`/`material3` artifacts are Android AARs, and the host's resolver materializes only `jar` packaging, so their `@Composable` API classes are not on the compile classpath. (A desktop/jvm compose path would want the `org.jetbrains.compose.*` coordinate family rather than the Android `androidx.compose.*` set; the AAR-unwrapping is a host resolver change and is deferred.)

## Explicitly deferred (out of scope)
- Full task-graph driver in the core `uliab` crate (tracked in Uliab)
Expand Down
21 changes: 11 additions & 10 deletions android-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,8 @@ mod bindings {
let compose_compiler_jar = if compose {
Some(find_compose_compiler_jar(&classpath).ok_or_else(|| {
"compose = true but the compose compiler plugin JAR was not found on the \
compile classpath; add a dependency such as \
\"org.jetbrains.kotlin:compose-compiler-plugin:<kotlin-version>\""
compile classpath; the host injects it when a module declares \
compose = true"
.to_owned()
})?)
} else {
Expand Down Expand Up @@ -1269,13 +1269,14 @@ fn compile_args(
args
}

/// The compose compiler plugin JAR is a Maven artifact whose filename
/// contains `compose-compiler-plugin`. Scanning the compile classpath
/// avoids hard-coding an artifact coordinate or path convention.
/// The compose compiler plugin JAR is the Maven artifact
/// `kotlin-compose-compiler-plugin`, whose resolved jar filename contains
/// `kotlin-compose-compiler-plugin`. Scanning the compile classpath avoids
/// hard-coding an artifact coordinate or path convention.
fn find_compose_compiler_jar(classpath: &[String]) -> Option<String> {
classpath
.iter()
.find(|p| p.contains("compose-compiler-plugin") && p.ends_with(".jar"))
.find(|p| p.contains("kotlin-compose-compiler-plugin") && p.ends_with(".jar"))
.cloned()
}

Expand Down Expand Up @@ -2048,10 +2049,10 @@ mod tests {
"/sdk/platforms/android-36/android.jar",
&[],
&["/proj/src/Main.kt".to_owned()],
Some("/maven/compose-compiler-plugin-2.0.jar"),
Some("/maven/kotlin-compose-compiler-plugin-2.0.jar"),
);
assert!(
args.contains(&"-Xplugin=/maven/compose-compiler-plugin-2.0.jar".to_owned()),
args.contains(&"-Xplugin=/maven/kotlin-compose-compiler-plugin-2.0.jar".to_owned()),
"compose JAR must be loaded via -Xplugin=<path>, got: {args:?}"
);
}
Expand All @@ -2075,11 +2076,11 @@ mod tests {
fn find_compose_compiler_jar_matches_jar_filename() {
let cp = vec![
"/maven/appcompat-1.7.0.jar".to_owned(),
"/maven/compose-compiler-plugin-2.0.0.jar".to_owned(),
"/maven/kotlin-compose-compiler-plugin-2.0.0.jar".to_owned(),
];
assert_eq!(
find_compose_compiler_jar(&cp),
Some("/maven/compose-compiler-plugin-2.0.0.jar".to_owned())
Some("/maven/kotlin-compose-compiler-plugin-2.0.0.jar".to_owned())
);
}

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A Cargo workspace (`resolver = "2"`, edition 2024) with four members:
| `hello-plugin` (0.5.0) | `ulite/hello` | Minimal world implementation; establishes the build/test path |
| `jvm-plugin` (0.6.0) | `ulite/jvm` | Compile Java/Kotlin, package a jar, run tests, run KSP2 |
| `android-plugin` (0.3.0) | `ulite/android` | Discover the Android SDK toolchain; compile against the platform jar |
| `kmp-plugin` (0.2.0) | `ulite/kmp` | Compile a Kotlin multiplatform module's shared and jvm source sets into a jar |
| `kmp-plugin` (0.3.0) | `ulite/kmp` | Compile a Kotlin multiplatform module's shared and jvm source sets into a jar |

All are `cdylib` crates, depend on `ulb-plugin-sdk` by path
(`../../Uliab/crates/ulb-plugin-sdk`), and share the workspace's
Expand Down
3 changes: 2 additions & 1 deletion docs/jvm-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ following keys:
| `sources` | list of strings | `.java` and/or `.kt` files to compile. At least one entry is required. |
| `classesDir` | string | Directory the compilers write `.class` files to. |
| `jarFile` | string | Output jar path the `assemble` task produces. |
| `compose` | boolean, optional | Whether the module targets desktop Jetpack Compose. When true, the compile classpath must carry the Compose compiler plugin jar (the host injects it, along with the managed `runtime`/`ui`/`material3` deps, whenever `jvm.compose = true`); the `compile-kotlin` task passes it to `kotlinc` as `-Xplugin`. Note: the managed `runtime`/`ui`/`material3` artifacts are Android AARs and the host's resolver currently materializes only `jar` packaging, so the `@Composable` API classes are not on the compile classpath yet; a real `@Composable` compile additionally requires a kotlinc host. |
| `testSources` | list of strings, optional | `.java` test files to compile. Kotlin test compilation is not supported yet. Requires `testClassesDir` and one of `testClass`/`testRunner`. |
| `testClassesDir` | string, optional | Directory `javac` writes test `.class` files to. |
| `testClass` | string, optional | Fully qualified class with a `main` method that the `test` task runs. Mutually exclusive with `testRunner`. |
Expand Down Expand Up @@ -156,7 +157,7 @@ list that a task's static inputs cannot enumerate after KSP has run.
|---|---|---|
| `ksp` | `java` | Runs the KSP2 command-line tool over the Kotlin sources (only when the module has `ksp` deps; see above) |
| `compile` | `javac` | `javac -d <classesDir> [-cp <classpath.compile, colon-separated>] <java sources>` (only when the module has `.java` sources; waits for `ksp` when present) |
| `compile-kotlin` | `kotlinc` | `kotlinc -d <classesDir> [-cp <classpath.compile:classesDir>] <kotlin sources + generated/ksp/kotlin>` (only when the module has `.kt` sources; waits for `compile` when both source sets exist and for `ksp` when present) |
| `compile-kotlin` | `kotlinc` | `kotlinc -d <classesDir> [-cp <classpath.compile:classesDir>] [-Xplugin=<compose-compiler-plugin jar>] <kotlin sources + generated/ksp/kotlin>` (only when the module has `.kt` sources; waits for `compile` when both source sets exist and for `ksp` when present; `-Xplugin` only when `jvm.compose` is true) |
| `assemble` | `jar` | `jar cf <jarFile> -C <classesDir> .` (after the present compile tasks) |
| `generate-test-runner` | — | Writes `build/generated-test-src/ulite/TestRunner.java` (only with `testRunner = "junit-platform"`) |
| `compile-tests` | `javac` | `javac -d <testClassesDir> -cp <classpath.testCompile:classesDir> <testSources>` — plus the generated runner source when `testRunner` is set (only when the test keys are set) |
Expand Down
Loading
Loading