Plan 015 phase 0 established that this app cross-compiles for Android with no source changes at all. A CGO_ENABLED=0 probe of the whole tree for android/arm64 fails on exactly two packages -- ebitengine/oto/v3 and wails/v3/pkg/application -- and both fail only because their Android implementation is cgo, which is what the NDK supplies. Notably modernc.org/sqlite, the entire database layer and the thing most likely to have no Android target, is clean. The fat APK (arm64-v8a + x86_64) builds in about 25 seconds. So build/android/ stops being ignored. This commit is the tree exactly as `wails3 generate build-assets` emits it, so that the next commit is a readable diff of what we changed and a future refresh has something to compare against. Two things about how it is carried: `wails3 update build-assets` does NOT generate it, contrary to what CLAUDE.md has claimed since the v3 migration. In beta.8 that command extracts only internal/commands/updatable_build_assets, which is darwin/ios/linux/windows; the android tree comes from `generate build-assets`, which rewrites the whole of build/. It was generated once into a scratch directory and copied across, so from here it is committed and hand-edited like source. Only its output is ignored -- jniLibs (~60MB of per-ABI c-shared libraries), gen/, overlay.json and Gradle's own directories. And it brings one Go file into ./... -- scripts/deps/install_deps.go, the interactive SDK installer behind `task android:install:deps`, which trips 24 of our strict linters. golangci excludes the directory rather than reformatting upstream's file, which the next refresh would undo and which would make the diff against upstream unreadable. This repo uses `make android-setup` instead.
267 lines
8.1 KiB
Go
267 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func main() {
|
|
fmt.Println("Checking Android development dependencies...")
|
|
fmt.Println()
|
|
|
|
errors := []string{}
|
|
|
|
// Check Go
|
|
if !checkCommand("go", "version") {
|
|
errors = append(errors, "Go is not installed. Install from https://go.dev/dl/")
|
|
} else {
|
|
fmt.Println("✓ Go is installed")
|
|
}
|
|
|
|
// Check ANDROID_HOME
|
|
androidHome := os.Getenv("ANDROID_HOME")
|
|
if androidHome == "" {
|
|
androidHome = os.Getenv("ANDROID_SDK_ROOT")
|
|
}
|
|
if androidHome == "" {
|
|
// Try common default locations
|
|
home, _ := os.UserHomeDir()
|
|
possiblePaths := []string{
|
|
filepath.Join(home, "Android", "Sdk"),
|
|
filepath.Join(home, "Library", "Android", "sdk"),
|
|
"/usr/local/share/android-sdk",
|
|
}
|
|
for _, p := range possiblePaths {
|
|
if _, err := os.Stat(p); err == nil {
|
|
androidHome = p
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if androidHome == "" {
|
|
errors = append(errors, "ANDROID_HOME not set. Install Android Studio and set ANDROID_HOME environment variable")
|
|
} else {
|
|
fmt.Printf("✓ ANDROID_HOME: %s\n", androidHome)
|
|
}
|
|
|
|
// Check adb
|
|
if !checkCommand("adb", "version") {
|
|
if androidHome != "" {
|
|
platformTools := filepath.Join(androidHome, "platform-tools")
|
|
errors = append(errors, fmt.Sprintf("adb not found. Add %s to PATH", platformTools))
|
|
} else {
|
|
errors = append(errors, "adb not found. Install Android SDK Platform-Tools")
|
|
}
|
|
} else {
|
|
fmt.Println("✓ adb is installed")
|
|
}
|
|
|
|
// Check emulator
|
|
if !checkCommand("emulator", "-list-avds") {
|
|
if androidHome != "" {
|
|
emulatorPath := filepath.Join(androidHome, "emulator")
|
|
errors = append(errors, fmt.Sprintf("emulator not found. Add %s to PATH", emulatorPath))
|
|
} else {
|
|
errors = append(errors, "emulator not found. Install Android Emulator via SDK Manager")
|
|
}
|
|
} else {
|
|
fmt.Println("✓ Android Emulator is installed")
|
|
}
|
|
|
|
// Check NDK
|
|
ndkHome := os.Getenv("ANDROID_NDK_HOME")
|
|
if ndkHome == "" && androidHome != "" {
|
|
// Look for NDK in default location
|
|
ndkDir := filepath.Join(androidHome, "ndk")
|
|
if entries, err := os.ReadDir(ndkDir); err == nil {
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
ndkHome = filepath.Join(ndkDir, entry.Name())
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ndkHome == "" {
|
|
errors = append(errors, "Android NDK not found. Install NDK via Android Studio > SDK Manager > SDK Tools > NDK (Side by side)")
|
|
} else {
|
|
fmt.Printf("✓ Android NDK: %s\n", ndkHome)
|
|
}
|
|
|
|
// Check Java
|
|
if !checkCommand("java", "-version") {
|
|
errors = append(errors, "Java not found. Install JDK 11+ (OpenJDK recommended)")
|
|
} else {
|
|
fmt.Println("✓ Java is installed")
|
|
}
|
|
|
|
// Check for AVD (Android Virtual Device)
|
|
if checkCommand("emulator", "-list-avds") {
|
|
cmd := exec.Command("emulator", "-list-avds")
|
|
output, err := cmd.Output()
|
|
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
|
avds := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
fmt.Printf("✓ Found %d Android Virtual Device(s)\n", len(avds))
|
|
} else {
|
|
// Mirror the iOS installer, which offers to create a simulator when
|
|
// none exist. Only create from an already-installed system image.
|
|
offerCreateAVD(androidHome)
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
if len(errors) > 0 {
|
|
fmt.Println("❌ Missing dependencies:")
|
|
for _, err := range errors {
|
|
fmt.Printf(" - %s\n", err)
|
|
}
|
|
fmt.Println()
|
|
fmt.Println("Setup instructions:")
|
|
fmt.Println("1. Install Android Studio: https://developer.android.com/studio")
|
|
fmt.Println("2. Open SDK Manager and install:")
|
|
fmt.Println(" - Android SDK Platform (API 35)")
|
|
fmt.Println(" - Android SDK Build-Tools")
|
|
fmt.Println(" - Android SDK Platform-Tools")
|
|
fmt.Println(" - Android Emulator")
|
|
fmt.Println(" - NDK (Side by side)")
|
|
fmt.Println("3. Set environment variables:")
|
|
if runtime.GOOS == "darwin" {
|
|
fmt.Println(" export ANDROID_HOME=$HOME/Library/Android/sdk")
|
|
} else {
|
|
fmt.Println(" export ANDROID_HOME=$HOME/Android/Sdk")
|
|
}
|
|
fmt.Println(" export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator")
|
|
fmt.Println("4. Create an AVD via Android Studio > Tools > Device Manager")
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("✓ All Android development dependencies are installed!")
|
|
}
|
|
|
|
func checkCommand(name string, args ...string) bool {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
return cmd.Run() == nil
|
|
}
|
|
|
|
// offerCreateAVD mirrors the iOS installer's simulator offer: when no AVD
|
|
// exists, offer to create one — but ONLY from a system image that is already
|
|
// installed. We never run sdkmanager here (a multi-GB download with a license
|
|
// prompt is a surprise the user should trigger themselves).
|
|
func offerCreateAVD(androidHome string) {
|
|
abi := "x86_64"
|
|
if runtime.GOARCH == "arm64" {
|
|
abi = "arm64-v8a"
|
|
}
|
|
|
|
// Find the highest-API installed system image matching the host ABI.
|
|
// The API level must be compared numerically: lexicographic sorting
|
|
// would rank android-9 above android-35.
|
|
var img string
|
|
if androidHome != "" {
|
|
matches, _ := filepath.Glob(filepath.Join(androidHome, "system-images", "android-*", "*", abi))
|
|
bestAPI := -1
|
|
for _, m := range matches {
|
|
apiDir := filepath.Base(filepath.Dir(filepath.Dir(m)))
|
|
api, err := strconv.Atoi(strings.TrimPrefix(apiDir, "android-"))
|
|
if err != nil {
|
|
continue // preview/extension images (e.g. android-35-ext14)
|
|
}
|
|
if api > bestAPI {
|
|
bestAPI = api
|
|
img = m
|
|
}
|
|
}
|
|
}
|
|
|
|
avdmanager := findAVDManager(androidHome)
|
|
|
|
if img == "" || avdmanager == "" {
|
|
fmt.Println("⚠ No Android Virtual Devices found.")
|
|
fmt.Println(" Install a system image and create an AVD, e.g.:")
|
|
fmt.Printf(" sdkmanager 'system-images;android-35;google_apis;%s'\n", abi)
|
|
fmt.Printf(" avdmanager create avd --name wails --package 'system-images;android-35;google_apis;%s' --device pixel_7\n", abi)
|
|
return
|
|
}
|
|
|
|
// Derive the package path from the installed image directory, e.g.
|
|
// <sdk>/system-images/android-35/google_apis/arm64-v8a
|
|
// -> system-images;android-35;google_apis;arm64-v8a
|
|
rel := strings.TrimPrefix(img, filepath.Join(androidHome, "system-images")+string(os.PathSeparator))
|
|
pkg := "system-images;" + strings.ReplaceAll(rel, string(os.PathSeparator), ";")
|
|
|
|
fmt.Println("⚠ No Android Virtual Devices found.")
|
|
fmt.Printf(" Would you like to create a 'wails' AVD from %s?\n", pkg)
|
|
if !promptUser("Create AVD?") {
|
|
fmt.Println(" Skipping AVD creation.")
|
|
fmt.Printf(" Create manually: avdmanager create avd --name wails --package '%s' --device pixel_7\n", pkg)
|
|
return
|
|
}
|
|
|
|
cmd := exec.Command(avdmanager, "create", "avd", "--name", "wails", "--package", pkg, "--device", "pixel_7", "--force")
|
|
cmd.Stdin = strings.NewReader("no\n") // decline the custom hardware-profile prompt
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Printf(" Failed to create AVD: %v\n", err)
|
|
} else {
|
|
fmt.Println(" ✅ 'wails' AVD created")
|
|
}
|
|
}
|
|
|
|
// findAVDManager returns the avdmanager path from PATH, or from the SDK's
|
|
// cmdline-tools (preferring the newest version), or "" if not found.
|
|
func findAVDManager(androidHome string) string {
|
|
if p, err := exec.LookPath("avdmanager"); err == nil {
|
|
return p
|
|
}
|
|
if androidHome != "" {
|
|
matches, _ := filepath.Glob(filepath.Join(androidHome, "cmdline-tools", "*", "bin", "avdmanager"))
|
|
// Prefer the "latest" alias; otherwise compare versions numerically
|
|
// ("9.0" would lexicographically outrank "11.0").
|
|
best := ""
|
|
bestVersion := -1.0
|
|
for _, m := range matches {
|
|
version := filepath.Base(filepath.Dir(filepath.Dir(m)))
|
|
if version == "latest" {
|
|
return m
|
|
}
|
|
v, err := strconv.ParseFloat(version, 64)
|
|
if err != nil {
|
|
v = 0
|
|
}
|
|
if v > bestVersion {
|
|
bestVersion = v
|
|
best = m
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func promptUser(question string) bool {
|
|
if os.Getenv("CI") != "" || os.Getenv("TASK_FORCE_YES") == "true" {
|
|
fmt.Printf("%s [y/N]: y (auto-accepted)\n", question)
|
|
return true
|
|
}
|
|
reader := bufio.NewReader(os.Stdin)
|
|
fmt.Printf("%s [y/N]: ", question)
|
|
response, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return false
|
|
}
|
|
response = strings.ToLower(strings.TrimSpace(response))
|
|
return response == "y" || response == "yes"
|
|
}
|