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 <noreply@anthropic.com>
This commit is contained in:
Kohei 2026-03-01 00:19:47 +09:00
parent 19333f6685
commit db2373b40d
6 changed files with 223 additions and 37 deletions

View file

@ -5,15 +5,25 @@ on:
branches: ["main"] branches: ["main"]
paths: paths:
- "android/**" - "android/**"
- "cmd/**"
- "pkg/**"
- "go.mod"
- "go.sum"
- "Makefile"
- ".github/workflows/android-build.yml" - ".github/workflows/android-build.yml"
pull_request: pull_request:
paths: paths:
- "android/**" - "android/**"
- "cmd/**"
- "pkg/**"
- "go.mod"
- "go.sum"
- "Makefile"
- ".github/workflows/android-build.yml" - ".github/workflows/android-build.yml"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: termux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
@ -43,12 +53,8 @@ jobs:
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: | run: |
cat > keystore.properties <<EOF printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \
storeFile=release.keystore "$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > keystore.properties
storePassword=$STORE_PASSWORD
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PASSWORD
EOF
- name: Build release APK - name: Build release APK
run: ./gradlew assembleTermuxRelease run: ./gradlew assembleTermuxRelease
@ -56,5 +62,55 @@ jobs:
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: clawdroid-release-apk name: clawdroid-termux-release-apk
path: android/app/build/outputs/apk/termuxRelease/*.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

View file

@ -77,8 +77,8 @@ jobs:
--draft=${{ inputs.draft }} \ --draft=${{ inputs.draft }} \
--prerelease=${{ inputs.prerelease }} --prerelease=${{ inputs.prerelease }}
android: android-termux:
name: Android Release APK name: Android Termux APK
needs: create-tag needs: create-tag
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -112,24 +112,94 @@ jobs:
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: | run: |
cat > keystore.properties <<EOF printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \
storeFile=release.keystore "$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > keystore.properties
storePassword=$STORE_PASSWORD
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PASSWORD
EOF
- name: Build release APK - name: Build release APK
run: ./gradlew assembleTermuxRelease run: ./gradlew assembleTermuxRelease
- name: Rename APK with version - name: Rename APK with version
env:
RELEASE_TAG: ${{ inputs.tag }}
run: | run: |
mv app/build/outputs/apk/termuxRelease/app-termux-release.apk \ mv app/build/outputs/apk/termux/release/app-termux-release.apk \
app/build/outputs/apk/termuxRelease/clawdroid-${{ inputs.tag }}.apk app/build/outputs/apk/termux/release/clawdroid-termux-"$RELEASE_TAG".apk
- name: Upload APK to release - name: Upload APK to release
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ inputs.tag }}
run: | run: |
gh release upload "${{ inputs.tag }}" \ gh release upload "$RELEASE_TAG" \
app/build/outputs/apk/termuxRelease/clawdroid-${{ inputs.tag }}.apk 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

View file

@ -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 # Build variables
BINARY_NAME=clawdroid BINARY_NAME=clawdroid
@ -23,6 +23,9 @@ INSTALL_PREFIX?=$(HOME)/.local
INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin
INSTALL_MAN_DIR=$(INSTALL_PREFIX)/share/man/man1 INSTALL_MAN_DIR=$(INSTALL_PREFIX)/share/man/man1
# Android
ANDROID_JNILIBS_DIR=android/app/src/embedded/jniLibs
# Workspace and Skills # Workspace and Skills
CLAWDROID_HOME?=$(HOME)/.clawdroid CLAWDROID_HOME?=$(HOME)/.clawdroid
WORKSPACE_DIR?=$(CLAWDROID_HOME)/workspace 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) GOOS=linux GOARCH=arm $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm ./$(CMD_DIR)
@echo "All builds complete" @echo "All builds complete"
## build-android: Build clawdroid for Android (embedded flavor jniLibs) ## build-android: Build clawdroid for Android (all architectures)
build-android: generate build-android: generate
@echo "Building $(BINARY_NAME) for Android..." @echo "Building $(BINARY_NAME) for Android (all architectures)..."
@mkdir -p android/app/src/embedded/jniLibs/arm64-v8a @mkdir -p $(ANDROID_JNILIBS_DIR)/arm64-v8a
@mkdir -p android/app/src/embedded/jniLibs/x86_64 @mkdir -p $(ANDROID_JNILIBS_DIR)/x86_64
@mkdir -p android/app/src/embedded/jniLibs/armeabi-v7a @mkdir -p $(ANDROID_JNILIBS_DIR)/armeabi-v7a
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -trimpath $(LDFLAGS) \ 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) \ 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) \ 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) -o $(ANDROID_JNILIBS_DIR)/armeabi-v7a/libclawdroid.so ./$(CMD_DIR)
@echo "Android build complete" @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: Install clawdroid to system and copy builtin skills
install: build install: build

View file

@ -60,6 +60,19 @@ android {
buildConfig = true buildConfig = true
} }
packaging {
jniLibs.useLegacyPackaging = true
}
splits {
abi {
isEnable = project.hasProperty("enableAbiSplit")
reset()
include("arm64-v8a", "armeabi-v7a", "x86_64")
isUniversalApk = true
}
}
sourceSets { sourceSets {
getByName("termux") { java.srcDirs("src/termux/java") } getByName("termux") { java.srcDirs("src/termux/java") }
getByName("embedded") { java.srcDirs("src/embedded/java") } getByName("embedded") { java.srcDirs("src/embedded/java") }

View file

@ -1,10 +1,12 @@
package io.clawdroid.di package io.clawdroid.di
import io.clawdroid.backend.api.BackendLifecycle 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 import org.koin.dsl.module
val flavorModule = module { val flavorModule = module {
// Step 13 で EmbeddedBackendLifecycle に差し替え予定 single { GatewayProcessManager(androidContext(), get()) }
single<BackendLifecycle> { NoopBackendLifecycle() } single<BackendLifecycle> { EmbeddedBackendLifecycle(androidContext(), get()) }
} }

View file

@ -2,6 +2,8 @@ package io.clawdroid
import android.app.Application import android.app.Application
import android.util.Log 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.api.GatewaySettingsStore
import io.clawdroid.backend.config.ConfigApiClient import io.clawdroid.backend.config.ConfigApiClient
import io.clawdroid.backend.config.configModule import io.clawdroid.backend.config.configModule
@ -10,7 +12,9 @@ import io.clawdroid.di.appModule
import io.clawdroid.di.flavorModule import io.clawdroid.di.flavorModule
import io.clawdroid.receiver.NotificationHelper import io.clawdroid.receiver.NotificationHelper
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@ -29,14 +33,22 @@ class ClawDroidApp : Application() {
NotificationHelper.createNotificationChannel(this) NotificationHelper.createNotificationChannel(this)
val koin = koinApp.koin val koin = koinApp.koin
val backendLifecycle: BackendLifecycle = koin.get()
val settingsStore: GatewaySettingsStore = koin.get() val settingsStore: GatewaySettingsStore = koin.get()
val wsClient: WebSocketClient = koin.get() val wsClient: WebSocketClient = koin.get()
val configApiClient: ConfigApiClient = koin.get() val configApiClient: ConfigApiClient = koin.get()
val scope: CoroutineScope = koin.get() val scope: CoroutineScope = koin.get()
scope.launch { backendLifecycle.start() }
scope.launch { scope.launch {
// Only react when httpPort or apiKey actually change // Wait for backend to be running before connecting WebSocket
settingsStore.settings combine(
.map { it.httpPort to it.apiKey } 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() .distinctUntilChanged()
.collect { .collect {
// Fetch WS connection info from config API // Fetch WS connection info from config API