This feature is necessary when the cross compilers need to be rebuilt because the goroot has been updated but the ndk version has not changed, or when rebuilding is required but there is no network. Currently the test for usable ndk compilers is simply checking the presence of a file that's created after the ndk tool chain is installed in the $GOPATH/pkg/gomobile/androind-<ndkversion> directory. The -u option forces update of the ndk tool chain regardless. Change-Id: I3d4f62266d436ddc52d158877f08481f92a6a690 Reviewed-on: https://go-review.googlesource.com/5140 Reviewed-by: David Crawshaw <crawshaw@golang.org>
530 lines
12 KiB
Go
530 lines
12 KiB
Go
// 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 {
|
|
if err := checkGoVersion(); 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], filepath.FromSlash("pkg/gomobile/android-"+ndkVersion))
|
|
if buildX {
|
|
fmt.Fprintln(xout, "NDKCCPATH="+ndkccpath)
|
|
}
|
|
|
|
sentinel := filepath.Join(ndkccpath, "ndk.sentinel")
|
|
needNDK := initU
|
|
if !needNDK {
|
|
if _, err := os.Stat(sentinel); 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(sentinel, []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"),
|
|
)
|
|
}
|
|
|
|
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 checkGoVersion() error {
|
|
if err := exec.Command("which", "go").Run(); err != nil {
|
|
return fmt.Errorf(`no Go tool on $PATH`)
|
|
}
|
|
buildHelp, err := exec.Command("go", "help", "build").Output()
|
|
if err != nil {
|
|
return fmt.Errorf("bad Go tool: %v", err)
|
|
}
|
|
if !bytes.Contains(buildHelp, []byte("-toolexec")) {
|
|
return fmt.Errorf("installed Go tool does not support -toolexec")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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))
|
|
}
|