2
0
mirror of synced 2025-02-24 15:28:28 +00:00
mobile/cmd/gomobile/init.go
Hyang-Ah Hana Kim 0a84d43930 cmd/gomobile: refactor android*Env to handle more architectures
This does not change the tool's behavior.

The global ndkConfig 'ndk' holds info on all supported architectures
with the latest Go and gomobile tools (tip), and provides Root and
Toolchain methods that returns the current NDK installation directory
and the toolchain info depending on the GOPATH and the current go
version.

The global androidEnv is a map from arch name to the env vars.

ndkccpath is replaced by ndk.Root.

For golang/go#10743

Change-Id: I70d8e7b3e9979836112eb82d50c468df4f4ab43f
Reviewed-on: https://go-review.googlesource.com/17720
Reviewed-by: David Crawshaw <crawshaw@golang.org>
2015-12-14 21:18:57 +00:00

663 lines
14 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): android/{386,arm64}
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"time"
)
// 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 = true
const ndkVersion = "ndk-r10e"
const openALVersion = "openal-soft-1.16.0.1"
var (
goos = runtime.GOOS
goarch = runtime.GOARCH
ndkarch string
)
func init() {
switch runtime.GOARCH {
case "amd64":
ndkarch = "x86_64"
case "386":
ndkarch = "x86"
default:
ndkarch = runtime.GOARCH
}
}
var cmdInit = &command{
run: runInit,
Name: "init",
Usage: "[-u]",
Short: "install android compiler toolchain",
Long: `
Init installs the Android C++ compiler toolchain and builds copies
of the Go standard library for mobile devices.
When first run, it downloads part of the Android NDK.
The toolchain is installed in $GOPATH/pkg/gomobile.
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 {
gopaths := filepath.SplitList(goEnv("GOPATH"))
if len(gopaths) == 0 {
return fmt.Errorf("GOPATH is not set")
}
gomobilepath = filepath.Join(gopaths[0], "pkg/gomobile")
verpath := filepath.Join(gomobilepath, "version")
if buildX || buildN {
fmt.Fprintln(xout, "GOMOBILE="+gomobilepath)
}
removeGomobilepkg()
if err := mkdir(ndk.Root()); err != nil {
return err
}
if buildN {
tmpdir = filepath.Join(gomobilepath, "work")
} else {
var err error
tmpdir, err = ioutil.TempDir(gomobilepath, "work-")
if err != nil {
return err
}
}
if buildX || buildN {
fmt.Fprintln(xout, "WORK="+tmpdir)
}
defer func() {
if buildWork {
fmt.Printf("WORK=%s\n", tmpdir)
return
}
removeAll(tmpdir)
}()
if err := fetchNDK(); err != nil {
return err
}
if err := fetchOpenAL(); err != nil {
return err
}
if err := envInit(); err != nil {
return err
}
if runtime.GOOS == "darwin" {
// Install common x/mobile packages for local development.
// These are often slow to compile (due to cgo) and easy to forget.
//
// Limited to darwin for now as it is common for linux to
// not have GLES installed.
//
// TODO: consider testing GLES installation and suggesting it here
for _, pkg := range commonPkgs {
if err := installPkg(pkg, nil); err != nil {
return err
}
}
}
// Install standard libraries for cross compilers.
start := time.Now()
var androidArgs []string
if goVersion == go1_6 {
// Ideally this would be -buildmode=c-shared.
// https://golang.org/issue/13234.
androidArgs = []string{"-gcflags=-shared", "-ldflags=-shared"}
}
if err := installStd(androidEnv["arm"], androidArgs...); err != nil {
return err
}
if err := installDarwin(); err != nil {
return err
}
if buildX || buildN {
printcmd("go version > %s", verpath)
}
if !buildN {
if err := ioutil.WriteFile(verpath, goVersionOut, 0644); err != nil {
return err
}
}
if buildV {
took := time.Since(start) / time.Second * time.Second
fmt.Fprintf(os.Stderr, "\nDone, build took %s.\n", took)
}
return nil
}
var commonPkgs = []string{
"golang.org/x/mobile/gl",
"golang.org/x/mobile/app",
"golang.org/x/mobile/exp/app/debug",
}
func installDarwin() error {
if goos != "darwin" {
return nil // Only build iOS compilers on OS X.
}
if err := installStd(darwinArmEnv); err != nil {
return err
}
if err := installStd(darwinArm64Env); err != nil {
return err
}
// TODO(crawshaw): darwin/386 for the iOS simulator?
if err := installStd(darwinAmd64Env, "-tags=ios"); err != nil {
return err
}
return nil
}
func installStd(env []string, args ...string) error {
return installPkg("std", env, args...)
}
func installPkg(pkg string, env []string, args ...string) error {
tOS, tArch, pd := getenv(env, "GOOS"), getenv(env, "GOARCH"), pkgdir(env)
if tOS != "" && tArch != "" {
if buildV {
fmt.Fprintf(os.Stderr, "\n# Installing %s for %s/%s.\n", pkg, tOS, tArch)
}
args = append(args, "-pkgdir="+pd)
} else {
if buildV {
fmt.Fprintf(os.Stderr, "\n# Installing %s.\n", pkg)
}
}
// The -p flag is to speed up darwin/arm builds.
// Remove when golang.org/issue/10477 is resolved.
cmd := exec.Command("go", "install", fmt.Sprintf("-p=%d", runtime.NumCPU()))
cmd.Args = append(cmd.Args, args...)
if buildV {
cmd.Args = append(cmd.Args, "-v")
}
if buildX {
cmd.Args = append(cmd.Args, "-x")
}
if buildWork {
cmd.Args = append(cmd.Args, "-work")
}
cmd.Args = append(cmd.Args, pkg)
cmd.Env = append([]string{}, env...)
return runCmd(cmd)
}
func removeGomobilepkg() {
dir, err := os.Open(gomobilepath)
if err != nil {
return
}
names, err := dir.Readdirnames(-1)
if err != nil {
return
}
for _, name := range names {
if name == "dl" {
continue
}
removeAll(filepath.Join(gomobilepath, name))
}
}
func move(dst, src string, names ...string) error {
for _, name := range names {
srcf := filepath.Join(src, name)
dstf := filepath.Join(dst, name)
if buildX || buildN {
printcmd("mv %s %s", srcf, dstf)
}
if buildN {
continue
}
if goos == "windows" {
// os.Rename fails if dstf already exists.
removeAll(dstf)
}
if err := os.Rename(srcf, dstf); err != nil {
return err
}
}
return nil
}
func mkdir(dir string) error {
if buildX || buildN {
printcmd("mkdir -p %s", dir)
}
if buildN {
return nil
}
return os.MkdirAll(dir, 0755)
}
func symlink(src, dst string) error {
if buildX || buildN {
printcmd("ln -s %s %s", src, dst)
}
if buildN {
return nil
}
if goos == "windows" {
return doCopyAll(dst, src)
}
return os.Symlink(src, dst)
}
func rm(name string) error {
if buildX || buildN {
printcmd("rm %s", name)
}
if buildN {
return nil
}
return os.Remove(name)
}
func fetchOpenAL() error {
url := "https://dl.google.com/go/mobile/gomobile-" + openALVersion + ".tar.gz"
archive, err := fetch(url)
if err != nil {
return err
}
if err := extract("openal", archive); err != nil {
return err
}
if goos == "windows" {
resetReadOnlyFlagAll(filepath.Join(tmpdir, "openal"))
}
ndkroot := ndk.Root()
dst := filepath.Join(ndkroot, "arm", "sysroot", "usr", "include")
src := filepath.Join(tmpdir, "openal", "include")
if err := move(dst, src, "AL"); err != nil {
return err
}
libDst := filepath.Join(ndkroot, "openal")
libSrc := filepath.Join(tmpdir, "openal")
if err := mkdir(libDst); err != nil {
return nil
}
if err := move(libDst, libSrc, "lib"); err != nil {
return err
}
return nil
}
func extract(dst, src string) error {
if buildX || buildN {
printcmd("tar xfz %s", src)
}
if buildN {
return nil
}
tf, err := os.Open(src)
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, dst+"/"+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 fetchNDK() error {
if useStrippedNDK {
if err := fetchStrippedNDK(); err != nil {
return err
}
} else {
if err := fetchFullNDK(); err != nil {
return err
}
}
if goos == "windows" {
resetReadOnlyFlagAll(filepath.Join(tmpdir, "android-"+ndkVersion))
}
dst := filepath.Join(ndk.Root(), "arm")
dstSysroot := filepath.Join(dst, "sysroot/usr")
if err := mkdir(dstSysroot); err != nil {
return err
}
srcSysroot := filepath.Join(tmpdir, "android-"+ndkVersion+"/platforms/android-15/arch-arm/usr")
if err := move(dstSysroot, srcSysroot, "include", "lib"); err != nil {
return err
}
ndkpath := filepath.Join(tmpdir, "android-"+ndkVersion+"/toolchains/arm-linux-androideabi-4.8/prebuilt")
if goos == "windows" && ndkarch == "x86" {
ndkpath = filepath.Join(ndkpath, "windows")
} else {
ndkpath = filepath.Join(ndkpath, 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 goos == "windows" {
name += ".exe"
}
if err := symlink(filepath.Join(dst, "bin", "arm-linux-androideabi-"+name), filepath.Join(linkpath, name)); err != nil {
return err
}
}
return nil
}
func fetchStrippedNDK() error {
url := "https://dl.google.com/go/mobile/gomobile-" + ndkVersion + "-" + goos + "-" + ndkarch + ".tar.gz"
archive, err := fetch(url)
if err != nil {
return err
}
return extract("", archive)
}
func fetchFullNDK() error {
url := "https://dl.google.com/android/ndk/android-" + ndkVersion + "-" + goos + "-" + ndkarch + "."
if goos == "windows" {
url += "exe"
} else {
url += "bin"
}
archive, err := fetch(url)
if err != nil {
return err
}
// The self-extracting ndk dist file for Windows terminates
// with an error (error code 2 - corrupted or incomplete file)
// but there are no details on what caused this.
//
// Strangely, if the file is launched from file browser or
// unzipped with 7z.exe no error is reported.
//
// In general we use the stripped NDK, so this code path
// is not used, and 7z.exe is not a normal dependency.
var inflate *exec.Cmd
if goos != "windows" {
// The downloaded archive is executed on linux and os x to unarchive.
// To do this execute permissions are needed.
os.Chmod(archive, 0755)
inflate = exec.Command(archive)
} else {
inflate = exec.Command("7z.exe", "x", archive)
}
inflate.Dir = tmpdir
return runCmd(inflate)
}
// fetch reads a URL into $GOPATH/pkg/gomobile/dl and returns the path
// to the downloaded file. Downloading is skipped if the file is
// already present.
func fetch(url string) (dst string, err error) {
if err := mkdir(filepath.Join(gomobilepath, "dl")); err != nil {
return "", err
}
name := path.Base(url)
dst = filepath.Join(gomobilepath, "dl", name)
// Use what's in the cache if force update is not required.
if !initU {
if buildX {
printcmd("stat %s", dst)
}
if _, err = os.Stat(dst); err == nil {
return dst, nil
}
}
if buildX {
printcmd("curl -o%s %s", dst, url)
}
if buildN {
return dst, nil
}
if buildV {
fmt.Fprintf(os.Stderr, "Downloading %s.\n", url)
}
f, err := ioutil.TempFile(tmpdir, "partial-"+name)
if err != nil {
return "", err
}
defer func() {
if err != nil {
f.Close()
os.Remove(f.Name())
}
}()
hashw := sha256.New()
resp, err := http.Get(url)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("error fetching %v, status: %v", url, resp.Status)
} else {
_, err = io.Copy(io.MultiWriter(hashw, f), resp.Body)
}
if err2 := resp.Body.Close(); err == nil {
err = err2
}
if err != nil {
return "", err
}
if err = f.Close(); err != nil {
return "", err
}
hash := hex.EncodeToString(hashw.Sum(nil))
if fetchHashes[name] != hash {
return "", fmt.Errorf("sha256 for %q: %v, want %v", name, hash, fetchHashes[name])
}
if err = os.Rename(f.Name(), dst); err != nil {
return "", err
}
return dst, nil
}
func doCopyAll(dst, src string) error {
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 || buildN {
printcmd(`rm -r -f "%s"`, path)
}
if buildN {
return nil
}
// os.RemoveAll behaves differently in windows.
// http://golang.org/issues/9606
if goos == "windows" {
resetReadOnlyFlagAll(path)
}
return os.RemoveAll(path)
}
func resetReadOnlyFlagAll(path string) error {
fi, err := os.Stat(path)
if err != nil {
return err
}
if !fi.IsDir() {
return os.Chmod(path, 0666)
}
fd, err := os.Open(path)
if err != nil {
return err
}
defer fd.Close()
names, _ := fd.Readdirnames(-1)
for _, name := range names {
resetReadOnlyFlagAll(path + string(filepath.Separator) + name)
}
return nil
}
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))
}
func runCmd(cmd *exec.Cmd) error {
if buildX || buildN {
dir := ""
if cmd.Dir != "" {
dir = "PWD=" + cmd.Dir + " "
}
env := strings.Join(cmd.Env, " ")
if env != "" {
env += " "
}
printcmd("%s%s%s", dir, env, strings.Join(cmd.Args, " "))
}
buf := new(bytes.Buffer)
buf.WriteByte('\n')
if buildV {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
cmd.Stdout = buf
cmd.Stderr = buf
}
if buildWork {
if goos == "windows" {
cmd.Env = append(cmd.Env, `TEMP=`+tmpdir)
cmd.Env = append(cmd.Env, `TMP=`+tmpdir)
} else {
cmd.Env = append(cmd.Env, `TMPDIR=`+tmpdir)
}
}
if !buildN {
cmd.Env = environ(cmd.Env)
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s failed: %v%s", strings.Join(cmd.Args, " "), err, buf)
}
}
return nil
}