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>
This commit is contained in:
Randy Reddig 2021-09-16 23:09:45 +00:00 committed by Hyang-Ah Hana Kim
parent 5d9a33257a
commit 6d8ad35e46
14 changed files with 621 additions and 365 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ last-change
*.apk
*.app
*.framework
*.xcframework
*.aar
*.iml
.idea

View File

@ -23,14 +23,14 @@ import (
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 @@ instruction sets (arm, arm64, 386, amd64). A subset of instruction sets
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 @@ func runBind(cmd *command) error {
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 @@ func runBind(cmd *command) error {
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 @@ func runBind(cmd *command) error {
// 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 @@ func writeFile(filename string, generate func(io.Writer) error) error {
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 @@ func packagesConfig(targetOS string) *packages.Config {
}
// 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 := append(buildTags[:], platformTags(targetPlatform)...)
tags := buildTags
// 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 @@ func getModuleVersions(targetOS string, targetArch string, src string) (*modfile
}
// 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 @@ func writeGoMod(targetOS string, targetArch string) error {
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
}

View File

@ -18,7 +18,7 @@ import (
"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 @@ func goAndroidBind(gobind string, pkgs []*packages.Package, androidArchs []strin
}
// 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 @@ func goAndroidBind(gobind string, pkgs []*packages.Package, androidArchs []strin
}
}
toolchain := ndk.Toolchain(arch)
toolchain := ndk.Toolchain(t.arch)
err := goBuildAt(
filepath.Join(tmpdir, "src"),
"./gobind",
@ -90,7 +90,7 @@ func goAndroidBind(gobind string, pkgs []*packages.Package, androidArchs []strin
}
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 @@ func buildSrcJar(src string) error {
// 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 @@ func buildAAR(srcDir, androidDir string, pkgs []*packages.Package, androidArchs
}
}
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 {

View File

@ -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 @@ const iosBindInfoPlist = `<?xml version="1.0" encoding="UTF-8"?>
</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 @@ func goIOSBindArchive(name string, env []string, gosrc string) (string, error) {
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}}

View File

@ -98,7 +98,7 @@ func TestBindAndroid(t *testing.T) {
}
}
func TestBindIOS(t *testing.T) {
func TestBindApple(t *testing.T) {
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
@ -112,7 +112,7 @@ func TestBindIOS(t *testing.T) {
}()
buildN = true
buildX = true
buildO = "Asset.framework"
buildO = "Asset.xcframework"
buildTarget = "ios/arm64"
tests := []struct {
@ -126,7 +126,7 @@ func TestBindIOS(t *testing.T) {
prefix: "Foo",
},
{
out: "Abcde.framework",
out: "Abcde.xcframework",
},
}
for _, tc := range tests {
@ -159,12 +159,12 @@ func TestBindIOS(t *testing.T) {
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 @@ PWD=$WORK/java javac -d $WORK/javac-output -source 1.7 -target 1.7 -bootclasspat
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 @@ func TestBindIOSAll(t *testing.T) {
}()
buildN = true
buildX = true
buildO = "Asset.framework"
buildO = "Asset.xcframework"
buildTarget = "ios"
buf := new(bytes.Buffer)
@ -290,7 +291,7 @@ func TestBindWithGoModules(t *testing.T) {
case "android":
out = filepath.Join(dir, "cgopkg.aar")
case "ios":
out = filepath.Join(dir, "Cgopkg.framework")
out = filepath.Join(dir, "Cgopkg.xcframework")
}
tests := []struct {

View File

@ -8,11 +8,13 @@ package main
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"golang.org/x/tools/go/packages"
@ -23,15 +25,15 @@ var tmpdir string
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 @@ instruction sets (arm, 386, amd64, arm64). A subset of instruction sets 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.
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 @@ func runBuildImpl(cmd *command) (*packages.Package, error) {
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 @@ func runBuildImpl(cmd *command) (*packages.Package, error) {
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 @@ func runBuildImpl(cmd *command) (*packages.Package, error) {
}
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 @@ func runBuildImpl(cmd *command) (*packages.Package, error) {
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 @@ func addBuildFlags(cmd *command) {
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 @@ func goCmdAt(at string, subcmd string, srcs []string, env []string, args ...stri
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 @@ func goModTidyAt(at string, env []string) error {
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
}

View File

@ -24,7 +24,7 @@ import (
"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 @@ func goAndroidBuild(pkg *packages.Package, androidArchs []string) (map[string]bo
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 @@ func goAndroidBuild(pkg *packages.Package, androidArchs []string) (map[string]bo
}
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 @@ func goAndroidBuild(pkg *packages.Package, androidArchs []string) (map[string]bo
}
}
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 @@ func goAndroidBuild(pkg *packages.Package, androidArchs []string) (map[string]bo
}
// 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

View File

@ -20,7 +20,7 @@ import (
"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 @@ func goIOSBuild(pkg *packages.Package, bundleID string, archs []string) (map[str
"-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 @@ func goIOSBuild(pkg *packages.Package, bundleID string, archs []string) (map[str
}
// 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 goIOSBuild(pkg *packages.Package, bundleID string, archs []string) (map[str
}
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 @@ func detectTeamID() (string, error) {
}
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 @@ const projPbxproj = `// !$*UTF8*$!
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";

View File

@ -12,7 +12,7 @@ import (
"text/template"
)
func TestIOSBuild(t *testing.T) {
func TestAppleBuild(t *testing.T) {
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
@ -41,10 +41,13 @@ func TestIOSBuild(t *testing.T) {
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 @@ func TestIOSBuild(t *testing.T) {
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 @@ func TestIOSBuild(t *testing.T) {
}
}
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}}
`))

View File

@ -114,46 +114,63 @@ mkdir -p $WORK/lib/armeabi-v7a
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)
}
})
}
}

View File

@ -35,13 +35,13 @@ Build a library for Android and iOS
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 @@ instruction sets (arm, arm64, 386, amd64). A subset of instruction sets
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 @@ Compile android APK and iOS app
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 @@ instruction sets (arm, 386, amd64, arm64). A subset of instruction sets 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.
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.

View File

@ -17,23 +17,95 @@ var (
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 @@ func envInit() (err error) {
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 isnt 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 ndkRoot() (string, error) {
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()

View File

@ -167,7 +167,7 @@ func installOpenAL(gomobilepath string) error {
}
}
for _, arch := range allArchs("android") {
for _, arch := range platformArchs("android") {
t := ndk[arch]
abi := t.arch
if abi == "arm" {

View File

@ -53,7 +53,7 @@ func runVersion(cmd *command) (err error) {
// Supported platforms
platforms := "android"
if xcodeAvailable() {
platforms = "android,ios"
platforms += "," + strings.Join(applePlatforms, ",")
}
// ANDROID_HOME, sdk build tool version