From db2373b40de82f943f811035135668da5a4b9b19 Mon Sep 17 00:00:00 2001 From: Kohei Date: Sun, 1 Mar 2026 00:19:47 +0900 Subject: [PATCH] feat: integrate dual-flavor CI, fix keystore heredoc, ABI split control, and backend startup order - Add embedded job to android-build.yml and release.yml with Go build step - Fix keystore.properties generation using printf instead of heredoc to avoid whitespace issues - Control ABI split via -PenableAbiSplit Gradle property (disabled by default, enabled only in CI embedded builds) - Wire EmbeddedBackendLifecycle in FlavorModule DI, replacing NoopBackendLifecycle - Fix WebSocket connect race by combining backendLifecycle.state with settings flow, waiting for BackendState.RUNNING before connecting - Add per-architecture Makefile targets (build-android-arm64, build-android-x86_64, build-android-arm) and clean-android target - Fix termux APK filename consistency in release workflow Co-Authored-By: Claude Opus 4.6 --- .github/workflows/android-build.yml | 74 +++++++++++++-- .github/workflows/release.yml | 94 ++++++++++++++++--- Makefile | 53 +++++++++-- android/app/build.gradle.kts | 13 +++ .../java/io/clawdroid/di/FlavorModule.kt | 8 +- .../main/java/io/clawdroid/ClawDroidApp.kt | 18 +++- 6 files changed, 223 insertions(+), 37 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 533a6a714..6fe5b7b6d 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -5,15 +5,25 @@ on: branches: ["main"] paths: - "android/**" + - "cmd/**" + - "pkg/**" + - "go.mod" + - "go.sum" + - "Makefile" - ".github/workflows/android-build.yml" pull_request: paths: - "android/**" + - "cmd/**" + - "pkg/**" + - "go.mod" + - "go.sum" + - "Makefile" - ".github/workflows/android-build.yml" workflow_dispatch: jobs: - build: + termux: runs-on: ubuntu-latest defaults: run: @@ -43,12 +53,8 @@ jobs: KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | - cat > keystore.properties < keystore.properties - name: Build release APK run: ./gradlew assembleTermuxRelease @@ -56,5 +62,55 @@ jobs: - name: Upload APK uses: actions/upload-artifact@v4 with: - name: clawdroid-release-apk - path: android/app/build/outputs/apk/termuxRelease/*.apk + name: clawdroid-termux-release-apk + path: android/app/build/outputs/apk/termux/release/*.apk + + embedded: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build Go binaries for Android + run: make build-android + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Decode keystore + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + working-directory: android + run: | + echo "$KEYSTORE_BASE64" | base64 -d > release.keystore + + - name: Create keystore.properties + env: + STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + working-directory: android + run: | + printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \ + "$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > keystore.properties + + - name: Build release APKs + working-directory: android + run: ./gradlew assembleEmbeddedRelease -PenableAbiSplit + + - name: Upload APKs + uses: actions/upload-artifact@v4 + with: + name: clawdroid-embedded-release-apks + path: android/app/build/outputs/apk/embedded/release/*.apk diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51257fd4b..3fbebe8ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,8 +77,8 @@ jobs: --draft=${{ inputs.draft }} \ --prerelease=${{ inputs.prerelease }} - android: - name: Android Release APK + android-termux: + name: Android Termux APK needs: create-tag runs-on: ubuntu-latest permissions: @@ -112,24 +112,94 @@ jobs: KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | - cat > keystore.properties < keystore.properties - name: Build release APK run: ./gradlew assembleTermuxRelease - name: Rename APK with version + env: + RELEASE_TAG: ${{ inputs.tag }} run: | - mv app/build/outputs/apk/termuxRelease/app-termux-release.apk \ - app/build/outputs/apk/termuxRelease/clawdroid-${{ inputs.tag }}.apk + mv app/build/outputs/apk/termux/release/app-termux-release.apk \ + app/build/outputs/apk/termux/release/clawdroid-termux-"$RELEASE_TAG".apk - name: Upload APK to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ inputs.tag }} run: | - gh release upload "${{ inputs.tag }}" \ - app/build/outputs/apk/termuxRelease/clawdroid-${{ inputs.tag }}.apk + gh release upload "$RELEASE_TAG" \ + app/build/outputs/apk/termux/release/clawdroid-termux-"$RELEASE_TAG".apk + + android-embedded: + name: Android Embedded APKs + needs: create-tag + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout tag + uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag }} + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build Go binaries for Android + run: make build-android + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Decode keystore + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + working-directory: android + run: echo "$KEYSTORE_BASE64" | base64 -d > release.keystore + + - name: Create keystore.properties + env: + STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + working-directory: android + run: | + printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \ + "$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > keystore.properties + + - name: Build release APKs + working-directory: android + run: ./gradlew assembleEmbeddedRelease -PenableAbiSplit + + - name: Rename APKs with version + env: + RELEASE_TAG: ${{ inputs.tag }} + working-directory: android/app/build/outputs/apk/embedded/release + run: | + mv app-embedded-arm64-v8a-release.apk clawdroid-"$RELEASE_TAG"-arm64-v8a.apk + mv app-embedded-armeabi-v7a-release.apk clawdroid-"$RELEASE_TAG"-armeabi-v7a.apk + mv app-embedded-x86_64-release.apk clawdroid-"$RELEASE_TAG"-x86_64.apk + mv app-embedded-universal-release.apk clawdroid-"$RELEASE_TAG"-universal.apk + + - name: Upload APKs to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ inputs.tag }} + working-directory: android/app/build/outputs/apk/embedded/release + run: | + gh release upload "$RELEASE_TAG" \ + clawdroid-"$RELEASE_TAG"-arm64-v8a.apk \ + clawdroid-"$RELEASE_TAG"-armeabi-v7a.apk \ + clawdroid-"$RELEASE_TAG"-x86_64.apk \ + clawdroid-"$RELEASE_TAG"-universal.apk diff --git a/Makefile b/Makefile index 0ac83c492..08f50efec 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build install uninstall clean help test build-android +.PHONY: all build install uninstall clean help test build-android build-android-arm64 build-android-x86_64 build-android-arm clean-android # Build variables BINARY_NAME=clawdroid @@ -23,6 +23,9 @@ INSTALL_PREFIX?=$(HOME)/.local INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin INSTALL_MAN_DIR=$(INSTALL_PREFIX)/share/man/man1 +# Android +ANDROID_JNILIBS_DIR=android/app/src/embedded/jniLibs + # Workspace and Skills CLAWDROID_HOME?=$(HOME)/.clawdroid WORKSPACE_DIR?=$(CLAWDROID_HOME)/workspace @@ -72,19 +75,49 @@ build-all: generate GOOS=linux GOARCH=arm $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm ./$(CMD_DIR) @echo "All builds complete" -## build-android: Build clawdroid for Android (embedded flavor jniLibs) +## build-android: Build clawdroid for Android (all architectures) build-android: generate - @echo "Building $(BINARY_NAME) for Android..." - @mkdir -p android/app/src/embedded/jniLibs/arm64-v8a - @mkdir -p android/app/src/embedded/jniLibs/x86_64 - @mkdir -p android/app/src/embedded/jniLibs/armeabi-v7a + @echo "Building $(BINARY_NAME) for Android (all architectures)..." + @mkdir -p $(ANDROID_JNILIBS_DIR)/arm64-v8a + @mkdir -p $(ANDROID_JNILIBS_DIR)/x86_64 + @mkdir -p $(ANDROID_JNILIBS_DIR)/armeabi-v7a CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -trimpath $(LDFLAGS) \ - -o android/app/src/embedded/jniLibs/arm64-v8a/libclawdroid.so ./$(CMD_DIR) + -o $(ANDROID_JNILIBS_DIR)/arm64-v8a/libclawdroid.so ./$(CMD_DIR) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath $(LDFLAGS) \ - -o android/app/src/embedded/jniLibs/x86_64/libclawdroid.so ./$(CMD_DIR) + -o $(ANDROID_JNILIBS_DIR)/x86_64/libclawdroid.so ./$(CMD_DIR) CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GO) build -trimpath $(LDFLAGS) \ - -o android/app/src/embedded/jniLibs/armeabi-v7a/libclawdroid.so ./$(CMD_DIR) - @echo "Android build complete" + -o $(ANDROID_JNILIBS_DIR)/armeabi-v7a/libclawdroid.so ./$(CMD_DIR) + @echo "Android build complete (all architectures)" + +## build-android-arm64: Build clawdroid for Android (arm64-v8a only) +build-android-arm64: generate + @echo "Building $(BINARY_NAME) for Android (arm64-v8a)..." + @mkdir -p $(ANDROID_JNILIBS_DIR)/arm64-v8a + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -trimpath $(LDFLAGS) \ + -o $(ANDROID_JNILIBS_DIR)/arm64-v8a/libclawdroid.so ./$(CMD_DIR) + @echo "Android build complete (arm64-v8a)" + +## build-android-x86_64: Build clawdroid for Android (x86_64 only) +build-android-x86_64: generate + @echo "Building $(BINARY_NAME) for Android (x86_64)..." + @mkdir -p $(ANDROID_JNILIBS_DIR)/x86_64 + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath $(LDFLAGS) \ + -o $(ANDROID_JNILIBS_DIR)/x86_64/libclawdroid.so ./$(CMD_DIR) + @echo "Android build complete (x86_64)" + +## build-android-arm: Build clawdroid for Android (armeabi-v7a only) +build-android-arm: generate + @echo "Building $(BINARY_NAME) for Android (armeabi-v7a)..." + @mkdir -p $(ANDROID_JNILIBS_DIR)/armeabi-v7a + CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GO) build -trimpath $(LDFLAGS) \ + -o $(ANDROID_JNILIBS_DIR)/armeabi-v7a/libclawdroid.so ./$(CMD_DIR) + @echo "Android build complete (armeabi-v7a)" + +## clean-android: Remove Android jniLibs build artifacts +clean-android: + @echo "Cleaning Android build artifacts..." + @rm -rf $(ANDROID_JNILIBS_DIR) + @echo "Android clean complete" ## install: Install clawdroid to system and copy builtin skills install: build diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 16329d117..b6bee367e 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -60,6 +60,19 @@ android { buildConfig = true } + packaging { + jniLibs.useLegacyPackaging = true + } + + splits { + abi { + isEnable = project.hasProperty("enableAbiSplit") + reset() + include("arm64-v8a", "armeabi-v7a", "x86_64") + isUniversalApk = true + } + } + sourceSets { getByName("termux") { java.srcDirs("src/termux/java") } getByName("embedded") { java.srcDirs("src/embedded/java") } diff --git a/android/app/src/embedded/java/io/clawdroid/di/FlavorModule.kt b/android/app/src/embedded/java/io/clawdroid/di/FlavorModule.kt index 0eda7f92e..18d6a020f 100644 --- a/android/app/src/embedded/java/io/clawdroid/di/FlavorModule.kt +++ b/android/app/src/embedded/java/io/clawdroid/di/FlavorModule.kt @@ -1,10 +1,12 @@ package io.clawdroid.di import io.clawdroid.backend.api.BackendLifecycle -import io.clawdroid.backend.api.NoopBackendLifecycle +import io.clawdroid.backend.loader.EmbeddedBackendLifecycle +import io.clawdroid.backend.loader.GatewayProcessManager +import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val flavorModule = module { - // Step 13 で EmbeddedBackendLifecycle に差し替え予定 - single { NoopBackendLifecycle() } + single { GatewayProcessManager(androidContext(), get()) } + single { EmbeddedBackendLifecycle(androidContext(), get()) } } diff --git a/android/app/src/main/java/io/clawdroid/ClawDroidApp.kt b/android/app/src/main/java/io/clawdroid/ClawDroidApp.kt index 718f7fa86..c0886a557 100644 --- a/android/app/src/main/java/io/clawdroid/ClawDroidApp.kt +++ b/android/app/src/main/java/io/clawdroid/ClawDroidApp.kt @@ -2,6 +2,8 @@ package io.clawdroid import android.app.Application import android.util.Log +import io.clawdroid.backend.api.BackendLifecycle +import io.clawdroid.backend.api.BackendState import io.clawdroid.backend.api.GatewaySettingsStore import io.clawdroid.backend.config.ConfigApiClient import io.clawdroid.backend.config.configModule @@ -10,7 +12,9 @@ import io.clawdroid.di.appModule import io.clawdroid.di.flavorModule import io.clawdroid.receiver.NotificationHelper import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.serialization.json.jsonObject @@ -29,14 +33,22 @@ class ClawDroidApp : Application() { NotificationHelper.createNotificationChannel(this) val koin = koinApp.koin + val backendLifecycle: BackendLifecycle = koin.get() val settingsStore: GatewaySettingsStore = koin.get() val wsClient: WebSocketClient = koin.get() val configApiClient: ConfigApiClient = koin.get() val scope: CoroutineScope = koin.get() + scope.launch { backendLifecycle.start() } scope.launch { - // Only react when httpPort or apiKey actually change - settingsStore.settings - .map { it.httpPort to it.apiKey } + // Wait for backend to be running before connecting WebSocket + combine( + settingsStore.settings + .map { it.httpPort to it.apiKey } + .distinctUntilChanged(), + backendLifecycle.state, + ) { settings, state -> settings to state } + .filter { (_, state) -> state == BackendState.RUNNING } + .map { (settings, _) -> settings } .distinctUntilChanged() .collect { // Fetch WS connection info from config API