// 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" "io" "io/ioutil" "os" "os/exec" "path/filepath" "strconv" "strings" "golang.org/x/tools/go/packages" ) func goAndroidBind(gobind string, pkgs []*packages.Package, targets []targetInfo) 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(buildTags) > 0 { cmd.Args = append(cmd.Args, "-tags="+strings.Join(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.PkgPath) } if err := runCmd(cmd); err != nil { return err } androidDir := filepath.Join(tmpdir, "android") modulesUsed, err := areGoModulesUsed() if err != nil { return err } // Generate binding code and java source code only when processing the first package. for _, t := range targets { if err := writeGoMod(tmpdir, "android", t.arch); err != nil { return err } env := androidEnv[t.arch] // Add the generated packages to GOPATH for reverse bindings. gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH")) env = append(env, gopath) // Run `go mod tidy` to force to create go.sum. // Without go.sum, `go build` fails as of Go 1.16. if modulesUsed { if err := goModTidyAt(filepath.Join(tmpdir, "src"), env); err != nil { return err } } toolchain := ndk.Toolchain(t.arch) err := goBuildAt( filepath.Join(tmpdir, "src"), "./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, targets); 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//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 []*packages.Package, targets []targetInfo) (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 = ` ` fmt.Fprintf(w, manifestFmt, "go."+pkgs[0].Name+".gojni", buildAndroidAPI) 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 { // TODO(hajimehoshi): This works only with Go tools that assume all source files are in one directory. // Fix this to work with other Go tools. assetsDir := filepath.Join(filepath.Dir(pkg.GoFiles[0]), "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.PkgPath, name, orig) } files[name] = pkg.PkgPath w, err := aarwcreate(name) if err != nil { return nil } _, err = io.Copy(w, f) return err }) if err != nil { return err } } } for _, t := range targets { toolchain := ndk.Toolchain(t.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 (API level: %d): %v", buildAndroidAPI, err) } var apiPath string var apiVer int for _, fi := range fis { name := fi.Name() if !strings.HasPrefix(name, "android-") { continue } n, err := strconv.Atoi(name[len("android-"):]) if err != nil || n < buildAndroidAPI { 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 (API level: %d) in %s", buildAndroidAPI, sdkDir.Name()) } return apiPath, nil }