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: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- [x] `ulite/android` Kotlin review fixes (2026-08-22): (1) CI workflow uses `$GITHUB_WORKSPACE` absolute path for plugin wasm copy (was relative `../ulb-plugins/` which failed after `cd /tmp/android`), (2) CI adds DEX-level assertion verifying `Lcom/example/ulite/Main;` class is present in the packaged dex (not just that `classes.dex` exists), (3) `d8_args` accepts `extra_jars` parameter; when Kotlin sources exist, `kotlin-stdlib.jar` is resolved from the compile classpath and included in D8 program inputs so the APK contains Kotlin runtime classes, (4) compile order documented — Java compiles before Kotlin because R.java must be compiled first for kotlinc to resolve R.* references; mutual Java↔Kotlin references beyond R.java need multi-pass compilation (future work). 31 android plugin unit tests, clippy/fmt clean.
- [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.

## 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
Expand Down
1 change: 1 addition & 0 deletions android-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license.workspace = true
crate-type = ["cdylib"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ulb-plugin-sdk = { path = "../../Uliab/crates/ulb-plugin-sdk" }
wit-bindgen = "0.51"
Expand Down
155 changes: 155 additions & 0 deletions android-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,157 @@
//! §5.2).

use serde_json::Value;
use ulb_plugin_sdk::UlbConfig;

/// Top-level module configuration for the `ulite/android` plugin.
///
/// The host serializes a module's `android {}` block (plus host-injected
/// keys like `projectDir` and `androidSdkDir`) into this shape and passes
/// it to `configure` as a JSON string.
#[derive(UlbConfig, serde::Deserialize)]
pub struct AndroidPluginConfig {
/// Host-injected project root directory; all relative block paths
/// resolve against this.
#[ulb(rename = "projectDir")]
pub project_dir: String,

/// The module's `android {}` block describing sources, SDK levels,
/// namespace, and resource directories.
pub android: AndroidBlock,

/// Host-injected Android SDK root (from `--android-sdk`, `ANDROID_HOME`,
/// or `ANDROID_SDK_ROOT`); overridden by `android.sdkDir` when present.
#[serde(default)]
#[ulb(rename = "androidSdkDir")]
pub android_sdk_dir: Option<String>,

/// The module's `signing {}` block; when absent, produced APKs are
/// unsigned.
#[serde(default)]
pub signing: Option<SigningBlock>,

/// Map of build-type name to its block. Keys define the build-type
/// dimension of the variant matrix. When absent, defaults to
/// `{ "debug": {}, "release": {} }`.
#[serde(default)]
#[ulb(rename = "buildTypes")]
#[ulb(
description = "Build-type map; keys become variant dimensions (default: debug + release)"
)]
pub build_types: Option<serde_json::Value>,

/// Map of flavor name to its block, plus optional top-level `dimension`
/// key. When absent, no flavor dimension exists.
#[serde(default)]
#[ulb(rename = "productFlavors")]
pub product_flavors: Option<serde_json::Value>,

/// Host-resolved dependency jars keyed by bucket name (`"compile"`,
/// `"testCompile"`, etc.).
#[serde(default)]
pub classpath: Option<ClasspathBlock>,
}

/// Fields declared inside the `android {}` DSL block.
#[derive(UlbConfig, serde::Deserialize)]
pub struct AndroidBlock {
/// Android API level to compile against (e.g. `36`). Determines which
/// `platforms/android-<N>/android.jar` the toolchain uses.
#[ulb(rename = "compileSdk")]
pub compile_sdk: i64,

/// Java package namespace (e.g. `"com.example.app"`). Drives R.java
/// generation and the `BuildConfig` package.
pub namespace: String,

/// Whether Jetpack Compose is enabled. When true, the compose compiler
/// plugin JAR must be present on the compile classpath.
#[serde(default)]
pub compose: Option<bool>,

/// Source file paths (`.java` and `.kt`) relative to the project
/// directory. Must be non-empty.
pub sources: Vec<String>,

/// Path to `AndroidManifest.xml`, relative to the project directory.
pub manifest: String,

/// Path to the Android resources directory for `aapt2 compile --dir`,
/// relative to the project directory.
#[ulb(rename = "resDir")]
pub res_dir: String,

/// Per-module Android SDK root override (resolved against `projectDir`).
/// Takes precedence over the host-injected `androidSdkDir`.
#[serde(default)]
#[ulb(rename = "sdkDir")]
pub sdk_dir: Option<String>,

/// Minimum Android API level. Passed to `d8 --min-api` and
/// `aapt2 link --min-sdk-version`. Product flavors may override this.
#[ulb(rename = "minSdk")]
pub min_sdk: i64,

/// Target Android API level. Passed to `aapt2 link --target-sdk-version`.
/// Defaults to `compileSdk` when absent.
#[serde(default)]
#[ulb(rename = "targetSdk")]
pub target_sdk: Option<i64>,

/// Version code written into the generated `BuildConfig.VERSION_CODE`.
/// Defaults to `1`.
#[serde(default)]
#[ulb(rename = "versionCode")]
pub version_code: Option<i64>,

/// Version name written into the generated `BuildConfig.VERSION_NAME`.
/// Defaults to the empty string.
#[serde(default)]
#[ulb(rename = "versionName")]
pub version_name: Option<String>,

/// User-defined BuildConfig fields. Each entry is a
/// `["TYPE", "NAME", "INITIALIZER"]` triple. The evaluator's
/// `insert_accumulating` may flatten the first triple and nest
/// subsequent ones as sub-arrays.
#[serde(default)]
#[ulb(rename = "buildConfigField")]
#[ulb(
description = "List of [\"TYPE\", \"NAME\", \"INITIALIZER\"] triples for custom BuildConfig fields"
)]
pub build_config_field: Option<Vec<serde_json::Value>>,
}

