| // 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 |
| |
| import ( |
| "archive/zip" |
| "fmt" |
| "go/build" |
| "io" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strconv" |
| "strings" |
| ) |
| |
| func goAndroidBind(gobind string, pkgs []*build.Package, androidArchs []string) error { |
| if sdkDir := os.Getenv("ANDROID_HOME"); sdkDir == "" { |
| return fmt.Errorf("this command requires ANDROID_HOME environment variable (path to the Android SDK)") |
| } |
| |
| // Run gobind to generate the bindings |
| cmd := exec.Command( |
| gobind, |
| "-lang=go,java", |
| "-outdir="+tmpdir, |
| ) |
| cmd.Env = append(cmd.Env, "GOOS=android") |
| cmd.Env = append(cmd.Env, "CGO_ENABLED=1") |
| if len(ctx.BuildTags) > 0 { |
| cmd.Args = append(cmd.Args, "-tags="+strings.Join(ctx.BuildTags, ",")) |
| } |
| if bindJavaPkg != "" { |
| cmd.Args = append(cmd.Args, "-javapkg="+bindJavaPkg) |
| } |
| if bindClasspath != "" { |
| cmd.Args = append(cmd.Args, "-classpath="+bindClasspath) |
| } |
| if bindBootClasspath != "" { |
| cmd.Args = append(cmd.Args, "-bootclasspath="+bindBootClasspath) |
| } |
| for _, p := range pkgs { |
| cmd.Args = append(cmd.Args, p.ImportPath) |
| } |
| if err := runCmd(cmd); err != nil { |
| return err |
| } |
| |
| androidDir := filepath.Join(tmpdir, "android") |
| |
| // Generate binding code and java source code only when processing the first package. |
| for _, arch := range androidArchs { |
| env := androidEnv[arch] |
| // Add the generated packages to GOPATH |
| gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH")) |
| env = append(env, gopath) |
| toolchain := ndk.Toolchain(arch) |
| |
| err := goBuild( |
| "gobind", |
| env, |
| "-buildmode=c-shared", |
| "-o="+filepath.Join(androidDir, "src/main/jniLibs/"+toolchain.abi+"/libgojni.so"), |
| ) |
| if err != nil { |
| return err |
| } |
| } |
| |
| jsrc := filepath.Join(tmpdir, "java") |
| if err := buildAAR(jsrc, androidDir, pkgs, androidArchs); err != nil { |
| return err |
| } |
| return buildSrcJar(jsrc) |
| } |
| |
| func buildSrcJar(src string) error { |
| var out io.Writer = ioutil.Discard |
| if !buildN { |
| ext := filepath.Ext(buildO) |
| f, err := os.Create(buildO[:len(buildO)-len(ext)] + "-sources.jar") |
| if err != nil { |
| return err |
| } |
| defer func() { |
| if cerr := f.Close(); err == nil { |
| err = cerr |
| } |
| }() |
| out = f |
| } |
| |
| return writeJar(out, src) |
| } |
| |
| // AAR is the format for the binary distribution of an Android Library Project |
| // and it is a ZIP archive with extension .aar. |
| // http://tools.android.com/tech-docs/new-build-system/aar-format |
| // |
| // These entries are directly at the root of the archive. |
| // |
| // AndroidManifest.xml (mandatory) |
| // classes.jar (mandatory) |
| // assets/ (optional) |
| // jni/<abi>/libgojni.so |
| // R.txt (mandatory) |
| // res/ (mandatory) |
| // libs/*.jar (optional, not relevant) |
| // proguard.txt (optional) |
| // lint.jar (optional, not relevant) |
| // aidl (optional, not relevant) |
| // |
| // javac and jar commands are needed to build classes.jar. |
| func buildAAR(srcDir, androidDir string, pkgs []*build.Package, androidArchs []string) (err error) { |
| var out io.Writer = ioutil.Discard |
| if buildO == "" { |
| buildO = pkgs[0].Name + ".aar" |
| } |
| if !strings.HasSuffix(buildO, ".aar") { |
| return fmt.Errorf("output file name %q does not end in '.aar'", buildO) |
| } |
| if !buildN { |
| f, err := os.Create(buildO) |
| if err != nil { |
| return err |
| } |
| defer func() { |
| if cerr := f.Close(); err == nil { |
| err = cerr |
| } |
| }() |
| out = f |
| } |
| |
| aarw := zip.NewWriter(out) |
| aarwcreate := func(name string) (io.Writer, error) { |
| if buildV { |
| fmt.Fprintf(os.Stderr, "aar: %s\n", name) |
| } |
| return aarw.Create(name) |
| } |
| w, err := aarwcreate("AndroidManifest.xml") |
| if err != nil { |
| return err |
| } |
| const manifestFmt = `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package=%q> |
| <uses-sdk android:minSdkVersion="%d"/></manifest>` |
| fmt.Fprintf(w, manifestFmt, "go."+pkgs[0].Name+".gojni", minAndroidAPI) |
| |
| w, err = aarwcreate("proguard.txt") |
| if err != nil { |
| return err |
| } |
| fmt.Fprintln(w, `-keep class go.** { *; }`) |
| if bindJavaPkg != "" { |
| fmt.Fprintln(w, `-keep class `+bindJavaPkg+`.** { *; }`) |
| } else { |
| for _, p := range pkgs { |
| fmt.Fprintln(w, `-keep class `+p.Name+`.** { *; }`) |
| } |
| } |
| |
| w, err = aarwcreate("classes.jar") |
| if err != nil { |
| return err |
| } |
| if err := buildJar(w, srcDir); err != nil { |
| return err |
| } |
| |
| files := map[string]string{} |
| for _, pkg := range pkgs { |
| assetsDir := filepath.Join(pkg.Dir, "assets") |
| assetsDirExists := false |
| if fi, err := os.Stat(assetsDir); err == nil { |
| assetsDirExists = fi.IsDir() |
| } else if !os.IsNotExist(err) { |
| return err |
| } |
| |
| if assetsDirExists { |
| err := filepath.Walk( |
| assetsDir, func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| if info.IsDir() { |
| return nil |
| } |
| f, err := os.Open(path) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| name := "assets/" + path[len(assetsDir)+1:] |
| if orig, exists := files[name]; exists { |
| return fmt.Errorf("package %s asset name conflict: %s already added from package %s", |
| pkg.ImportPath, name, orig) |
| } |
| files[name] = pkg.ImportPath |
| w, err := aarwcreate(name) |
| if err != nil { |
| return nil |
| } |
| _, err = io.Copy(w, f) |
| return err |
| }) |
| if err != nil { |
| return err |
| } |
| } |
| } |
| |
| for _, arch := range androidArchs { |
| toolchain := ndk.Toolchain(arch) |
| lib := toolchain.abi + "/libgojni.so" |
| w, err = aarwcreate("jni/" + lib) |
| if err != nil { |
| return err |
| } |
| if !buildN { |
| r, err := os.Open(filepath.Join(androidDir, "src/main/jniLibs/"+lib)) |
| if err != nil { |
| return err |
| } |
| defer r.Close() |
| if _, err := io.Copy(w, r); err != nil { |
| return err |
| } |
| } |
| } |
| |
| // TODO(hyangah): do we need to use aapt to create R.txt? |
| w, err = aarwcreate("R.txt") |
| if err != nil { |
| return err |
| } |
| |
| w, err = aarwcreate("res/") |
| if err != nil { |
| return err |
| } |
| |
| return aarw.Close() |
| } |
| |
| const ( |
| javacTargetVer = "1.7" |
| minAndroidAPI = 15 |
| ) |
| |
| func buildJar(w io.Writer, srcDir string) error { |
| var srcFiles []string |
| if buildN { |
| srcFiles = []string{"*.java"} |
| } else { |
| err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| if filepath.Ext(path) == ".java" { |
| srcFiles = append(srcFiles, filepath.Join(".", path[len(srcDir):])) |
| } |
| return nil |
| }) |
| if err != nil { |
| return err |
| } |
| } |
| |
| dst := filepath.Join(tmpdir, "javac-output") |
| if !buildN { |
| if err := os.MkdirAll(dst, 0700); err != nil { |
| return err |
| } |
| } |
| |
| bClspath, err := bootClasspath() |
| |
| if err != nil { |
| return err |
| } |
| |
| args := []string{ |
| "-d", dst, |
| "-source", javacTargetVer, |
| "-target", javacTargetVer, |
| "-bootclasspath", bClspath, |
| } |
| if bindClasspath != "" { |
| args = append(args, "-classpath", bindClasspath) |
| } |
| |
| args = append(args, srcFiles...) |
| |
| javac := exec.Command("javac", args...) |
| javac.Dir = srcDir |
| if err := runCmd(javac); err != nil { |
| return err |
| } |
| |
| if buildX { |
| printcmd("jar c -C %s .", dst) |
| } |
| return writeJar(w, dst) |
| } |
| |
| func writeJar(w io.Writer, dir string) error { |
| if buildN { |
| return nil |
| } |
| jarw := zip.NewWriter(w) |
| jarwcreate := func(name string) (io.Writer, error) { |
| if buildV { |
| fmt.Fprintf(os.Stderr, "jar: %s\n", name) |
| } |
| return jarw.Create(name) |
| } |
| f, err := jarwcreate("META-INF/MANIFEST.MF") |
| if err != nil { |
| return err |
| } |
| fmt.Fprintf(f, manifestHeader) |
| |
| err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| if info.IsDir() { |
| return nil |
| } |
| out, err := jarwcreate(filepath.ToSlash(path[len(dir)+1:])) |
| if err != nil { |
| return err |
| } |
| in, err := os.Open(path) |
| if err != nil { |
| return err |
| } |
| defer in.Close() |
| _, err = io.Copy(out, in) |
| return err |
| }) |
| if err != nil { |
| return err |
| } |
| return jarw.Close() |
| } |
| |
| // androidAPIPath returns an android SDK platform directory under ANDROID_HOME. |
| // If there are multiple platforms that satisfy the minimum version requirement |
| // androidAPIPath returns the latest one among them. |
| func androidAPIPath() (string, error) { |
| sdk := os.Getenv("ANDROID_HOME") |
| if sdk == "" { |
| return "", fmt.Errorf("ANDROID_HOME environment var is not set") |
| } |
| sdkDir, err := os.Open(filepath.Join(sdk, "platforms")) |
| if err != nil { |
| return "", fmt.Errorf("failed to find android SDK platform: %v", err) |
| } |
| defer sdkDir.Close() |
| fis, err := sdkDir.Readdir(-1) |
| if err != nil { |
| return "", fmt.Errorf("failed to find android SDK platform (min API level: %d): %v", minAndroidAPI, err) |
| } |
| |
| var apiPath string |
| var apiVer int |
| for _, fi := range fis { |
| name := fi.Name() |
| if !fi.IsDir() || !strings.HasPrefix(name, "android-") { |
| continue |
| } |
| n, err := strconv.Atoi(name[len("android-"):]) |
| if err != nil || n < minAndroidAPI { |
| continue |
| } |
| p := filepath.Join(sdkDir.Name(), name) |
| _, err = os.Stat(filepath.Join(p, "android.jar")) |
| if err == nil && apiVer < n { |
| apiPath = p |
| apiVer = n |
| } |
| } |
| if apiVer == 0 { |
| return "", fmt.Errorf("failed to find android SDK platform (min API level: %d) in %s", |
| minAndroidAPI, sdkDir.Name()) |
| } |
| return apiPath, nil |
| } |