diff --git a/.gitignore b/.gitignore index c7abc869b..0e17991f9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ last-change *.apk *.app *.framework +*.xcframework *.aar *.iml .idea diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go index 6a80e53b5..efbc89699 100644 --- a/cmd/gomobile/bind.go +++ b/cmd/gomobile/bind.go @@ -23,14 +23,14 @@ import ( var cmdBind = &command{ run: runBind, Name: "bind", - Usage: "[-target android|ios] [-bootclasspath ] [-classpath ] [-o output] [build flags] [package]", + Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-bootclasspath ] [-classpath ] [-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 } diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go index 9eb7ce66c..8ae9d4d27 100644 --- a/cmd/gomobile/bind_androidapp.go +++ b/cmd/gomobile/bind_androidapp.go @@ -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 { diff --git a/cmd/gomobile/bind_iosapp.go b/cmd/gomobile/bind_iosapp.go index e9615e8f3..bf0f37db6 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 - } + versionsAHeadersDir := filepath.Join(versionsADir, "Headers") + if err := mkdir(versionsAHeadersDir); err != nil { + return err + } + 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 + } - // 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", + lipoCmd := exec.Command( + "xcrun", + "lipo", path, "-create", "-o", titlePath, ) - if err != nil { + if err := runCmd(lipoCmd); err != nil { return err } - } else { - for i, fileBase := range fileBases { - headerFiles[i] = fileBase + ".objc.h" + + // 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 + } + } + + if err := mkdir(filepath.Join(versionsADir, "Resources")); err != nil { + return err + } + if err := symlink("Versions/Current/Resources", filepath.Join(frameworkDir, "Resources")); err != nil { + return err } - err := copyFile( - headers+"/ref.h", - srcDir+"/ref.h") + 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 } - 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, - }) + + 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 } - 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 + // Finally combine all frameworks to an XCFramework + xcframeworkArgs := []string{"-create-xcframework"} + + for _, dir := range frameworkDirs { + xcframeworkArgs = append(xcframeworkArgs, "-framework", dir) } - return symlink("Versions/Current/Modules", buildO+"/Modules") + + xcframeworkArgs = append(xcframeworkArgs, "-output", buildO) + cmd := exec.Command("xcodebuild", xcframeworkArgs...) + err = runCmd(cmd) + return err } -const iosBindInfoPlist = ` +const appleBindInfoPlist = ` @@ -190,16 +242,15 @@ const iosBindInfoPlist = ` ` -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}} diff --git a/cmd/gomobile/bind_test.go b/cmd/gomobile/bind_test.go index ee8d35ce4..5970c04d3 100644 --- a/cmd/gomobile/bind_test.go +++ b/cmd/gomobile/bind_test.go @@ -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 { diff --git a/cmd/gomobile/build.go b/cmd/gomobile/build.go index 79705adc0..bd65f1c1a 100644 --- a/cmd/gomobile/build.go +++ b/cmd/gomobile/build.go @@ -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) } + } + + var isAndroid, isApple bool + for _, target := range strings.Split(buildTarget, ",") { + tuple := strings.SplitN(target, "/", 2) + platform := tuple[0] + hasArch := len(tuple) == 2 - if len(osarch) == 1 { - all = true + 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 b97e9454e..b06ea2929 100644 --- a/cmd/gomobile/build_androidapp.go +++ b/cmd/gomobile/build_androidapp.go @@ -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 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 0e9e06352..2adaf3df5 100644 --- a/cmd/gomobile/build_iosapp.go +++ b/cmd/gomobile/build_apple.go @@ -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"; diff --git a/cmd/gomobile/build_darwin_test.go b/cmd/gomobile/build_darwin_test.go index 12d9a4bd7..2fac27c50 100644 --- a/cmd/gomobile/build_darwin_test.go +++ b/cmd/gomobile/build_darwin_test.go @@ -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}} `)) diff --git a/cmd/gomobile/build_test.go b/cmd/gomobile/build_test.go index f7eab6c69..ff21f8747 100644 --- a/cmd/gomobile/build_test.go +++ b/cmd/gomobile/build_test.go @@ -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/arm64,iossimulator/arm64,iossimulator/amd64"}, + {"ios,ios/arm64", false, "ios/arm64"}, + {"ios/arm64", false, "ios/arm64"}, - {"ios", false, "ios", iosArchs}, - {"ios,ios/arm64", false, "ios", iosArchs}, - {"ios/arm64", false, "ios", "arm64"}, - {"ios/amd64", false, "ios", "amd64"}, + {"iossimulator", false, "iossimulator/arm64,iossimulator/amd64"}, + {"iossimulator/amd64", false, "iossimulator/amd64"}, - {"", true, "", ""}, - {"linux", true, "", ""}, - {"android/x86", true, "", ""}, - {"android/arm5", true, "", ""}, - {"ios/mips", true, "", ""}, - {"android,ios", true, "", ""}, - {"ios,android", true, "", ""}, + {"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 aeff5f6f5..8522dd62c 100644 --- a/cmd/gomobile/doc.go +++ b/cmd/gomobile/doc.go @@ -35,13 +35,13 @@ Build a library for Android and iOS Usage: - gomobile bind [-target android|ios] [-bootclasspath ] [-classpath ] [-o output] [build flags] [package] + gomobile bind [-target android|ios|iossimulator|macos|maccatalyst] [-bootclasspath ] [-classpath ] [-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. diff --git a/cmd/gomobile/env.go b/cmd/gomobile/env.go index a178489e2..69bb71052 100644 --- a/cmd/gomobile/env.go +++ b/cmd/gomobile/env.go @@ -17,21 +17,93 @@ 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) { @@ -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)) - } - if err != nil { - return err + 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 } - 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() diff --git a/cmd/gomobile/init.go b/cmd/gomobile/init.go index 00b9a5632..172d015d1 100644 --- a/cmd/gomobile/init.go +++ b/cmd/gomobile/init.go @@ -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" { diff --git a/cmd/gomobile/version.go b/cmd/gomobile/version.go index 8c09a44e5..b7915563a 100644 --- a/cmd/gomobile/version.go +++ b/cmd/gomobile/version.go @@ -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