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  }