github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/mobile/cmd/gomobile/init.go (about)

     1  // Copyright 2015 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  // TODO(crawshaw): android/{386,arm64}
     8  
     9  import (
    10  	"archive/tar"
    11  	"bytes"
    12  	"compress/gzip"
    13  	"crypto/sha256"
    14  	"encoding/hex"
    15  	"fmt"
    16  	"io"
    17  	"io/ioutil"
    18  	"net/http"
    19  	"os"
    20  	"os/exec"
    21  	"path"
    22  	"path/filepath"
    23  	"runtime"
    24  	"strings"
    25  	"time"
    26  )
    27  
    28  // useStrippedNDK determines whether the init subcommand fetches the GCC
    29  // toolchain from the original Android NDK, or from the stripped-down NDK
    30  // hosted specifically for the gomobile tool.
    31  //
    32  // There is a significant size different (400MB compared to 30MB).
    33  var useStrippedNDK = true
    34  
    35  const ndkVersion = "ndk-r10e"
    36  const openALVersion = "openal-soft-1.16.0.1"
    37  
    38  var (
    39  	goos    = runtime.GOOS
    40  	goarch  = runtime.GOARCH
    41  	ndkarch string
    42  )
    43  
    44  func init() {
    45  	switch runtime.GOARCH {
    46  	case "amd64":
    47  		ndkarch = "x86_64"
    48  	case "386":
    49  		ndkarch = "x86"
    50  	default:
    51  		ndkarch = runtime.GOARCH
    52  	}
    53  }
    54  
    55  var cmdInit = &command{
    56  	run:   runInit,
    57  	Name:  "init",
    58  	Usage: "[-u]",
    59  	Short: "install android compiler toolchain",
    60  	Long: `
    61  Init installs the Android C++ compiler toolchain and builds copies
    62  of the Go standard library for mobile devices.
    63  
    64  When first run, it downloads part of the Android NDK.
    65  The toolchain is installed in $GOPATH/pkg/gomobile.
    66  
    67  The -u option forces download and installation of the new toolchain
    68  even when the toolchain exists.
    69  `,
    70  }
    71  
    72  var initU bool // -u
    73  
    74  func init() {
    75  	cmdInit.flag.BoolVar(&initU, "u", false, "force toolchain download")
    76  }
    77  
    78  func runInit(cmd *command) error {
    79  	gopaths := filepath.SplitList(goEnv("GOPATH"))
    80  	if len(gopaths) == 0 {
    81  		return fmt.Errorf("GOPATH is not set")
    82  	}
    83  	gomobilepath = filepath.Join(gopaths[0], "pkg/gomobile")
    84  
    85  	verpath := filepath.Join(gomobilepath, "version")
    86  	if buildX || buildN {
    87  		fmt.Fprintln(xout, "GOMOBILE="+gomobilepath)
    88  	}
    89  	removeGomobilepkg()
    90  
    91  	if err := mkdir(ndk.Root()); err != nil {
    92  		return err
    93  	}
    94  
    95  	if buildN {
    96  		tmpdir = filepath.Join(gomobilepath, "work")
    97  	} else {
    98  		var err error
    99  		tmpdir, err = ioutil.TempDir(gomobilepath, "work-")
   100  		if err != nil {
   101  			return err
   102  		}
   103  	}
   104  	if buildX || buildN {
   105  		fmt.Fprintln(xout, "WORK="+tmpdir)
   106  	}
   107  	defer func() {
   108  		if buildWork {
   109  			fmt.Printf("WORK=%s\n", tmpdir)
   110  			return
   111  		}
   112  		removeAll(tmpdir)
   113  	}()
   114  
   115  	if err := envInit(); err != nil {
   116  		return err
   117  	}
   118  
   119  	if err := fetchNDK(); err != nil {
   120  		return err
   121  	}
   122  	if err := fetchOpenAL(); err != nil {
   123  		return err
   124  	}
   125  
   126  	if runtime.GOOS == "darwin" {
   127  		// Install common x/mobile packages for local development.
   128  		// These are often slow to compile (due to cgo) and easy to forget.
   129  		//
   130  		// Limited to darwin for now as it is common for linux to
   131  		// not have GLES installed.
   132  		//
   133  		// TODO: consider testing GLES installation and suggesting it here
   134  		for _, pkg := range commonPkgs {
   135  			if err := installPkg(pkg, nil); err != nil {
   136  				return err
   137  			}
   138  		}
   139  	}
   140  
   141  	// Install standard libraries for cross compilers.
   142  	start := time.Now()
   143  	var androidArgs []string
   144  	if goVersion == go1_6 {
   145  		// Ideally this would be -buildmode=c-shared.
   146  		// https://golang.org/issue/13234.
   147  		androidArgs = []string{"-gcflags=-shared", "-ldflags=-shared"}
   148  	}
   149  	for _, env := range androidEnv {
   150  		if err := installStd(env, androidArgs...); err != nil {
   151  			return err
   152  		}
   153  	}
   154  
   155  	if err := installDarwin(); err != nil {
   156  		return err
   157  	}
   158  
   159  	if buildX || buildN {
   160  		printcmd("go version > %s", verpath)
   161  	}
   162  	if !buildN {
   163  		if err := ioutil.WriteFile(verpath, goVersionOut, 0644); err != nil {
   164  			return err
   165  		}
   166  	}
   167  	if buildV {
   168  		took := time.Since(start) / time.Second * time.Second
   169  		fmt.Fprintf(os.Stderr, "\nDone, build took %s.\n", took)
   170  	}
   171  	return nil
   172  }
   173  
   174  var commonPkgs = []string{
   175  	"golang.org/x/mobile/gl",
   176  	"golang.org/x/mobile/app",
   177  	"golang.org/x/mobile/exp/app/debug",
   178  }
   179  
   180  func installDarwin() error {
   181  	if goos != "darwin" {
   182  		return nil // Only build iOS compilers on OS X.
   183  	}
   184  	if err := installStd(darwinArmEnv); err != nil {
   185  		return err
   186  	}
   187  	if err := installStd(darwinArm64Env); err != nil {
   188  		return err
   189  	}
   190  	// TODO(crawshaw): darwin/386 for the iOS simulator?
   191  	if err := installStd(darwinAmd64Env, "-tags=ios"); err != nil {
   192  		return err
   193  	}
   194  	return nil
   195  }
   196  
   197  func installStd(env []string, args ...string) error {
   198  	return installPkg("std", env, args...)
   199  }
   200  
   201  func installPkg(pkg string, env []string, args ...string) error {
   202  	tOS, tArch, pd := getenv(env, "GOOS"), getenv(env, "GOARCH"), pkgdir(env)
   203  	if tOS != "" && tArch != "" {
   204  		if buildV {
   205  			fmt.Fprintf(os.Stderr, "\n# Installing %s for %s/%s.\n", pkg, tOS, tArch)
   206  		}
   207  		args = append(args, "-pkgdir="+pd)
   208  	} else {
   209  		if buildV {
   210  			fmt.Fprintf(os.Stderr, "\n# Installing %s.\n", pkg)
   211  		}
   212  	}
   213  
   214  	// The -p flag is to speed up darwin/arm builds.
   215  	// Remove when golang.org/issue/10477 is resolved.
   216  	cmd := exec.Command("go", "install", fmt.Sprintf("-p=%d", runtime.NumCPU()))
   217  	cmd.Args = append(cmd.Args, args...)
   218  	if buildV {
   219  		cmd.Args = append(cmd.Args, "-v")
   220  	}
   221  	if buildX {
   222  		cmd.Args = append(cmd.Args, "-x")
   223  	}
   224  	if buildWork {
   225  		cmd.Args = append(cmd.Args, "-work")
   226  	}
   227  	cmd.Args = append(cmd.Args, pkg)
   228  	cmd.Env = append([]string{}, env...)
   229  	return runCmd(cmd)
   230  }
   231  
   232  func removeGomobilepkg() {
   233  	dir, err := os.Open(gomobilepath)
   234  	if err != nil {
   235  		return
   236  	}
   237  	names, err := dir.Readdirnames(-1)
   238  	if err != nil {
   239  		return
   240  	}
   241  	for _, name := range names {
   242  		if name == "dl" {
   243  			continue
   244  		}
   245  		removeAll(filepath.Join(gomobilepath, name))
   246  	}
   247  }
   248  
   249  func move(dst, src string, names ...string) error {
   250  	for _, name := range names {
   251  		srcf := filepath.Join(src, name)
   252  		dstf := filepath.Join(dst, name)
   253  		if buildX || buildN {
   254  			printcmd("mv %s %s", srcf, dstf)
   255  		}
   256  		if buildN {
   257  			continue
   258  		}
   259  		if goos == "windows" {
   260  			// os.Rename fails if dstf already exists.
   261  			removeAll(dstf)
   262  		}
   263  		if err := os.Rename(srcf, dstf); err != nil {
   264  			return err
   265  		}
   266  	}
   267  	return nil
   268  }
   269  
   270  func mkdir(dir string) error {
   271  	if buildX || buildN {
   272  		printcmd("mkdir -p %s", dir)
   273  	}
   274  	if buildN {
   275  		return nil
   276  	}
   277  	return os.MkdirAll(dir, 0755)
   278  }
   279  
   280  func symlink(src, dst string) error {
   281  	if buildX || buildN {
   282  		printcmd("ln -s %s %s", src, dst)
   283  	}
   284  	if buildN {
   285  		return nil
   286  	}
   287  	if goos == "windows" {
   288  		return doCopyAll(dst, src)
   289  	}
   290  	return os.Symlink(src, dst)
   291  }
   292  
   293  func rm(name string) error {
   294  	if buildX || buildN {
   295  		printcmd("rm %s", name)
   296  	}
   297  	if buildN {
   298  		return nil
   299  	}
   300  	return os.Remove(name)
   301  }
   302  
   303  func fetchOpenAL() error {
   304  	url := "https://dl.google.com/go/mobile/gomobile-" + openALVersion + ".tar.gz"
   305  	archive, err := fetch(url)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	if err := extract("openal", archive); err != nil {
   310  		return err
   311  	}
   312  	if goos == "windows" {
   313  		resetReadOnlyFlagAll(filepath.Join(tmpdir, "openal"))
   314  	}
   315  	ndkroot := ndk.Root()
   316  	src := filepath.Join(tmpdir, "openal/include/AL")
   317  	for arch := range androidEnv {
   318  		toolchain := ndk.Toolchain(arch)
   319  		dst := filepath.Join(ndkroot, toolchain.arch+"/sysroot/usr/include/AL")
   320  		if buildX || buildN {
   321  			printcmd("cp -r %s %s", src, dst)
   322  		}
   323  		if buildN {
   324  			continue
   325  		}
   326  		if err := doCopyAll(dst, src); err != nil {
   327  			return err
   328  		}
   329  	}
   330  	libDst := filepath.Join(ndkroot, "openal")
   331  	libSrc := filepath.Join(tmpdir, "openal")
   332  	if err := mkdir(libDst); err != nil {
   333  		return nil
   334  	}
   335  	if err := move(libDst, libSrc, "lib"); err != nil {
   336  		return err
   337  	}
   338  	return nil
   339  }
   340  
   341  func extract(dst, src string) error {
   342  	if buildX || buildN {
   343  		printcmd("tar xfz %s", src)
   344  	}
   345  	if buildN {
   346  		return nil
   347  	}
   348  	tf, err := os.Open(src)
   349  	if err != nil {
   350  		return err
   351  	}
   352  	defer tf.Close()
   353  	zr, err := gzip.NewReader(tf)
   354  	if err != nil {
   355  		return err
   356  	}
   357  	tr := tar.NewReader(zr)
   358  	for {
   359  		hdr, err := tr.Next()
   360  		if err == io.EOF {
   361  			break
   362  		}
   363  		if err != nil {
   364  			return err
   365  		}
   366  		dst := filepath.Join(tmpdir, dst+"/"+hdr.Name)
   367  		if hdr.Typeflag == tar.TypeSymlink {
   368  			if err := symlink(hdr.Linkname, dst); err != nil {
   369  				return err
   370  			}
   371  			continue
   372  		}
   373  		if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
   374  			return err
   375  		}
   376  		f, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.FileMode(hdr.Mode)&0777)
   377  		if err != nil {
   378  			return err
   379  		}
   380  		if _, err := io.Copy(f, tr); err != nil {
   381  			return err
   382  		}
   383  		if err := f.Close(); err != nil {
   384  			return err
   385  		}
   386  	}
   387  	return nil
   388  }
   389  
   390  func fetchNDK() error {
   391  	if useStrippedNDK {
   392  		if err := fetchStrippedNDK(); err != nil {
   393  			return err
   394  		}
   395  	} else {
   396  		if err := fetchFullNDK(); err != nil {
   397  			return err
   398  		}
   399  	}
   400  	if goos == "windows" {
   401  		resetReadOnlyFlagAll(filepath.Join(tmpdir, "android-"+ndkVersion))
   402  	}
   403  
   404  	for arch := range androidEnv {
   405  		toolchain := ndk.Toolchain(arch)
   406  		dst := filepath.Join(ndk.Root(), toolchain.arch)
   407  		dstSysroot := filepath.Join(dst, "sysroot")
   408  		if err := mkdir(dstSysroot); err != nil {
   409  			return err
   410  		}
   411  
   412  		srcSysroot := filepath.Join(tmpdir, fmt.Sprintf(
   413  			"android-%s/platforms/%s/arch-%s", ndkVersion, toolchain.platform, toolchain.arch))
   414  		if err := move(dstSysroot, srcSysroot, "usr"); err != nil {
   415  			return err
   416  		}
   417  
   418  		ndkpath := filepath.Join(tmpdir, fmt.Sprintf(
   419  			"android-%s/toolchains/%s/prebuilt", ndkVersion, toolchain.gcc))
   420  		if goos == "windows" && ndkarch == "x86" {
   421  			ndkpath = filepath.Join(ndkpath, "windows")
   422  		} else {
   423  			ndkpath = filepath.Join(ndkpath, goos+"-"+ndkarch)
   424  		}
   425  		if err := move(dst, ndkpath, "bin", "lib", "libexec"); err != nil {
   426  			return err
   427  		}
   428  
   429  		linkpath := filepath.Join(dst, toolchain.toolPrefix+"/bin")
   430  		if err := mkdir(linkpath); err != nil {
   431  			return err
   432  		}
   433  
   434  		for _, name := range []string{"ld", "as", "gcc", "g++"} {
   435  			if goos == "windows" {
   436  				name += ".exe"
   437  			}
   438  			if err := symlink(filepath.Join(dst, "bin", toolchain.toolPrefix+"-"+name), filepath.Join(linkpath, name)); err != nil {
   439  				return err
   440  			}
   441  		}
   442  	}
   443  	return nil
   444  }
   445  
   446  func fetchStrippedNDK() error {
   447  	url := "https://dl.google.com/go/mobile/gomobile-" + ndkVersion + "-" + goos + "-" + ndkarch + ".tar.gz"
   448  	archive, err := fetch(url)
   449  	if err != nil {
   450  		return err
   451  	}
   452  	return extract("", archive)
   453  }
   454  
   455  func fetchFullNDK() error {
   456  	url := "https://dl.google.com/android/ndk/android-" + ndkVersion + "-" + goos + "-" + ndkarch + "."
   457  	if goos == "windows" {
   458  		url += "exe"
   459  	} else {
   460  		url += "bin"
   461  	}
   462  	archive, err := fetch(url)
   463  	if err != nil {
   464  		return err
   465  	}
   466  
   467  	// The self-extracting ndk dist file for Windows terminates
   468  	// with an error (error code 2 - corrupted or incomplete file)
   469  	// but there are no details on what caused this.
   470  	//
   471  	// Strangely, if the file is launched from file browser or
   472  	// unzipped with 7z.exe no error is reported.
   473  	//
   474  	// In general we use the stripped NDK, so this code path
   475  	// is not used, and 7z.exe is not a normal dependency.
   476  	var inflate *exec.Cmd
   477  	if goos != "windows" {
   478  		// The downloaded archive is executed on linux and os x to unarchive.
   479  		// To do this execute permissions are needed.
   480  		os.Chmod(archive, 0755)
   481  
   482  		inflate = exec.Command(archive)
   483  	} else {
   484  		inflate = exec.Command("7z.exe", "x", archive)
   485  	}
   486  	inflate.Dir = tmpdir
   487  	return runCmd(inflate)
   488  }
   489  
   490  // fetch reads a URL into $GOPATH/pkg/gomobile/dl and returns the path
   491  // to the downloaded file. Downloading is skipped if the file is
   492  // already present.
   493  func fetch(url string) (dst string, err error) {
   494  	if err := mkdir(filepath.Join(gomobilepath, "dl")); err != nil {
   495  		return "", err
   496  	}
   497  	name := path.Base(url)
   498  	dst = filepath.Join(gomobilepath, "dl", name)
   499  
   500  	// Use what's in the cache if force update is not required.
   501  	if !initU {
   502  		if buildX {
   503  			printcmd("stat %s", dst)
   504  		}
   505  		if _, err = os.Stat(dst); err == nil {
   506  			return dst, nil
   507  		}
   508  	}
   509  	if buildX {
   510  		printcmd("curl -o%s %s", dst, url)
   511  	}
   512  	if buildN {
   513  		return dst, nil
   514  	}
   515  
   516  	if buildV {
   517  		fmt.Fprintf(os.Stderr, "Downloading %s.\n", url)
   518  	}
   519  
   520  	f, err := ioutil.TempFile(tmpdir, "partial-"+name)
   521  	if err != nil {
   522  		return "", err
   523  	}
   524  	defer func() {
   525  		if err != nil {
   526  			f.Close()
   527  			os.Remove(f.Name())
   528  		}
   529  	}()
   530  	hashw := sha256.New()
   531  
   532  	resp, err := http.Get(url)
   533  	if err != nil {
   534  		return "", err
   535  	}
   536  	if resp.StatusCode != http.StatusOK {
   537  		err = fmt.Errorf("error fetching %v, status: %v", url, resp.Status)
   538  	} else {
   539  		_, err = io.Copy(io.MultiWriter(hashw, f), resp.Body)
   540  	}
   541  	if err2 := resp.Body.Close(); err == nil {
   542  		err = err2
   543  	}
   544  	if err != nil {
   545  		return "", err
   546  	}
   547  	if err = f.Close(); err != nil {
   548  		return "", err
   549  	}
   550  	hash := hex.EncodeToString(hashw.Sum(nil))
   551  	if fetchHashes[name] != hash {
   552  		return "", fmt.Errorf("sha256 for %q: %v, want %v", name, hash, fetchHashes[name])
   553  	}
   554  	if err = os.Rename(f.Name(), dst); err != nil {
   555  		return "", err
   556  	}
   557  	return dst, nil
   558  }
   559  
   560  func doCopyAll(dst, src string) error {
   561  	return filepath.Walk(src, func(path string, info os.FileInfo, errin error) (err error) {
   562  		if errin != nil {
   563  			return errin
   564  		}
   565  		prefixLen := len(src)
   566  		if len(path) > prefixLen {
   567  			prefixLen++ // file separator
   568  		}
   569  		outpath := filepath.Join(dst, path[prefixLen:])
   570  		if info.IsDir() {
   571  			return os.Mkdir(outpath, 0755)
   572  		}
   573  		in, err := os.Open(path)
   574  		if err != nil {
   575  			return err
   576  		}
   577  		defer in.Close()
   578  		out, err := os.OpenFile(outpath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode())
   579  		if err != nil {
   580  			return err
   581  		}
   582  		defer func() {
   583  			if errc := out.Close(); err == nil {
   584  				err = errc
   585  			}
   586  		}()
   587  		_, err = io.Copy(out, in)
   588  		return err
   589  	})
   590  }
   591  
   592  func removeAll(path string) error {
   593  	if buildX || buildN {
   594  		printcmd(`rm -r -f "%s"`, path)
   595  	}
   596  	if buildN {
   597  		return nil
   598  	}
   599  
   600  	// os.RemoveAll behaves differently in windows.
   601  	// http://golang.org/issues/9606
   602  	if goos == "windows" {
   603  		resetReadOnlyFlagAll(path)
   604  	}
   605  
   606  	return os.RemoveAll(path)
   607  }
   608  
   609  func resetReadOnlyFlagAll(path string) error {
   610  	fi, err := os.Stat(path)
   611  	if err != nil {
   612  		return err
   613  	}
   614  	if !fi.IsDir() {
   615  		return os.Chmod(path, 0666)
   616  	}
   617  	fd, err := os.Open(path)
   618  	if err != nil {
   619  		return err
   620  	}
   621  	defer fd.Close()
   622  
   623  	names, _ := fd.Readdirnames(-1)
   624  	for _, name := range names {
   625  		resetReadOnlyFlagAll(path + string(filepath.Separator) + name)
   626  	}
   627  	return nil
   628  }
   629  
   630  func goEnv(name string) string {
   631  	if val := os.Getenv(name); val != "" {
   632  		return val
   633  	}
   634  	val, err := exec.Command("go", "env", name).Output()
   635  	if err != nil {
   636  		panic(err) // the Go tool was tested to work earlier
   637  	}
   638  	return strings.TrimSpace(string(val))
   639  }
   640  
   641  func runCmd(cmd *exec.Cmd) error {
   642  	if buildX || buildN {
   643  		dir := ""
   644  		if cmd.Dir != "" {
   645  			dir = "PWD=" + cmd.Dir + " "
   646  		}
   647  		env := strings.Join(cmd.Env, " ")
   648  		if env != "" {
   649  			env += " "
   650  		}
   651  		printcmd("%s%s%s", dir, env, strings.Join(cmd.Args, " "))
   652  	}
   653  
   654  	buf := new(bytes.Buffer)
   655  	buf.WriteByte('\n')
   656  	if buildV {
   657  		cmd.Stdout = os.Stdout
   658  		cmd.Stderr = os.Stderr
   659  	} else {
   660  		cmd.Stdout = buf
   661  		cmd.Stderr = buf
   662  	}
   663  
   664  	if buildWork {
   665  		if goos == "windows" {
   666  			cmd.Env = append(cmd.Env, `TEMP=`+tmpdir)
   667  			cmd.Env = append(cmd.Env, `TMP=`+tmpdir)
   668  		} else {
   669  			cmd.Env = append(cmd.Env, `TMPDIR=`+tmpdir)
   670  		}
   671  	}
   672  
   673  	if !buildN {
   674  		cmd.Env = environ(cmd.Env)
   675  		if err := cmd.Run(); err != nil {
   676  			return fmt.Errorf("%s failed: %v%s", strings.Join(cmd.Args, " "), err, buf)
   677  		}
   678  	}
   679  	return nil
   680  }