diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go index efbc89699..72fe2ae9c 100644 --- a/cmd/gomobile/bind.go +++ b/cmd/gomobile/bind.go @@ -126,10 +126,13 @@ func runBind(cmd *command) error { case isAndroidPlatform(targets[0].platform): return goAndroidBind(gobind, pkgs, targets) case isApplePlatform(targets[0].platform): + if buildWorkerError := validateBuildWorkers(buildWorkers); buildWorkerError != nil { + return buildWorkerError + } if !xcodeAvailable() { return fmt.Errorf("-target=%q requires Xcode", buildTarget) } - return goAppleBind(gobind, pkgs, targets) + return goAppleBind(gobind, pkgs, targets, buildWorkers) default: return fmt.Errorf(`invalid -target=%q`, buildTarget) } @@ -321,3 +324,11 @@ func areGoModulesUsed() (bool, error) { } return true, nil } + +func validateBuildWorkers(workers int) error { + if workers < 1 { + return fmt.Errorf("invalid workers %d: must be >= 1", workers) + } else { + return nil + } +} diff --git a/cmd/gomobile/bind_iosapp.go b/cmd/gomobile/bind_iosapp.go index bf0f37db6..6e9bef3ab 100644 --- a/cmd/gomobile/bind_iosapp.go +++ b/cmd/gomobile/bind_iosapp.go @@ -12,12 +12,18 @@ import ( "path/filepath" "strconv" "strings" + "sync" "text/template" "golang.org/x/tools/go/packages" ) -func goAppleBind(gobind string, pkgs []*packages.Package, targets []targetInfo) error { +type archBuildResult struct { + titlePath string + frameworkPath string +} + +func goAppleBind(gobind string, pkgs []*packages.Package, targets []targetInfo, buildWorkers int) error { var name string var title string @@ -43,189 +49,66 @@ func goAppleBind(gobind string, pkgs []*packages.Package, targets []targetInfo) return err } - 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 := appleEnv[t.String()][:] - sdk := getenv(env, "DARWIN_SDK") + var platformBuildResults = make(map[string][]archBuildResult) + var targetBuildResultMutex sync.Mutex - 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" + var waitGroup sync.WaitGroup + waitGroup.Add(len(targets)) - // Add the generated packages to GOPATH for reverse bindings. - gopath := fmt.Sprintf("GOPATH=%s%c%s", outDir, filepath.ListSeparator, goEnv("GOPATH")) - env = append(env, gopath) + parallelBuildErrorBuffer := make(chan error, len(targets)) + semaphore := make(chan struct{}, buildWorkers) - if err := writeGoMod(outDir, t.platform, t.arch); err != nil { - return err - } + for _, target := range targets { + go func(target targetInfo) { + semaphore <- struct{}{} + defer func() { + <-semaphore + waitGroup.Done() + }() - // 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(outSrcDir, env); err != nil { - return err + buildResult, err := buildTargetArch(target, gobind, pkgs, title, name, modulesUsed) + if err != nil { + parallelBuildErrorBuffer <- fmt.Errorf("cannot build %s [%s]: %v", target.platform, target.arch, err) + return } - } - path, err := goAppleBindArchive(name+"-"+t.platform+"-"+t.arch, env, outSrcDir) - if err != nil { - return fmt.Errorf("%s/%s: %v", t.platform, t.arch, err) - } + targetBuildResultMutex.Lock() + defer targetBuildResultMutex.Unlock() + platformBuildResults[target.platform] = append(platformBuildResults[target.platform], *buildResult) - 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 - } + }(target) + } - 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 - } + waitGroup.Wait() + close(parallelBuildErrorBuffer) - lipoCmd := exec.Command( - "xcrun", - "lipo", path, "-create", "-o", titlePath, - ) - if err := runCmd(lipoCmd); err != nil { - return err - } + for buildErr := range parallelBuildErrorBuffer { + return buildErr + } - // Copy header file next to output archive. - var headerFiles []string - if len(fileBases) == 1 { - headerFiles = append(headerFiles, title+".h") - err := copyFile( - 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 - } - } + // Finally combine all frameworks to an XCFramework + xcframeworkArgs := []string{"-create-xcframework"} - 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 = writeFile(filepath.Join(frameworkDir, "Resources", "Info.plist"), func(w io.Writer) error { - _, err := w.Write([]byte(appleBindInfoPlist)) - return err - }) - if err != nil { - return err - } + refinedBuildResults := []archBuildResult{} - 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 + // Merge binary for single target + for _, buildResults := range platformBuildResults { + if len(buildResults) == 2 { + mergeArchsForSinglePlatform(buildResults[0].titlePath, buildResults[1].titlePath) + refinedBuildResults = append(refinedBuildResults, buildResults[1]) + } else if len(buildResults) == 1 { + refinedBuildResults = append(refinedBuildResults, buildResults[0]) + } else { + err = fmt.Errorf("unexpected number of build results: %v", len(buildResults)) } - } - // Finally combine all frameworks to an XCFramework - xcframeworkArgs := []string{"-create-xcframework"} + if err != nil { + return err + } - for _, dir := range frameworkDirs { - xcframeworkArgs = append(xcframeworkArgs, "-framework", dir) + for _, result := range refinedBuildResults { + xcframeworkArgs = append(xcframeworkArgs, "-framework", result.frameworkPath) } xcframeworkArgs = append(xcframeworkArgs, "-output", buildO) @@ -258,6 +141,184 @@ func goAppleBindArchive(name string, env []string, gosrc string) (string, error) return archive, nil } +func mergeArchsForSinglePlatform(from string, to string) error { + fatCmd := exec.Command( + "xcrun", + "lipo", from, to, "-create", "-output", to, + ) + if err := runCmd(fatCmd); err != nil { + return err + } + return nil +} + +func buildTargetArch(t targetInfo, gobindCommandPath string, pkgs []*packages.Package, title string, name string, modulesUsed bool) (buildResult *archBuildResult, err error) { + // 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") + } + + outDir := filepath.Join(tmpdir, t.platform, t.arch) // adding arch + outSrcDir := filepath.Join(outDir, "src") + gobindDir := filepath.Join(outSrcDir, "gobind") + + // Run gobind once per platform to generate the bindings + cmd := exec.Command( + gobindCommandPath, + "-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 nil, err + } + + env := appleEnv[t.String()][:] + sdk := getenv(env, "DARWIN_SDK") + + frameworkDir := filepath.Join(tmpdir, t.platform, sdk, t.arch, title+".framework") + + 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", outDir, filepath.ListSeparator, goEnv("GOPATH")) + env = append(env, gopath) + + if err := writeGoMod(outDir, t.platform, t.arch); err != nil { + return nil, 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(outSrcDir, env); err != nil { + return nil, err + } + } + + staticLibPath, err := goAppleBindArchive(name+"-"+t.platform+"-"+t.arch, env, outSrcDir) + if err != nil { + return nil, fmt.Errorf("%s/%s: %v", t.platform, t.arch, err) + } + + versionsDir := filepath.Join(frameworkDir, "Versions") + versionsADir := filepath.Join(versionsDir, "A") + titlePath := filepath.Join(versionsADir, title) + versionsAHeadersDir := filepath.Join(versionsADir, "Headers") + if err := mkdir(versionsAHeadersDir); err != nil { + return nil, err + } + if err := symlink("A", filepath.Join(versionsDir, "Current")); err != nil { + return nil, err + } + if err := symlink("Versions/Current/Headers", filepath.Join(frameworkDir, "Headers")); err != nil { + return nil, err + } + if err := symlink(filepath.Join("Versions/Current", title), filepath.Join(frameworkDir, title)); err != nil { + return nil, err + } + + lipoCmd := exec.Command( + "xcrun", + "lipo", staticLibPath, "-create", "-o", titlePath, + ) + if err := runCmd(lipoCmd); err != nil { + return nil, err + } + + // Copy header file next to output archive. + var headerFiles []string + if len(fileBases) == 1 { + headerFiles = append(headerFiles, title+".h") + err := copyFile( + filepath.Join(versionsAHeadersDir, title+".h"), + filepath.Join(gobindDir, bindPrefix+title+".objc.h"), + ) + if err != nil { + return nil, 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 nil, err + } + } + err := copyFile( + filepath.Join(versionsAHeadersDir, "ref.h"), + filepath.Join(gobindDir, "ref.h"), + ) + if err != nil { + return nil, 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 nil, err + } + } + + if err := mkdir(filepath.Join(versionsADir, "Resources")); err != nil { + return nil, err + } + + if err := symlink("Versions/Current/Resources", filepath.Join(frameworkDir, "Resources")); err != nil { + return nil, 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 nil, 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 nil, err + } + err = symlink(filepath.Join("Versions/Current/Modules"), filepath.Join(frameworkDir, "Modules")) + if err != nil { + return nil, err + } + return &archBuildResult{ + titlePath: titlePath, + frameworkPath: frameworkDir, + }, err +} + var appleBindHeaderTmpl = template.Must(template.New("apple.h").Parse(` // Objective-C API for talking to the following Go packages // diff --git a/cmd/gomobile/build.go b/cmd/gomobile/build.go index bd65f1c1a..f7f0eee1e 100644 --- a/cmd/gomobile/build.go +++ b/cmd/gomobile/build.go @@ -14,6 +14,7 @@ import ( "os" "os/exec" "regexp" + "runtime" "strconv" "strings" @@ -243,6 +244,7 @@ var ( buildTarget string // -target buildTrimpath bool // -trimpath buildWork bool // -work + buildWorkers int // -j buildBundleID string // -bundleid buildIOSVersion string // -iosversion buildAndroidAPI int // -androidapi @@ -262,6 +264,7 @@ func addBuildFlags(cmd *command) { cmd.flag.BoolVar(&buildI, "i", false, "") cmd.flag.BoolVar(&buildTrimpath, "trimpath", false, "") cmd.flag.Var(&buildTags, "tags", "") + cmd.flag.IntVar(&buildWorkers, "j", runtime.NumCPU(), "") } func addBuildFlagsNVXWork(cmd *command) {