cmd/gomobile: improve support for macOS and Catalyst

This is is a follow-up from my previous PR (#65). It makes gomobile
aware of GOOS=ios and adds support for specifying specific Apple
platforms, instead of overloading the "ios" platform.

Supported platforms: ios, iossimulator, macos, and maccatalyst

These can now be specified the -target argument to gomobile, e.g.:
gomobile build -target=ios,iossimulator,macos,maccatalyst

It preserves the current behavior of -target=ios, which will build for
ios and iossimulator on supported architectures (arm64 and amd64).

It adds platform-specific build tags so Go code can discriminate between
different Apple platforms like maccatalyst (UIKit on macOS).

This PR also fixes a number of broken tests.

TODO: cgo has a bug where c-archive builds targeting Catalyst will fail
unless -tags=ios is supplied. See https://golang.org/issues/47228

Fixes https://golang.org/issues/47212
Updates https://golang.org/issues/47228

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