github.com/provpn/mobile@v0.0.0-20210315122651-28c475f89f6c/cmd/gomobile/env.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "runtime" 11 "strings" 12 ) 13 14 // General mobile build environment. Initialized by envInit. 15 var ( 16 gomobilepath string // $GOPATH/pkg/gomobile 17 18 androidEnv map[string][]string // android arch -> []string 19 20 darwinEnv map[string][]string 21 22 androidArmNM string 23 darwinArmNM string 24 25 bitcodeEnabled bool 26 ) 27 28 func allArchs(targetOS string) []string { 29 switch targetOS { 30 case "darwin": 31 return []string{"arm64", "amd64"} 32 case "ios": 33 return []string{"arm64", "amd64"} 34 case "android": 35 return []string{"arm", "arm64", "386", "amd64"} 36 default: 37 panic(fmt.Sprintf("unexpected target OS: %s", targetOS)) 38 } 39 } 40 41 func buildEnvInit() (cleanup func(), err error) { 42 // Find gomobilepath. 43 gopath := goEnv("GOPATH") 44 for _, p := range filepath.SplitList(gopath) { 45 gomobilepath = filepath.Join(p, "pkg", "gomobile") 46 if _, err := os.Stat(gomobilepath); buildN || err == nil { 47 break 48 } 49 } 50 51 if buildX { 52 fmt.Fprintln(xout, "GOMOBILE="+gomobilepath) 53 } 54 55 // Check the toolchain is in a good state. 56 // Pick a temporary directory for assembling an apk/app. 57 if gomobilepath == "" { 58 return nil, errors.New("toolchain not installed, run `gomobile init`") 59 } 60 61 cleanupFn := func() { 62 if buildWork { 63 fmt.Printf("WORK=%s\n", tmpdir) 64 return 65 } 66 removeAll(tmpdir) 67 } 68 if buildN { 69 tmpdir = "$WORK" 70 cleanupFn = func() {} 71 } else { 72 tmpdir, err = ioutil.TempDir("", "gomobile-work-") 73 if err != nil { 74 return nil, err 75 } 76 } 77 if buildX { 78 fmt.Fprintln(xout, "WORK="+tmpdir) 79 } 80 81 if err := envInit(); err != nil { 82 return nil, err 83 } 84 85 return cleanupFn, nil 86 } 87 88 func envInit() (err error) { 89 // Check the current Go version by go-list. 90 // An arbitrary standard package ('runtime' here) is given to go-list. 91 // This is because go-list tries to analyze the module at the current directory if no packages are given, 92 // and if the module doesn't have any Go file, go-list fails. See golang/go#36668. 93 cmd := exec.Command("go", "list", "-e", "-f", `{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}`, "runtime") 94 cmd.Stderr = os.Stderr 95 out, err := cmd.Output() 96 if err != nil { 97 return err 98 } 99 if len(strings.TrimSpace(string(out))) > 0 { 100 bitcodeEnabled = true 101 } 102 103 // Setup the cross-compiler environments. 104 if ndkRoot, err := ndkRoot(); err == nil { 105 androidEnv = make(map[string][]string) 106 if buildAndroidAPI < minAndroidAPI { 107 return fmt.Errorf("gomobile requires Android API level >= %d", minAndroidAPI) 108 } 109 for arch, toolchain := range ndk { 110 clang := toolchain.Path(ndkRoot, "clang") 111 clangpp := toolchain.Path(ndkRoot, "clang++") 112 if !buildN { 113 tools := []string{clang, clangpp} 114 if runtime.GOOS == "windows" { 115 // Because of https://github.com/android-ndk/ndk/issues/920, 116 // we require r19c, not just r19b. Fortunately, the clang++.cmd 117 // script only exists in r19c. 118 tools = append(tools, clangpp+".cmd") 119 } 120 for _, tool := range tools { 121 _, err = os.Stat(tool) 122 if err != nil { 123 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) 124 } 125 } 126 } 127 androidEnv[arch] = []string{ 128 "GOOS=android", 129 "GOARCH=" + arch, 130 "CC=" + clang, 131 "CXX=" + clangpp, 132 "CGO_ENABLED=1", 133 } 134 if arch == "arm" { 135 androidEnv[arch] = append(androidEnv[arch], "GOARM=7") 136 } 137 } 138 } 139 140 if !xcodeAvailable() { 141 return nil 142 } 143 144 darwinArmNM = "nm" 145 darwinEnv = make(map[string][]string) 146 for _, arch := range allArchs("ios") { 147 var env []string 148 var err error 149 var clang, cflags string 150 switch arch { 151 case "arm64": 152 clang, cflags, err = envClang("iphoneos") 153 cflags += " -miphoneos-version-min=" + buildIOSVersion 154 case "amd64": 155 clang, cflags, err = envClang("iphonesimulator") 156 cflags += " -mios-simulator-version-min=" + buildIOSVersion 157 default: 158 panic(fmt.Errorf("unknown GOARCH: %q", arch)) 159 } 160 if err != nil { 161 return err 162 } 163 164 if bitcodeEnabled { 165 cflags += " -fembed-bitcode" 166 } 167 env = append(env, 168 "GOOS=darwin", 169 "GOARCH="+arch, 170 "CC="+clang, 171 "CXX="+clang+"++", 172 "CGO_CFLAGS="+cflags+" -arch "+archClang(arch), 173 "CGO_CXXFLAGS="+cflags+" -arch "+archClang(arch), 174 "CGO_LDFLAGS="+cflags+" -arch "+archClang(arch), 175 "CGO_ENABLED=1", 176 ) 177 darwinEnv[arch] = env 178 } 179 180 for _, arch := range allArchs("darwin") { 181 var env []string 182 var clang, cflags string 183 184 clang, cflags, err = envClang("macosx") 185 cflags += " -mmacosx-version-min=10.14" 186 187 if bitcodeEnabled { 188 cflags += " -fembed-bitcode" 189 } 190 191 env = append(env, 192 "GOOS=darwin", 193 "GOARCH="+arch, 194 "CC="+clang, 195 "CXX="+clang+"++", 196 "CGO_CFLAGS="+cflags+" -arch "+archClang(arch), 197 "CGO_CXXFLAGS="+cflags+" -arch "+archClang(arch), 198 "CGO_LDFLAGS="+cflags+" -arch "+archClang(arch), 199 "CGO_ENABLED=1", 200 ) 201 darwinEnv[arch] = env 202 } 203 204 return nil 205 } 206 207 func ndkRoot() (string, error) { 208 if buildN { 209 return "$NDK_PATH", nil 210 } 211 212 androidHome := os.Getenv("ANDROID_HOME") 213 if androidHome != "" { 214 ndkRoot := filepath.Join(androidHome, "ndk-bundle") 215 _, err := os.Stat(ndkRoot) 216 if err == nil { 217 return ndkRoot, nil 218 } 219 } 220 221 ndkRoot := os.Getenv("ANDROID_NDK_HOME") 222 if ndkRoot != "" { 223 _, err := os.Stat(ndkRoot) 224 if err == nil { 225 return ndkRoot, nil 226 } 227 } 228 229 return "", fmt.Errorf("no Android NDK found in $ANDROID_HOME/ndk-bundle nor in $ANDROID_NDK_HOME") 230 } 231 232 func envClang(sdkName string) (clang, cflags string, err error) { 233 if buildN { 234 return sdkName + "-clang", "-isysroot=" + sdkName, nil 235 } 236 cmd := exec.Command("xcrun", "--sdk", sdkName, "--find", "clang") 237 out, err := cmd.CombinedOutput() 238 if err != nil { 239 return "", "", fmt.Errorf("xcrun --find: %v\n%s", err, out) 240 } 241 clang = strings.TrimSpace(string(out)) 242 243 cmd = exec.Command("xcrun", "--sdk", sdkName, "--show-sdk-path") 244 out, err = cmd.CombinedOutput() 245 if err != nil { 246 return "", "", fmt.Errorf("xcrun --show-sdk-path: %v\n%s", err, out) 247 } 248 sdk := strings.TrimSpace(string(out)) 249 return clang, "-isysroot " + sdk, nil 250 } 251 252 func archClang(goarch string) string { 253 switch goarch { 254 case "arm": 255 return "armv7" 256 case "arm64": 257 return "arm64" 258 case "386": 259 return "i386" 260 case "amd64": 261 return "x86_64" 262 case "x86": 263 return "x86_64" 264 default: 265 panic(fmt.Sprintf("unknown GOARCH: %q", goarch)) 266 } 267 } 268 269 // environ merges os.Environ and the given "key=value" pairs. 270 // If a key is in both os.Environ and kv, kv takes precedence. 271 func environ(kv []string) []string { 272 cur := os.Environ() 273 new := make([]string, 0, len(cur)+len(kv)) 274 275 envs := make(map[string]string, len(cur)) 276 for _, ev := range cur { 277 elem := strings.SplitN(ev, "=", 2) 278 if len(elem) != 2 || elem[0] == "" { 279 // pass the env var of unusual form untouched. 280 // e.g. Windows may have env var names starting with "=". 281 new = append(new, ev) 282 continue 283 } 284 if goos == "windows" { 285 elem[0] = strings.ToUpper(elem[0]) 286 } 287 envs[elem[0]] = elem[1] 288 } 289 for _, ev := range kv { 290 elem := strings.SplitN(ev, "=", 2) 291 if len(elem) != 2 || elem[0] == "" { 292 panic(fmt.Sprintf("malformed env var %q from input", ev)) 293 } 294 if goos == "windows" { 295 elem[0] = strings.ToUpper(elem[0]) 296 } 297 envs[elem[0]] = elem[1] 298 } 299 for k, v := range envs { 300 new = append(new, k+"="+v) 301 } 302 return new 303 } 304 305 func getenv(env []string, key string) string { 306 prefix := key + "=" 307 for _, kv := range env { 308 if strings.HasPrefix(kv, prefix) { 309 return kv[len(prefix):] 310 } 311 } 312 return "" 313 } 314 315 func archNDK() string { 316 if runtime.GOOS == "windows" && runtime.GOARCH == "386" { 317 return "windows" 318 } else { 319 var arch string 320 switch runtime.GOARCH { 321 case "386": 322 arch = "x86" 323 case "amd64": 324 arch = "x86_64" 325 default: 326 panic("unsupported GOARCH: " + runtime.GOARCH) 327 } 328 return runtime.GOOS + "-" + arch 329 } 330 } 331 332 type ndkToolchain struct { 333 arch string 334 abi string 335 minAPI int 336 toolPrefix string 337 clangPrefix string 338 } 339 340 func (tc *ndkToolchain) ClangPrefix() string { 341 if buildAndroidAPI < tc.minAPI { 342 return fmt.Sprintf("%s%d", tc.clangPrefix, tc.minAPI) 343 } 344 return fmt.Sprintf("%s%d", tc.clangPrefix, buildAndroidAPI) 345 } 346 347 func (tc *ndkToolchain) Path(ndkRoot, toolName string) string { 348 var pref string 349 switch toolName { 350 case "clang", "clang++": 351 pref = tc.ClangPrefix() 352 default: 353 pref = tc.toolPrefix 354 } 355 return filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK(), "bin", pref+"-"+toolName) 356 } 357 358 type ndkConfig map[string]ndkToolchain // map: GOOS->androidConfig. 359 360 func (nc ndkConfig) Toolchain(arch string) ndkToolchain { 361 tc, ok := nc[arch] 362 if !ok { 363 panic(`unsupported architecture: ` + arch) 364 } 365 return tc 366 } 367 368 var ndk = ndkConfig{ 369 "arm": { 370 arch: "arm", 371 abi: "armeabi-v7a", 372 minAPI: 16, 373 toolPrefix: "arm-linux-androideabi", 374 clangPrefix: "armv7a-linux-androideabi", 375 }, 376 "arm64": { 377 arch: "arm64", 378 abi: "arm64-v8a", 379 minAPI: 21, 380 toolPrefix: "aarch64-linux-android", 381 clangPrefix: "aarch64-linux-android", 382 }, 383 384 "386": { 385 arch: "x86", 386 abi: "x86", 387 minAPI: 16, 388 toolPrefix: "i686-linux-android", 389 clangPrefix: "i686-linux-android", 390 }, 391 "amd64": { 392 arch: "x86_64", 393 abi: "x86_64", 394 minAPI: 21, 395 toolPrefix: "x86_64-linux-android", 396 clangPrefix: "x86_64-linux-android", 397 }, 398 } 399 400 func xcodeAvailable() bool { 401 err := exec.Command("xcrun", "xcodebuild", "-version").Run() 402 return err == nil 403 }