Files
yellowjacket/build/android/scripts/deps/install_deps.go
logan 48b37f6301 build(android): carry the Wails Android scaffolding verbatim
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.
2026-08-16 15:30:13 -04:00

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