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