cmd/gomobile: use nm to build list of packages

Today we only look at direct imports of package main for an import of
golang.org/x/mobile/app, which is unfortunate. We also do a complete
package tree load using go/build looking for the OpenAL import when
building for android, which involves reading a lot of files.

The compiler and linker have already done all of this work for us.
Run nm on the output binary and extract package names from it.

Change-Id: Ie4f07befede5017bbca7d24325062369d4b5c30d
Reviewed-on: https://go-review.googlesource.com/12645
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
This commit is contained in:
David Crawshaw
2015-07-24 16:47:16 -04:00
parent cf7179576b
commit 2928ad7782
5 changed files with 92 additions and 71 deletions

View File

@ -7,11 +7,13 @@
package main
import (
"bufio"
"fmt"
"go/build"
"io"
"os"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
@ -90,12 +92,14 @@ func runBuild(cmd *command) (err error) {
return fmt.Errorf("cannot set -o when building non-main package")
}
var nmpkgs map[string]bool
switch buildTarget {
case "android":
if pkg.Name != "main" {
return goBuild(pkg.ImportPath, androidArmEnv)
}
if err := goAndroidBuild(pkg); err != nil {
nmpkgs, err = goAndroidBuild(pkg)
if err != nil {
return err
}
case "ios":
@ -108,22 +112,53 @@ func runBuild(cmd *command) (err error) {
}
return goBuild(pkg.ImportPath, darwinArm64Env)
}
if err := goIOSBuild(pkg); err != nil {
nmpkgs, err = goIOSBuild(pkg)
if err != nil {
return err
}
}
// TODO(crawshaw): This is an incomplete package scan.
// A complete package scan would be too expensive. Instead,
// fake it. After the binary is built, scan its symbols
// with nm and look for the app and al packages.
if err := importsApp(pkg); err != nil {
return err
if !nmpkgs["golang.org/x/mobile/app"] {
return fmt.Errorf(`%s does not import "golang.org/x/mobile/app"`, pkg.ImportPath)
}
return nil
}
var nmRE = regexp.MustCompile(`[0-9a-f]{8} t (golang.org/x.*/[^.]*)`)
func extractPkgs(nm string, path string) (map[string]bool, error) {
if buildN {
return map[string]bool{"golang.org/x/mobile/app": true}, nil
}
r, w := io.Pipe()
cmd := exec.Command(nm, path)
cmd.Stdout = w
cmd.Stderr = os.Stderr
nmpkgs := make(map[string]bool)
errc := make(chan error, 1)
go func() {
s := bufio.NewScanner(r)
for s.Scan() {
if res := nmRE.FindStringSubmatch(s.Text()); res != nil {
nmpkgs[res[1]] = true
}
}
errc <- s.Err()
}()
err := cmd.Run()
w.Close()
if err != nil {
return nil, fmt.Errorf("%s %s: %v", nm, path, err)
}
if err := <-errc; err != nil {
return nil, fmt.Errorf("%s %s: %v", nm, path, err)
}
return nmpkgs, nil
}
func importsApp(pkg *build.Package) error {
// Building a program, make sure it is appropriate for mobile.
for _, path := range pkg.Imports {

View File

@ -21,12 +21,12 @@ import (
"strings"
)
func goAndroidBuild(pkg *build.Package) error {
func goAndroidBuild(pkg *build.Package) (map[string]bool, error) {
libName := path.Base(pkg.ImportPath)
manifestData, err := ioutil.ReadFile(filepath.Join(pkg.Dir, "AndroidManifest.xml"))
if err != nil {
if !os.IsNotExist(err) {
return err
return nil, err
}
buf := new(bytes.Buffer)
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?>`)
@ -37,7 +37,7 @@ func goAndroidBuild(pkg *build.Package) error {
LibName: libName,
})
if err != nil {
return err
return nil, err
}
manifestData = buf.Bytes()
if buildV {
@ -46,7 +46,7 @@ func goAndroidBuild(pkg *build.Package) error {
} else {
libName, err = manifestLibName(manifestData)
if err != nil {
return err
return nil, err
}
}
libPath := filepath.Join(tmpdir, "lib"+libName+".so")
@ -58,28 +58,34 @@ func goAndroidBuild(pkg *build.Package) error {
"-o", libPath,
)
if err != nil {
return err
return nil, err
}
nmpkgs, err := extractPkgs(androidArmNM, libPath)
if err != nil {
return nil, err
}
block, _ := pem.Decode([]byte(debugCert))
if block == nil {
return errors.New("no debug cert")
return nil, errors.New("no debug cert")
}
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return err
return nil, err
}
if buildO == "" {
buildO = filepath.Base(pkg.Dir) + ".apk"
}
if !strings.HasSuffix(buildO, ".apk") {
return fmt.Errorf("output file name %q does not end in '.apk'", buildO)
return nil, fmt.Errorf("output file name %q does not end in '.apk'", buildO)
}
var out io.Writer
if !buildN {
f, err := os.Create(buildO)
if err != nil {
return err
return nil, err
}
defer func() {
if cerr := f.Close(); err == nil {
@ -105,39 +111,39 @@ func goAndroidBuild(pkg *build.Package) error {
w, err := apkwcreate("AndroidManifest.xml")
if err != nil {
return err
return nil, err
}
if _, err := w.Write(manifestData); err != nil {
return err
return nil, err
}
w, err = apkwcreate("classes.dex")
if err != nil {
return err
return nil, err
}
dexData, err := base64.StdEncoding.DecodeString(dexStr)
if err != nil {
log.Fatal("internal error bad dexStr: %v", err)
}
if _, err := w.Write(dexData); err != nil {
return err
return nil, err
}
w, err = apkwcreate("lib/armeabi/lib" + libName + ".so")
if err != nil {
return err
return nil, err
}
if !buildN {
r, err := os.Open(libPath)
if err != nil {
return err
return nil, err
}
if _, err := io.Copy(w, r); err != nil {
return err
return nil, err
}
}
if pkgImportsAL(pkg) {
if nmpkgs["golang.org/x/mobile/exp/audio/al"] {
alDir := filepath.Join(ndkccpath, "openal/lib")
filepath.Walk(alDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
@ -171,7 +177,7 @@ func goAndroidBuild(pkg *build.Package) error {
if os.IsNotExist(err) {
assetsDirExists = false
} else {
return err
return nil, err
}
} else {
assetsDirExists = fi.IsDir()
@ -198,7 +204,7 @@ func goAndroidBuild(pkg *build.Package) error {
return err
})
if err != nil {
return fmt.Errorf("asset %v", err)
return nil, fmt.Errorf("asset %v", err)
}
}
@ -206,39 +212,11 @@ func goAndroidBuild(pkg *build.Package) error {
if !buildN {
if err := apkw.Close(); err != nil {
return err
return nil, err
}
}
return nil
}
var importsALPkg = make(map[string]struct{})
// pkgImportsAL returns true if the given package or one of its
// dependencies imports the x/mobile/exp/audio/al package.
func pkgImportsAL(pkg *build.Package) bool {
for _, path := range pkg.Imports {
if path == "C" {
continue
}
if _, ok := importsALPkg[path]; ok {
continue
}
importsALPkg[path] = struct{}{}
if strings.HasPrefix(path, "golang.org/x/mobile/exp/audio/al") {
return true
}
dPkg, err := ctx.Import(path, "", build.ImportComment)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading OpenAL library: %v", err)
os.Exit(2)
}
if pkgImportsAL(dPkg) {
return true
}
}
return false
return nmpkgs, nil
}
// A random uninteresting private key.

View File

@ -17,17 +17,17 @@ import (
"text/template"
)
func goIOSBuild(pkg *build.Package) error {
func goIOSBuild(pkg *build.Package) (map[string]bool, error) {
src := pkg.ImportPath
if buildO != "" && !strings.HasSuffix(buildO, ".app") {
return fmt.Errorf("-o must have an .app for target=ios")
return nil, fmt.Errorf("-o must have an .app for target=ios")
}
infoplist := new(bytes.Buffer)
if err := infoplistTmpl.Execute(infoplist, manifestTmplData{
Name: strings.Title(path.Base(pkg.ImportPath)),
}); err != nil {
return err
return nil, err
}
files := []struct {
@ -41,26 +41,30 @@ func goIOSBuild(pkg *build.Package) error {
for _, file := range files {
if err := mkdir(filepath.Dir(file.name)); err != nil {
return err
return nil, err
}
if buildX {
printcmd("echo \"%s\" > %s", file.contents, file.name)
}
if !buildN {
if err := ioutil.WriteFile(file.name, file.contents, 0644); err != nil {
return err
return nil, err
}
}
}
armPath := filepath.Join(tmpdir, "arm")
if err := goBuild(src, darwinArmEnv, "-tags=ios", "-o="+armPath); err != nil {
return err
return nil, err
}
nmpkgs, err := extractPkgs(darwinArmNM, armPath)
if err != nil {
return nil, err
}
arm64Path := filepath.Join(tmpdir, "arm64")
if err := goBuild(src, darwinArm64Env, "-tags=ios", "-o="+arm64Path); err != nil {
return err
return nil, err
}
// Apple requires builds to target both darwin/arm and darwin/arm64.
@ -73,12 +77,12 @@ func goIOSBuild(pkg *build.Package) error {
"-o", filepath.Join(tmpdir, "main/main"),
)
if err := runCmd(cmd); err != nil {
return err
return nil, err
}
// TODO(jbd): Set the launcher icon.
if err := iosCopyAssets(pkg, tmpdir); err != nil {
return err
return nil, err
}
// Build and move the release build to the output directory.
@ -88,7 +92,7 @@ func goIOSBuild(pkg *build.Package) error {
"-project", tmpdir+"/main.xcodeproj",
)
if err := runCmd(cmd); err != nil {
return err
return nil, err
}
// TODO(jbd): Fallback to copying if renaming fails.
@ -101,13 +105,13 @@ func goIOSBuild(pkg *build.Package) error {
if !buildN {
// if output already exists, remove.
if err := os.RemoveAll(buildO); err != nil {
return err
return nil, err
}
if err := os.Rename(tmpdir+"/build/Release-iphoneos/main.app", buildO); err != nil {
return err
return nil, err
}
}
return nil
return nmpkgs, nil
}
func iosCopyAssets(pkg *build.Package, xcodeProjDir string) error {

View File

@ -23,6 +23,9 @@ var (
darwinArm64Env []string
darwin386Env []string
darwinAmd64Env []string
androidArmNM string
darwinArmNM string
)
func buildEnvInit() (cleanup func(), err error) {
@ -104,6 +107,7 @@ func envInit() (err error) {
"CXX=" + filepath.Join(ndkccbin, "arm-linux-androideabi-g++"+exe),
"CGO_ENABLED=1",
}
androidArmNM = filepath.Join(ndkccbin, "arm-linux-androideabi-nm"+exe)
if runtime.GOOS != "darwin" {
return nil
@ -123,6 +127,7 @@ func envInit() (err error) {
"CGO_LDFLAGS=" + cflags + " -arch " + archClang("arm"),
"CGO_ENABLED=1",
}
darwinArmNM = "nm"
darwinArm64Env = []string{
"GOOS=darwin",
"GOARCH=arm64",

View File

@ -430,7 +430,6 @@ func fetchFullNDK() error {
}
inflate.Dir = tmpdir
return runCmd(inflate)
return nil
}
// fetch reads a URL into $GOPATH/pkg/gomobile/dl and returns the path