/// APK signing configuration. All four fields are required when the block
/// is present. Produces two password files shared across all variants.
#[derive(UlbConfig, serde::Deserialize)]
pub struct SigningBlock {
/// Path to the keystore file (resolved against the project directory).
#[ulb(rename = "storeFile")]
pub store_file: String,

/// Keystore password, written to a temporary file and passed to
/// `apksigner` via `--ks-pass file:`.
#[ulb(rename = "storePassword")]
pub store_password: String,

/// Key alias within the keystore.
#[ulb(rename = "keyAlias")]
pub key_alias: String,

/// Key password, written to a temporary file and passed to `apksigner`
/// via `--key-pass file:`.
#[ulb(rename = "keyPassword")]
pub key_password: String,
}

/// Host-resolved dependency jars keyed by bucket name.
#[derive(UlbConfig, serde::Deserialize)]
pub struct ClasspathBlock {
/// Compile-time dependency jar paths; prepended after the platform jar.
#[serde(default)]
pub compile: Option<Vec<String>>,
}

mod bindings {
#![allow(unsafe_code)]
Expand All @@ -78,6 +229,7 @@ mod bindings {
world: "plugin",
});

use crate::AndroidPluginConfig;
use crate::{
BuildConfigParams, android_jar, bool_value, classpath_bucket, compile_args,
compute_variants, d8_args, find_compose_compiler_jar, generate_buildconfig_source,
Expand All @@ -87,8 +239,11 @@ mod bindings {
};
use exports::ulite::ulb::ulb_plugin::{Guest, PluginManifest};
use serde_json::Value;
use ulb_plugin_sdk::embed_schema;
use ulite::ulb::task_registrar::{self, Action, AllowlistedTool, RunToolArgs, Task};

embed_schema!(AndroidPluginConfig);

/// Implements the exported `ulb-plugin` interface.
struct AndroidPlugin;

Expand Down
1 change: 1 addition & 0 deletions hello-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license.workspace = true
crate-type = ["cdylib"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ulb-plugin-sdk = { path = "../../Uliab/crates/ulb-plugin-sdk" }
wit-bindgen = "0.51"
Expand Down
24 changes: 20 additions & 4 deletions hello-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@
//! `configure` — required for the full-world instantiation every plugin
//! goes through — and, having no build tasks, declares no tools and
//! accepts any well-formed module configuration.
//!
//! Starting with ABI 0.8 the plugin derives its config schema from a
//! typed struct via `#[derive(UlbConfig)]`. The schema is embedded into
//! the compiled `.wasm` as a custom section so the host can extract it
//! without instantiating the plugin.

use ulb_plugin_sdk::UlbConfig;

/// Typed plugin configuration — currently empty because hello-plugin has
/// no build tasks. The derive macro generates both `serde::Deserialize`
/// and the schema metadata the host reads from the wasm custom section.
#[derive(UlbConfig, serde::Deserialize)]
pub struct PluginConfig;

ulb_plugin_sdk::embed_schema!(PluginConfig);

/// The whole plugin lives inside this module because the `export!` macro
/// resolves `self::exports` relative to the module where `generate!` ran.
Expand All @@ -26,6 +41,8 @@ mod bindings {

use exports::ulite::ulb::ulb_plugin::{Guest, PluginManifest};

use crate::PluginConfig;

/// Implements the exported `ulb-plugin` interface.
struct HelloPlugin;

Expand All @@ -47,10 +64,9 @@ mod bindings {
}

fn configure(module_config: String) -> Result<(), String> {
// The hello-plugin has no tasks to register; it still parses
// the configuration so a malformed module block surfaces as a
// configure error rather than a silent success.
serde_json::from_str::<serde_json::Value>(&module_config)
// Parse into the typed config; a malformed module block
// surfaces as a configure error rather than a silent success.
serde_json::from_str::<PluginConfig>(&module_config)
.map_err(|error| format!("invalid module config JSON: {error}"))?;
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions jvm-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license.workspace = true
crate-type = ["cdylib"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ulb-plugin-sdk = { path = "../../Uliab/crates/ulb-plugin-sdk" }
wit-bindgen = "0.51"
Expand Down
Loading
Loading