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 `))