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