env.go 17 KB

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