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"]
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 <<EOF
storeFile=release.keystore
storePassword=$STORE_PASSWORD
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PASSWORD
EOF
printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \
"$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > 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

View file

@ -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 <<EOF
storeFile=release.keystore
storePassword=$STORE_PASSWORD
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PASSWORD
EOF
printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \
"$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > 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

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
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

View file

@ -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") }

View file

@ -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<BackendLifecycle> { NoopBackendLifecycle() }
single { GatewayProcessManager(androidContext(), get()) }
single<BackendLifecycle> { EmbeddedBackendLifecycle(androidContext(), get()) }
}

View file

@ -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
// 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