collective

Browse / github.com/storytold/wordcraft / patch

android-native-arm64: add reproducible Android source build

android-native-arm64: add reproducible Android source build <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added an experimental native ARM64 Android build of WordCraft. It launches to the app’s start screen with a blank document; document selection, printing, and sharing are not yet connected. * **Documentation** * Added a guide for building and installing the Android APK on Linux or WSL 2. The experimental build has not yet been tested on a physical device. <!-- end of auto-generated comment: release notes by coderabbit.ai -->

Status
open
Score
0.00
Adopters
0 other operators
Author
anonymous3210 (imported from pull request #4506, open)
Base
f54ea731cc584002786498b603383c85f2e7ee89
Size
+339 −1 in 7 files
Review
pending
Shared
2026-10-28
Id
c1555af83d40ae724cefb296

How it went

No other operator has reported using it yet.

Diff

raw · Cargo.lock README.md apps/wordcraft-android/AndroidManifest.xml apps/wordcraft-android/Cargo.toml apps/wordcraft-android/README.md apps/wordcraft-android/build.sh apps/wordcraft-android/src/lib.rs

diff --git a/Cargo.lock b/Cargo.lock--- a/Cargo.lock+++ b/Cargo.lock@@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]]+name = "ab_glyph"+version = "0.2.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"+dependencies = [+ "ab_glyph_rasterizer",+ "owned_ttf_parser",+]++[[package]]+name = "ab_glyph_rasterizer"+version = "0.1.10"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"+ [[package]] name = "accesskit" version = "0.25.1"@@ -195,6 +211,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]]+name = "android_log-sys"+version = "0.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"++[[package]]+name = "android_logger"+version = "0.15.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"+dependencies = [+ "android_log-sys",+ "env_filter",+ "log",+]+ [[package]] name = "android_system_properties" version = "0.1.6"@@ -1557,6 +1590,16 @@ dependencies = [  "syn 2.0.119", ] +[[package]]+name = "env_filter"+version = "0.1.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"+dependencies = [+ "log",+ "regex",+]+ [[package]] name = "epaint" version = "0.37.0"@@ -2871,7 +2914,7 @@ dependencies = [  "skrifa 0.42.1",  "smallvec",  "subsetter",- "tiny-skia-path",+ "tiny-skia-path 0.12.0",  "xmp-writer",  "yoke",  "zune-jpeg",@@ -3575,6 +3618,15 @@ dependencies = [  "windows-sys 0.61.2", ] +[[package]]+name = "owned_ttf_parser"+version = "0.25.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"+dependencies = [+ "ttf-parser",+]+ [[package]] name = "parking" version = "2.2.1"@@ -4557,6 +4609,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]]+name = "sctk-adwaita"+version = "0.10.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"+dependencies = [+ "ab_glyph",+ "log",+ "memmap2",+ "smithay-client-toolkit 0.19.2",+ "tiny-skia",+]+ [[package]] name = "security-framework" version = "3.7.0"@@ -5368,6 +5433,31 @@ dependencies = [  "time-core", ] +[[package]]+name = "tiny-skia"+version = "0.11.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"+dependencies = [+ "arrayref",+ "arrayvec",+ "bytemuck",+ "cfg-if",+ "log",+ "tiny-skia-path 0.11.4",+]++[[package]]+name = "tiny-skia-path"+version = "0.11.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"+dependencies = [+ "arrayref",+ "bytemuck",+ "strict-num",+]+ [[package]] name = "tiny-skia-path" version = "0.12.0"@@ -5600,6 +5690,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]]+name = "ttf-parser"+version = "0.25.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"+ [[package]] name = "type-map" version = "0.5.1"@@ -6547,6 +6643,7 @@ dependencies = [  "raw-window-handle",  "redox_syscall 0.4.1",  "rustix 0.38.44",+ "sctk-adwaita",  "smithay-client-toolkit 0.19.2",  "smol_str",  "tracing",@@ -6680,6 +6777,19 @@ dependencies = [  "zbus", ] +[[package]]+name = "wordcraft-android"+version = "0.11.0"+dependencies = [+ "android_logger",+ "eframe",+ "egui",+ "log",+ "winit",+ "wordcraft-engine",+ "wordcraft-ui-egui",+]+ [[package]] name = "wordcraft-cli" version = "0.11.0"diff --git a/README.md b/README.md--- a/README.md+++ b/README.md@@ -153,6 +153,8 @@ wordcraft-cli run --script job.json                 # the same from a file: comm  Web: `cd apps/wordcraft-web && trunk serve`, then open <http://127.0.0.1:8771/?sample>. +Experimental Android ARM64: build the native APK from the full workspace with the [Android build guide](apps/wordcraft-android/README.md).+ ## For agents: CLI and MCP  WordCraft was designed to be driven by people *and* by AI agents.diff --git a/apps/wordcraft-android/AndroidManifest.xml b/apps/wordcraft-android/AndroidManifest.xmlnew file mode 100644--- /dev/null+++ b/apps/wordcraft-android/AndroidManifest.xml@@ -0,0 +1,21 @@+<?xml version="1.0" encoding="utf-8"?>+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ai.storyteller.wordcraft">+    <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="35" />+    <application+        android:label="WordCraft"+        android:hasCode="false"+        android:theme="@android:style/Theme.Material.Light.NoActionBar">+        <activity+            android:name="android.app.NativeActivity"+            android:label="WordCraft"+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"+            android:exported="true"+            android:theme="@android:style/Theme.Material.Light.NoActionBar">+            <meta-data android:name="android.app.lib_name" android:value="wordcraft_android" />+            <intent-filter>+                <action android:name="android.intent.action.MAIN" />+                <category android:name="android.intent.category.LAUNCHER" />+            </intent-filter>+        </activity>+    </application>+</manifest>diff --git a/apps/wordcraft-android/Cargo.toml b/apps/wordcraft-android/Cargo.tomlnew file mode 100644--- /dev/null+++ b/apps/wordcraft-android/Cargo.toml@@ -0,0 +1,48 @@+[package]+name = "wordcraft-android"+version.workspace = true+edition.workspace = true+license.workspace = true+publish = false+description = "WordCraft native Android app"+[lints.rust]+unsafe_code = "allow"+[lints.clippy]+dbg_macro = "warn"+todo = "warn"+too_many_arguments = "allow"+type_complexity = "allow"+[lib]+crate-type = ["cdylib"]+[dependencies]+wordcraft-engine = { workspace = true }+wordcraft-ui-egui = { workspace = true }+eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }+egui = { workspace = true }+winit = { version = "0.30", features = ["android-native-activity"] }+android_logger = "0.15.1"+log = { workspace = true }+[package.metadata.android]+package = "ai.storyteller.wordcraft"+apk_name = "wordcraft"+use_aapt2 = false+manifest_override = "AndroidManifest.xml"+build_targets = ["aarch64-linux-android"]+include_cplusplus_shared = true+[package.metadata.android.sdk]+min_sdk_version = 26+target_sdk_version = 35++[package.metadata.android.application]+has_code = false++[[package.metadata.android.application.activity]]+name = "android.app.NativeActivity"++[[package.metadata.android.application.activity.meta_data]]+name = "android.app.lib_name"+value = "wordcraft_android"++[[package.metadata.android.application.activity.intent_filter]]+actions = ["android.intent.action.MAIN"]+categories = ["android.intent.category.LAUNCHER"]diff --git a/apps/wordcraft-android/README.md b/apps/wordcraft-android/README.mdnew file mode 100644--- /dev/null+++ b/apps/wordcraft-android/README.md@@ -0,0 +1,76 @@+# WordCraft para Android ARM64 (experimental)++Este é o alvo Android nativo do WordCraft. Ele reutiliza o motor e a interface egui do workspace completo, sem WebView. O APK é compilado para `arm64-v8a` (`aarch64-linux-android`) e requer Android 8.0/API 26 ou posterior.++## Compilar do zero no Linux ou WSL 2++### 1. Instale as ferramentas++Instale Rust pelo [rustup](https://rustup.rs/), o **JDK 21 completo** (precisa incluir `java`, `javac` e `keytool`) e as Android Command-line Tools. Com `ANDROID_HOME` apontando para o SDK, aceite a licença Android interativamente e instale as versões usadas por este alvo:++```sh+export ANDROID_HOME="$HOME/Android/Sdk"+export ANDROID_SDK_ROOT="$ANDROID_HOME"+"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses+"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install \+  "platform-tools" \+  "platforms;android-35" \+  "build-tools;35.0.0" \+  "ndk;30.0.16248370"+export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/30.0.16248370"+```++O projeto fixa o API de compilação em 35, o Build Tools em 35.0.0 e o NDK em 30.0.16248370. As licenças precisam ser aceitas no computador antes do primeiro build.++### 2. Clone a branch Android++```sh+git clone --branch android-native-arm64 https://github.com/anonymous3210/wordcraft.git+cd wordcraft+```++Se o repositório já estiver clonado, entre na pasta e faça checkout da branch `android-native-arm64`.++### 3. Gere o APK++```sh+./apps/wordcraft-android/build.sh+```++O script verifica as ferramentas, instala `cargo-apk2` 1.5.0 se estiver faltando, adiciona o target Rust ARM64 e compila usando o `Cargo.lock` do repositório. O arquivo de saída é:++```text+target/debug/apk/wordcraft.apk+```++Para instalar num aparelho ARM64 conectado com depuração USB:++```sh+adb install -r target/debug/apk/wordcraft.apk+```++Também é possível executar manualmente, sem o script, depois de preparar as ferramentas:++```sh+rustup target add aarch64-linux-android+cargo install cargo-apk2 --version 1.5.0 --locked+cargo apk2 build --manifest-path apps/wordcraft-android/Cargo.toml+```++## Build release++Uma build release otimizada exige uma chave de assinatura própria. Guarde o arquivo e a senha fora do repositório e não reutilize a chave de depuração:++```sh+export CARGO_APK_RELEASE_KEYSTORE="$HOME/.android/wordcraft-release.jks"+read -rsp "Senha do keystore: " CARGO_APK_RELEASE_KEYSTORE_PASSWORD; echo+export CARGO_APK_RELEASE_KEYSTORE_PASSWORD+cargo apk2 build --release --manifest-path apps/wordcraft-android/Cargo.toml+unset CARGO_APK_RELEASE_KEYSTORE CARGO_APK_RELEASE_KEYSTORE_PASSWORD+```++O APK otimizado sai em `target/release/apk/wordcraft.apk`. O APK gerado pelo script padrão é de depuração e serve para instalação/teste; não o publique numa loja.++## Escopo atual++A inicialização, o renderizador e a interface são nativos. Ainda não estão conectados o seletor Android de documentos nem os serviços de impressão e compartilhamento. A interface existente foi desenhada para ribbon de desktop, portanto a ergonomia precisa ser validada num aparelho. Esta integração compila e empacota, mas não foi testada em dispositivo físico.diff --git a/apps/wordcraft-android/build.sh b/apps/wordcraft-android/build.shnew file mode 100755--- /dev/null+++ b/apps/wordcraft-android/build.sh@@ -0,0 +1,38 @@+#!/usr/bin/env bash+set -euo pipefail++ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"+cd "$ROOT"++fail() {+  printf 'ERRO: %s\n' "$1" >&2+  exit 1+}++[[ "$(uname -s)" == "Linux" ]] || fail "Este script suporta Linux e WSL. No Windows, use WSL 2 com a toolchain Android Linux."+command -v cargo >/dev/null 2>&1 || fail "Rust/Cargo não encontrado. Instale Rust pelo rustup: https://rustup.rs/"+command -v rustup >/dev/null 2>&1 || fail "rustup não encontrado. Instale Rust pelo rustup: https://rustup.rs/"+command -v java >/dev/null 2>&1 || fail "Java não encontrado. Instale JDK 21 (incluindo java, javac e keytool)."+command -v javac >/dev/null 2>&1 || fail "javac não encontrado. Instale o JDK completo, não apenas o Java Runtime."+command -v keytool >/dev/null 2>&1 || fail "keytool não encontrado. Instale o JDK completo."++export ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}"+export ANDROID_SDK_ROOT="$ANDROID_HOME"+export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/30.0.16248370}"+export NDK_HOME="$ANDROID_NDK_HOME"++[[ -f "$ANDROID_HOME/platforms/android-35/android.jar" ]] || fail "Android SDK API 35 ausente. Instale: platforms;android-35"+[[ -x "$ANDROID_HOME/build-tools/35.0.0/aapt" ]] || fail "Android Build Tools 35.0.0 ausentes. Instale: build-tools;35.0.0"+[[ -x "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android26-clang" ]] || fail "NDK 30.0.16248370 ausente ou inválido em $ANDROID_NDK_HOME"++if ! command -v cargo-apk2 >/dev/null 2>&1; then+  printf 'Instalando cargo-apk2 1.5.0...\n'+  cargo install cargo-apk2 --version 1.5.0 --locked+fi++rustup target add aarch64-linux-android+cargo apk2 build --manifest-path "$ROOT/apps/wordcraft-android/Cargo.toml"++APK="$ROOT/target/debug/apk/wordcraft.apk"+[[ -s "$APK" ]] || fail "O build terminou sem produzir $APK"+printf '\nAPK criado com sucesso:\n%s\n' "$APK"diff --git a/apps/wordcraft-android/src/lib.rs b/apps/wordcraft-android/src/lib.rsnew file mode 100644--- /dev/null+++ b/apps/wordcraft-android/src/lib.rs@@ -0,0 +1,43 @@+#![cfg(target_os = "android")]++use eframe::{App, Frame, NativeOptions, egui};+use winit::platform::android::activity::AndroidApp;+use wordcraft_engine::Session;+use wordcraft_ui_egui::{Services, WordApp};++struct AndroidWordCraft(WordApp);++impl App for AndroidWordCraft {+    fn logic(&mut self, ctx: &egui::Context, _frame: &mut Frame) {+        self.0.logic(ctx);+    }++    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut Frame) {+        self.0.ui(ui);+    }+}++#[unsafe(no_mangle)]+fn android_main(android_app: AndroidApp) {+    android_logger::init_once(android_logger::Config::default().with_max_level(log::LevelFilter::Info));++    let options =+        NativeOptions { android_app: Some(android_app), viewport: egui::ViewportBuilder::default().with_title("WordCraft"), ..Default::default() };++    let result = eframe::run_native(+        "WordCraft",+        options,+        Box::new(|_creation_context| {+            let region = wordcraft_ui_egui::i18n::system_region();+            let mut session = Session::new(wordcraft_engine::blank_document(&region));+            session.system_region = region;+            let mut app = WordApp::new(session, Services::default());+            app.start_screen();+            Ok(Box::new(AndroidWordCraft(app)))+        }),+    );++    if let Err(error) = result {+        log::error!("WordCraft Android startup failed: {error}");+    }+}