build.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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. //go:generate go run gendex.go -o dex.go
  5. package main
  6. import (
  7. "bufio"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "os"
  12. "os/exec"
  13. "regexp"
  14. "strconv"
  15. "strings"
  16. "golang.org/x/mobile/internal/sdkpath"
  17. "golang.org/x/tools/go/packages"
  18. )
  19. var tmpdir string
  20. var cmdBuild = &command{
  21. run: runBuild,
  22. Name: "build",
  23. Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-o output] [-bundleid bundleID] [build flags] [package]",
  24. Short: "compile android APK and iOS app",
  25. Long: `
  26. Build compiles and encodes the app named by the import path.
  27. The named package must define a main function.
  28. The -target flag takes either android (the default), or one or more
  29. comma-delimited Apple platforms (` + strings.Join(applePlatforms, ", ") + `).
  30. For -target android, if an AndroidManifest.xml is defined in the
  31. package directory, it is added to the APK output. Otherwise, a default
  32. manifest is generated. By default, this builds a fat APK for all supported
  33. instruction sets (arm, 386, amd64, arm64). A subset of instruction sets can
  34. be selected by specifying target type with the architecture name. E.g.
  35. -target=android/arm,android/386.
  36. For Apple -target platforms, gomobile must be run on an OS X machine with
  37. Xcode installed.
  38. By default, -target ios will generate an XCFramework for both ios
  39. and iossimulator. Multiple Apple targets can be specified, creating a "fat"
  40. XCFramework with each slice. To generate a fat XCFramework that supports
  41. iOS, macOS, and macCatalyst for all supportec architectures (amd64 and arm64),
  42. specify -target ios,macos,maccatalyst. A subset of instruction sets can be
  43. selectged by specifying the platform with an architecture name. E.g.
  44. -target=ios/arm64,maccatalyst/arm64.
  45. If the package directory contains an assets subdirectory, its contents
  46. are copied into the output.
  47. Flag -iosversion sets the minimal version of the iOS SDK to compile against.
  48. The default version is 13.0.
  49. Flag -androidapi sets the Android API version to compile against.
  50. The default and minimum is 16.
  51. The -bundleid flag is required for -target ios and sets the bundle ID to use
  52. with the app.
  53. The -o flag specifies the output file name. If not specified, the
  54. output file name depends on the package built.
  55. The -v flag provides verbose output, including the list of packages built.
  56. The build flags -a, -i, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work are
  57. shared with the build command. For documentation, see 'go help build'.
  58. `,
  59. }
  60. func runBuild(cmd *command) (err error) {
  61. _, err = runBuildImpl(cmd)
  62. return
  63. }
  64. // runBuildImpl builds a package for mobiles based on the given commands.
  65. // runBuildImpl returns a built package information and an error if exists.
  66. func runBuildImpl(cmd *command) (*packages.Package, error) {
  67. cleanup, err := buildEnvInit()
  68. if err != nil {
  69. return nil, err
  70. }
  71. defer cleanup()
  72. args := cmd.flag.Args()
  73. targets, err := parseBuildTarget(buildTarget)
  74. if err != nil {
  75. return nil, fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err)
  76. }
  77. var buildPath string
  78. switch len(args) {
  79. case 0:
  80. buildPath = "."
  81. case 1:
  82. buildPath = args[0]
  83. default:
  84. cmd.usage()
  85. os.Exit(1)
  86. }
  87. // TODO(ydnar): this should work, unless build tags affect loading a single package.
  88. // Should we try to import packages with different build tags per platform?
  89. pkgs, err := packages.Load(packagesConfig(targets[0]), buildPath)
  90. if err != nil {
  91. return nil, err
  92. }
  93. // len(pkgs) can be more than 1 e.g., when the specified path includes `...`.
  94. if len(pkgs) != 1 {
  95. cmd.usage()
  96. os.Exit(1)
  97. }
  98. pkg := pkgs[0]
  99. if pkg.Name != "main" && buildO != "" {
  100. return nil, fmt.Errorf("cannot set -o when building non-main package")
  101. }
  102. var nmpkgs map[string]bool
  103. switch {
  104. case isAndroidPlatform(targets[0].platform):
  105. if pkg.Name != "main" {
  106. for _, t := range targets {
  107. if err := goBuild(pkg.PkgPath, androidEnv[t.arch]); err != nil {
  108. return nil, err
  109. }
  110. }
  111. return pkg, nil
  112. }
  113. nmpkgs, err = goAndroidBuild(pkg, targets)
  114. if err != nil {
  115. return nil, err
  116. }
  117. case isApplePlatform(targets[0].platform):
  118. if !xcodeAvailable() {
  119. return nil, fmt.Errorf("-target=%s requires XCode", buildTarget)
  120. }
  121. if pkg.Name != "main" {
  122. for _, t := range targets {
  123. // Catalyst support requires iOS 13+
  124. v, _ := strconv.ParseFloat(buildIOSVersion, 64)
  125. if t.platform == "maccatalyst" && v < 13.0 {
  126. return nil, errors.New("catalyst requires -iosversion=13 or higher")
  127. }
  128. if err := goBuild(pkg.PkgPath, appleEnv[t.String()]); err != nil {
  129. return nil, err
  130. }
  131. }
  132. return pkg, nil
  133. }
  134. if buildBundleID == "" {
  135. return nil, fmt.Errorf("-target=%s requires -bundleid set", buildTarget)
  136. }
  137. nmpkgs, err = goAppleBuild(pkg, buildBundleID, targets)
  138. if err != nil {
  139. return nil, err
  140. }
  141. }
  142. if !nmpkgs["golang.org/x/mobile/app"] {
  143. return nil, fmt.Errorf(`%s does not import "golang.org/x/mobile/app"`, pkg.PkgPath)
  144. }
  145. return pkg, nil
  146. }
  147. var nmRE = regexp.MustCompile(`[0-9a-f]{8} t _?(?:.*/vendor/)?(golang.org/x.*/[^.]*)`)
  148. func extractPkgs(nm string, path string) (map[string]bool, error) {
  149. if buildN {
  150. return map[string]bool{"golang.org/x/mobile/app": true}, nil
  151. }
  152. r, w := io.Pipe()
  153. cmd := exec.Command(nm, path)
  154. cmd.Stdout = w
  155. cmd.Stderr = os.Stderr
  156. nmpkgs := make(map[string]bool)
  157. errc := make(chan error, 1)
  158. go func() {
  159. s := bufio.NewScanner(r)
  160. for s.Scan() {
  161. if res := nmRE.FindStringSubmatch(s.Text()); res != nil {
  162. nmpkgs[res[1]] = true
  163. }
  164. }
  165. errc <- s.Err()
  166. }()
  167. err := cmd.Run()
  168. w.Close()
  169. if err != nil {
  170. return nil, fmt.Errorf("%s %s: %v", nm, path, err)
  171. }
  172. if err := <-errc; err != nil {
  173. return nil, fmt.Errorf("%s %s: %v", nm, path, err)
  174. }
  175. return nmpkgs, nil
  176. }
  177. var xout io.Writer = os.Stderr
  178. func printcmd(format string, args ...interface{}) {
  179. cmd := fmt.Sprintf(format+"\n", args...)
  180. if tmpdir != "" {
  181. cmd = strings.Replace(cmd, tmpdir, "$WORK", -1)
  182. }
  183. if androidHome, err := sdkpath.AndroidHome(); err == nil {
  184. cmd = strings.Replace(cmd, androidHome, "$ANDROID_HOME", -1)
  185. }
  186. if gomobilepath != "" {
  187. cmd = strings.Replace(cmd, gomobilepath, "$GOMOBILE", -1)
  188. }
  189. if gopath := goEnv("GOPATH"); gopath != "" {
  190. cmd = strings.Replace(cmd, gopath, "$GOPATH", -1)
  191. }
  192. if env := os.Getenv("HOMEPATH"); env != "" {
  193. cmd = strings.Replace(cmd, env, "$HOMEPATH", -1)
  194. }
  195. fmt.Fprint(xout, cmd)
  196. }
  197. // "Build flags", used by multiple commands.
  198. var (
  199. buildA bool // -a
  200. buildI bool // -i
  201. buildN bool // -n
  202. buildV bool // -v
  203. buildX bool // -x
  204. buildO string // -o
  205. buildGcflags string // -gcflags
  206. buildLdflags string // -ldflags
  207. buildTarget string // -target
  208. buildTrimpath bool // -trimpath
  209. buildWork bool // -work
  210. buildBundleID string // -bundleid
  211. buildIOSVersion string // -iosversion
  212. buildAndroidAPI int // -androidapi
  213. buildTags stringsFlag // -tags
  214. )
  215. func addBuildFlags(cmd *command) {
  216. cmd.flag.StringVar(&buildO, "o", "", "")
  217. cmd.flag.StringVar(&buildGcflags, "gcflags", "", "")
  218. cmd.flag.StringVar(&buildLdflags, "ldflags", "", "")
  219. cmd.flag.StringVar(&buildTarget, "target", "android", "")
  220. cmd.flag.StringVar(&buildBundleID, "bundleid", "", "")
  221. cmd.flag.StringVar(&buildIOSVersion, "iosversion", "13.0", "")
  222. cmd.flag.IntVar(&buildAndroidAPI, "androidapi", minAndroidAPI, "")
  223. cmd.flag.BoolVar(&buildA, "a", false, "")
  224. cmd.flag.BoolVar(&buildI, "i", false, "")
  225. cmd.flag.BoolVar(&buildTrimpath, "trimpath", false, "")
  226. cmd.flag.Var(&buildTags, "tags", "")
  227. }
  228. func addBuildFlagsNVXWork(cmd *command) {
  229. cmd.flag.BoolVar(&buildN, "n", false, "")
  230. cmd.flag.BoolVar(&buildV, "v", false, "")
  231. cmd.flag.BoolVar(&buildX, "x", false, "")
  232. cmd.flag.BoolVar(&buildWork, "work", false, "")
  233. }
  234. func init() {
  235. addBuildFlags(cmdBuild)
  236. addBuildFlagsNVXWork(cmdBuild)
  237. addBuildFlags(cmdInstall)
  238. addBuildFlagsNVXWork(cmdInstall)
  239. addBuildFlagsNVXWork(cmdInit)
  240. addBuildFlags(cmdBind)
  241. addBuildFlagsNVXWork(cmdBind)
  242. addBuildFlagsNVXWork(cmdClean)
  243. }
  244. func goBuild(src string, env []string, args ...string) error {
  245. return goCmd("build", []string{src}, env, args...)
  246. }
  247. func goBuildAt(at string, src string, env []string, args ...string) error {
  248. return goCmdAt(at, "build", []string{src}, env, args...)
  249. }
  250. func goInstall(srcs []string, env []string, args ...string) error {
  251. return goCmd("install", srcs, env, args...)
  252. }
  253. func goCmd(subcmd string, srcs []string, env []string, args ...string) error {
  254. return goCmdAt("", subcmd, srcs, env, args...)
  255. }
  256. func goCmdAt(at string, subcmd string, srcs []string, env []string, args ...string) error {
  257. cmd := exec.Command("go", subcmd)
  258. tags := buildTags
  259. if len(tags) > 0 {
  260. cmd.Args = append(cmd.Args, "-tags", strings.Join(tags, ","))
  261. }
  262. if buildV {
  263. cmd.Args = append(cmd.Args, "-v")
  264. }
  265. if subcmd != "install" && buildI {
  266. cmd.Args = append(cmd.Args, "-i")
  267. }
  268. if buildX {
  269. cmd.Args = append(cmd.Args, "-x")
  270. }
  271. if buildGcflags != "" {
  272. cmd.Args = append(cmd.Args, "-gcflags", buildGcflags)
  273. }
  274. if buildLdflags != "" {
  275. cmd.Args = append(cmd.Args, "-ldflags", buildLdflags)
  276. }
  277. if buildTrimpath {
  278. cmd.Args = append(cmd.Args, "-trimpath")
  279. }
  280. if buildWork {
  281. cmd.Args = append(cmd.Args, "-work")
  282. }
  283. cmd.Args = append(cmd.Args, args...)
  284. cmd.Args = append(cmd.Args, srcs...)
  285. // Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod,
  286. // but the path varies when GOPATH is specified at env, which results in cold cache.
  287. if gmc, err := goModCachePath(); err == nil {
  288. env = append([]string{"GOMODCACHE=" + gmc}, env...)
  289. } else {
  290. env = append([]string{}, env...)
  291. }
  292. cmd.Env = env
  293. cmd.Dir = at
  294. return runCmd(cmd)
  295. }
  296. func goModTidyAt(at string, env []string) error {
  297. cmd := exec.Command("go", "mod", "tidy")
  298. if buildV {
  299. cmd.Args = append(cmd.Args, "-v")
  300. }
  301. // Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod,
  302. // but the path varies when GOPATH is specified at env, which results in cold cache.
  303. if gmc, err := goModCachePath(); err == nil {
  304. env = append([]string{"GOMODCACHE=" + gmc}, env...)
  305. } else {
  306. env = append([]string{}, env...)
  307. }
  308. cmd.Env = env
  309. cmd.Dir = at
  310. return runCmd(cmd)
  311. }
  312. // parseBuildTarget parses buildTarget into 1 or more platforms and architectures.
  313. // Returns an error if buildTarget contains invalid input.
  314. // Example valid target strings:
  315. //
  316. // android
  317. // android/arm64,android/386,android/amd64
  318. // ios,iossimulator,maccatalyst
  319. // macos/amd64
  320. func parseBuildTarget(buildTarget string) ([]targetInfo, error) {
  321. if buildTarget == "" {
  322. return nil, fmt.Errorf(`invalid target ""`)
  323. }
  324. targets := []targetInfo{}
  325. targetsAdded := make(map[targetInfo]bool)
  326. addTarget := func(platform, arch string) {
  327. t := targetInfo{platform, arch}
  328. if targetsAdded[t] {
  329. return
  330. }
  331. targets = append(targets, t)
  332. targetsAdded[t] = true
  333. }
  334. addPlatform := func(platform string) {
  335. for _, arch := range platformArchs(platform) {
  336. addTarget(platform, arch)
  337. }
  338. }
  339. var isAndroid, isApple bool
  340. for _, target := range strings.Split(buildTarget, ",") {
  341. tuple := strings.SplitN(target, "/", 2)
  342. platform := tuple[0]
  343. hasArch := len(tuple) == 2
  344. if isAndroidPlatform(platform) {
  345. isAndroid = true
  346. } else if isApplePlatform(platform) {
  347. isApple = true
  348. } else {
  349. return nil, fmt.Errorf("unsupported platform: %q", platform)
  350. }
  351. if isAndroid && isApple {
  352. return nil, fmt.Errorf(`cannot mix android and Apple platforms`)
  353. }
  354. if hasArch {
  355. arch := tuple[1]
  356. if !isSupportedArch(platform, arch) {
  357. return nil, fmt.Errorf(`unsupported platform/arch: %q`, target)
  358. }
  359. addTarget(platform, arch)
  360. } else {
  361. addPlatform(platform)
  362. }
  363. }
  364. // Special case to build iossimulator if -target=ios
  365. if buildTarget == "ios" {
  366. addPlatform("iossimulator")
  367. }
  368. return targets, nil
  369. }
  370. type targetInfo struct {
  371. platform string
  372. arch string
  373. }
  374. func (t targetInfo) String() string {
  375. return t.platform + "/" + t.arch
  376. }
  377. func goModCachePath() (string, error) {
  378. out, err := exec.Command("go", "env", "GOMODCACHE").Output()
  379. if err != nil {
  380. return "", err
  381. }
  382. return strings.TrimSpace(string(out)), nil
  383. }