github.com/adwpc/xmobile@v0.0.0-20231212131043-3f9720cf0e99/cmd/gomobile/bind_iosapp.go (about)

     1  // Copyright 2015 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  	"text/template"
    16  
    17  	"golang.org/x/sync/errgroup"
    18  	"golang.org/x/tools/go/packages"
    19  )
    20  
    21  func goAppleBind(gobind string, pkgs []*packages.Package, targets []targetInfo) error {
    22  	var name string
    23  	var title string
    24  
    25  	if buildO == "" {
    26  		name = pkgs[0].Name
    27  		title = strings.Title(name)
    28  		buildO = title + ".xcframework"
    29  	} else {
    30  		if !strings.HasSuffix(buildO, ".xcframework") {
    31  			return fmt.Errorf("static framework name %q missing .xcframework suffix", buildO)
    32  		}
    33  		base := filepath.Base(buildO)
    34  		name = base[:len(base)-len(".xcframework")]
    35  		title = strings.Title(name)
    36  	}
    37  
    38  	if err := removeAll(buildO); err != nil {
    39  		return err
    40  	}
    41  
    42  	outDirsForPlatform := map[string]string{}
    43  	for _, t := range targets {
    44  		outDirsForPlatform[t.platform] = filepath.Join(tmpdir, t.platform)
    45  	}
    46  
    47  	// Run the gobind command for each platform
    48  	var gobindWG errgroup.Group
    49  	for platform, outDir := range outDirsForPlatform {
    50  		platform := platform
    51  		outDir := outDir
    52  		gobindWG.Go(func() error {
    53  			// Catalyst support requires iOS 13+
    54  			v, _ := strconv.ParseFloat(buildIOSVersion, 64)
    55  			if platform == "maccatalyst" && v < 13.0 {
    56  				return errors.New("catalyst requires -iosversion=13 or higher")
    57  			}
    58  
    59  			// Run gobind once per platform to generate the bindings
    60  			cmd := exec.Command(
    61  				gobind,
    62  				"-lang=go,objc",
    63  				"-outdir="+outDir,
    64  			)
    65  			cmd.Env = append(cmd.Env, "GOOS="+platformOS(platform))
    66  			cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
    67  			tags := append(buildTags[:], platformTags(platform)...)
    68  			cmd.Args = append(cmd.Args, "-tags="+strings.Join(tags, ","))
    69  			if bindPrefix != "" {
    70  				cmd.Args = append(cmd.Args, "-prefix="+bindPrefix)
    71  			}
    72  			for _, p := range pkgs {
    73  				cmd.Args = append(cmd.Args, p.PkgPath)
    74  			}
    75  			if err := runCmd(cmd); err != nil {
    76  				return err
    77  			}
    78  			return nil
    79  		})
    80  	}
    81  	if err := gobindWG.Wait(); err != nil {
    82  		return err
    83  	}
    84  
    85  	modulesUsed, err := areGoModulesUsed()
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	// Build archive files.
    91  	var buildWG errgroup.Group
    92  	for _, t := range targets {
    93  		t := t
    94  		buildWG.Go(func() error {
    95  			outDir := outDirsForPlatform[t.platform]
    96  			outSrcDir := filepath.Join(outDir, "src")
    97  
    98  			if modulesUsed {
    99  				// Copy the source directory for each architecture for concurrent building.
   100  				newOutSrcDir := filepath.Join(outDir, "src-"+t.arch)
   101  				if !buildN {
   102  					if err := doCopyAll(newOutSrcDir, outSrcDir); err != nil {
   103  						return err
   104  					}
   105  				}
   106  				outSrcDir = newOutSrcDir
   107  			}
   108  
   109  			// Copy the environment variables to make this function concurrent-safe.
   110  			env := make([]string, len(appleEnv[t.String()]))
   111  			copy(env, appleEnv[t.String()])
   112  
   113  			// Add the generated packages to GOPATH for reverse bindings.
   114  			gopath := fmt.Sprintf("GOPATH=%s%c%s", outDir, filepath.ListSeparator, goEnv("GOPATH"))
   115  			env = append(env, gopath)
   116  
   117  			// Run `go mod tidy` to force to create go.sum.
   118  			// Without go.sum, `go build` fails as of Go 1.16.
   119  			if modulesUsed {
   120  				if err := writeGoMod(outSrcDir, t.platform, t.arch); err != nil {
   121  					return err
   122  				}
   123  				if err := goModTidyAt(outSrcDir, env); err != nil {
   124  					return err
   125  				}
   126  			}
   127  
   128  			if err := goAppleBindArchive(appleArchiveFilepath(name, t), env, outSrcDir); err != nil {
   129  				return fmt.Errorf("%s/%s: %v", t.platform, t.arch, err)
   130  			}
   131  
   132  			return nil
   133  		})
   134  	}
   135  	if err := buildWG.Wait(); err != nil {
   136  		return err
   137  	}
   138  
   139  	var frameworkDirs []string
   140  	frameworkArchCount := map[string]int{}
   141  	for _, t := range targets {
   142  		outDir := outDirsForPlatform[t.platform]
   143  		gobindDir := filepath.Join(outDir, "src", "gobind")
   144  
   145  		env := appleEnv[t.String()][:]
   146  		sdk := getenv(env, "DARWIN_SDK")
   147  
   148  		frameworkDir := filepath.Join(tmpdir, t.platform, sdk, title+".framework")
   149  		frameworkDirs = append(frameworkDirs, frameworkDir)
   150  		frameworkArchCount[frameworkDir] = frameworkArchCount[frameworkDir] + 1
   151  
   152  		versionsDir := filepath.Join(frameworkDir, "Versions")
   153  		versionsADir := filepath.Join(versionsDir, "A")
   154  		titlePath := filepath.Join(versionsADir, title)
   155  		if frameworkArchCount[frameworkDir] > 1 {
   156  			// Not the first static lib, attach to a fat library and skip create headers
   157  			fatCmd := exec.Command(
   158  				"xcrun",
   159  				"lipo", appleArchiveFilepath(name, t), titlePath, "-create", "-output", titlePath,
   160  			)
   161  			if err := runCmd(fatCmd); err != nil {
   162  				return err
   163  			}
   164  			continue
   165  		}
   166  
   167  		versionsAHeadersDir := filepath.Join(versionsADir, "Headers")
   168  		if err := mkdir(versionsAHeadersDir); err != nil {
   169  			return err
   170  		}
   171  		if err := symlink("A", filepath.Join(versionsDir, "Current")); err != nil {
   172  			return err
   173  		}
   174  		if err := symlink("Versions/Current/Headers", filepath.Join(frameworkDir, "Headers")); err != nil {
   175  			return err
   176  		}
   177  		if err := symlink(filepath.Join("Versions/Current", title), filepath.Join(frameworkDir, title)); err != nil {
   178  			return err
   179  		}
   180  
   181  		lipoCmd := exec.Command(
   182  			"xcrun",
   183  			"lipo", appleArchiveFilepath(name, t), "-create", "-o", titlePath,
   184  		)
   185  		if err := runCmd(lipoCmd); err != nil {
   186  			return err
   187  		}
   188  
   189  		fileBases := make([]string, len(pkgs)+1)
   190  		for i, pkg := range pkgs {
   191  			fileBases[i] = bindPrefix + strings.Title(pkg.Name)
   192  		}
   193  		fileBases[len(fileBases)-1] = "Universe"
   194  
   195  		// Copy header file next to output archive.
   196  		var headerFiles []string
   197  		if len(fileBases) == 1 {
   198  			headerFiles = append(headerFiles, title+".h")
   199  			err := copyFile(
   200  				filepath.Join(versionsAHeadersDir, title+".h"),
   201  				filepath.Join(gobindDir, bindPrefix+title+".objc.h"),
   202  			)
   203  			if err != nil {
   204  				return err
   205  			}
   206  		} else {
   207  			for _, fileBase := range fileBases {
   208  				headerFiles = append(headerFiles, fileBase+".objc.h")
   209  				err := copyFile(
   210  					filepath.Join(versionsAHeadersDir, fileBase+".objc.h"),
   211  					filepath.Join(gobindDir, fileBase+".objc.h"),
   212  				)
   213  				if err != nil {
   214  					return err
   215  				}
   216  			}
   217  			err := copyFile(
   218  				filepath.Join(versionsAHeadersDir, "ref.h"),
   219  				filepath.Join(gobindDir, "ref.h"),
   220  			)
   221  			if err != nil {
   222  				return err
   223  			}
   224  			headerFiles = append(headerFiles, title+".h")
   225  			err = writeFile(filepath.Join(versionsAHeadersDir, title+".h"), func(w io.Writer) error {
   226  				return appleBindHeaderTmpl.Execute(w, map[string]interface{}{
   227  					"pkgs": pkgs, "title": title, "bases": fileBases,
   228  				})
   229  			})
   230  			if err != nil {
   231  				return err
   232  			}
   233  		}
   234  
   235  		if err := mkdir(filepath.Join(versionsADir, "Resources")); err != nil {
   236  			return err
   237  		}
   238  		if err := symlink("Versions/Current/Resources", filepath.Join(frameworkDir, "Resources")); err != nil {
   239  			return err
   240  		}
   241  		err = writeFile(filepath.Join(frameworkDir, "Resources", "Info.plist"), func(w io.Writer) error {
   242  			_, err := w.Write([]byte(appleBindInfoPlist))
   243  			return err
   244  		})
   245  		if err != nil {
   246  			return err
   247  		}
   248  
   249  		var mmVals = struct {
   250  			Module  string
   251  			Headers []string
   252  		}{
   253  			Module:  title,
   254  			Headers: headerFiles,
   255  		}
   256  		err = writeFile(filepath.Join(versionsADir, "Modules", "module.modulemap"), func(w io.Writer) error {
   257  			return appleModuleMapTmpl.Execute(w, mmVals)
   258  		})
   259  		if err != nil {
   260  			return err
   261  		}
   262  		err = symlink(filepath.Join("Versions/Current/Modules"), filepath.Join(frameworkDir, "Modules"))
   263  		if err != nil {
   264  			return err
   265  		}
   266  	}
   267  
   268  	// Finally combine all frameworks to an XCFramework
   269  	xcframeworkArgs := []string{"-create-xcframework"}
   270  
   271  	for _, dir := range frameworkDirs {
   272  		// On macOS, a temporary directory starts with /var, which is a symbolic link to /private/var.
   273  		// And in gomobile, a temporary directory is usually used as a working directly.
   274  		// Unfortunately, xcodebuild in Xcode 15 seems to have a bug and might not be able to understand fullpaths with symbolic links.
   275  		// As a workaround, resolve the path with symbolic links by filepath.EvalSymlinks.
   276  		dir, err := filepath.EvalSymlinks(dir)
   277  		if err != nil {
   278  			return err
   279  		}
   280  		xcframeworkArgs = append(xcframeworkArgs, "-framework", dir)
   281  	}
   282  
   283  	xcframeworkArgs = append(xcframeworkArgs, "-output", buildO)
   284  	cmd := exec.Command("xcodebuild", xcframeworkArgs...)
   285  	err = runCmd(cmd)
   286  	return err
   287  }
   288  
   289  const appleBindInfoPlist = `<?xml version="1.0" encoding="UTF-8"?>
   290      <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   291      <plist version="1.0">
   292        <dict>
   293        </dict>
   294      </plist>
   295  `
   296  
   297  var appleModuleMapTmpl = template.Must(template.New("iosmmap").Parse(`framework module "{{.Module}}" {
   298  	header "ref.h"
   299  {{range .Headers}}    header "{{.}}"
   300  {{end}}
   301      export *
   302  }`))
   303  
   304  func appleArchiveFilepath(name string, t targetInfo) string {
   305  	return filepath.Join(tmpdir, name+"-"+t.platform+"-"+t.arch+".a")
   306  }
   307  
   308  func goAppleBindArchive(out string, env []string, gosrc string) error {
   309  	return goBuildAt(gosrc, "./gobind", env, "-buildmode=c-archive", "-o", out)
   310  }
   311  
   312  var appleBindHeaderTmpl = template.Must(template.New("apple.h").Parse(`
   313  // Objective-C API for talking to the following Go packages
   314  //
   315  {{range .pkgs}}//	{{.PkgPath}}
   316  {{end}}//
   317  // File is generated by gomobile bind. Do not edit.
   318  #ifndef __{{.title}}_FRAMEWORK_H__
   319  #define __{{.title}}_FRAMEWORK_H__
   320  
   321  {{range .bases}}#include "{{.}}.objc.h"
   322  {{end}}
   323  #endif
   324  `))