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