cmd/gomobile: improve support for macOS and Catalyst
This is is a follow-up from my previous PR (#65). It makes gomobile
aware of GOOS=ios and adds support for specifying specific Apple
platforms, instead of overloading the "ios" platform.
Supported platforms: ios, iossimulator, macos, and maccatalyst
These can now be specified the -target argument to gomobile, e.g.:
gomobile build -target=ios,iossimulator,macos,maccatalyst
It preserves the current behavior of -target=ios, which will build for
ios and iossimulator on supported architectures (arm64 and amd64).
It adds platform-specific build tags so Go code can discriminate between
different Apple platforms like maccatalyst (UIKit on macOS).
This PR also fixes a number of broken tests.
TODO: cgo has a bug where c-archive builds targeting Catalyst will fail
unless -tags=ios is supplied. See https://golang.org/issues/47228
Fixes https://golang.org/issues/47212
Updates https://golang.org/issues/47228
Change-Id: Ib1a2f5302c5edd0704c13ffbe8f4061211f50d4e
GitHub-Last-Rev: 01ab28e63fe6890a9f9783e3fc41b1c895b0274d
GitHub-Pull-Request: golang/mobile#70
Reviewed-on: https://go-review.googlesource.com/c/mobile/+/334689
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Trust: Hajime Hoshi <hajimehoshi@gmail.com>
diff --git a/.gitignore b/.gitignore
index c7abc86..0e17991 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
*.apk
*.app
*.framework
+*.xcframework
*.aar
*.iml
.idea
diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go
index 6a80e53..efbc896 100644
--- a/cmd/gomobile/bind.go
+++ b/cmd/gomobile/bind.go
@@ -23,14 +23,14 @@
var cmdBind = &command{
run: runBind,
Name: "bind",
- Usage: "[-target android|ios] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]",
+ Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]",
Short: "build a library for Android and iOS",
Long: `
Bind generates language bindings for the package named by the import
path, and compiles a library for the named target system.
-The -target flag takes a target system name, either android (the
-default) or ios.
+The -target flag takes either android (the default), or one or more
+comma-delimited Apple platforms (` + strings.Join(applePlatforms, ", ") + `).
For -target android, the bind command produces an AAR (Android ARchive)
file that archives the precompiled Java API stub classes, the compiled
@@ -52,9 +52,9 @@
can be selected by specifying target type with the architecture name. E.g.,
-target=android/arm,android/386.
-For -target ios, gomobile must be run on an OS X machine with Xcode
-installed. The generated Objective-C types can be prefixed with the -prefix
-flag.
+For Apple -target platforms, gomobile must be run on an OS X machine with
+Xcode installed. The generated Objective-C types can be prefixed with the
+-prefix flag.
For -target android, the -bootclasspath and -classpath flags are used to
control the bootstrap classpath and the classpath for Go wrappers to Java
@@ -76,29 +76,29 @@
args := cmd.flag.Args()
- targetOS, targetArchs, err := parseBuildTarget(buildTarget)
+ targets, err := parseBuildTarget(buildTarget)
if err != nil {
return fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err)
}
- if bindJavaPkg != "" && targetOS != "android" {
- return fmt.Errorf("-javapkg is supported only for android target")
- }
- if bindPrefix != "" && targetOS != "ios" {
- return fmt.Errorf("-prefix is supported only for ios target")
- }
-
- if targetOS == "android" {
+ if isAndroidPlatform(targets[0].platform) {
+ if bindPrefix != "" {
+ return fmt.Errorf("-prefix is supported only for Apple targets")
+ }
if _, err := ndkRoot(); err != nil {
return err
}
+ } else {
+ if bindJavaPkg != "" {
+ return fmt.Errorf("-javapkg is supported only for android target")
+ }
}
var gobind string
if !buildN {
gobind, err = exec.LookPath("gobind")
if err != nil {
- return errors.New("gobind was not found. Please run gomobile init before trying again.")
+ return errors.New("gobind was not found. Please run gomobile init before trying again")
}
} else {
gobind = "gobind"
@@ -107,7 +107,10 @@
if len(args) == 0 {
args = append(args, ".")
}
- pkgs, err := importPackages(args, targetOS)
+
+ // TODO(ydnar): this should work, unless build tags affect loading a single package.
+ // Should we try to import packages with different build tags per platform?
+ pkgs, err := packages.Load(packagesConfig(targets[0]), args...)
if err != nil {
return err
}
@@ -115,28 +118,23 @@
// check if any of the package is main
for _, pkg := range pkgs {
if pkg.Name == "main" {
- return fmt.Errorf("binding 'main' package (%s) is not supported", pkg.PkgPath)
+ return fmt.Errorf(`binding "main" package (%s) is not supported`, pkg.PkgPath)
}
}
- switch targetOS {
- case "android":
- return goAndroidBind(gobind, pkgs, targetArchs)
- case "ios":
+ switch {
+ case isAndroidPlatform(targets[0].platform):
+ return goAndroidBind(gobind, pkgs, targets)
+ case isApplePlatform(targets[0].platform):
if !xcodeAvailable() {
- return fmt.Errorf("-target=ios requires XCode")
+ return fmt.Errorf("-target=%q requires Xcode", buildTarget)
}
- return goIOSBind(gobind, pkgs, targetArchs)
+ return goAppleBind(gobind, pkgs, targets)
default:
return fmt.Errorf(`invalid -target=%q`, buildTarget)
}
}
-func importPackages(args []string, targetOS string) ([]*packages.Package, error) {
- config := packagesConfig(targetOS)
- return packages.Load(config, args...)
-}
-
var (
bindPrefix string // -prefix
bindJavaPkg string // -javapkg
@@ -212,11 +210,12 @@
return generate(f)
}
-func packagesConfig(targetOS string) *packages.Config {
+func packagesConfig(t targetInfo) *packages.Config {
config := &packages.Config{}
// Add CGO_ENABLED=1 explicitly since Cgo is disabled when GOOS is different from host OS.
- config.Env = append(os.Environ(), "GOARCH=arm64", "GOOS="+targetOS, "CGO_ENABLED=1")
- tags := buildTags
+ config.Env = append(os.Environ(), "GOARCH="+t.arch, "GOOS="+platformOS(t.platform), "CGO_ENABLED=1")
+ tags := append(buildTags[:], platformTags(t.platform)...)
+
if len(tags) > 0 {
config.BuildFlags = []string{"-tags=" + strings.Join(tags, ",")}
}
@@ -224,11 +223,12 @@
}
// getModuleVersions returns a module information at the directory src.
-func getModuleVersions(targetOS string, targetArch string, src string) (*modfile.File, error) {
+func getModuleVersions(targetPlatform string, targetArch string, src string) (*modfile.File, error) {
cmd := exec.Command("go", "list")
- cmd.Env = append(os.Environ(), "GOOS="+targetOS, "GOARCH="+targetArch)
+ cmd.Env = append(os.Environ(), "GOOS="+platformOS(targetPlatform), "GOARCH="+targetArch)
- tags := buildTags
+ tags := append(buildTags[:], platformTags(targetPlatform)...)
+
// TODO(hyangah): probably we don't need to add all the dependencies.
cmd.Args = append(cmd.Args, "-m", "-json", "-tags="+strings.Join(tags, ","), "all")
cmd.Dir = src
@@ -281,7 +281,7 @@
}
// writeGoMod writes go.mod file at $WORK/src when Go modules are used.
-func writeGoMod(targetOS string, targetArch string) error {
+func writeGoMod(dir, targetPlatform, targetArch string) error {
m, err := areGoModulesUsed()
if err != nil {
return err
@@ -291,8 +291,8 @@
return nil
}
- return writeFile(filepath.Join(tmpdir, "src", "go.mod"), func(w io.Writer) error {
- f, err := getModuleVersions(targetOS, targetArch, ".")
+ return writeFile(filepath.Join(dir, "src", "go.mod"), func(w io.Writer) error {
+ f, err := getModuleVersions(targetPlatform, targetArch, ".")
if err != nil {
return err
}
diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go
index 9eb7ce6..8ae9d4d 100644
--- a/cmd/gomobile/bind_androidapp.go
+++ b/cmd/gomobile/bind_androidapp.go
@@ -18,7 +18,7 @@
"golang.org/x/tools/go/packages"
)
-func goAndroidBind(gobind string, pkgs []*packages.Package, androidArchs []string) error {
+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)")
}
@@ -58,12 +58,12 @@
}
// Generate binding code and java source code only when processing the first package.
- for _, arch := range androidArchs {
- if err := writeGoMod("android", arch); err != nil {
+ for _, t := range targets {
+ if err := writeGoMod(tmpdir, "android", t.arch); err != nil {
return err
}
- env := androidEnv[arch]
+ env := androidEnv[t.arch]
// Add the generated packages to GOPATH for reverse bindings.
gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH"))
env = append(env, gopath)
@@ -76,7 +76,7 @@
}
}
- toolchain := ndk.Toolchain(arch)
+ toolchain := ndk.Toolchain(t.arch)
err := goBuildAt(
filepath.Join(tmpdir, "src"),
"./gobind",
@@ -90,7 +90,7 @@
}
jsrc := filepath.Join(tmpdir, "java")
- if err := buildAAR(jsrc, androidDir, pkgs, androidArchs); err != nil {
+ if err := buildAAR(jsrc, androidDir, pkgs, targets); err != nil {
return err
}
return buildSrcJar(jsrc)
@@ -133,7 +133,7 @@
// aidl (optional, not relevant)
//
// javac and jar commands are needed to build classes.jar.
-func buildAAR(srcDir, androidDir string, pkgs []*packages.Package, androidArchs []string) (err error) {
+func buildAAR(srcDir, androidDir string, pkgs []*packages.Package, targets []targetInfo) (err error) {
var out io.Writer = ioutil.Discard
if buildO == "" {
buildO = pkgs[0].Name + ".aar"
@@ -235,8 +235,8 @@
}
}
- for _, arch := range androidArchs {
- toolchain := ndk.Toolchain(arch)
+ for _, t := range targets {
+ toolchain := ndk.Toolchain(t.arch)
lib := toolchain.abi + "/libgojni.so"
w, err = aarwcreate("jni/" + lib)
if err != nil {
diff --git a/cmd/gomobile/bind_iosapp.go b/cmd/gomobile/bind_iosapp.go
index e9615e8..bf0f37d 100644
--- a/cmd/gomobile/bind_iosapp.go
+++ b/cmd/gomobile/bind_iosapp.go
@@ -5,184 +5,236 @@
package main
import (
+ "errors"
"fmt"
"io"
"os/exec"
"path/filepath"
+ "strconv"
"strings"
"text/template"
"golang.org/x/tools/go/packages"
)
-func goIOSBind(gobind string, pkgs []*packages.Package, archs []string) error {
- // Run gobind to generate the bindings
- cmd := exec.Command(
- gobind,
- "-lang=go,objc",
- "-outdir="+tmpdir,
- )
- cmd.Env = append(cmd.Env, "GOOS=darwin")
- cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
- tags := append(buildTags, "ios")
- cmd.Args = append(cmd.Args, "-tags="+strings.Join(tags, ","))
- if bindPrefix != "" {
- cmd.Args = append(cmd.Args, "-prefix="+bindPrefix)
- }
- for _, p := range pkgs {
- cmd.Args = append(cmd.Args, p.PkgPath)
- }
- if err := runCmd(cmd); err != nil {
- return err
- }
-
- srcDir := filepath.Join(tmpdir, "src", "gobind")
-
+func goAppleBind(gobind string, pkgs []*packages.Package, targets []targetInfo) error {
var name string
var title string
+
if buildO == "" {
name = pkgs[0].Name
title = strings.Title(name)
- buildO = title + ".framework"
+ buildO = title + ".xcframework"
} else {
- if !strings.HasSuffix(buildO, ".framework") {
- return fmt.Errorf("static framework name %q missing .framework suffix", buildO)
+ if !strings.HasSuffix(buildO, ".xcframework") {
+ return fmt.Errorf("static framework name %q missing .xcframework suffix", buildO)
}
base := filepath.Base(buildO)
- name = base[:len(base)-len(".framework")]
+ name = base[:len(base)-len(".xcframework")]
title = strings.Title(name)
}
- fileBases := make([]string, len(pkgs)+1)
- for i, pkg := range pkgs {
- fileBases[i] = bindPrefix + strings.Title(pkg.Name)
+ if err := removeAll(buildO); err != nil {
+ return err
}
- fileBases[len(fileBases)-1] = "Universe"
-
- cmd = exec.Command("xcrun", "lipo", "-create")
modulesUsed, err := areGoModulesUsed()
if err != nil {
return err
}
- for _, arch := range archs {
- if err := writeGoMod("ios", arch); err != nil {
+ var frameworkDirs []string
+ frameworkArchCount := map[string]int{}
+ for _, t := range targets {
+ // Catalyst support requires iOS 13+
+ v, _ := strconv.ParseFloat(buildIOSVersion, 64)
+ if t.platform == "maccatalyst" && v < 13.0 {
+ return errors.New("catalyst requires -iosversion=13 or higher")
+ }
+
+ outDir := filepath.Join(tmpdir, t.platform)
+ outSrcDir := filepath.Join(outDir, "src")
+ gobindDir := filepath.Join(outSrcDir, "gobind")
+
+ // Run gobind once per platform to generate the bindings
+ cmd := exec.Command(
+ gobind,
+ "-lang=go,objc",
+ "-outdir="+outDir,
+ )
+ cmd.Env = append(cmd.Env, "GOOS="+platformOS(t.platform))
+ cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
+ tags := append(buildTags[:], platformTags(t.platform)...)
+ cmd.Args = append(cmd.Args, "-tags="+strings.Join(tags, ","))
+ if bindPrefix != "" {
+ cmd.Args = append(cmd.Args, "-prefix="+bindPrefix)
+ }
+ for _, p := range pkgs {
+ cmd.Args = append(cmd.Args, p.PkgPath)
+ }
+ if err := runCmd(cmd); err != nil {
return err
}
- env := iosEnv[arch]
+ env := appleEnv[t.String()][:]
+ sdk := getenv(env, "DARWIN_SDK")
+
+ frameworkDir := filepath.Join(tmpdir, t.platform, sdk, title+".framework")
+ frameworkDirs = append(frameworkDirs, frameworkDir)
+ frameworkArchCount[frameworkDir] = frameworkArchCount[frameworkDir] + 1
+
+ fileBases := make([]string, len(pkgs)+1)
+ for i, pkg := range pkgs {
+ fileBases[i] = bindPrefix + strings.Title(pkg.Name)
+ }
+ fileBases[len(fileBases)-1] = "Universe"
+
// Add the generated packages to GOPATH for reverse bindings.
- gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH"))
+ gopath := fmt.Sprintf("GOPATH=%s%c%s", outDir, filepath.ListSeparator, goEnv("GOPATH"))
env = append(env, gopath)
+ if err := writeGoMod(outDir, t.platform, t.arch); err != nil {
+ return err
+ }
+
// Run `go mod tidy` to force to create go.sum.
// Without go.sum, `go build` fails as of Go 1.16.
if modulesUsed {
- if err := goModTidyAt(filepath.Join(tmpdir, "src"), env); err != nil {
+ if err := goModTidyAt(outSrcDir, env); err != nil {
return err
}
}
- path, err := goIOSBindArchive(name, env, filepath.Join(tmpdir, "src"))
+ path, err := goAppleBindArchive(name+"-"+t.platform+"-"+t.arch, env, outSrcDir)
if err != nil {
- return fmt.Errorf("ios-%s: %v", arch, err)
+ return fmt.Errorf("%s/%s: %v", t.platform, t.arch, err)
}
- cmd.Args = append(cmd.Args, "-arch", archClang(arch), path)
- }
- // Build static framework output directory.
- if err := removeAll(buildO); err != nil {
- return err
- }
- headers := buildO + "/Versions/A/Headers"
- if err := mkdir(headers); err != nil {
- return err
- }
- if err := symlink("A", buildO+"/Versions/Current"); err != nil {
- return err
- }
- if err := symlink("Versions/Current/Headers", buildO+"/Headers"); err != nil {
- return err
- }
- if err := symlink("Versions/Current/"+title, buildO+"/"+title); err != nil {
- return err
- }
+ versionsDir := filepath.Join(frameworkDir, "Versions")
+ versionsADir := filepath.Join(versionsDir, "A")
+ titlePath := filepath.Join(versionsADir, title)
+ if frameworkArchCount[frameworkDir] > 1 {
+ // Not the first static lib, attach to a fat library and skip create headers
+ fatCmd := exec.Command(
+ "xcrun",
+ "lipo", path, titlePath, "-create", "-output", titlePath,
+ )
+ if err := runCmd(fatCmd); err != nil {
+ return err
+ }
+ continue
+ }
- cmd.Args = append(cmd.Args, "-o", buildO+"/Versions/A/"+title)
- if err := runCmd(cmd); err != nil {
- return err
- }
-
- // Copy header file next to output archive.
- headerFiles := make([]string, len(fileBases))
- if len(fileBases) == 1 {
- headerFiles[0] = title + ".h"
- err := copyFile(
- headers+"/"+title+".h",
- srcDir+"/"+bindPrefix+title+".objc.h",
- )
- if err != nil {
+ versionsAHeadersDir := filepath.Join(versionsADir, "Headers")
+ if err := mkdir(versionsAHeadersDir); err != nil {
return err
}
- } else {
- for i, fileBase := range fileBases {
- headerFiles[i] = fileBase + ".objc.h"
+ if err := symlink("A", filepath.Join(versionsDir, "Current")); err != nil {
+ return err
+ }
+ if err := symlink("Versions/Current/Headers", filepath.Join(frameworkDir, "Headers")); err != nil {
+ return err
+ }
+ if err := symlink(filepath.Join("Versions/Current", title), filepath.Join(frameworkDir, title)); err != nil {
+ return err
+ }
+
+ lipoCmd := exec.Command(
+ "xcrun",
+ "lipo", path, "-create", "-o", titlePath,
+ )
+ if err := runCmd(lipoCmd); err != nil {
+ return err
+ }
+
+ // Copy header file next to output archive.
+ var headerFiles []string
+ if len(fileBases) == 1 {
+ headerFiles = append(headerFiles, title+".h")
err := copyFile(
- headers+"/"+fileBase+".objc.h",
- srcDir+"/"+fileBase+".objc.h")
+ filepath.Join(versionsAHeadersDir, title+".h"),
+ filepath.Join(gobindDir, bindPrefix+title+".objc.h"),
+ )
+ if err != nil {
+ return err
+ }
+ } else {
+ for _, fileBase := range fileBases {
+ headerFiles = append(headerFiles, fileBase+".objc.h")
+ err := copyFile(
+ filepath.Join(versionsAHeadersDir, fileBase+".objc.h"),
+ filepath.Join(gobindDir, fileBase+".objc.h"),
+ )
+ if err != nil {
+ return err
+ }
+ }
+ err := copyFile(
+ filepath.Join(versionsAHeadersDir, "ref.h"),
+ filepath.Join(gobindDir, "ref.h"),
+ )
+ if err != nil {
+ return err
+ }
+ headerFiles = append(headerFiles, title+".h")
+ err = writeFile(filepath.Join(versionsAHeadersDir, title+".h"), func(w io.Writer) error {
+ return appleBindHeaderTmpl.Execute(w, map[string]interface{}{
+ "pkgs": pkgs, "title": title, "bases": fileBases,
+ })
+ })
if err != nil {
return err
}
}
- err := copyFile(
- headers+"/ref.h",
- srcDir+"/ref.h")
- if err != nil {
+
+ if err := mkdir(filepath.Join(versionsADir, "Resources")); err != nil {
return err
}
- headerFiles = append(headerFiles, title+".h")
- err = writeFile(headers+"/"+title+".h", func(w io.Writer) error {
- return iosBindHeaderTmpl.Execute(w, map[string]interface{}{
- "pkgs": pkgs, "title": title, "bases": fileBases,
- })
+ if err := symlink("Versions/Current/Resources", filepath.Join(frameworkDir, "Resources")); err != nil {
+ return err
+ }
+ err = writeFile(filepath.Join(frameworkDir, "Resources", "Info.plist"), func(w io.Writer) error {
+ _, err := w.Write([]byte(appleBindInfoPlist))
+ return err
})
if err != nil {
return err
}
+
+ var mmVals = struct {
+ Module string
+ Headers []string
+ }{
+ Module: title,
+ Headers: headerFiles,
+ }
+ err = writeFile(filepath.Join(versionsADir, "Modules", "module.modulemap"), func(w io.Writer) error {
+ return appleModuleMapTmpl.Execute(w, mmVals)
+ })
+ if err != nil {
+ return err
+ }
+ err = symlink(filepath.Join("Versions/Current/Modules"), filepath.Join(frameworkDir, "Modules"))
+ if err != nil {
+ return err
+ }
+
}
- resources := buildO + "/Versions/A/Resources"
- if err := mkdir(resources); err != nil {
- return err
- }
- if err := symlink("Versions/Current/Resources", buildO+"/Resources"); err != nil {
- return err
- }
- if err := writeFile(buildO+"/Resources/Info.plist", func(w io.Writer) error {
- _, err := w.Write([]byte(iosBindInfoPlist))
- return err
- }); err != nil {
- return err
+ // Finally combine all frameworks to an XCFramework
+ xcframeworkArgs := []string{"-create-xcframework"}
+
+ for _, dir := range frameworkDirs {
+ xcframeworkArgs = append(xcframeworkArgs, "-framework", dir)
}
- var mmVals = struct {
- Module string
- Headers []string
- }{
- Module: title,
- Headers: headerFiles,
- }
- err = writeFile(buildO+"/Versions/A/Modules/module.modulemap", func(w io.Writer) error {
- return iosModuleMapTmpl.Execute(w, mmVals)
- })
- if err != nil {
- return err
- }
- return symlink("Versions/Current/Modules", buildO+"/Modules")
+ xcframeworkArgs = append(xcframeworkArgs, "-output", buildO)
+ cmd := exec.Command("xcodebuild", xcframeworkArgs...)
+ err = runCmd(cmd)
+ return err
}
-const iosBindInfoPlist = `<?xml version="1.0" encoding="UTF-8"?>
+const appleBindInfoPlist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
@@ -190,16 +242,15 @@
</plist>
`
-var iosModuleMapTmpl = template.Must(template.New("iosmmap").Parse(`framework module "{{.Module}}" {
+var appleModuleMapTmpl = template.Must(template.New("iosmmap").Parse(`framework module "{{.Module}}" {
header "ref.h"
{{range .Headers}} header "{{.}}"
{{end}}
export *
}`))
-func goIOSBindArchive(name string, env []string, gosrc string) (string, error) {
- arch := getenv(env, "GOARCH")
- archive := filepath.Join(tmpdir, name+"-"+arch+".a")
+func goAppleBindArchive(name string, env []string, gosrc string) (string, error) {
+ archive := filepath.Join(tmpdir, name+".a")
err := goBuildAt(gosrc, "./gobind", env, "-buildmode=c-archive", "-o", archive)
if err != nil {
return "", err
@@ -207,7 +258,7 @@
return archive, nil
}
-var iosBindHeaderTmpl = template.Must(template.New("ios.h").Parse(`
+var appleBindHeaderTmpl = template.Must(template.New("apple.h").Parse(`
// Objective-C API for talking to the following Go packages
//
{{range .pkgs}}// {{.PkgPath}}
diff --git a/cmd/gomobile/bind_test.go b/cmd/gomobile/bind_test.go
index ee8d35c..5970c04 100644
--- a/cmd/gomobile/bind_test.go
+++ b/cmd/gomobile/bind_test.go
@@ -98,7 +98,7 @@
}
}
-func TestBindIOS(t *testing.T) {
+func TestBindApple(t *testing.T) {
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
@@ -112,7 +112,7 @@
}()
buildN = true
buildX = true
- buildO = "Asset.framework"
+ buildO = "Asset.xcframework"
buildTarget = "ios/arm64"
tests := []struct {
@@ -126,7 +126,7 @@
prefix: "Foo",
},
{
- out: "Abcde.framework",
+ out: "Abcde.xcframework",
},
}
for _, tc := range tests {
@@ -159,12 +159,12 @@
Prefix string
}{
outputData: output,
- Output: buildO[:len(buildO)-len(".framework")],
+ Output: buildO[:len(buildO)-len(".xcframework")],
Prefix: tc.prefix,
}
wantBuf := new(bytes.Buffer)
- if err := bindIOSTmpl.Execute(wantBuf, data); err != nil {
+ if err := bindAppleTmpl.Execute(wantBuf, data); err != nil {
t.Errorf("%+v: computing diff failed: %v", tc, err)
continue
}
@@ -190,33 +190,34 @@
jar c -C $WORK/javac-output .
`))
-var bindIOSTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
+var bindAppleTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
WORK=$WORK
-GOOS=darwin CGO_ENABLED=1 gobind -lang=go,objc -outdir=$WORK -tags=ios{{if .Prefix}} -prefix={{.Prefix}}{{end}} golang.org/x/mobile/asset
-mkdir -p $WORK/src
-PWD=$WORK/src GOOS=ios GOARCH=arm64 CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 GOPATH=$WORK:$GOPATH go mod tidy
-PWD=$WORK/src GOOS=ios GOARCH=arm64 CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 GOPATH=$WORK:$GOPATH go build -x -buildmode=c-archive -o $WORK/{{.Output}}-arm64.a ./gobind
-rm -r -f "{{.Output}}.framework"
-mkdir -p {{.Output}}.framework/Versions/A/Headers
-ln -s A {{.Output}}.framework/Versions/Current
-ln -s Versions/Current/Headers {{.Output}}.framework/Headers
-ln -s Versions/Current/{{.Output}} {{.Output}}.framework/{{.Output}}
-xcrun lipo -create -arch arm64 $WORK/{{.Output}}-arm64.a -o {{.Output}}.framework/Versions/A/{{.Output}}
-cp $WORK/src/gobind/{{.Prefix}}Asset.objc.h {{.Output}}.framework/Versions/A/Headers/{{.Prefix}}Asset.objc.h
-mkdir -p {{.Output}}.framework/Versions/A/Headers
-cp $WORK/src/gobind/Universe.objc.h {{.Output}}.framework/Versions/A/Headers/Universe.objc.h
-mkdir -p {{.Output}}.framework/Versions/A/Headers
-cp $WORK/src/gobind/ref.h {{.Output}}.framework/Versions/A/Headers/ref.h
-mkdir -p {{.Output}}.framework/Versions/A/Headers
-mkdir -p {{.Output}}.framework/Versions/A/Headers
-mkdir -p {{.Output}}.framework/Versions/A/Resources
-ln -s Versions/Current/Resources {{.Output}}.framework/Resources
-mkdir -p {{.Output}}.framework/Resources
-mkdir -p {{.Output}}.framework/Versions/A/Modules
-ln -s Versions/Current/Modules {{.Output}}.framework/Modules
+rm -r -f "{{.Output}}.xcframework"
+GOOS=ios CGO_ENABLED=1 gobind -lang=go,objc -outdir=$WORK/ios -tags=ios{{if .Prefix}} -prefix={{.Prefix}}{{end}} golang.org/x/mobile/asset
+mkdir -p $WORK/ios/src
+PWD=$WORK/ios/src GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphoneos GOPATH=$WORK/ios:$GOPATH go mod tidy
+PWD=$WORK/ios/src GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphoneos GOPATH=$WORK/ios:$GOPATH go build -x -buildmode=c-archive -o $WORK/{{.Output}}-ios-arm64.a ./gobind
+mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
+ln -s A $WORK/ios/iphoneos/{{.Output}}.framework/Versions/Current
+ln -s Versions/Current/Headers $WORK/ios/iphoneos/{{.Output}}.framework/Headers
+ln -s Versions/Current/{{.Output}} $WORK/ios/iphoneos/{{.Output}}.framework/{{.Output}}
+xcrun lipo $WORK/{{.Output}}-ios-arm64.a -create -o $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/{{.Output}}
+cp $WORK/ios/src/gobind/{{.Prefix}}Asset.objc.h $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers/{{.Prefix}}Asset.objc.h
+mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
+cp $WORK/ios/src/gobind/Universe.objc.h $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers/Universe.objc.h
+mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
+cp $WORK/ios/src/gobind/ref.h $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers/ref.h
+mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
+mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
+mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Resources
+ln -s Versions/Current/Resources $WORK/ios/iphoneos/{{.Output}}.framework/Resources
+mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Resources
+mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Modules
+ln -s Versions/Current/Modules $WORK/ios/iphoneos/{{.Output}}.framework/Modules
+xcodebuild -create-xcframework -framework $WORK/ios/iphoneos/{{.Output}}.framework -output {{.Output}}.xcframework
`))
-func TestBindIOSAll(t *testing.T) {
+func TestBindAppleAll(t *testing.T) {
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
@@ -230,7 +231,7 @@
}()
buildN = true
buildX = true
- buildO = "Asset.framework"
+ buildO = "Asset.xcframework"
buildTarget = "ios"
buf := new(bytes.Buffer)
@@ -290,7 +291,7 @@
case "android":
out = filepath.Join(dir, "cgopkg.aar")
case "ios":
- out = filepath.Join(dir, "Cgopkg.framework")
+ out = filepath.Join(dir, "Cgopkg.xcframework")
}
tests := []struct {
diff --git a/cmd/gomobile/build.go b/cmd/gomobile/build.go
index 79705ad..bd65f1c 100644
--- a/cmd/gomobile/build.go
+++ b/cmd/gomobile/build.go
@@ -8,11 +8,13 @@
import (
"bufio"
+ "errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
+ "strconv"
"strings"
"golang.org/x/tools/go/packages"
@@ -23,15 +25,15 @@
var cmdBuild = &command{
run: runBuild,
Name: "build",
- Usage: "[-target android|ios] [-o output] [-bundleid bundleID] [build flags] [package]",
+ Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-o output] [-bundleid bundleID] [build flags] [package]",
Short: "compile android APK and iOS app",
Long: `
Build compiles and encodes the app named by the import path.
The named package must define a main function.
-The -target flag takes a target system name, either android (the
-default) or ios.
+The -target flag takes either android (the default), or one or more
+comma-delimited Apple platforms (` + strings.Join(applePlatforms, ", ") + `).
For -target android, if an AndroidManifest.xml is defined in the
package directory, it is added to the APK output. Otherwise, a default
@@ -40,14 +42,22 @@
be selected by specifying target type with the architecture name. E.g.
-target=android/arm,android/386.
-For -target ios, gomobile must be run on an OS X machine with Xcode
-installed.
+For Apple -target platforms, gomobile must be run on an OS X machine with
+Xcode installed.
+
+By default, -target ios will generate an XCFramework for both ios
+and iossimulator. Multiple Apple targets can be specified, creating a "fat"
+XCFramework with each slice. To generate a fat XCFramework that supports
+iOS, macOS, and macCatalyst for all supportec architectures (amd64 and arm64),
+specify -target ios,macos,maccatalyst. A subset of instruction sets can be
+selectged by specifying the platform with an architecture name. E.g.
+-target=ios/arm64,maccatalyst/arm64.
If the package directory contains an assets subdirectory, its contents
are copied into the output.
Flag -iosversion sets the minimal version of the iOS SDK to compile against.
-The default version is 7.0.
+The default version is 13.0.
Flag -androidapi sets the Android API version to compile against.
The default and minimum is 15.
@@ -81,7 +91,7 @@
args := cmd.flag.Args()
- targetOS, targetArchs, err := parseBuildTarget(buildTarget)
+ targets, err := parseBuildTarget(buildTarget)
if err != nil {
return nil, fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err)
}
@@ -96,10 +106,14 @@
cmd.usage()
os.Exit(1)
}
- pkgs, err := packages.Load(packagesConfig(targetOS), buildPath)
+
+ // TODO(ydnar): this should work, unless build tags affect loading a single package.
+ // Should we try to import packages with different build tags per platform?
+ pkgs, err := packages.Load(packagesConfig(targets[0]), buildPath)
if err != nil {
return nil, err
}
+
// len(pkgs) can be more than 1 e.g., when the specified path includes `...`.
if len(pkgs) != 1 {
cmd.usage()
@@ -113,27 +127,32 @@
}
var nmpkgs map[string]bool
- switch targetOS {
- case "android":
+ switch {
+ case isAndroidPlatform(targets[0].platform):
if pkg.Name != "main" {
- for _, arch := range targetArchs {
- if err := goBuild(pkg.PkgPath, androidEnv[arch]); err != nil {
+ for _, t := range targets {
+ if err := goBuild(pkg.PkgPath, androidEnv[t.arch]); err != nil {
return nil, err
}
}
return pkg, nil
}
- nmpkgs, err = goAndroidBuild(pkg, targetArchs)
+ nmpkgs, err = goAndroidBuild(pkg, targets)
if err != nil {
return nil, err
}
- case "ios":
+ case isApplePlatform(targets[0].platform):
if !xcodeAvailable() {
- return nil, fmt.Errorf("-target=ios requires XCode")
+ return nil, fmt.Errorf("-target=%s requires XCode", buildTarget)
}
if pkg.Name != "main" {
- for _, arch := range targetArchs {
- if err := goBuild(pkg.PkgPath, iosEnv[arch]); err != nil {
+ for _, t := range targets {
+ // Catalyst support requires iOS 13+
+ v, _ := strconv.ParseFloat(buildIOSVersion, 64)
+ if t.platform == "maccatalyst" && v < 13.0 {
+ return nil, errors.New("catalyst requires -iosversion=13 or higher")
+ }
+ if err := goBuild(pkg.PkgPath, appleEnv[t.String()]); err != nil {
return nil, err
}
}
@@ -142,7 +161,7 @@
if buildBundleID == "" {
return nil, fmt.Errorf("-target=ios requires -bundleid set")
}
- nmpkgs, err = goIOSBuild(pkg, buildBundleID, targetArchs)
+ nmpkgs, err = goAppleBuild(pkg, buildBundleID, targets)
if err != nil {
return nil, err
}
@@ -236,7 +255,7 @@
cmd.flag.StringVar(&buildLdflags, "ldflags", "", "")
cmd.flag.StringVar(&buildTarget, "target", "android", "")
cmd.flag.StringVar(&buildBundleID, "bundleid", "", "")
- cmd.flag.StringVar(&buildIOSVersion, "iosversion", "7.0", "")
+ cmd.flag.StringVar(&buildIOSVersion, "iosversion", "13.0", "")
cmd.flag.IntVar(&buildAndroidAPI, "androidapi", minAndroidAPI, "")
cmd.flag.BoolVar(&buildA, "a", false, "")
@@ -292,7 +311,7 @@
cmd := exec.Command("go", subcmd)
tags := buildTags
if len(tags) > 0 {
- cmd.Args = append(cmd.Args, "-tags", strings.Join(tags, " "))
+ cmd.Args = append(cmd.Args, "-tags", strings.Join(tags, ","))
}
if buildV {
cmd.Args = append(cmd.Args, "-v")
@@ -332,60 +351,77 @@
return runCmd(cmd)
}
-func parseBuildTarget(buildTarget string) (os string, archs []string, _ error) {
+// parseBuildTarget parses buildTarget into 1 or more platforms and architectures.
+// Returns an error if buildTarget contains invalid input.
+// Example valid target strings:
+// android
+// android/arm64,android/386,android/amd64
+// ios,iossimulator,maccatalyst
+// macos/amd64
+func parseBuildTarget(buildTarget string) ([]targetInfo, error) {
if buildTarget == "" {
- return "", nil, fmt.Errorf(`invalid target ""`)
+ return nil, fmt.Errorf(`invalid target ""`)
}
- all := false
- archNames := []string{}
- for i, p := range strings.Split(buildTarget, ",") {
- osarch := strings.SplitN(p, "/", 2) // len(osarch) > 0
- if osarch[0] != "android" && osarch[0] != "ios" {
- return "", nil, fmt.Errorf(`unsupported os`)
- }
+ targets := []targetInfo{}
+ targetsAdded := make(map[targetInfo]bool)
- if i == 0 {
- os = osarch[0]
+ addTarget := func(platform, arch string) {
+ t := targetInfo{platform, arch}
+ if targetsAdded[t] {
+ return
}
+ targets = append(targets, t)
+ targetsAdded[t] = true
+ }
- if os != osarch[0] {
- return "", nil, fmt.Errorf(`cannot target different OSes`)
+ addPlatform := func(platform string) {
+ for _, arch := range platformArchs(platform) {
+ addTarget(platform, arch)
}
+ }
- if len(osarch) == 1 {
- all = true
+ var isAndroid, isApple bool
+ for _, target := range strings.Split(buildTarget, ",") {
+ tuple := strings.SplitN(target, "/", 2)
+ platform := tuple[0]
+ hasArch := len(tuple) == 2
+
+ if isAndroidPlatform(platform) {
+ isAndroid = true
+ } else if isApplePlatform(platform) {
+ isApple = true
} else {
- archNames = append(archNames, osarch[1])
+ return nil, fmt.Errorf("unsupported platform: %q", platform)
}
- }
+ if isAndroid && isApple {
+ return nil, fmt.Errorf(`cannot mix android and Apple platforms`)
+ }
- // verify all archs are supported one while deduping.
- isSupported := func(os, arch string) bool {
- for _, a := range allArchs(os) {
- if a == arch {
- return true
+ if hasArch {
+ arch := tuple[1]
+ if !isSupportedArch(platform, arch) {
+ return nil, fmt.Errorf(`unsupported platform/arch: %q`, target)
}
+ addTarget(platform, arch)
+ } else {
+ addPlatform(platform)
}
- return false
}
- targetOS := os
- seen := map[string]bool{}
- for _, arch := range archNames {
- if _, ok := seen[arch]; ok {
- continue
- }
- if !isSupported(os, arch) {
- return "", nil, fmt.Errorf(`unsupported arch: %q`, arch)
- }
-
- seen[arch] = true
- archs = append(archs, arch)
+ // Special case to build iossimulator if -target=ios
+ if buildTarget == "ios" {
+ addPlatform("iossimulator")
}
- if all {
- return targetOS, allArchs(os), nil
- }
- return targetOS, archs, nil
+ return targets, nil
+}
+
+type targetInfo struct {
+ platform string
+ arch string
+}
+
+func (t targetInfo) String() string {
+ return t.platform + "/" + t.arch
}
diff --git a/cmd/gomobile/build_androidapp.go b/cmd/gomobile/build_androidapp.go
index b97e945..b06ea29 100644
--- a/cmd/gomobile/build_androidapp.go
+++ b/cmd/gomobile/build_androidapp.go
@@ -24,7 +24,7 @@
"golang.org/x/tools/go/packages"
)
-func goAndroidBuild(pkg *packages.Package, androidArchs []string) (map[string]bool, error) {
+func goAndroidBuild(pkg *packages.Package, targets []targetInfo) (map[string]bool, error) {
ndkRoot, err := ndkRoot()
if err != nil {
return nil, err
@@ -68,8 +68,8 @@
libFiles := []string{}
nmpkgs := make(map[string]map[string]bool) // map: arch -> extractPkgs' output
- for _, arch := range androidArchs {
- toolchain := ndk.Toolchain(arch)
+ for _, t := range targets {
+ toolchain := ndk.Toolchain(t.arch)
libPath := "lib/" + toolchain.abi + "/lib" + libName + ".so"
libAbsPath := filepath.Join(tmpdir, libPath)
if err := mkdir(filepath.Dir(libAbsPath)); err != nil {
@@ -77,14 +77,14 @@
}
err = goBuild(
pkg.PkgPath,
- androidEnv[arch],
+ androidEnv[t.arch],
"-buildmode=c-shared",
"-o", libAbsPath,
)
if err != nil {
return nil, err
}
- nmpkgs[arch], err = extractPkgs(toolchain.Path(ndkRoot, "nm"), libAbsPath)
+ nmpkgs[t.arch], err = extractPkgs(toolchain.Path(ndkRoot, "nm"), libAbsPath)
if err != nil {
return nil, err
}
@@ -169,9 +169,9 @@
}
}
- for _, arch := range androidArchs {
- toolchain := ndk.Toolchain(arch)
- if nmpkgs[arch]["golang.org/x/mobile/exp/audio/al"] {
+ for _, t := range targets {
+ toolchain := ndk.Toolchain(t.arch)
+ if nmpkgs[t.arch]["golang.org/x/mobile/exp/audio/al"] {
dst := "lib/" + toolchain.abi + "/libopenal.so"
src := filepath.Join(gomobilepath, dst)
if _, err := os.Stat(src); err != nil {
@@ -282,7 +282,7 @@
}
// TODO: return nmpkgs
- return nmpkgs[androidArchs[0]], nil
+ return nmpkgs[targets[0].arch], nil
}
// androidPkgName sanitizes the go package name to be acceptable as a android
diff --git a/cmd/gomobile/build_iosapp.go b/cmd/gomobile/build_apple.go
similarity index 95%
rename from cmd/gomobile/build_iosapp.go
rename to cmd/gomobile/build_apple.go
index 0e9e063..2adaf3d 100644
--- a/cmd/gomobile/build_iosapp.go
+++ b/cmd/gomobile/build_apple.go
@@ -20,7 +20,7 @@
"golang.org/x/tools/go/packages"
)
-func goIOSBuild(pkg *packages.Package, bundleID string, archs []string) (map[string]bool, error) {
+func goAppleBuild(pkg *packages.Package, bundleID string, targets []targetInfo) (map[string]bool, error) {
src := pkg.PkgPath
if buildO != "" && !strings.HasSuffix(buildO, ".app") {
return nil, fmt.Errorf("-o must have an .app for -target=ios")
@@ -69,21 +69,32 @@
"-o", filepath.Join(tmpdir, "main/main"),
"-create",
)
+
var nmpkgs map[string]bool
- for _, arch := range archs {
- path := filepath.Join(tmpdir, arch)
+ builtArch := map[string]bool{}
+ for _, t := range targets {
+ // Only one binary per arch allowed
+ // e.g. ios/arm64 + iossimulator/amd64
+ if builtArch[t.arch] {
+ continue
+ }
+ builtArch[t.arch] = true
+
+ path := filepath.Join(tmpdir, t.platform, t.arch)
+
// Disable DWARF; see golang.org/issues/25148.
- if err := goBuild(src, iosEnv[arch], "-ldflags=-w", "-o="+path); err != nil {
+ if err := goBuild(src, appleEnv[t.String()], "-ldflags=-w", "-o="+path); err != nil {
return nil, err
}
if nmpkgs == nil {
var err error
- nmpkgs, err = extractPkgs(iosArmNM, path)
+ nmpkgs, err = extractPkgs(appleNM, path)
if err != nil {
return nil, err
}
}
cmd.Args = append(cmd.Args, path)
+
}
if err := runCmd(cmd); err != nil {
@@ -91,7 +102,7 @@
}
// TODO(jbd): Set the launcher icon.
- if err := iosCopyAssets(pkg, tmpdir); err != nil {
+ if err := appleCopyAssets(pkg, tmpdir); err != nil {
return nil, err
}
@@ -145,7 +156,7 @@
}
func detectTeamID() (string, error) {
- // Grabs the certificate for "Apple Development"; will not work if there
+ // Grabs the first certificate for "Apple Development"; will not work if there
// are multiple certificates and the first is not desired.
cmd := exec.Command(
"security", "find-certificate",
@@ -170,14 +181,14 @@
}
if len(cert.Subject.OrganizationalUnit) == 0 {
- err = fmt.Errorf("the signing certificate has no organizational unit (team ID).")
+ err = fmt.Errorf("the signing certificate has no organizational unit (team ID)")
return "", err
}
return cert.Subject.OrganizationalUnit[0], nil
}
-func iosCopyAssets(pkg *packages.Package, xcodeProjDir string) error {
+func appleCopyAssets(pkg *packages.Package, xcodeProjDir string) error {
dstAssets := xcodeProjDir + "/main/assets"
if err := mkdir(dstAssets); err != nil {
return err
@@ -424,7 +435,6 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
diff --git a/cmd/gomobile/build_darwin_test.go b/cmd/gomobile/build_darwin_test.go
index 12d9a4b..2fac27c 100644
--- a/cmd/gomobile/build_darwin_test.go
+++ b/cmd/gomobile/build_darwin_test.go
@@ -12,7 +12,7 @@
"text/template"
)
-func TestIOSBuild(t *testing.T) {
+func TestAppleBuild(t *testing.T) {
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
@@ -41,10 +41,13 @@
for _, test := range tests {
buf := new(bytes.Buffer)
xout = buf
+ var tmpl *template.Template
if test.main {
buildO = "basic.app"
+ tmpl = appleMainBuildTmpl
} else {
buildO = ""
+ tmpl = appleOtherBuildTmpl
}
cmdBuild.flag.Parse([]string{test.pkg})
err := runBuild(cmdBuild)
@@ -68,18 +71,20 @@
TeamID string
Pkg string
Main bool
+ BuildO string
}{
outputData: output,
TeamID: teamID,
Pkg: test.pkg,
Main: test.main,
+ BuildO: buildO,
}
got := filepath.ToSlash(buf.String())
wantBuf := new(bytes.Buffer)
- if err := iosBuildTmpl.Execute(wantBuf, data); err != nil {
+ if err := tmpl.Execute(wantBuf, data); err != nil {
t.Fatalf("computing diff failed: %v", err)
}
@@ -94,18 +99,25 @@
}
}
-var iosBuildTmpl = template.Must(infoplistTmpl.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
-WORK=$WORK{{if .Main}}
+var appleMainBuildTmpl = template.Must(infoplistTmpl.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
+WORK=$WORK
mkdir -p $WORK/main.xcodeproj
echo "{{.Xproj}}" > $WORK/main.xcodeproj/project.pbxproj
mkdir -p $WORK/main
echo "{{template "infoplist" .Xinfo}}" > $WORK/main/Info.plist
mkdir -p $WORK/main/Images.xcassets/AppIcon.appiconset
-echo "{{.Xcontents}}" > $WORK/main/Images.xcassets/AppIcon.appiconset/Contents.json{{end}}
-GOOS=ios GOARCH=arm64 CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 go build -tags tag1 -x {{if .Main}}-ldflags=-w -o=$WORK/arm64 {{end}}{{.Pkg}}
-GOOS=ios GOARCH=amd64 CC=iphonesimulator-clang CXX=iphonesimulator-clang++ CGO_CFLAGS=-isysroot=iphonesimulator -mios-simulator-version-min=7.0 -fembed-bitcode -arch x86_64 CGO_CXXFLAGS=-isysroot=iphonesimulator -mios-simulator-version-min=7.0 -fembed-bitcode -arch x86_64 CGO_LDFLAGS=-isysroot=iphonesimulator -mios-simulator-version-min=7.0 -fembed-bitcode -arch x86_64 CGO_ENABLED=1 go build -tags tag1 -x {{if .Main}}-ldflags=-w -o=$WORK/amd64 {{end}}{{.Pkg}}{{if .Main}}
-xcrun lipo -o $WORK/main/main -create $WORK/arm64 $WORK/amd64
+echo "{{.Xcontents}}" > $WORK/main/Images.xcassets/AppIcon.appiconset/Contents.json
+GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphoneos go build -tags tag1 -x -ldflags=-w -o=$WORK/ios/arm64 {{.Pkg}}
+GOOS=ios GOARCH=amd64 GOFLAGS=-tags=ios CC=iphonesimulator-clang CXX=iphonesimulator-clang++ CGO_CFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_CXXFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_LDFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_ENABLED=1 DARWIN_SDK=iphonesimulator go build -tags tag1 -x -ldflags=-w -o=$WORK/iossimulator/amd64 {{.Pkg}}
+xcrun lipo -o $WORK/main/main -create $WORK/ios/arm64 $WORK/iossimulator/amd64
mkdir -p $WORK/main/assets
xcrun xcodebuild -configuration Release -project $WORK/main.xcodeproj -allowProvisioningUpdates DEVELOPMENT_TEAM={{.TeamID}}
-mv $WORK/build/Release-iphoneos/main.app basic.app{{end}}
+mv $WORK/build/Release-iphoneos/main.app {{.BuildO}}
+`))
+
+var appleOtherBuildTmpl = template.Must(infoplistTmpl.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
+WORK=$WORK
+GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphoneos go build -tags tag1 -x {{.Pkg}}
+GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphonesimulator-clang CXX=iphonesimulator-clang++ CGO_CFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphonesimulator go build -tags tag1 -x {{.Pkg}}
+GOOS=ios GOARCH=amd64 GOFLAGS=-tags=ios CC=iphonesimulator-clang CXX=iphonesimulator-clang++ CGO_CFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_CXXFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_LDFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_ENABLED=1 DARWIN_SDK=iphonesimulator go build -tags tag1 -x {{.Pkg}}
`))
diff --git a/cmd/gomobile/build_test.go b/cmd/gomobile/build_test.go
index f7eab6c..ff21f87 100644
--- a/cmd/gomobile/build_test.go
+++ b/cmd/gomobile/build_test.go
@@ -114,46 +114,63 @@
GOOS=android GOARCH=arm CC=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang CXX=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang++ CGO_ENABLED=1 GOARM=7 go build -tags tag1 -x -buildmode=c-shared -o $WORK/lib/armeabi-v7a/libbasic.so golang.org/x/mobile/example/basic
`))
-func TestParseBuildTargetFlag(t *testing.T) {
- androidArchs := strings.Join(allArchs("android"), ",")
- iosArchs := strings.Join(allArchs("ios"), ",")
+func TestParseBuildTarget(t *testing.T) {
+ wantAndroid := "android/" + strings.Join(platformArchs("android"), ",android/")
tests := []struct {
- in string
- wantErr bool
- wantOS string
- wantArchs string
+ in string
+ wantErr bool
+ want string
}{
- {"android", false, "android", androidArchs},
- {"android,android/arm", false, "android", androidArchs},
- {"android/arm", false, "android", "arm"},
+ {"android", false, wantAndroid},
+ {"android,android/arm", false, wantAndroid},
+ {"android/arm", false, "android/arm"},
- {"ios", false, "ios", iosArchs},
- {"ios,ios/arm64", false, "ios", iosArchs},
- {"ios/arm64", false, "ios", "arm64"},
- {"ios/amd64", false, "ios", "amd64"},
+ {"ios", false, "ios/arm64,iossimulator/arm64,iossimulator/amd64"},
+ {"ios,ios/arm64", false, "ios/arm64"},
+ {"ios/arm64", false, "ios/arm64"},
- {"", true, "", ""},
- {"linux", true, "", ""},
- {"android/x86", true, "", ""},
- {"android/arm5", true, "", ""},
- {"ios/mips", true, "", ""},
- {"android,ios", true, "", ""},
- {"ios,android", true, "", ""},
+ {"iossimulator", false, "iossimulator/arm64,iossimulator/amd64"},
+ {"iossimulator/amd64", false, "iossimulator/amd64"},
+
+ {"macos", false, "macos/arm64,macos/amd64"},
+ {"macos,ios/arm64", false, "macos/arm64,macos/amd64,ios/arm64"},
+ {"macos/arm64", false, "macos/arm64"},
+ {"macos/amd64", false, "macos/amd64"},
+
+ {"maccatalyst", false, "maccatalyst/arm64,maccatalyst/amd64"},
+ {"maccatalyst,ios/arm64", false, "maccatalyst/arm64,maccatalyst/amd64,ios/arm64"},
+ {"maccatalyst/arm64", false, "maccatalyst/arm64"},
+ {"maccatalyst/amd64", false, "maccatalyst/amd64"},
+
+ {"", true, ""},
+ {"linux", true, ""},
+ {"android/x86", true, ""},
+ {"android/arm5", true, ""},
+ {"ios/mips", true, ""},
+ {"android,ios", true, ""},
+ {"ios,android", true, ""},
+ {"ios/amd64", true, ""},
}
for _, tc := range tests {
- gotOS, gotArchs, err := parseBuildTarget(tc.in)
- if tc.wantErr {
- if err == nil {
- t.Errorf("-target=%q; want error, got (%q, %q, nil)", tc.in, gotOS, gotArchs)
+ t.Run(tc.in, func(t *testing.T) {
+ targets, err := parseBuildTarget(tc.in)
+ var s []string
+ for _, t := range targets {
+ s = append(s, t.String())
}
- continue
- }
- if err != nil || gotOS != tc.wantOS || strings.Join(gotArchs, ",") != tc.wantArchs {
- t.Errorf("-target=%q; want (%v, [%v], nil), got (%q, %q, %v)",
- tc.in, tc.wantOS, tc.wantArchs, gotOS, gotArchs, err)
- }
+ got := strings.Join(s, ",")
+ if tc.wantErr {
+ if err == nil {
+ t.Errorf("-target=%q; want error, got (%q, nil)", tc.in, got)
+ }
+ return
+ }
+ if err != nil || got != tc.want {
+ t.Errorf("-target=%q; want (%q, nil), got (%q, %v)", tc.in, tc.want, got, err)
+ }
+ })
}
}
diff --git a/cmd/gomobile/doc.go b/cmd/gomobile/doc.go
index aeff5f6..8522dd6 100644
--- a/cmd/gomobile/doc.go
+++ b/cmd/gomobile/doc.go
@@ -35,13 +35,13 @@
Usage:
- gomobile bind [-target android|ios] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]
+ gomobile bind [-target android|ios|iossimulator|macos|maccatalyst] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]
Bind generates language bindings for the package named by the import
path, and compiles a library for the named target system.
-The -target flag takes a target system name, either android (the
-default) or ios.
+The -target flag takes either android (the default), or one or more
+comma-delimited Apple platforms (ios, iossimulator, macos, maccatalyst).
For -target android, the bind command produces an AAR (Android ARchive)
file that archives the precompiled Java API stub classes, the compiled
@@ -63,9 +63,9 @@
can be selected by specifying target type with the architecture name. E.g.,
-target=android/arm,android/386.
-For -target ios, gomobile must be run on an OS X machine with Xcode
-installed. The generated Objective-C types can be prefixed with the -prefix
-flag.
+For Apple -target platforms, gomobile must be run on an OS X machine with
+Xcode installed. The generated Objective-C types can be prefixed with the
+-prefix flag.
For -target android, the -bootclasspath and -classpath flags are used to
control the bootstrap classpath and the classpath for Go wrappers to Java
@@ -81,14 +81,14 @@
Usage:
- gomobile build [-target android|ios] [-o output] [-bundleid bundleID] [build flags] [package]
+ gomobile build [-target android|ios|iossimulator|macos|maccatalyst] [-o output] [-bundleid bundleID] [build flags] [package]
Build compiles and encodes the app named by the import path.
The named package must define a main function.
-The -target flag takes a target system name, either android (the
-default) or ios.
+The -target flag takes either android (the default), or one or more
+comma-delimited Apple platforms (ios, iossimulator, macos, maccatalyst).
For -target android, if an AndroidManifest.xml is defined in the
package directory, it is added to the APK output. Otherwise, a default
@@ -97,14 +97,22 @@
be selected by specifying target type with the architecture name. E.g.
-target=android/arm,android/386.
-For -target ios, gomobile must be run on an OS X machine with Xcode
-installed.
+For Apple -target platforms, gomobile must be run on an OS X machine with
+Xcode installed.
+
+By default, -target ios will generate an XCFramework for both ios
+and iossimulator. Multiple Apple targets can be specified, creating a "fat"
+XCFramework with each slice. To generate a fat XCFramework that supports
+iOS, macOS, and macCatalyst for all supportec architectures (amd64 and arm64),
+specify -target ios,macos,maccatalyst. A subset of instruction sets can be
+selectged by specifying the platform with an architecture name. E.g.
+-target=ios/arm64,maccatalyst/arm64.
If the package directory contains an assets subdirectory, its contents
are copied into the output.
Flag -iosversion sets the minimal version of the iOS SDK to compile against.
-The default version is 7.0.
+The default version is 13.0.
Flag -androidapi sets the Android API version to compile against.
The default and minimum is 15.
diff --git a/cmd/gomobile/env.go b/cmd/gomobile/env.go
index a178489..69bb710 100644
--- a/cmd/gomobile/env.go
+++ b/cmd/gomobile/env.go
@@ -17,23 +17,95 @@
androidEnv map[string][]string // android arch -> []string
- iosEnv map[string][]string
+ appleEnv map[string][]string
androidArmNM string
- iosArmNM string
+ appleNM string
)
-func allArchs(targetOS string) []string {
- switch targetOS {
+func isAndroidPlatform(platform string) bool {
+ return platform == "android"
+}
+
+func isApplePlatform(platform string) bool {
+ return contains(applePlatforms, platform)
+}
+
+var applePlatforms = []string{"ios", "iossimulator", "macos", "maccatalyst"}
+
+func platformArchs(platform string) []string {
+ switch platform {
case "ios":
+ return []string{"arm64"}
+ case "iossimulator":
+ return []string{"arm64", "amd64"}
+ case "macos", "maccatalyst":
return []string{"arm64", "amd64"}
case "android":
return []string{"arm", "arm64", "386", "amd64"}
default:
- panic(fmt.Sprintf("unexpected target OS: %s", targetOS))
+ panic(fmt.Sprintf("unexpected platform: %s", platform))
}
}
+func isSupportedArch(platform, arch string) bool {
+ return contains(platformArchs(platform), arch)
+}
+
+// platformOS returns the correct GOOS value for platform.
+func platformOS(platform string) string {
+ switch platform {
+ case "android":
+ return "android"
+ case "ios", "iossimulator":
+ return "ios"
+ case "macos", "maccatalyst":
+ // For "maccatalyst", Go packages should be built with GOOS=darwin,
+ // not GOOS=ios, since the underlying OS (and kernel, runtime) is macOS.
+ // We also apply a "macos" or "maccatalyst" build tag, respectively.
+ // See below for additional context.
+ return "darwin"
+ default:
+ panic(fmt.Sprintf("unexpected platform: %s", platform))
+ }
+}
+
+func platformTags(platform string) []string {
+ switch platform {
+ case "android":
+ return []string{"android"}
+ case "ios", "iossimulator":
+ return []string{"ios"}
+ case "macos":
+ return []string{"macos"}
+ case "maccatalyst":
+ // Mac Catalyst is a subset of iOS APIs made available on macOS
+ // designed to ease porting apps developed for iPad to macOS.
+ // See https://developer.apple.com/mac-catalyst/.
+ // Because of this, when building a Go package targeting maccatalyst,
+ // GOOS=darwin (not ios). To bridge the gap and enable maccatalyst
+ // packages to be compiled, we also specify the "ios" build tag.
+ // To help discriminate between darwin, ios, macos, and maccatalyst
+ // targets, there is also a "maccatalyst" tag.
+ // Some additional context on this can be found here:
+ // https://stackoverflow.com/questions/12132933/preprocessor-macro-for-os-x-targets/49560690#49560690
+ // TODO(ydnar): remove tag "ios" when cgo supports Catalyst
+ // See golang.org/issues/47228
+ return []string{"ios", "macos", "maccatalyst"}
+ default:
+ panic(fmt.Sprintf("unexpected platform: %s", platform))
+ }
+}
+
+func contains(haystack []string, needle string) bool {
+ for _, v := range haystack {
+ if v == needle {
+ return true
+ }
+ }
+ return false
+}
+
func buildEnvInit() (cleanup func(), err error) {
// Find gomobilepath.
gopath := goEnv("GOPATH")
@@ -123,37 +195,85 @@
return nil
}
- iosArmNM = "nm"
- iosEnv = make(map[string][]string)
- for _, arch := range allArchs("ios") {
- var env []string
- var err error
- var clang, cflags string
- switch arch {
- case "arm64":
- clang, cflags, err = envClang("iphoneos")
- cflags += " -miphoneos-version-min=" + buildIOSVersion
- case "amd64":
- clang, cflags, err = envClang("iphonesimulator")
- cflags += " -mios-simulator-version-min=" + buildIOSVersion
- default:
- panic(fmt.Errorf("unknown GOARCH: %q", arch))
+ appleNM = "nm"
+ appleEnv = make(map[string][]string)
+ for _, platform := range applePlatforms {
+ for _, arch := range platformArchs(platform) {
+ var env []string
+ var goos, sdk, clang, cflags string
+ var err error
+ switch platform {
+ case "ios":
+ goos = "ios"
+ sdk = "iphoneos"
+ clang, cflags, err = envClang(sdk)
+ cflags += " -miphoneos-version-min=" + buildIOSVersion
+ cflags += " -fembed-bitcode"
+ case "iossimulator":
+ goos = "ios"
+ sdk = "iphonesimulator"
+ clang, cflags, err = envClang(sdk)
+ cflags += " -mios-simulator-version-min=" + buildIOSVersion
+ cflags += " -fembed-bitcode"
+ case "maccatalyst":
+ // Mac Catalyst is a subset of iOS APIs made available on macOS
+ // designed to ease porting apps developed for iPad to macOS.
+ // See https://developer.apple.com/mac-catalyst/.
+ // Because of this, when building a Go package targeting maccatalyst,
+ // GOOS=darwin (not ios). To bridge the gap and enable maccatalyst
+ // packages to be compiled, we also specify the "ios" build tag.
+ // To help discriminate between darwin, ios, macos, and maccatalyst
+ // targets, there is also a "maccatalyst" tag.
+ // Some additional context on this can be found here:
+ // https://stackoverflow.com/questions/12132933/preprocessor-macro-for-os-x-targets/49560690#49560690
+ goos = "darwin"
+ sdk = "macosx"
+ clang, cflags, err = envClang(sdk)
+ // TODO(ydnar): the following 3 lines MAY be needed to compile
+ // packages or apps for maccatalyst. Commenting them out now in case
+ // it turns out they are necessary. Currently none of the example
+ // apps will build for macos or maccatalyst because they have a
+ // GLKit dependency, which is deprecated on all Apple platforms, and
+ // broken on maccatalyst (GLKView isn’t available).
+ // sysroot := strings.SplitN(cflags, " ", 2)[1]
+ // cflags += " -isystem " + sysroot + "/System/iOSSupport/usr/include"
+ // cflags += " -iframework " + sysroot + "/System/iOSSupport/System/Library/Frameworks"
+ switch arch {
+ case "amd64":
+ cflags += " -target x86_64-apple-ios" + buildIOSVersion + "-macabi"
+ case "arm64":
+ cflags += " -target arm64-apple-ios" + buildIOSVersion + "-macabi"
+ cflags += " -fembed-bitcode"
+ }
+ case "macos":
+ goos = "darwin"
+ sdk = "macosx" // Note: the SDK is called "macosx", not "macos"
+ clang, cflags, err = envClang(sdk)
+ if arch == "arm64" {
+ cflags += " -fembed-bitcode"
+ }
+ default:
+ panic(fmt.Errorf("unknown Apple target: %s/%s", platform, arch))
+ }
+
+ if err != nil {
+ return err
+ }
+
+ env = append(env,
+ "GOOS="+goos,
+ "GOARCH="+arch,
+ "GOFLAGS="+"-tags="+strings.Join(platformTags(platform), ","),
+ "CC="+clang,
+ "CXX="+clang+"++",
+ "CGO_CFLAGS="+cflags+" -arch "+archClang(arch),
+ "CGO_CXXFLAGS="+cflags+" -arch "+archClang(arch),
+ "CGO_LDFLAGS="+cflags+" -arch "+archClang(arch),
+ "CGO_ENABLED=1",
+ "DARWIN_SDK="+sdk,
+ )
+ appleEnv[platform+"/"+arch] = env
}
- if err != nil {
- return err
- }
- cflags += " -fembed-bitcode"
- env = append(env,
- "GOOS=ios",
- "GOARCH="+arch,
- "CC="+clang,
- "CXX="+clang+"++",
- "CGO_CFLAGS="+cflags+" -arch "+archClang(arch),
- "CGO_CXXFLAGS="+cflags+" -arch "+archClang(arch),
- "CGO_LDFLAGS="+cflags+" -arch "+archClang(arch),
- "CGO_ENABLED=1",
- )
- iosEnv[arch] = env
}
return nil
@@ -186,7 +306,7 @@
func envClang(sdkName string) (clang, cflags string, err error) {
if buildN {
- return sdkName + "-clang", "-isysroot=" + sdkName, nil
+ return sdkName + "-clang", "-isysroot " + sdkName, nil
}
cmd := exec.Command("xcrun", "--sdk", sdkName, "--find", "clang")
out, err := cmd.CombinedOutput()
diff --git a/cmd/gomobile/init.go b/cmd/gomobile/init.go
index 00b9a56..172d015 100644
--- a/cmd/gomobile/init.go
+++ b/cmd/gomobile/init.go
@@ -167,7 +167,7 @@
}
}
- for _, arch := range allArchs("android") {
+ for _, arch := range platformArchs("android") {
t := ndk[arch]
abi := t.arch
if abi == "arm" {
diff --git a/cmd/gomobile/version.go b/cmd/gomobile/version.go
index 8c09a44..b791556 100644
--- a/cmd/gomobile/version.go
+++ b/cmd/gomobile/version.go
@@ -53,7 +53,7 @@
// Supported platforms
platforms := "android"
if xcodeAvailable() {
- platforms = "android,ios"
+ platforms += "," + strings.Join(applePlatforms, ",")
}
// ANDROID_HOME, sdk build tool version