env.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  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. "bufio"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "io/fs"
  11. "os"
  12. "os/exec"
  13. "path/filepath"
  14. "runtime"
  15. "strings"
  16. "golang.org/x/mobile/internal/sdkpath"
  17. )
  18. // General mobile build environment. Initialized by envInit.
  19. var (
  20. gomobilepath string // $GOPATH/pkg/gomobile
  21. androidEnv map[string][]string // android arch -> []string
  22. appleEnv map[string][]string
  23. appleNM string
  24. )
  25. func isAndroidPlatform(platform string) bool {
  26. return platform == "android"
  27. }
  28. func isApplePlatform(platform string) bool {
  29. return contains(applePlatforms, platform)
  30. }
  31. var applePlatforms = []string{"ios", "iossimulator", "macos", "maccatalyst"}
  32. func platformArchs(platform string) []string {
  33. switch platform {
  34. case "ios":
  35. return []string{"arm64"}
  36. case "iossimulator":
  37. return []string{"arm64", "amd64"}
  38. case "macos", "maccatalyst":
  39. return []string{"arm64", "amd64"}
  40. case "android":
  41. return []string{"arm", "arm64", "386", "amd64"}
  42. default:
  43. panic(fmt.Sprintf("unexpected platform: %s", platform))
  44. }
  45. }
  46. func isSupportedArch(platform, arch string) bool {
  47. return contains(platformArchs(platform), arch)
  48. }
  49. // platformOS returns the correct GOOS value for platform.
  50. func platformOS(platform string) string {
  51. switch platform {
  52. case "android":
  53. return "android"
  54. case "ios", "iossimulator":
  55. return "ios"
  56. case "macos", "maccatalyst":
  57. // For "maccatalyst", Go packages should be built with GOOS=darwin,
  58. // not GOOS=ios, since the underlying OS (and kernel, runtime) is macOS.
  59. // But, using GOOS=darwin with build-tag ios leads to corrupt builds: https://go.dev/issue/52299
  60. // => So we use GOOS=ios for now.
  61. // We also apply a "macos" or "maccatalyst" build tag, respectively.
  62. // See below for additional context.
  63. return "ios"
  64. default:
  65. panic(fmt.Sprintf("unexpected platform: %s", platform))
  66. }
  67. }
  68. func platformTags(platform string) []string {
  69. switch platform {
  70. case "android":
  71. return []string{"android"}
  72. case "ios", "iossimulator":
  73. return []string{"ios"}
  74. case "macos":
  75. return []string{"macos"}
  76. case "maccatalyst":
  77. // Mac Catalyst is a subset of iOS APIs made available on macOS
  78. // designed to ease porting apps developed for iPad to macOS.
  79. // See
  80. // https://developer.apple.com/mac-catalyst/.
  81. // https://stackoverflow.com/questions/12132933/preprocessor-macro-for-os-x-targets/49560690#49560690
  82. //
  83. // Historically gomobile used GOOS=darwin with build tag ios when
  84. // targeting Mac Catalyst. However, this configuration is not officially
  85. // supported and leads to corrupt builds after go1.18: https://go.dev/issues/52299
  86. // Use GOOS=ios.
  87. // To help discriminate between darwin, ios, macos, and maccatalyst
  88. // targets, there is also a "maccatalyst" tag.
  89. return []string{"macos", "maccatalyst"}
  90. default:
  91. panic(fmt.Sprintf("unexpected platform: %s", platform))
  92. }
  93. }
  94. func contains(haystack []string, needle string) bool {
  95. for _, v := range haystack {
  96. if v == needle {
  97. return true
  98. }
  99. }
  100. return false
  101. }
  102. func buildEnvInit() (cleanup func(), err error) {
  103. // Find gomobilepath.
  104. gopath := goEnv("GOPATH")
  105. for _, p := range filepath.SplitList(gopath) {
  106. gomobilepath = filepath.Join(p, "pkg", "gomobile")
  107. if _, err := os.Stat(gomobilepath); buildN || err == nil {
  108. break
  109. }
  110. }
  111. if buildX {
  112. fmt.Fprintln(xout, "GOMOBILE="+gomobilepath)
  113. }
  114. // Check the toolchain is in a good state.
  115. // Pick a temporary directory for assembling an apk/app.
  116. if gomobilepath == "" {
  117. return nil, errors.New("toolchain not installed, run `gomobile init`")
  118. }
  119. cleanupFn := func() {
  120. if buildWork {
  121. fmt.Printf("WORK=%s\n", tmpdir)
  122. return
  123. }
  124. removeAll(tmpdir)
  125. }
  126. if buildN {
  127. tmpdir = "$WORK"
  128. cleanupFn = func() {}
  129. } else {
  130. tmpdir, err = os.MkdirTemp("", "gomobile-work-")
  131. if err != nil {
  132. return nil, err
  133. }
  134. }
  135. if buildX {
  136. fmt.Fprintln(xout, "WORK="+tmpdir)
  137. }
  138. if err := envInit(); err != nil {
  139. return nil, err
  140. }
  141. return cleanupFn, nil
  142. }
  143. func envInit() (err error) {
  144. // Setup the cross-compiler environments.
  145. if ndkRoot, err := ndkRoot(); err == nil {
  146. androidEnv = make(map[string][]string)
  147. if buildAndroidAPI < minAndroidAPI {
  148. return fmt.Errorf("gomobile requires Android API level >= %d", minAndroidAPI)
  149. }
  150. for arch, toolchain := range ndk {
  151. clang := toolchain.Path(ndkRoot, "clang")
  152. clangpp := toolchain.Path(ndkRoot, "clang++")
  153. if !buildN {
  154. tools := []string{clang, clangpp}
  155. if runtime.GOOS == "windows" {
  156. // Because of https://github.com/android-ndk/ndk/issues/920,
  157. // we require r19c, not just r19b. Fortunately, the clang++.cmd
  158. // script only exists in r19c.
  159. tools = append(tools, clangpp+".cmd")
  160. }
  161. for _, tool := range tools {
  162. _, err = os.Stat(tool)
  163. if err != nil {
  164. return fmt.Errorf("No compiler for %s was found in the NDK (tried %s). Make sure your NDK version is >= r19c. Use `sdkmanager --update` to update it.", arch, tool)
  165. }
  166. }
  167. }
  168. androidEnv[arch] = []string{
  169. "GOOS=android",
  170. "GOARCH=" + arch,
  171. "CC=" + clang,
  172. "CXX=" + clangpp,
  173. "CGO_ENABLED=1",
  174. }
  175. if arch == "arm" {
  176. androidEnv[arch] = append(androidEnv[arch], "GOARM=7")
  177. }
  178. }
  179. }
  180. if !xcodeAvailable() {
  181. return nil
  182. }
  183. appleNM = "nm"
  184. appleEnv = make(map[string][]string)
  185. for _, platform := range applePlatforms {
  186. for _, arch := range platformArchs(platform) {
  187. var env []string
  188. var goos, sdk, clang, cflags string
  189. var err error
  190. switch platform {
  191. case "ios":
  192. goos = "ios"
  193. sdk = "iphoneos"
  194. clang, cflags, err = envClang(sdk)
  195. cflags += " -miphoneos-version-min=" + buildIOSVersion
  196. cflags += " -fembed-bitcode"
  197. case "iossimulator":
  198. goos = "ios"
  199. sdk = "iphonesimulator"
  200. clang, cflags, err = envClang(sdk)
  201. cflags += " -mios-simulator-version-min=" + buildIOSVersion
  202. cflags += " -fembed-bitcode"
  203. case "maccatalyst":
  204. // See the comment about maccatalyst's GOOS, build tags configuration
  205. // in platformOS and platformTags.
  206. // Using GOOS=darwin with build-tag ios leads to corrupt builds: https://go.dev/issue/52299
  207. // => So we use GOOS=ios for now.
  208. goos = "ios"
  209. sdk = "macosx"
  210. clang, cflags, err = envClang(sdk)
  211. // TODO(ydnar): the following 3 lines MAY be needed to compile
  212. // packages or apps for maccatalyst. Commenting them out now in case
  213. // it turns out they are necessary. Currently none of the example
  214. // apps will build for macos or maccatalyst because they have a
  215. // GLKit dependency, which is deprecated on all Apple platforms, and
  216. // broken on maccatalyst (GLKView isn’t available).
  217. // sysroot := strings.SplitN(cflags, " ", 2)[1]
  218. // cflags += " -isystem " + sysroot + "/System/iOSSupport/usr/include"
  219. // cflags += " -iframework " + sysroot + "/System/iOSSupport/System/Library/Frameworks"
  220. switch arch {
  221. case "amd64":
  222. cflags += " -target x86_64-apple-ios" + buildIOSVersion + "-macabi"
  223. case "arm64":
  224. cflags += " -target arm64-apple-ios" + buildIOSVersion + "-macabi"
  225. cflags += " -fembed-bitcode"
  226. }
  227. case "macos":
  228. goos = "darwin"
  229. sdk = "macosx" // Note: the SDK is called "macosx", not "macos"
  230. clang, cflags, err = envClang(sdk)
  231. if arch == "arm64" {
  232. cflags += " -fembed-bitcode"
  233. }
  234. default:
  235. panic(fmt.Errorf("unknown Apple target: %s/%s", platform, arch))
  236. }
  237. if err != nil {
  238. return err
  239. }
  240. env = append(env,
  241. "GOOS="+goos,
  242. "GOARCH="+arch,
  243. "GOFLAGS="+"-tags="+strings.Join(platformTags(platform), ","),
  244. "CC="+clang,
  245. "CXX="+clang+"++",
  246. "CGO_CFLAGS="+cflags+" -arch "+archClang(arch),
  247. "CGO_CXXFLAGS="+cflags+" -arch "+archClang(arch),
  248. "CGO_LDFLAGS="+cflags+" -arch "+archClang(arch),
  249. "CGO_ENABLED=1",
  250. "DARWIN_SDK="+sdk,
  251. )
  252. appleEnv[platform+"/"+arch] = env
  253. }
  254. }
  255. return nil
  256. }
  257. // abi maps GOARCH values to Android ABI strings.
  258. // See https://developer.android.com/ndk/guides/abis
  259. func abi(goarch string) string {
  260. switch goarch {
  261. case "arm":
  262. return "armeabi-v7a"
  263. case "arm64":
  264. return "arm64-v8a"
  265. case "386":
  266. return "x86"
  267. case "amd64":
  268. return "x86_64"
  269. default:
  270. return ""
  271. }
  272. }
  273. // checkNDKRoot returns nil if the NDK in `ndkRoot` supports the current configured
  274. // API version and all the specified Android targets.
  275. func checkNDKRoot(ndkRoot string, targets []targetInfo) error {
  276. platformsJson, err := os.Open(filepath.Join(ndkRoot, "meta", "platforms.json"))
  277. if err != nil {
  278. return err
  279. }
  280. defer platformsJson.Close()
  281. decoder := json.NewDecoder(platformsJson)
  282. supportedVersions := struct {
  283. Min int
  284. Max int
  285. }{}
  286. if err := decoder.Decode(&supportedVersions); err != nil {
  287. return err
  288. }
  289. if supportedVersions.Min > buildAndroidAPI ||
  290. supportedVersions.Max < buildAndroidAPI {
  291. return fmt.Errorf("unsupported API version %d (not in %d..%d)", buildAndroidAPI, supportedVersions.Min, supportedVersions.Max)
  292. }
  293. abisJson, err := os.Open(filepath.Join(ndkRoot, "meta", "abis.json"))
  294. if err != nil {
  295. return err
  296. }
  297. defer abisJson.Close()
  298. decoder = json.NewDecoder(abisJson)
  299. abis := make(map[string]struct{})
  300. if err := decoder.Decode(&abis); err != nil {
  301. return err
  302. }
  303. for _, target := range targets {
  304. if !isAndroidPlatform(target.platform) {
  305. continue
  306. }
  307. if _, found := abis[abi(target.arch)]; !found {
  308. return fmt.Errorf("ndk does not support %s", target.platform)
  309. }
  310. }
  311. return nil
  312. }
  313. // compatibleNDKRoots searches the side-by-side NDK dirs for compatible SDKs.
  314. func compatibleNDKRoots(ndkForest string, targets []targetInfo) ([]string, error) {
  315. ndkDirs, err := os.ReadDir(ndkForest)
  316. if err != nil {
  317. return nil, err
  318. }
  319. compatibleNDKRoots := []string{}
  320. var lastErr error
  321. for _, dirent := range ndkDirs {
  322. ndkRoot := filepath.Join(ndkForest, dirent.Name())
  323. lastErr = checkNDKRoot(ndkRoot, targets)
  324. if lastErr == nil {
  325. compatibleNDKRoots = append(compatibleNDKRoots, ndkRoot)
  326. }
  327. }
  328. if len(compatibleNDKRoots) > 0 {
  329. return compatibleNDKRoots, nil
  330. }
  331. return nil, lastErr
  332. }
  333. // ndkVersion returns the full version number of an installed copy of the NDK,
  334. // or "" if it cannot be determined.
  335. func ndkVersion(ndkRoot string) string {
  336. properties, err := os.Open(filepath.Join(ndkRoot, "source.properties"))
  337. if err != nil {
  338. return ""
  339. }
  340. defer properties.Close()
  341. // Parse the version number out of the .properties file.
  342. // See https://en.wikipedia.org/wiki/.properties
  343. scanner := bufio.NewScanner(properties)
  344. for scanner.Scan() {
  345. line := scanner.Text()
  346. tokens := strings.SplitN(line, "=", 2)
  347. if len(tokens) != 2 {
  348. continue
  349. }
  350. if strings.TrimSpace(tokens[0]) == "Pkg.Revision" {
  351. return strings.TrimSpace(tokens[1])
  352. }
  353. }
  354. return ""
  355. }
  356. // ndkRoot returns the root path of an installed NDK that supports all the
  357. // specified Android targets. For details of NDK locations, see
  358. // https://github.com/android/ndk-samples/wiki/Configure-NDK-Path
  359. func ndkRoot(targets ...targetInfo) (string, error) {
  360. if buildN {
  361. return "$NDK_PATH", nil
  362. }
  363. // Try the ANDROID_NDK_HOME variable. This approach is deprecated, but it
  364. // has the highest priority because it represents an explicit user choice.
  365. if ndkRoot := os.Getenv("ANDROID_NDK_HOME"); ndkRoot != "" {
  366. if err := checkNDKRoot(ndkRoot, targets); err != nil {
  367. return "", fmt.Errorf("ANDROID_NDK_HOME specifies %s, which is unusable: %w", ndkRoot, err)
  368. }
  369. return ndkRoot, nil
  370. }
  371. androidHome, err := sdkpath.AndroidHome()
  372. if err != nil {
  373. return "", fmt.Errorf("could not locate Android SDK: %w", err)
  374. }
  375. // Use the newest compatible NDK under the side-by-side path arrangement.
  376. ndkForest := filepath.Join(androidHome, "ndk")
  377. ndkRoots, sideBySideErr := compatibleNDKRoots(ndkForest, targets)
  378. if len(ndkRoots) != 0 {
  379. // Choose the latest version that supports the build configuration.
  380. // NDKs whose version cannot be determined will be least preferred.
  381. // In the event of a tie, the later ndkRoot will win.
  382. maxVersion := ""
  383. var selected string
  384. for _, ndkRoot := range ndkRoots {
  385. version := ndkVersion(ndkRoot)
  386. if version >= maxVersion {
  387. maxVersion = version
  388. selected = ndkRoot
  389. }
  390. }
  391. return selected, nil
  392. }
  393. // Try the deprecated NDK location.
  394. ndkRoot := filepath.Join(androidHome, "ndk-bundle")
  395. if legacyErr := checkNDKRoot(ndkRoot, targets); legacyErr != nil {
  396. return "", fmt.Errorf("no usable NDK in %s: %w, %v", androidHome, sideBySideErr, legacyErr)
  397. }
  398. return ndkRoot, nil
  399. }
  400. func envClang(sdkName string) (clang, cflags string, err error) {
  401. if buildN {
  402. return sdkName + "-clang", "-isysroot " + sdkName, nil
  403. }
  404. cmd := exec.Command("xcrun", "--sdk", sdkName, "--find", "clang")
  405. out, err := cmd.Output()
  406. if err != nil {
  407. if ee := (*exec.ExitError)(nil); errors.As(err, &ee) {
  408. out = append(out, ee.Stderr...)
  409. }
  410. return "", "", fmt.Errorf("xcrun --find: %v\n%s", err, out)
  411. }
  412. clang = strings.TrimSpace(string(out))
  413. cmd = exec.Command("xcrun", "--sdk", sdkName, "--show-sdk-path")
  414. out, err = cmd.Output()
  415. if err != nil {
  416. if ee := (*exec.ExitError)(nil); errors.As(err, &ee) {
  417. out = append(out, ee.Stderr...)
  418. }
  419. return "", "", fmt.Errorf("xcrun --show-sdk-path: %v\n%s", err, out)
  420. }
  421. sdk := strings.TrimSpace(string(out))
  422. return clang, "-isysroot " + sdk, nil
  423. }
  424. func archClang(goarch string) string {
  425. switch goarch {
  426. case "arm":
  427. return "armv7"
  428. case "arm64":
  429. return "arm64"
  430. case "386":
  431. return "i386"
  432. case "amd64":
  433. return "x86_64"
  434. default:
  435. panic(fmt.Sprintf("unknown GOARCH: %q", goarch))
  436. }
  437. }
  438. // environ merges os.Environ and the given "key=value" pairs.
  439. // If a key is in both os.Environ and kv, kv takes precedence.
  440. func environ(kv []string) []string {
  441. cur := os.Environ()
  442. new := make([]string, 0, len(cur)+len(kv))
  443. envs := make(map[string]string, len(cur))
  444. for _, ev := range cur {
  445. elem := strings.SplitN(ev, "=", 2)
  446. if len(elem) != 2 || elem[0] == "" {
  447. // pass the env var of unusual form untouched.
  448. // e.g. Windows may have env var names starting with "=".
  449. new = append(new, ev)
  450. continue
  451. }
  452. if goos == "windows" {
  453. elem[0] = strings.ToUpper(elem[0])
  454. }
  455. envs[elem[0]] = elem[1]
  456. }
  457. for _, ev := range kv {
  458. elem := strings.SplitN(ev, "=", 2)
  459. if len(elem) != 2 || elem[0] == "" {
  460. panic(fmt.Sprintf("malformed env var %q from input", ev))
  461. }
  462. if goos == "windows" {
  463. elem[0] = strings.ToUpper(elem[0])
  464. }
  465. envs[elem[0]] = elem[1]
  466. }
  467. for k, v := range envs {
  468. new = append(new, k+"="+v)
  469. }
  470. return new
  471. }
  472. func getenv(env []string, key string) string {
  473. prefix := key + "="
  474. for _, kv := range env {
  475. if strings.HasPrefix(kv, prefix) {
  476. return kv[len(prefix):]
  477. }
  478. }
  479. return ""
  480. }
  481. func archNDK() string {
  482. if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
  483. return "windows"
  484. } else {
  485. var arch string
  486. switch runtime.GOARCH {
  487. case "386":
  488. arch = "x86"
  489. case "amd64":
  490. arch = "x86_64"
  491. case "arm64":
  492. // Android NDK does not contain arm64 toolchains (until and
  493. // including NDK 23), use use x86_64 instead. See:
  494. // https://github.com/android/ndk/issues/1299
  495. if runtime.GOOS == "darwin" {
  496. arch = "x86_64"
  497. break
  498. }
  499. fallthrough
  500. default:
  501. panic("unsupported GOARCH: " + runtime.GOARCH)
  502. }
  503. return runtime.GOOS + "-" + arch
  504. }
  505. }
  506. type ndkToolchain struct {
  507. arch string
  508. abi string
  509. minAPI int
  510. toolPrefix string
  511. clangPrefix string
  512. }
  513. func (tc *ndkToolchain) ClangPrefix() string {
  514. if buildAndroidAPI < tc.minAPI {
  515. return fmt.Sprintf("%s%d", tc.clangPrefix, tc.minAPI)
  516. }
  517. return fmt.Sprintf("%s%d", tc.clangPrefix, buildAndroidAPI)
  518. }
  519. func (tc *ndkToolchain) Path(ndkRoot, toolName string) string {
  520. cmdFromPref := func(pref string) string {
  521. return filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK(), "bin", pref+"-"+toolName)
  522. }
  523. var cmd string
  524. switch toolName {
  525. case "clang", "clang++":
  526. cmd = cmdFromPref(tc.ClangPrefix())
  527. default:
  528. cmd = cmdFromPref(tc.toolPrefix)
  529. // Starting from NDK 23, GNU binutils are fully migrated to LLVM binutils.
  530. // See https://android.googlesource.com/platform/ndk/+/master/docs/Roadmap.md#ndk-r23
  531. if _, err := os.Stat(cmd); errors.Is(err, fs.ErrNotExist) {
  532. cmd = cmdFromPref("llvm")
  533. }
  534. }
  535. return cmd
  536. }
  537. type ndkConfig map[string]ndkToolchain // map: GOOS->androidConfig.
  538. func (nc ndkConfig) Toolchain(arch string) ndkToolchain {
  539. tc, ok := nc[arch]
  540. if !ok {
  541. panic(`unsupported architecture: ` + arch)
  542. }
  543. return tc
  544. }
  545. var ndk = ndkConfig{
  546. "arm": {
  547. arch: "arm",
  548. abi: "armeabi-v7a",
  549. minAPI: 16,
  550. toolPrefix: "arm-linux-androideabi",
  551. clangPrefix: "armv7a-linux-androideabi",
  552. },
  553. "arm64": {
  554. arch: "arm64",
  555. abi: "arm64-v8a",
  556. minAPI: 21,
  557. toolPrefix: "aarch64-linux-android",
  558. clangPrefix: "aarch64-linux-android",
  559. },
  560. "386": {
  561. arch: "x86",
  562. abi: "x86",
  563. minAPI: 16,
  564. toolPrefix: "i686-linux-android",
  565. clangPrefix: "i686-linux-android",
  566. },
  567. "amd64": {
  568. arch: "x86_64",
  569. abi: "x86_64",
  570. minAPI: 21,
  571. toolPrefix: "x86_64-linux-android",
  572. clangPrefix: "x86_64-linux-android",
  573. },
  574. }
  575. func xcodeAvailable() bool {
  576. err := exec.Command("xcrun", "xcodebuild", "-version").Run()
  577. return err == nil
  578. }