| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/xml" |
| "errors" |
| "fmt" |
| "io" |
| "os/exec" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "text/template" |
| |
| "golang.org/x/sync/errgroup" |
| "golang.org/x/tools/go/packages" |
| ) |
| |
| 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 + ".xcframework" |
| } else { |
| 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(".xcframework")] |
| title = strings.Title(name) |
| } |
| |
| if err := removeAll(buildO); err != nil { |
| return err |
| } |
| |
| outDirsForPlatform := map[string]string{} |
| for _, t := range targets { |
| outDirsForPlatform[t.platform] = filepath.Join(tmpdir, t.platform) |
| } |
| |
| // Run the gobind command for each platform |
| var gobindWG errgroup.Group |
| for platform, outDir := range outDirsForPlatform { |
| platform := platform |
| outDir := outDir |
| gobindWG.Go(func() error { |
| // Catalyst support requires iOS 13+ |
| v, _ := strconv.ParseFloat(buildIOSVersion, 64) |
| if platform == "maccatalyst" && v < 13.0 { |
| return errors.New("catalyst requires -iosversion=13 or higher") |
| } |
| |
| // 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(platform)) |
| cmd.Env = append(cmd.Env, "CGO_ENABLED=1") |
| tags := append(buildTags[:], platformTags(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 |
| } |
| return nil |
| }) |
| } |
| if err := gobindWG.Wait(); err != nil { |
| return err |
| } |
| |
| modulesUsed, err := areGoModulesUsed() |
| if err != nil { |
| return err |
| } |
| |
| // Build archive files. |
| var buildWG errgroup.Group |
| for _, t := range targets { |
| t := t |
| buildWG.Go(func() error { |
| outDir := outDirsForPlatform[t.platform] |
| outSrcDir := filepath.Join(outDir, "src") |
| |
| if modulesUsed { |
| // Copy the source directory for each architecture for concurrent building. |
| newOutSrcDir := filepath.Join(outDir, "src-"+t.arch) |
| if !buildN { |
| if err := doCopyAll(newOutSrcDir, outSrcDir); err != nil { |
| return err |
| } |
| } |
| outSrcDir = newOutSrcDir |
| } |
| |
| // Copy the environment variables to make this function concurrent-safe. |
| env := make([]string, len(appleEnv[t.String()])) |
| copy(env, appleEnv[t.String()]) |
| |
| // 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) |
| |
| // 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 := writeGoMod(outSrcDir, t.platform, t.arch); err != nil { |
| return err |
| } |
| if err := goModTidyAt(outSrcDir, env); err != nil { |
| return err |
| } |
| } |
| |
| if err := goAppleBindArchive(appleArchiveFilepath(name, t), env, outSrcDir); err != nil { |
| return fmt.Errorf("%s/%s: %v", t.platform, t.arch, err) |
| } |
| |
| return nil |
| }) |
| } |
| if err := buildWG.Wait(); err != nil { |
| return err |
| } |
| |
| var frameworkDirs []string |
| frameworkArchCount := map[string]int{} |
| for _, t := range targets { |
| outDir := outDirsForPlatform[t.platform] |
| gobindDir := filepath.Join(outDir, "src", "gobind") |
| |
| 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 |
| |
| frameworkLayout, err := frameworkLayoutForTarget(t, title) |
| if err != nil { |
| return err |
| } |
| |
| titlePath := filepath.Join(frameworkDir, frameworkLayout.binaryPath, title) |
| if frameworkArchCount[frameworkDir] > 1 { |
| // Not the first static lib, attach to a fat library and skip create headers |
| fatCmd := exec.Command( |
| "xcrun", |
| "lipo", appleArchiveFilepath(name, t), titlePath, "-create", "-output", titlePath, |
| ) |
| if err := runCmd(fatCmd); err != nil { |
| return err |
| } |
| continue |
| } |
| |
| headersDir := filepath.Join(frameworkDir, frameworkLayout.headerPath) |
| if err := mkdir(headersDir); err != nil { |
| return err |
| } |
| |
| lipoCmd := exec.Command( |
| "xcrun", |
| "lipo", appleArchiveFilepath(name, t), "-create", "-o", titlePath, |
| ) |
| if err := runCmd(lipoCmd); err != nil { |
| return err |
| } |
| |
| fileBases := make([]string, len(pkgs)+1) |
| for i, pkg := range pkgs { |
| fileBases[i] = bindPrefix + strings.Title(pkg.Name) |
| } |
| fileBases[len(fileBases)-1] = "Universe" |
| |
| // Copy header file next to output archive. |
| var headerFiles []string |
| if len(fileBases) == 1 { |
| headerFiles = append(headerFiles, title+".h") |
| err := copyFile( |
| filepath.Join(headersDir, 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(headersDir, fileBase+".objc.h"), |
| filepath.Join(gobindDir, fileBase+".objc.h"), |
| ) |
| if err != nil { |
| return err |
| } |
| } |
| err := copyFile( |
| filepath.Join(headersDir, "ref.h"), |
| filepath.Join(gobindDir, "ref.h"), |
| ) |
| if err != nil { |
| return err |
| } |
| headerFiles = append(headerFiles, title+".h") |
| err = writeFile(filepath.Join(headersDir, 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 |
| } |
| } |
| |
| frameworkInfoPlistDir := filepath.Join(frameworkDir, frameworkLayout.infoPlistPath) |
| if err := mkdir(frameworkInfoPlistDir); err != nil { |
| return err |
| } |
| err = writeFile(filepath.Join(frameworkInfoPlistDir, "Info.plist"), func(w io.Writer) error { |
| infoFrameworkPlistlData := infoFrameworkPlistlData{ |
| BundleID: escapePlistValue(rfc1034Label(title)), |
| ExecutableName: escapePlistValue(title), |
| } |
| infoplist := new(bytes.Buffer) |
| if err := infoFrameworkPlistTmpl.Execute(infoplist, infoFrameworkPlistlData); err != nil { |
| return err |
| } |
| _, err := w.Write(infoplist.Bytes()) |
| return err |
| }) |
| if err != nil { |
| return err |
| } |
| |
| var mmVals = struct { |
| Module string |
| Headers []string |
| }{ |
| Module: title, |
| Headers: headerFiles, |
| } |
| modulesDir := filepath.Join(frameworkDir, frameworkLayout.modulePath) |
| err = writeFile(filepath.Join(modulesDir, "module.modulemap"), func(w io.Writer) error { |
| return appleModuleMapTmpl.Execute(w, mmVals) |
| }) |
| if err != nil { |
| return err |
| } |
| |
| for src, dst := range frameworkLayout.symlinks { |
| if err := symlink(src, filepath.Join(frameworkDir, dst)); err != nil { |
| return err |
| } |
| } |
| } |
| |
| // Finally combine all frameworks to an XCFramework |
| xcframeworkArgs := []string{"-create-xcframework"} |
| |
| for _, dir := range frameworkDirs { |
| // On macOS, a temporary directory starts with /var, which is a symbolic link to /private/var. |
| // And in gomobile, a temporary directory is usually used as a working directly. |
| // Unfortunately, xcodebuild in Xcode 15 seems to have a bug and might not be able to understand fullpaths with symbolic links. |
| // As a workaround, resolve the path with symbolic links by filepath.EvalSymlinks. |
| dir, err := filepath.EvalSymlinks(dir) |
| if err != nil { |
| return err |
| } |
| xcframeworkArgs = append(xcframeworkArgs, "-framework", dir) |
| } |
| |
| xcframeworkArgs = append(xcframeworkArgs, "-output", buildO) |
| cmd := exec.Command("xcodebuild", xcframeworkArgs...) |
| err = runCmd(cmd) |
| return err |
| } |
| |
| type frameworkLayout struct { |
| headerPath string |
| binaryPath string |
| modulePath string |
| infoPlistPath string |
| // symlinks to create in the framework. Maps src (relative to dst) -> dst (relative to framework bundle root) |
| symlinks map[string]string |
| } |
| |
| // frameworkLayoutForTarget generates the filestructure for a framework for the given target platform (macos, ios, etc), |
| // according to Apple's spec https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle |
| func frameworkLayoutForTarget(t targetInfo, title string) (*frameworkLayout, error) { |
| switch t.platform { |
| case "macos", "maccatalyst": |
| return &frameworkLayout{ |
| headerPath: "Versions/A/Headers", |
| binaryPath: "Versions/A", |
| modulePath: "Versions/A/Modules", |
| infoPlistPath: "Versions/A/Resources", |
| symlinks: map[string]string{ |
| "A": "Versions/Current", |
| "Versions/Current/Resources": "Resources", |
| "Versions/Current/Headers": "Headers", |
| "Versions/Current/Modules": "Modules", |
| filepath.Join("Versions/Current", title): title, |
| }, |
| }, nil |
| case "ios", "iossimulator": |
| return &frameworkLayout{ |
| headerPath: "Headers", |
| binaryPath: ".", |
| modulePath: "Modules", |
| infoPlistPath: ".", |
| }, nil |
| } |
| |
| return nil, fmt.Errorf("unsupported platform %q", t.platform) |
| } |
| |
| type infoFrameworkPlistlData struct { |
| BundleID string |
| ExecutableName string |
| } |
| |
| // infoFrameworkPlistTmpl is a template for the Info.plist file in a framework. |
| // Minimum OS version == 100.0 is a workaround for SPM issue |
| // https://github.com/firebase/firebase-ios-sdk/pull/12439/files#diff-f4eb4ff5ec89af999cbe8fa3ffe5647d7853ffbc9c1515b337ca043c684b6bb4R679 |
| var infoFrameworkPlistTmpl = template.Must(template.New("infoFrameworkPlist").Parse(`<?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> |
| <key>CFBundleExecutable</key> |
| <string>{{.ExecutableName}}</string> |
| <key>CFBundleIdentifier</key> |
| <string>{{.BundleID}}</string> |
| <key>MinimumOSVersion</key> |
| <string>100.0</string> |
| <key>CFBundlePackageType</key> |
| <string>FMWK</string> |
| </dict> |
| </plist> |
| `)) |
| |
| func escapePlistValue(value string) string { |
| var b bytes.Buffer |
| xml.EscapeText(&b, []byte(value)) |
| return b.String() |
| } |
| |
| var appleModuleMapTmpl = template.Must(template.New("iosmmap").Parse(`framework module "{{.Module}}" { |
| header "ref.h" |
| {{range .Headers}} header "{{.}}" |
| {{end}} |
| export * |
| }`)) |
| |
| func appleArchiveFilepath(name string, t targetInfo) string { |
| return filepath.Join(tmpdir, name+"-"+t.platform+"-"+t.arch+".a") |
| } |
| |
| func goAppleBindArchive(out string, env []string, gosrc string) error { |
| return goBuildAt(gosrc, "./gobind", env, "-buildmode=c-archive", "-o", out) |
| } |
| |
| var appleBindHeaderTmpl = template.Must(template.New("apple.h").Parse(` |
| // Objective-C API for talking to the following Go packages |
| // |
| {{range .pkgs}}// {{.PkgPath}} |
| {{end}}// |
| // File is generated by gomobile bind. Do not edit. |
| #ifndef __{{.title}}_FRAMEWORK_H__ |
| #define __{{.title}}_FRAMEWORK_H__ |
| |
| {{range .bases}}#include "{{.}}.objc.h" |
| {{end}} |
| #endif |
| `)) |