// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main // TODO(crawshaw): build darwin/arm cross compiler on darwin/{386,amd64} // TODO(crawshaw): android/{386,arm64} import ( "archive/tar" "bytes" "compress/gzip" "fmt" "io" "io/ioutil" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" ) // useStrippedNDK determines whether the init subcommand fetches the GCC // toolchain from the original Android NDK, or from the stripped-down NDK // hosted specifically for the gomobile tool. // // There is a significant size different (400MB compared to 30MB). var useStrippedNDK = goos == "linux" || goos == "darwin" const ndkVersion = "ndk-r10d" var ( goos = runtime.GOOS goarch = runtime.GOARCH ndkarch string ) func init() { if runtime.GOARCH == "amd64" { ndkarch = "x86_64" } else { ndkarch = runtime.GOARCH } } var cmdInit = &command{ run: runInit, Name: "init", Short: "install android compiler toolchain", Long: ` Init downloads and installs the Android C++ compiler toolchain. The toolchain is installed in $GOPATH/pkg/gomobile. If the Android C++ compiler toolchain already exists in the path, it skips download and uses the existing toolchain. The -u option forces download and installation of the new toolchain even when the toolchain exists. `, } var initU bool // -u func init() { cmdInit.flag.BoolVar(&initU, "u", false, "force toolchain download") } func runInit(cmd *command) error { version, err := goVersion() if err != nil { return err } gopaths := filepath.SplitList(goEnv("GOPATH")) if len(gopaths) == 0 { return fmt.Errorf("GOPATH is not set") } ndkccpath = filepath.Join(gopaths[0], "pkg", "gomobile", "android-"+ndkVersion) ndkccdl := filepath.Join(ndkccpath, "downloaded") verpath := filepath.Join(gopaths[0], "pkg", "gomobile", "version") if buildX { fmt.Fprintln(xout, "NDKCCPATH="+ndkccpath) } needNDK := initU if !needNDK { if _, err := os.Stat(ndkccdl); err != nil { needNDK = true } } if needNDK { if err := removeAll(ndkccpath); err != nil && !os.IsExist(err) { return err } if err := mkdir(ndkccpath); err != nil { return err } } if buildN { tmpdir = filepath.Join(ndkccpath, "work") } else { var err error tmpdir, err = ioutil.TempDir(ndkccpath, "gomobile-init-") if err != nil { return err } } if buildX { fmt.Fprintln(xout, "WORK="+tmpdir) } defer removeAll(tmpdir) goroot := goEnv("GOROOT") tmpGoroot := filepath.Join(tmpdir, "go") if err := copyGoroot(tmpGoroot, goroot); err != nil { return err } if needNDK { if err := fetchNDK(); err != nil { return err } if !buildN { if err := ioutil.WriteFile(ndkccdl, []byte("done"), 0644); err != nil { return err } } } dst := filepath.Join(ndkccpath, "arm") // TODO(crawshaw): make.bat on windows ndkccbin := filepath.Join(dst, "bin") envpath := os.Getenv("PATH") if buildN { envpath = "$PATH" } make := exec.Command(filepath.Join(tmpGoroot, "src", "make.bash"), "--no-clean") make.Dir = filepath.Join(tmpGoroot, "src") make.Env = []string{ `PATH=` + envpath, `TMPDIR=` + tmpdir, `HOME=` + os.Getenv("HOME"), // for default the go1.4 bootstrap `GOOS=android`, `GOARCH=arm`, `GOARM=7`, `CGO_ENABLED=1`, `CC_FOR_TARGET=` + filepath.Join(ndkccbin, "arm-linux-androideabi-gcc"), `CXX_FOR_TARGET=` + filepath.Join(ndkccbin, "arm-linux-androideabi-g++"), } if v := goEnv("GOROOT_BOOTSTRAP"); v != "" { make.Env = append(make.Env, `GOROOT_BOOTSTRAP=`+v) } if buildV { fmt.Fprintf(os.Stderr, "building android/arm cross compiler\n") make.Stdout = os.Stdout make.Stderr = os.Stderr } if buildX { printcmd("%s", strings.Join(make.Env, " ")+" "+strings.Join(make.Args, " ")) } if !buildN { if err := make.Run(); err != nil { return err } } // Move the Go cross compiler toolchain into GOPATH. gotoolsrc := filepath.Join(tmpGoroot, "pkg", "tool", goos+"_"+goarch) if err := move(ndkccbin, gotoolsrc, "5a", "5l", "5g", "cgo", "nm", "pack", "link"); err != nil { return err } // Build toolexec command. toolexecSrc := filepath.Join(tmpdir, "toolexec.go") if !buildN { if err := ioutil.WriteFile(toolexecSrc, []byte(toolexec), 0644); err != nil { return err } } make = exec.Command("go", "build", "-o", filepath.Join(ndkccbin, "toolexec"), toolexecSrc) if buildV { fmt.Fprintf(os.Stderr, "building gomobile toolexec\n") make.Stdout = os.Stdout make.Stderr = os.Stderr } if buildX { printcmd("%s", strings.Join(make.Args, " ")) } if !buildN { if err := make.Run(); err != nil { return err } } // Move pre-compiled stdlib for android into GOROOT. This is // the only time we modify the user's GOROOT. cannotRemove := false if err := removeAll(filepath.Join(goroot, "pkg", "android_arm")); err != nil { cannotRemove = true } if err := move(filepath.Join(goroot, "pkg"), filepath.Join(tmpGoroot, "pkg"), "android_arm"); err != nil { // Move android_arm into a temp directory that outlives // this process and give the user installation instructions. dir, err := ioutil.TempDir("", "gomobile-") if err != nil { return err } if err := move(dir, filepath.Join(tmpGoroot, "pkg"), "android_arm"); err != nil { return err } // TODO: modify instructions for windows. remove := "" if cannotRemove { remove = "\trm -r -f %s/pkg/android_arm\n" } return fmt.Errorf( `Cannot install android/arm in GOROOT. Make GOROOT writable (possibly by becoming the super user, using sudo) and run: %s mv %s %s`, remove, filepath.Join(dir, "android_arm"), filepath.Join(goroot, "pkg"), ) } if buildX { printcmd("go version > %s", verpath) } if !buildN { if err := ioutil.WriteFile(verpath, version, 0644); err != nil { return err } } return nil } // toolexec is the source of a small program designed to be passed to // the -toolexec flag of go build. const toolexec = `package main import ( "fmt" "os" "os/exec" "path/filepath" ) func main() { args := append([]string{}, os.Args[1:]...) args[0] = filepath.Join(os.Getenv("GOMOBILEPATH"), filepath.Base(args[0])) cmd := exec.Command(args[0], args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } } ` func move(dst, src string, names ...string) error { for _, name := range names { srcf := filepath.Join(src, name) dstf := filepath.Join(dst, name) if buildX { printcmd("mv %s %s", srcf, dstf) } if buildN { continue } if err := os.Rename(srcf, dstf); err != nil { return err } } return nil } func mkdir(dir string) error { if buildX { printcmd("mkdir -p %s", dir) } if buildN { return nil } return os.MkdirAll(dir, 0755) } func symlink(src, dst string) error { if buildX { printcmd("ln -s %s %s", src, dst) } if buildN { return nil } return os.Symlink(src, dst) } func goVersion() ([]byte, error) { if err := exec.Command("which", "go").Run(); err != nil { return nil, fmt.Errorf(`no Go tool on $PATH`) } buildHelp, err := exec.Command("go", "help", "build").Output() if err != nil { return nil, fmt.Errorf("bad Go tool: %v", err) } if !bytes.Contains(buildHelp, []byte("-toolexec")) { return nil, fmt.Errorf("installed Go tool does not support -toolexec") } return exec.Command("go", "version").Output() } func fetchNDK() error { if useStrippedNDK { if err := fetchStrippedNDK(); err != nil { return err } } else { ndkName := "android-" + ndkVersion + "-" + goos + "-" + ndkarch + "." if goos == "windows" { ndkName += "exe" } else { ndkName += "bin" } url := "https://dl.google.com/android/ndk/" + ndkName if err := fetch(filepath.Join(tmpdir, ndkName), url); err != nil { return err } inflate := exec.Command(filepath.Join(tmpdir, ndkName)) inflate.Dir = tmpdir if buildX { printcmd("%s", inflate.Args[0]) } if !buildN { out, err := inflate.CombinedOutput() if err != nil { if buildV { os.Stderr.Write(out) } return err } } } dst := filepath.Join(ndkccpath, "arm") dstSysroot := filepath.Join(dst, "sysroot", "usr") if err := mkdir(dstSysroot); err != nil { return err } srcSysroot := filepath.Join(tmpdir, "android-ndk-r10d", "platforms", "android-15", "arch-arm", "usr") if err := move(dstSysroot, srcSysroot, "include", "lib"); err != nil { return err } ndkpath := filepath.Join(tmpdir, "android-ndk-r10d", "toolchains", "arm-linux-androideabi-4.8", "prebuilt", goos+"-"+ndkarch) if err := move(dst, ndkpath, "bin", "lib", "libexec"); err != nil { return err } linkpath := filepath.Join(dst, "arm-linux-androideabi", "bin") if err := mkdir(linkpath); err != nil { return err } for _, name := range []string{"ld", "as", "gcc", "g++"} { if err := symlink(filepath.Join(dst, "bin", "arm-linux-androideabi-"+name), filepath.Join(linkpath, name)); err != nil { return err } } return nil } func fetchStrippedNDK() error { name := "gomobile-ndk-r10d-" + goos + "-" + ndkarch + ".tar.gz" url := "https://dl.google.com/go/mobile/" + name if err := fetch(filepath.Join(tmpdir, name), url); err != nil { return err } if buildX { printcmd("tar xfz %s", name) } if buildN { return nil } tf, err := os.Open(filepath.Join(tmpdir, name)) if err != nil { return err } defer tf.Close() zr, err := gzip.NewReader(tf) if err != nil { return err } tr := tar.NewReader(zr) for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { return err } dst := filepath.Join(tmpdir, hdr.Name) if hdr.Typeflag == tar.TypeSymlink { if err := symlink(hdr.Linkname, dst); err != nil { return err } continue } if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } f, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.FileMode(hdr.Mode)&0777) if err != nil { return err } if _, err := io.Copy(f, tr); err != nil { return err } if err := f.Close(); err != nil { return err } } return nil } func fetch(dst, url string) error { if buildV { fmt.Fprintf(os.Stderr, "fetching %s\n", url) } if buildX { printcmd("curl -o%s %s", dst, url) } if buildN { return nil } f, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0755) if err != nil { return err } resp, err := http.Get(url) if err != nil { return err } _, err = io.Copy(f, resp.Body) err2 := resp.Body.Close() err3 := f.Close() if err != nil { return err } if err2 != nil { return err2 } return err3 } // copyGoroot copies GOROOT from src to dst. // // It skips the pkg directory, which is not necessary for make.bash, // and symlinks .git to avoid a 70MB copy. func copyGoroot(dst, src string) error { if err := mkdir(filepath.Join(dst, "pkg")); err != nil { return err } for _, dir := range []string{"include", "lib", "src"} { if err := copyAll(filepath.Join(dst, dir), filepath.Join(src, dir)); err != nil { return err } } return symlink(filepath.Join(src, ".git"), filepath.Join(dst, ".git")) } func copyAll(dst, src string) error { if buildX { printcmd("cp -a %s %s", src, dst) } if buildN { return nil } return filepath.Walk(src, func(path string, info os.FileInfo, errin error) (err error) { if errin != nil { return errin } prefixLen := len(src) if len(path) > prefixLen { prefixLen++ // file separator } outpath := filepath.Join(dst, path[prefixLen:]) if info.IsDir() { return os.Mkdir(outpath, 0755) } in, err := os.Open(path) if err != nil { return err } defer in.Close() out, err := os.OpenFile(outpath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode()) if err != nil { return err } defer func() { if errc := out.Close(); err == nil { err = errc } }() _, err = io.Copy(out, in) return err }) } func removeAll(path string) error { if buildX { printcmd("rm -r -f %q", path) } if buildN { return nil } return os.RemoveAll(path) } func goEnv(name string) string { if val := os.Getenv(name); val != "" { return val } val, err := exec.Command("go", "env", name).Output() if err != nil { panic(err) // the Go tool was tested to work earlier } return strings.TrimSpace(string(val)) }