bind_iosapp.go 11 KB

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