all: modernize handling of Android SDK and NDK paths
This change removes Gomobile's dependency on ANDROID_HOME and
ANDROID_NDK_HOME. Setting ANDROID_HOME is generally optional,
and ANDROID_NDK_HOME is deprecated.
This change also increases the minimum API version to 16, as
all SDKs that supported API 15 are now deprecated.
Fixes golang/go#52470
Change-Id: I546365774a089e5d7ae1be0a538efd72741d92ac
Reviewed-on: https://go-review.googlesource.com/c/mobile/+/401574
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Daniel Skinner <daniel@dasa.cc>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Hajime Hoshi <hajimehoshi@gmail.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/bind/java/seq_test.go b/bind/java/seq_test.go
index 8f741c6..a5151d4 100644
--- a/bind/java/seq_test.go
+++ b/bind/java/seq_test.go
@@ -17,6 +17,7 @@
"testing"
"golang.org/x/mobile/internal/importers/java"
+ "golang.org/x/mobile/internal/sdkpath"
)
var gomobileBin string
@@ -98,8 +99,8 @@
// runTest runs the Android java test class specified with javaCls. If javaPkg is
// set, it is passed with the -javapkg flag to gomobile. The pkgNames lists the Go
// packages to bind for the test.
-// This requires the gradle command in PATH and
-// the Android SDK whose path is available through ANDROID_HOME environment variable.
+// This requires the gradle command to be in PATH and the Android SDK to be
+// installed.
func runTest(t *testing.T, pkgNames []string, javaPkg, javaCls string) {
if gomobileBin == "" {
t.Skipf("no gomobile on %s", runtime.GOOS)
@@ -108,8 +109,8 @@
if err != nil {
t.Skip("command gradle not found, skipping")
}
- if sdk := os.Getenv("ANDROID_HOME"); sdk == "" {
- t.Skip("ANDROID_HOME environment var not set, skipping")
+ if _, err := sdkpath.AndroidHome(); err != nil {
+ t.Skip("Android SDK not found, skipping")
}
cwd, err := os.Getwd()
diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go
index efbc896..c4552e3 100644
--- a/cmd/gomobile/bind.go
+++ b/cmd/gomobile/bind.go
@@ -16,6 +16,7 @@
"path/filepath"
"strings"
+ "golang.org/x/mobile/internal/sdkpath"
"golang.org/x/mod/modfile"
"golang.org/x/tools/go/packages"
)
@@ -42,9 +43,10 @@
the module import wizard (File > New > New Module > Import .JAR or
.AAR package), and setting it as a new dependency
(File > Project Structure > Dependencies). This requires 'javac'
-(version 1.7+) and Android SDK (API level 15 or newer) to build the
-library for Android. The environment variable ANDROID_HOME must be set
-to the path to Android SDK. Use the -javapkg flag to specify the Java
+(version 1.7+) and Android SDK (API level 16 or newer) to build the
+library for Android. The ANDROID_HOME and ANDROID_NDK_HOME environment
+variables can be used to specify the Android SDK and NDK if they are
+not in the default locations. Use the -javapkg flag to specify the Java
package prefix for the generated classes.
By default, -target=android builds shared libraries for all supported
@@ -85,7 +87,7 @@
if bindPrefix != "" {
return fmt.Errorf("-prefix is supported only for Apple targets")
}
- if _, err := ndkRoot(); err != nil {
+ if _, err := ndkRoot(targets[0]); err != nil {
return err
}
} else {
@@ -156,7 +158,7 @@
if bindBootClasspath != "" {
return bindBootClasspath, nil
}
- apiPath, err := androidAPIPath()
+ apiPath, err := sdkpath.AndroidAPIPath(buildAndroidAPI)
if err != nil {
return "", err
}
diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go
index 7703868..a56fd82 100644
--- a/cmd/gomobile/bind_androidapp.go
+++ b/cmd/gomobile/bind_androidapp.go
@@ -12,15 +12,15 @@
"os"
"os/exec"
"path/filepath"
- "strconv"
"strings"
+ "golang.org/x/mobile/internal/sdkpath"
"golang.org/x/tools/go/packages"
)
func goAndroidBind(gobind string, pkgs []*packages.Package, targets []targetInfo) error {
- if sdkDir := os.Getenv("ANDROID_HOME"); sdkDir == "" {
- return fmt.Errorf("this command requires ANDROID_HOME environment variable (path to the Android SDK)")
+ if _, err := sdkpath.AndroidHome(); err != nil {
+ return fmt.Errorf("this command requires the Android SDK to be installed: %w", err)
}
// Run gobind to generate the bindings
@@ -270,7 +270,7 @@
const (
javacTargetVer = "1.7"
- minAndroidAPI = 15
+ minAndroidAPI = 16
)
func buildJar(w io.Writer, srcDir string) error {
@@ -370,46 +370,3 @@
}
return jarw.Close()
}
-
-// androidAPIPath returns an android SDK platform directory under ANDROID_HOME.
-// If there are multiple platforms that satisfy the minimum version requirement
-// androidAPIPath returns the latest one among them.
-func androidAPIPath() (string, error) {
- sdk := os.Getenv("ANDROID_HOME")
- if sdk == "" {
- return "", fmt.Errorf("ANDROID_HOME environment var is not set")
- }
- sdkDir, err := os.Open(filepath.Join(sdk, "platforms"))
- if err != nil {
- return "", fmt.Errorf("failed to find android SDK platform: %v", err)
- }
- defer sdkDir.Close()
- fis, err := sdkDir.Readdir(-1)
- if err != nil {
- return "", fmt.Errorf("failed to find android SDK platform (API level: %d): %v", buildAndroidAPI, err)
- }
-
- var apiPath string
- var apiVer int
- for _, fi := range fis {
- name := fi.Name()
- if !strings.HasPrefix(name, "android-") {
- continue
- }
- n, err := strconv.Atoi(name[len("android-"):])
- if err != nil || n < buildAndroidAPI {
- continue
- }
- p := filepath.Join(sdkDir.Name(), name)
- _, err = os.Stat(filepath.Join(p, "android.jar"))
- if err == nil && apiVer < n {
- apiPath = p
- apiVer = n
- }
- }
- if apiVer == 0 {
- return "", fmt.Errorf("failed to find android SDK platform (API level: %d) in %s",
- buildAndroidAPI, sdkDir.Name())
- }
- return apiPath, nil
-}
diff --git a/cmd/gomobile/bind_test.go b/cmd/gomobile/bind_test.go
index 0e9f401..cac3511 100644
--- a/cmd/gomobile/bind_test.go
+++ b/cmd/gomobile/bind_test.go
@@ -14,18 +14,22 @@
"strings"
"testing"
"text/template"
+
+ "golang.org/x/mobile/internal/sdkpath"
)
func TestBindAndroid(t *testing.T) {
- androidHome := os.Getenv("ANDROID_HOME")
- if androidHome == "" {
- t.Skip("ANDROID_HOME not found, skipping bind")
- }
- platform, err := androidAPIPath()
+ platform, err := sdkpath.AndroidAPIPath(minAndroidAPI)
if err != nil {
- t.Skip("No android API platform found in $ANDROID_HOME, skipping bind")
+ t.Skip("No compatible Android API platform found, skipping bind")
}
- platform = strings.Replace(platform, androidHome, "$ANDROID_HOME", -1)
+ // platform is a path like "/path/to/Android/sdk/platforms/android-32"
+ components := strings.Split(platform, string(filepath.Separator))
+ if len(components) < 2 {
+ t.Fatalf("API path is too short: %s", platform)
+ }
+ components = components[len(components)-2:]
+ platformRel := filepath.Join("$ANDROID_HOME", components[0], components[1])
defer func() {
xout = os.Stderr
@@ -77,7 +81,7 @@
JavaPkg string
}{
outputData: output,
- AndroidPlatform: platform,
+ AndroidPlatform: platformRel,
JavaPkg: tc.javaPkg,
}
@@ -273,12 +277,8 @@
t.Run(target, func(t *testing.T) {
switch target {
case "android":
- androidHome := os.Getenv("ANDROID_HOME")
- if androidHome == "" {
- t.Skip("ANDROID_HOME not found, skipping bind")
- }
- if _, err := androidAPIPath(); err != nil {
- t.Skip("No android API platform found in $ANDROID_HOME, skipping bind")
+ if _, err := sdkpath.AndroidAPIPath(minAndroidAPI); err != nil {
+ t.Skip("No compatible Android API platform found, skipping bind")
}
case "ios":
if !xcodeAvailable() {
diff --git a/cmd/gomobile/build.go b/cmd/gomobile/build.go
index 4c83ca0..64a28fe 100644
--- a/cmd/gomobile/build.go
+++ b/cmd/gomobile/build.go
@@ -17,6 +17,7 @@
"strconv"
"strings"
+ "golang.org/x/mobile/internal/sdkpath"
"golang.org/x/tools/go/packages"
)
@@ -60,7 +61,7 @@
The default version is 13.0.
Flag -androidapi sets the Android API version to compile against.
-The default and minimum is 15.
+The default and minimum is 16.
The -bundleid flag is required for -target ios and sets the bundle ID to use
with the app.
@@ -215,7 +216,7 @@
if tmpdir != "" {
cmd = strings.Replace(cmd, tmpdir, "$WORK", -1)
}
- if androidHome := os.Getenv("ANDROID_HOME"); androidHome != "" {
+ if androidHome, err := sdkpath.AndroidHome(); err == nil {
cmd = strings.Replace(cmd, androidHome, "$ANDROID_HOME", -1)
}
if gomobilepath != "" {
diff --git a/cmd/gomobile/build_androidapp.go b/cmd/gomobile/build_androidapp.go
index b06ea29..bcd2664 100644
--- a/cmd/gomobile/build_androidapp.go
+++ b/cmd/gomobile/build_androidapp.go
@@ -25,7 +25,7 @@
)
func goAndroidBuild(pkg *packages.Package, targets []targetInfo) (map[string]bool, error) {
- ndkRoot, err := ndkRoot()
+ ndkRoot, err := ndkRoot(targets...)
if err != nil {
return nil, err
}
diff --git a/cmd/gomobile/build_test.go b/cmd/gomobile/build_test.go
index ff21f87..448c6dd 100644
--- a/cmd/gomobile/build_test.go
+++ b/cmd/gomobile/build_test.go
@@ -14,6 +14,8 @@
"strings"
"testing"
"text/template"
+
+ "golang.org/x/mobile/internal/sdkpath"
)
func TestRFC1034Label(t *testing.T) {
@@ -228,12 +230,8 @@
t.Run(target, func(t *testing.T) {
switch target {
case "android":
- androidHome := os.Getenv("ANDROID_HOME")
- if androidHome == "" {
- t.Skip("ANDROID_HOME not found, skipping bind")
- }
- if _, err := androidAPIPath(); err != nil {
- t.Skip("No android API platform found in $ANDROID_HOME, skipping bind")
+ if _, err := sdkpath.AndroidAPIPath(minAndroidAPI); err != nil {
+ t.Skip("No compatible android API platform found, skipping bind")
}
case "ios":
if !xcodeAvailable() {
diff --git a/cmd/gomobile/env.go b/cmd/gomobile/env.go
index ff32517..43f24b9 100644
--- a/cmd/gomobile/env.go
+++ b/cmd/gomobile/env.go
@@ -1,6 +1,8 @@
package main
import (
+ "bufio"
+ "encoding/json"
"errors"
"fmt"
"io/fs"
@@ -10,6 +12,8 @@
"path/filepath"
"runtime"
"strings"
+
+ "golang.org/x/mobile/internal/sdkpath"
)
// General mobile build environment. Initialized by envInit.
@@ -276,29 +280,155 @@
return nil
}
-func ndkRoot() (string, error) {
+// abi maps GOARCH values to Android ABI strings.
+// See https://developer.android.com/ndk/guides/abis
+func abi(goarch string) string {
+ switch goarch {
+ case "arm":
+ return "armeabi-v7a"
+ case "arm64":
+ return "arm64-v8a"
+ case "386":
+ return "x86"
+ case "amd64":
+ return "x86_64"
+ default:
+ return ""
+ }
+}
+
+// checkNDKRoot returns nil if the NDK in `ndkRoot` supports the current configured
+// API version and all the specified Android targets.
+func checkNDKRoot(ndkRoot string, targets []targetInfo) error {
+ platformsJson, err := os.Open(filepath.Join(ndkRoot, "meta", "platforms.json"))
+ if err != nil {
+ return err
+ }
+ defer platformsJson.Close()
+ decoder := json.NewDecoder(platformsJson)
+ supportedVersions := struct {
+ Min int
+ Max int
+ }{}
+ if err := decoder.Decode(&supportedVersions); err != nil {
+ return err
+ }
+ if supportedVersions.Min > buildAndroidAPI ||
+ supportedVersions.Max < buildAndroidAPI {
+ return fmt.Errorf("unsupported API version %d (not in %d..%d)", buildAndroidAPI, supportedVersions.Min, supportedVersions.Max)
+ }
+ abisJson, err := os.Open(filepath.Join(ndkRoot, "meta", "abis.json"))
+ if err != nil {
+ return err
+ }
+ defer abisJson.Close()
+ decoder = json.NewDecoder(abisJson)
+ abis := make(map[string]struct{})
+ if err := decoder.Decode(&abis); err != nil {
+ return err
+ }
+ for _, target := range targets {
+ if !isAndroidPlatform(target.platform) {
+ continue
+ }
+ if _, found := abis[abi(target.arch)]; !found {
+ return fmt.Errorf("ndk does not support %s", target.platform)
+ }
+ }
+ return nil
+}
+
+// compatibleNDKRoots searches the side-by-side NDK dirs for compatible SDKs.
+func compatibleNDKRoots(ndkForest string, targets []targetInfo) ([]string, error) {
+ ndkDirs, err := ioutil.ReadDir(ndkForest)
+ if err != nil {
+ return nil, err
+ }
+ compatibleNDKRoots := []string{}
+ var lastErr error
+ for _, dirent := range ndkDirs {
+ ndkRoot := filepath.Join(ndkForest, dirent.Name())
+ lastErr = checkNDKRoot(ndkRoot, targets)
+ if lastErr == nil {
+ compatibleNDKRoots = append(compatibleNDKRoots, ndkRoot)
+ }
+ }
+ if len(compatibleNDKRoots) > 0 {
+ return compatibleNDKRoots, nil
+ }
+ return nil, lastErr
+}
+
+// ndkVersion returns the full version number of an installed copy of the NDK,
+// or "" if it cannot be determined.
+func ndkVersion(ndkRoot string) string {
+ properties, err := os.Open(filepath.Join(ndkRoot, "source.properties"))
+ if err != nil {
+ return ""
+ }
+ defer properties.Close()
+ // Parse the version number out of the .properties file.
+ // See https://en.wikipedia.org/wiki/.properties
+ scanner := bufio.NewScanner(properties)
+ for scanner.Scan() {
+ line := scanner.Text()
+ tokens := strings.SplitN(line, "=", 2)
+ if len(tokens) != 2 {
+ continue
+ }
+ if strings.TrimSpace(tokens[0]) == "Pkg.Revision" {
+ return strings.TrimSpace(tokens[1])
+ }
+ }
+ return ""
+}
+
+// ndkRoot returns the root path of an installed NDK that supports all the
+// specified Android targets. For details of NDK locations, see
+// https://github.com/android/ndk-samples/wiki/Configure-NDK-Path
+func ndkRoot(targets ...targetInfo) (string, error) {
if buildN {
return "$NDK_PATH", nil
}
- androidHome := os.Getenv("ANDROID_HOME")
- if androidHome != "" {
- ndkRoot := filepath.Join(androidHome, "ndk-bundle")
- _, err := os.Stat(ndkRoot)
- if err == nil {
- return ndkRoot, nil
+ // Try the ANDROID_NDK_HOME variable. This approach is deprecated, but it
+ // has the highest priority because it represents an explicit user choice.
+ if ndkRoot := os.Getenv("ANDROID_NDK_HOME"); ndkRoot != "" {
+ if err := checkNDKRoot(ndkRoot, targets); err != nil {
+ return "", fmt.Errorf("ANDROID_NDK_HOME specifies %s, which is unusable: %w", ndkRoot, err)
}
+ return ndkRoot, nil
}
- ndkRoot := os.Getenv("ANDROID_NDK_HOME")
- if ndkRoot != "" {
- _, err := os.Stat(ndkRoot)
- if err == nil {
- return ndkRoot, nil
- }
+ androidHome, err := sdkpath.AndroidHome()
+ if err != nil {
+ return "", fmt.Errorf("could not locate Android SDK: %w", err)
}
- return "", fmt.Errorf("no Android NDK found in $ANDROID_HOME/ndk-bundle nor in $ANDROID_NDK_HOME")
+ // Use the newest compatible NDK under the side-by-side path arrangement.
+ ndkForest := filepath.Join(androidHome, "ndk")
+ ndkRoots, sideBySideErr := compatibleNDKRoots(ndkForest, targets)
+ if len(ndkRoots) != 0 {
+ // Choose the latest version that supports the build configuration.
+ // NDKs whose version cannot be determined will be least preferred.
+ // In the event of a tie, the later ndkRoot will win.
+ maxVersion := ""
+ var selected string
+ for _, ndkRoot := range ndkRoots {
+ version := ndkVersion(ndkRoot)
+ if version >= maxVersion {
+ maxVersion = version
+ selected = ndkRoot
+ }
+ }
+ return selected, nil
+ }
+ // Try the deprecated NDK location.
+ ndkRoot := filepath.Join(androidHome, "ndk-bundle")
+ if legacyErr := checkNDKRoot(ndkRoot, targets); legacyErr != nil {
+ return "", fmt.Errorf("no usable NDK in %s: %w, %v", androidHome, sideBySideErr, legacyErr)
+ }
+ return ndkRoot, nil
}
func envClang(sdkName string) (clang, cflags string, err error) {
diff --git a/cmd/gomobile/env_test.go b/cmd/gomobile/env_test.go
index 2eef8ab..e5ad282 100644
--- a/cmd/gomobile/env_test.go
+++ b/cmd/gomobile/env_test.go
@@ -5,6 +5,7 @@
package main
import (
+ "fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -18,47 +19,143 @@
}
homeorig := os.Getenv("ANDROID_HOME")
+ os.Unsetenv("ANDROID_HOME")
ndkhomeorig := os.Getenv("ANDROID_NDK_HOME")
+ os.Unsetenv("ANDROID_NDK_HOME")
+
defer func() {
os.Setenv("ANDROID_HOME", homeorig)
os.Setenv("ANDROID_NDK_HOME", ndkhomeorig)
os.RemoveAll(home)
}()
- os.Setenv("ANDROID_HOME", home)
- sdkNDK := filepath.Join(home, "ndk-bundle")
- envNDK := filepath.Join(home, "android-ndk")
- os.Setenv("ANDROID_NDK_HOME", envNDK)
-
- if ndk, err := ndkRoot(); err == nil {
- t.Errorf("expected error but got %q", ndk)
- }
-
- for _, dir := range []string{sdkNDK, envNDK} {
+ makeMockNDK := func(path, version, platforms, abis string) string {
+ dir := filepath.Join(home, path)
if err := os.Mkdir(dir, 0755); err != nil {
t.Fatalf("couldn't mkdir %q", dir)
}
+ propertiesPath := filepath.Join(dir, "source.properties")
+ propertiesData := []byte("Pkg.Revision = " + version)
+ if err := os.WriteFile(propertiesPath, propertiesData, 0644); err != nil {
+ t.Fatalf("couldn't write source.properties: %v", err)
+ }
+ metaDir := filepath.Join(dir, "meta")
+ if err := os.Mkdir(metaDir, 0755); err != nil {
+ t.Fatalf("couldn't mkdir %q", metaDir)
+ }
+ platformsPath := filepath.Join(metaDir, "platforms.json")
+ platformsData := []byte(platforms)
+ if err := os.WriteFile(platformsPath, platformsData, 0644); err != nil {
+ t.Fatalf("couldn't write platforms.json: %v", err)
+ }
+ abisPath := filepath.Join(metaDir, "abis.json")
+ abisData := []byte(abis)
+ if err := os.WriteFile(abisPath, abisData, 0644); err != nil {
+ t.Fatalf("couldn't populate abis.json: %v", err)
+ }
+ return dir
}
- if ndk, _ := ndkRoot(); ndk != sdkNDK {
- t.Errorf("got %q want %q", ndk, sdkNDK)
- }
+ t.Run("no NDK in the default location", func(t *testing.T) {
+ os.Setenv("ANDROID_HOME", home)
+ defer os.Unsetenv("ANDROID_HOME")
+ if ndk, err := ndkRoot(); err == nil {
+ t.Errorf("expected error but got %q", ndk)
+ }
+ })
- os.Unsetenv("ANDROID_HOME")
+ t.Run("NDK location is set but is wrong", func(t *testing.T) {
+ os.Setenv("ANDROID_NDK_HOME", filepath.Join(home, "no-such-path"))
+ defer os.Unsetenv("ANDROID_NDK_HOME")
+ if ndk, err := ndkRoot(); err == nil {
+ t.Errorf("expected error but got %q", ndk)
+ }
+ })
- if ndk, _ := ndkRoot(); ndk != envNDK {
- t.Errorf("got %q want %q", ndk, envNDK)
- }
+ t.Run("Two NDKs installed", func(t *testing.T) {
+ // Default path for pre-side-by-side NDKs.
+ sdkNDK := makeMockNDK("ndk-bundle", "fake-version", `{"min":16,"max":32}`, "{}")
+ defer os.RemoveAll(sdkNDK)
+ // Arbitrary location for testing ANDROID_NDK_HOME.
+ envNDK := makeMockNDK("custom-location", "fake-version", `{"min":16,"max":32}`, "{}")
- os.RemoveAll(envNDK)
+ // ANDROID_NDK_HOME is sufficient.
+ os.Setenv("ANDROID_NDK_HOME", envNDK)
+ if ndk, err := ndkRoot(); ndk != envNDK {
+ t.Errorf("got (%q, %v) want (%q, nil)", ndk, err, envNDK)
+ }
- if ndk, err := ndkRoot(); err == nil {
- t.Errorf("expected error but got %q", ndk)
- }
+ // ANDROID_NDK_HOME takes precedence over ANDROID_HOME.
+ os.Setenv("ANDROID_HOME", home)
+ if ndk, err := ndkRoot(); ndk != envNDK {
+ t.Errorf("got (%q, %v) want (%q, nil)", ndk, err, envNDK)
+ }
- os.Setenv("ANDROID_HOME", home)
+ // ANDROID_NDK_HOME is respected even if there is no NDK there.
+ os.RemoveAll(envNDK)
+ if ndk, err := ndkRoot(); err == nil {
+ t.Errorf("expected error but got %q", ndk)
+ }
- if ndk, _ := ndkRoot(); ndk != sdkNDK {
- t.Errorf("got %q want %q", ndk, sdkNDK)
- }
+ // ANDROID_HOME is used if ANDROID_NDK_HOME is not set.
+ os.Unsetenv("ANDROID_NDK_HOME")
+ if ndk, err := ndkRoot(); ndk != sdkNDK {
+ t.Errorf("got (%q, %v) want (%q, nil)", ndk, err, envNDK)
+ }
+ })
+
+ t.Run("Modern 'side-by-side' NDK selection", func(t *testing.T) {
+ defer func() {
+ buildAndroidAPI = minAndroidAPI
+ }()
+
+ ndkForest := filepath.Join(home, "ndk")
+ if err := os.Mkdir(ndkForest, 0755); err != nil {
+ t.Fatalf("couldn't mkdir %q", ndkForest)
+ }
+
+ path := filepath.Join("ndk", "newer")
+ platforms := `{"min":19,"max":32}`
+ abis := `{"arm64-v8a": {}, "armeabi-v7a": {}, "x86_64": {}}`
+ version := "17.2.0"
+ newerNDK := makeMockNDK(path, version, platforms, abis)
+
+ path = filepath.Join("ndk", "older")
+ platforms = `{"min":16,"max":31}`
+ abis = `{"arm64-v8a": {}, "armeabi-v7a": {}, "x86": {}}`
+ version = "17.1.0"
+ olderNDK := makeMockNDK(path, version, platforms, abis)
+
+ testCases := []struct {
+ api int
+ targets []targetInfo
+ wantNDKRoot string
+ }{
+ {15, nil, ""},
+ {16, nil, olderNDK},
+ {16, []targetInfo{{"android", "arm"}}, olderNDK},
+ {16, []targetInfo{{"android", "arm"}, {"android", "arm64"}}, olderNDK},
+ {16, []targetInfo{{"android", "x86_64"}}, ""},
+ {19, nil, newerNDK},
+ {19, []targetInfo{{"android", "arm"}}, newerNDK},
+ {19, []targetInfo{{"android", "arm"}, {"android", "arm64"}, {"android", "386"}}, olderNDK},
+ {32, nil, newerNDK},
+ {32, []targetInfo{{"android", "arm"}}, newerNDK},
+ {32, []targetInfo{{"android", "386"}}, ""},
+ }
+
+ for i, tc := range testCases {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ buildAndroidAPI = tc.api
+ ndk, err := ndkRoot(tc.targets...)
+ if len(tc.wantNDKRoot) != 0 {
+ if ndk != tc.wantNDKRoot || err != nil {
+ t.Errorf("got (%q, %v), want (%q, nil)", ndk, err, tc.wantNDKRoot)
+ }
+ } else if err == nil {
+ t.Error("expected error")
+ }
+ })
+ }
+ })
}
diff --git a/cmd/gomobile/gendex.go b/cmd/gomobile/gendex.go
index be7470c..88cf554 100644
--- a/cmd/gomobile/gendex.go
+++ b/cmd/gomobile/gendex.go
@@ -13,7 +13,7 @@
// however that would limit gomobile to working with newer versions of
// the Android OS, so we do this while we wait.
//
-// Requires ANDROID_HOME be set to the path of the Android SDK, and
+// Respects ANDROID_HOME to set the path of the Android SDK.
// javac must be on the PATH.
package main
@@ -29,6 +29,8 @@
"os"
"os/exec"
"path/filepath"
+
+ "golang.org/x/mobile/internal/sdkpath"
)
var outfile = flag.String("o", "", "result will be written file")
@@ -52,9 +54,9 @@
}
func gendex() error {
- androidHome := os.Getenv("ANDROID_HOME")
- if androidHome == "" {
- return errors.New("ANDROID_HOME not set")
+ androidHome, err := sdkpath.AndroidHome()
+ if err != nil {
+ return fmt.Errorf("couldn't find Android SDK: %w", err)
}
if err := os.MkdirAll(tmpdir+"/work/org/golang/app", 0775); err != nil {
return err
diff --git a/cmd/gomobile/init.go b/cmd/gomobile/init.go
index 26da302..e7392df 100644
--- a/cmd/gomobile/init.go
+++ b/cmd/gomobile/init.go
@@ -16,6 +16,8 @@
"runtime"
"strings"
"time"
+
+ "golang.org/x/mobile/internal/sdkpath"
)
var (
@@ -122,11 +124,10 @@
if buildN {
cmake = "cmake"
} else {
- sdkRoot := os.Getenv("ANDROID_HOME")
- if sdkRoot == "" {
+ sdkRoot, err := sdkpath.AndroidHome()
+ if err != nil {
return nil
}
- var err error
cmake, err = exec.LookPath("cmake")
if err != nil {
cmakePath := filepath.Join(sdkRoot, "cmake")
diff --git a/cmd/gomobile/version.go b/cmd/gomobile/version.go
index b791556..27fd6bd 100644
--- a/cmd/gomobile/version.go
+++ b/cmd/gomobile/version.go
@@ -11,6 +11,8 @@
"os/exec"
"path/filepath"
"strings"
+
+ "golang.org/x/mobile/internal/sdkpath"
)
var cmdVersion = &command{
@@ -56,8 +58,7 @@
platforms += "," + strings.Join(applePlatforms, ",")
}
- // ANDROID_HOME, sdk build tool version
- androidapi, _ := androidAPIPath()
+ androidapi, _ := sdkpath.AndroidAPIPath(buildAndroidAPI)
fmt.Printf("gomobile version %s (%s); androidSDK=%s\n", version, platforms, androidapi)
return nil
diff --git a/example/ivy/android/README.md b/example/ivy/android/README.md
index d5923ea..dc56712 100644
--- a/example/ivy/android/README.md
+++ b/example/ivy/android/README.md
@@ -11,16 +11,12 @@
- Android NDK
- `golang.org/x/mobile/cmd/gomobile`
-The `gomobile` command uses `ANDROID_HOME` and `ANDROID_NDK_HOME` environment variables.
+The `gomobile` command respects the `ANDROID_HOME` and `ANDROID_NDK_HOME` environment variables. If `gomobile` can't find your SDK and NDK, you can set these environment variables to specify their locations:
```
export ANDROID_HOME=/path/to/sdk-directory
export ANDROID_NDK_HOME=/path/to/ndk-directory
```
-Note: Both ANDROID_SDK_ROOT and ANDROID_NDK_HOME are deprecated in Android tooling, but `gomobile` still uses it. In many cases, `ANDROID_HOME` corresponds to the new [`ANDROID_SDK_ROOT`](https://developer.android.com/studio/command-line/variables). If you installed NDK using [Android Studio's SDK manager](https://developer.android.com/studio/projects/install-ndk#default-version), use the `$ANDROID_SDK_ROOT/ndk/<ndk-version>/` directory as `ANDROID_NDK_HOME` (where `<ndk-version>` is the NDK version you want to use when compiling `robpike.io/ivy` for Android.
-
-(TODO: update `gomobile` to work with modern Android layout)
-
From this directory, run:
```sh
diff --git a/internal/binres/binres.go b/internal/binres/binres.go
index 78b874f..d77bde2 100644
--- a/internal/binres/binres.go
+++ b/internal/binres/binres.go
@@ -82,10 +82,13 @@
ResXMLResourceMap ResType = 0x0180
- ResTablePackage ResType = 0x0200
- ResTableType ResType = 0x0201
- ResTableTypeSpec ResType = 0x0202
- ResTableLibrary ResType = 0x0203
+ ResTablePackage ResType = 0x0200
+ ResTableType ResType = 0x0201
+ ResTableTypeSpec ResType = 0x0202
+ ResTableLibrary ResType = 0x0203
+ ResTableOverlayable ResType = 0x0204
+ ResTableOverlayablePolicy ResType = 0x0205
+ ResTableStagedAlias ResType = 0x0206
)
var (
@@ -247,14 +250,14 @@
Space: "",
Local: "platformBuildVersionCode",
},
- Value: "15",
+ Value: "16",
},
xml.Attr{
Name: xml.Name{
Space: "",
Local: "platformBuildVersionName",
},
- Value: "4.0.4-1406430",
+ Value: "4.1.2-1425332",
})
q = append(q, ltoken{tkn, line})
diff --git a/internal/binres/binres_test.go b/internal/binres/binres_test.go
index 93e7a18..dbac875 100644
--- a/internal/binres/binres_test.go
+++ b/internal/binres/binres_test.go
@@ -16,6 +16,8 @@
"sort"
"strings"
"testing"
+
+ "golang.org/x/mobile/internal/sdkpath"
)
func init() {
@@ -408,10 +410,9 @@
}
}
if err == nil && v == "__" {
- if !strings.HasPrefix(x, "4.0.") {
+ if !strings.HasPrefix(x, "4.1.") {
// as of the time of this writing, the current version of build tools being targeted
- // reports 4.0.4-1406430. Previously, this was 4.0.3. This number is likely still due
- // to change so only report error if 4.x incremented.
+ // reports 4.1.2-1425332.
//
// TODO this check has the potential to hide real errors but can be fixed once more
// of the xml document is unmarshalled and XML can be queried to assure this is related
@@ -455,9 +456,8 @@
}
func TestOpenTable(t *testing.T) {
- sdkdir := os.Getenv("ANDROID_HOME")
- if sdkdir == "" {
- t.Skip("ANDROID_HOME env var not set")
+ if _, err := sdkpath.AndroidHome(); err != nil {
+ t.Skipf("Could not locate Android SDK: %v", err)
}
tbl, err := OpenTable()
if err != nil {
@@ -577,8 +577,10 @@
if typ.entryCount != xtyp.entryCount {
t.Fatal("typ.entryCount doesn't match")
}
- if typ.entriesStart != xtyp.entriesStart {
- t.Fatal("typ.entriesStart doesn't match")
+ // Config size can differ after serialization due to the loss of extended fields
+ // during reserialization, but the fixed portions of the Type header must not change.
+ if uint32(typ.headerByteSize)-typ.config.size != uint32(xtyp.headerByteSize)-uint32(xtyp.config.size) {
+ t.Fatal("fixed size header portions don't match")
}
if len(typ.indices) != len(xtyp.indices) {
t.Fatal("typ.indices length don't match")
@@ -629,9 +631,8 @@
func checkResources(t *testing.T) {
t.Helper()
- sdkdir := os.Getenv("ANDROID_HOME")
- if sdkdir == "" {
- t.Skip("ANDROID_HOME env var not set")
+ if _, err := sdkpath.AndroidHome(); err != nil {
+ t.Skip("Could not locate Android SDK")
}
rscPath, err := apiResourcesPath()
if err != nil {
@@ -643,9 +644,8 @@
}
func BenchmarkTableRefByName(b *testing.B) {
- sdkdir := os.Getenv("ANDROID_HOME")
- if sdkdir == "" {
- b.Fatal("ANDROID_HOME env var not set")
+ if _, err := sdkpath.AndroidHome(); err != nil {
+ b.Fatal("Could not locate Android SDK")
}
b.ReportAllocs()
diff --git a/internal/binres/genarsc.go b/internal/binres/genarsc.go
index e93ae88..4ec35fb 100644
--- a/internal/binres/genarsc.go
+++ b/internal/binres/genarsc.go
@@ -8,8 +8,7 @@
// Genarsc generates stripped down version of android.jar resources used
// for validation of manifest entries.
//
-// Requires ANDROID_HOME be set to the path of the Android SDK and the
-// current sdk platform installed that matches MinSDK.
+// Requires the selected Android SDK to support the MinSDK platform version.
package main
import (
diff --git a/internal/binres/sdk.go b/internal/binres/sdk.go
index 6c1c343..607e0b7 100644
--- a/internal/binres/sdk.go
+++ b/internal/binres/sdk.go
@@ -7,13 +7,14 @@
"fmt"
"io"
"os"
- "path"
+ "path/filepath"
+
+ "golang.org/x/mobile/internal/sdkpath"
)
// MinSDK is the targeted sdk version for support by package binres.
-const MinSDK = 15
+const MinSDK = 16
-// Requires environment variable ANDROID_HOME to be set.
func apiResources() ([]byte, error) {
apiResPath, err := apiResourcesPath()
if err != nil {
@@ -50,14 +51,11 @@
}
func apiResourcesPath() (string, error) {
- // TODO(elias.naur): use the logic from gomobile's androidAPIPath and use the any installed version of the
- // Android SDK instead. Currently, the binres_test.go tests fail on anything newer than android-15.
- sdkdir := os.Getenv("ANDROID_HOME")
- if sdkdir == "" {
- return "", fmt.Errorf("ANDROID_HOME env var not set")
+ platformDir, err := sdkpath.AndroidAPIPath(MinSDK)
+ if err != nil {
+ return "", err
}
- platform := fmt.Sprintf("android-%v", MinSDK)
- return path.Join(sdkdir, "platforms", platform, "android.jar"), nil
+ return filepath.Join(platformDir, "android.jar"), nil
}
// PackResources produces a stripped down gzip version of the resources.arsc from api jar.
diff --git a/internal/binres/table.go b/internal/binres/table.go
index 304736d..b552cba 100644
--- a/internal/binres/table.go
+++ b/internal/binres/table.go
@@ -248,7 +248,8 @@
typePool *Pool // type names; e.g. theme
keyPool *Pool // resource names; e.g. Theme.NoTitleBar
- specs []*TypeSpec
+ aliases []*StagedAlias
+ specs []*TypeSpec
}
func (pkg *Package) UnmarshalBinary(bin []byte) error {
@@ -298,7 +299,7 @@
return nil
}
- buf := bin[idOffset:]
+ buf := bin[idOffset:pkg.byteSize]
for len(buf) > 0 {
t := ResType(btou16(buf))
switch t {
@@ -317,8 +318,15 @@
last := pkg.specs[len(pkg.specs)-1]
last.types = append(last.types, typ)
buf = buf[typ.byteSize:]
+ case ResTableStagedAlias:
+ alias := new(StagedAlias)
+ if err := alias.UnmarshalBinary(buf); err != nil {
+ return err
+ }
+ pkg.aliases = append(pkg.aliases, alias)
+ buf = buf[alias.byteSize:]
default:
- return errWrongType(t, ResTableTypeSpec, ResTableType)
+ return errWrongType(t, ResTableTypeSpec, ResTableType, ResTableStagedAlias)
}
}
@@ -371,6 +379,14 @@
bin = append(bin, b...)
}
+ for _, alias := range pkg.aliases {
+ b, err := alias.MarshalBinary()
+ if err != nil {
+ return nil, err
+ }
+ bin = append(bin, b...)
+ }
+
for _, spec := range pkg.specs {
b, err := spec.MarshalBinary()
if err != nil {
@@ -537,14 +553,15 @@
// fmt.Println("language/country:", u16tos(typ.config.locale.language), u16tos(typ.config.locale.country))
buf := bin[typ.headerByteSize:typ.entriesStart]
- for len(buf) > 0 {
- typ.indices = append(typ.indices, btou32(buf))
- buf = buf[4:]
+ if len(buf) != 4*int(typ.entryCount) {
+ return fmt.Errorf("index buffer len[%v] doesn't match entryCount[%v]", len(buf), typ.entryCount)
}
- if len(typ.indices) != int(typ.entryCount) {
- return fmt.Errorf("indices len[%v] doesn't match entryCount[%v]", len(typ.indices), typ.entryCount)
+ typ.indices = make([]uint32, typ.entryCount)
+ for i := range typ.indices {
+ typ.indices[i] = btou32(buf[i*4:])
}
+
typ.entries = make([]*Entry, typ.entryCount)
for i, x := range typ.indices {
@@ -572,9 +589,9 @@
putu32(bin[12:], uint32(len(typ.entries)))
putu32(bin[16:], uint32(56+len(typ.entries)*4))
- // assure typ.config.size is always written as 52; extended configuration beyond supported
+ // assure typ.config.size is always written as 36; extended configuration beyond supported
// API level is not supported by this marshal implementation but will be forward-compatible.
- putu32(bin[20:], 52)
+ putu32(bin[20:], 36)
putu16(bin[24:], typ.config.imsi.mcc)
putu16(bin[26:], typ.config.imsi.mnc)
@@ -616,6 +633,65 @@
return bin, nil
}
+type StagedAliasEntry struct {
+ stagedID uint32
+ finalizedID uint32
+}
+
+func (ae *StagedAliasEntry) MarshalBinary() ([]byte, error) {
+ bin := make([]byte, 8)
+ putu32(bin, ae.stagedID)
+ putu32(bin[4:], ae.finalizedID)
+ return bin, nil
+}
+
+func (ae *StagedAliasEntry) UnmarshalBinary(bin []byte) error {
+ ae.stagedID = btou32(bin)
+ ae.finalizedID = btou32(bin[4:])
+ return nil
+}
+
+type StagedAlias struct {
+ chunkHeader
+ count uint32
+ entries []StagedAliasEntry
+}
+
+func (a *StagedAlias) UnmarshalBinary(bin []byte) error {
+ if err := (&a.chunkHeader).UnmarshalBinary(bin); err != nil {
+ return err
+ }
+ if a.typ != ResTableStagedAlias {
+ return errWrongType(a.typ, ResTableStagedAlias)
+ }
+ a.count = btou32(bin[8:])
+ a.entries = make([]StagedAliasEntry, a.count)
+ for i := range a.entries {
+ if err := a.entries[i].UnmarshalBinary(bin[12+i*8:]); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (a *StagedAlias) MarshalBinary() ([]byte, error) {
+ chunkHeaderBin, err := a.chunkHeader.MarshalBinary()
+ if err != nil {
+ return nil, err
+ }
+ countBin := make([]byte, 4)
+ putu32(countBin, a.count)
+ bin := append(chunkHeaderBin, countBin...)
+ for _, entry := range a.entries {
+ entryBin, err := entry.MarshalBinary()
+ if err != nil {
+ return nil, err
+ }
+ bin = append(bin, entryBin...)
+ }
+ return bin, nil
+}
+
// Entry is a resource key typically followed by a value or resource map.
type Entry struct {
size uint16
diff --git a/internal/binres/testdata/bootstrap.arsc b/internal/binres/testdata/bootstrap.arsc
index 60c1dda..4705191 100644
--- a/internal/binres/testdata/bootstrap.arsc
+++ b/internal/binres/testdata/bootstrap.arsc
Binary files differ
diff --git a/internal/binres/testdata/bootstrap.bin b/internal/binres/testdata/bootstrap.bin
index 35d3df6..ff5617c 100644
--- a/internal/binres/testdata/bootstrap.bin
+++ b/internal/binres/testdata/bootstrap.bin
Binary files differ
diff --git a/internal/binres/testdata/gen.sh b/internal/binres/testdata/gen.sh
index e37f21b..b19318f 100755
--- a/internal/binres/testdata/gen.sh
+++ b/internal/binres/testdata/gen.sh
@@ -1,10 +1,10 @@
#! /usr/bin/env bash
# version of build-tools tests run against
-AAPT=${ANDROID_HOME}/build-tools/23.0.1/aapt
+AAPT=${ANDROID_HOME:-${HOME}/Android/Sdk}/build-tools/32.0.0/aapt
# minimum version of android api for resource identifiers supported
-APIJAR=${ANDROID_HOME}/platforms/android-15/android.jar
+APIJAR=${ANDROID_HOME:-${HOME}/Android/Sdk}/platforms/android-16/android.jar
for f in *.xml; do
RES=""
diff --git a/internal/sdkpath/sdkpath.go b/internal/sdkpath/sdkpath.go
new file mode 100644
index 0000000..91e9a51
--- /dev/null
+++ b/internal/sdkpath/sdkpath.go
@@ -0,0 +1,89 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package sdkpath provides functions for locating the Android SDK.
+// These functions respect the ANDROID_HOME environment variable, and
+// otherwise use the default SDK location.
+package sdkpath
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+)
+
+// AndroidHome returns the absolute path of the selected Android SDK,
+// if one can be found.
+func AndroidHome() (string, error) {
+ androidHome := os.Getenv("ANDROID_HOME")
+ if androidHome == "" {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ switch runtime.GOOS {
+ case "windows":
+ // See https://android.googlesource.com/platform/tools/adt/idea/+/85b4bfb7a10ad858a30ffa4003085b54f9424087/native/installer/win/setup_android_studio.nsi#100
+ androidHome = filepath.Join(home, "AppData", "Local", "Android", "sdk")
+ case "darwin":
+ // See https://android.googlesource.com/platform/tools/asuite/+/67e0cd9604379e9663df57f16a318d76423c0aa8/aidegen/lib/ide_util.py#88
+ androidHome = filepath.Join(home, "Library", "Android", "sdk")
+ default: // Linux, BSDs, etc.
+ // See LINUX_ANDROID_SDK_PATH in ide_util.py above.
+ androidHome = filepath.Join(home, "Android", "Sdk")
+ }
+ }
+ if info, err := os.Stat(androidHome); err != nil {
+ return "", fmt.Errorf("%w; Android SDK was not found at %s", err, androidHome)
+ } else if !info.IsDir() {
+ return "", fmt.Errorf("%s is not a directory", androidHome)
+ }
+ return androidHome, nil
+}
+
+// AndroidAPIPath returns an android SDK platform directory within the configured SDK.
+// If there are multiple platforms that satisfy the minimum version requirement,
+// AndroidAPIPath returns the latest one among them.
+func AndroidAPIPath(api int) (string, error) {
+ sdk, err := AndroidHome()
+ if err != nil {
+ return "", err
+ }
+ sdkDir, err := os.Open(filepath.Join(sdk, "platforms"))
+ if err != nil {
+ return "", fmt.Errorf("failed to find android SDK platform: %w", err)
+ }
+ defer sdkDir.Close()
+ fis, err := sdkDir.Readdir(-1)
+ if err != nil {
+ return "", fmt.Errorf("failed to find android SDK platform (API level: %d): %w", api, err)
+ }
+
+ var apiPath string
+ var apiVer int
+ for _, fi := range fis {
+ name := fi.Name()
+ if !strings.HasPrefix(name, "android-") {
+ continue
+ }
+ n, err := strconv.Atoi(name[len("android-"):])
+ if err != nil || n < api {
+ continue
+ }
+ p := filepath.Join(sdkDir.Name(), name)
+ _, err = os.Stat(filepath.Join(p, "android.jar"))
+ if err == nil && apiVer < n {
+ apiPath = p
+ apiVer = n
+ }
+ }
+ if apiVer == 0 {
+ return "", fmt.Errorf("failed to find android SDK platform (API level: %d) in %s",
+ api, sdkDir.Name())
+ }
+ return apiPath, nil
+}