github.com/cyrilou242/gomobile-java@v0.0.0-20220215185836-09daef25a210/cmd/gomobile/bind_androidapp.go (about)

     1  // Copyright 2015 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"archive/zip"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"golang.org/x/tools/go/packages"
    19  )
    20  
    21  func goAndroidBind(gobind string, pkgs []*packages.Package, targets []targetInfo) error {
    22  	if sdkDir := os.Getenv("ANDROID_HOME"); sdkDir == "" {
    23  		return fmt.Errorf("this command requires ANDROID_HOME environment variable (path to the Android SDK)")
    24  	}
    25  
    26  	// Run gobind to generate the bindings
    27  	cmd := exec.Command(
    28  		gobind,
    29  		"-lang=go,java",
    30  		"-outdir="+tmpdir,
    31  	)
    32  	cmd.Env = append(cmd.Env, "GOOS=android")
    33  	cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
    34  	if len(buildTags) > 0 {
    35  		cmd.Args = append(cmd.Args, "-tags="+strings.Join(buildTags, ","))
    36  	}
    37  	if bindJavaPkg != "" {
    38  		cmd.Args = append(cmd.Args, "-javapkg="+bindJavaPkg)
    39  	}
    40  	if bindClasspath != "" {
    41  		cmd.Args = append(cmd.Args, "-classpath="+bindClasspath)
    42  	}
    43  	if bindBootClasspath != "" {
    44  		cmd.Args = append(cmd.Args, "-bootclasspath="+bindBootClasspath)
    45  	}
    46  	for _, p := range pkgs {
    47  		cmd.Args = append(cmd.Args, p.PkgPath)
    48  	}
    49  	if err := runCmd(cmd); err != nil {
    50  		return err
    51  	}
    52  
    53  	androidDir := filepath.Join(tmpdir, "android")
    54  
    55  	modulesUsed, err := areGoModulesUsed()
    56  	if err != nil {
    57  		return err
    58  	}
    59  
    60  	// Generate binding code and java source code only when processing the first package.
    61  	for _, t := range targets {
    62  		if err := writeGoMod(tmpdir, "android", t.arch); err != nil {
    63  			return err
    64  		}
    65  
    66  		env := androidEnv[t.arch]
    67  		// Add the generated packages to GOPATH for reverse bindings.
    68  		gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH"))
    69  		env = append(env, gopath)
    70  
    71  		// Run `go mod tidy` to force to create go.sum.
    72  		// Without go.sum, `go build` fails as of Go 1.16.
    73  		if modulesUsed {
    74  			if err := goModTidyAt(filepath.Join(tmpdir, "src"), env); err != nil {
    75  				return err
    76  			}
    77  		}
    78  
    79  		toolchain := ndk.Toolchain(t.arch)
    80  		err := goBuildAt(
    81  			filepath.Join(tmpdir, "src"),
    82  			"./gobind",
    83  			env,
    84  			"-buildmode=c-shared",
    85  			"-o="+filepath.Join(androidDir, "src/main/jniLibs/"+toolchain.abi+"/libgojni.so"),
    86  		)
    87  		if err != nil {
    88  			return err
    89  		}
    90  	}
    91  
    92  	jsrc := filepath.Join(tmpdir, "java")
    93  	if err := buildAAR(jsrc, androidDir, pkgs, targets); err != nil {
    94  		return err
    95  	}
    96  	return buildSrcJar(jsrc)
    97  }
    98  
    99  func buildSrcJar(src string) error {
   100  	var out io.Writer = ioutil.Discard
   101  	if !buildN {
   102  		ext := filepath.Ext(buildO)
   103  		f, err := os.Create(buildO[:len(buildO)-len(ext)] + "-sources.jar")
   104  		if err != nil {
   105  			return err
   106  		}
   107  		defer func() {
   108  			if cerr := f.Close(); err == nil {
   109  				err = cerr
   110  			}
   111  		}()
   112  		out = f
   113  	}
   114  
   115  	return writeJar(out, src)
   116  }
   117  
   118  // AAR is the format for the binary distribution of an Android Library Project
   119  // and it is a ZIP archive with extension .aar.
   120  // http://tools.android.com/tech-docs/new-build-system/aar-format
   121  //
   122  // These entries are directly at the root of the archive.
   123  //
   124  //	AndroidManifest.xml (mandatory)
   125  // 	classes.jar (mandatory)
   126  //	assets/ (optional)
   127  //	jni/<abi>/libgojni.so
   128  //	R.txt (mandatory)
   129  //	res/ (mandatory)
   130  //	libs/*.jar (optional, not relevant)
   131  //	proguard.txt (optional)
   132  //	lint.jar (optional, not relevant)
   133  //	aidl (optional, not relevant)
   134  //
   135  // javac and jar commands are needed to build classes.jar.
   136  func buildAAR(srcDir, androidDir string, pkgs []*packages.Package, targets []targetInfo) (err error) {
   137  	var out io.Writer = ioutil.Discard
   138  	if buildO == "" {
   139  		buildO = pkgs[0].Name + ".aar"
   140  	}
   141  	if !strings.HasSuffix(buildO, ".aar") {
   142  		return fmt.Errorf("output file name %q does not end in '.aar'", buildO)
   143  	}
   144  	if !buildN {
   145  		f, err := os.Create(buildO)
   146  		if err != nil {
   147  			return err
   148  		}
   149  		defer func() {
   150  			if cerr := f.Close(); err == nil {
   151  				err = cerr
   152  			}
   153  		}()
   154  		out = f
   155  	}
   156  
   157  	aarw := zip.NewWriter(out)
   158  	aarwcreate := func(name string) (io.Writer, error) {
   159  		if buildV {
   160  			fmt.Fprintf(os.Stderr, "aar: %s\n", name)
   161  		}
   162  		return aarw.Create(name)
   163  	}
   164  	w, err := aarwcreate("AndroidManifest.xml")
   165  	if err != nil {
   166  		return err
   167  	}
   168  	const manifestFmt = `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package=%q>
   169  <uses-sdk android:minSdkVersion="%d"/></manifest>`
   170  	fmt.Fprintf(w, manifestFmt, "go."+pkgs[0].Name+".gojni", buildAndroidAPI)
   171  
   172  	w, err = aarwcreate("proguard.txt")
   173  	if err != nil {
   174  		return err
   175  	}
   176  	fmt.Fprintln(w, `-keep class go.** { *; }`)
   177  	if bindJavaPkg != "" {
   178  		fmt.Fprintln(w, `-keep class `+bindJavaPkg+`.** { *; }`)
   179  	} else {
   180  		for _, p := range pkgs {
   181  			fmt.Fprintln(w, `-keep class `+p.Name+`.** { *; }`)
   182  		}
   183  	}
   184  
   185  	w, err = aarwcreate("classes.jar")
   186  	if err != nil {
   187  		return err
   188  	}
   189  	if err := buildJar(w, srcDir); err != nil {
   190  		return err
   191  	}
   192  
   193  	files := map[string]string{}
   194  	for _, pkg := range pkgs {
   195  		// TODO(hajimehoshi): This works only with Go tools that assume all source files are in one directory.
   196  		// Fix this to work with other Go tools.
   197  		assetsDir := filepath.Join(filepath.Dir(pkg.GoFiles[0]), "assets")
   198  		assetsDirExists := false
   199  		if fi, err := os.Stat(assetsDir); err == nil {
   200  			assetsDirExists = fi.IsDir()
   201  		} else if !os.IsNotExist(err) {
   202  			return err
   203  		}
   204  
   205  		if assetsDirExists {
   206  			err := filepath.Walk(
   207  				assetsDir, func(path string, info os.FileInfo, err error) error {
   208  					if err != nil {
   209  						return err
   210  					}
   211  					if info.IsDir() {
   212  						return nil
   213  					}
   214  					f, err := os.Open(path)
   215  					if err != nil {
   216  						return err
   217  					}
   218  					defer f.Close()
   219  					name := "assets/" + path[len(assetsDir)+1:]
   220  					if orig, exists := files[name]; exists {
   221  						return fmt.Errorf("package %s asset name conflict: %s already added from package %s",
   222  							pkg.PkgPath, name, orig)
   223  					}
   224  					files[name] = pkg.PkgPath
   225  					w, err := aarwcreate(name)
   226  					if err != nil {
   227  						return nil
   228  					}
   229  					_, err = io.Copy(w, f)
   230  					return err
   231  				})
   232  			if err != nil {
   233  				return err
   234  			}
   235  		}
   236  	}
   237  
   238  	for _, t := range targets {
   239  		toolchain := ndk.Toolchain(t.arch)
   240  		lib := toolchain.abi + "/libgojni.so"
   241  		w, err = aarwcreate("jni/" + lib)
   242  		if err != nil {
   243  			return err
   244  		}
   245  		if !buildN {
   246  			r, err := os.Open(filepath.Join(androidDir, "src/main/jniLibs/"+lib))
   247  			if err != nil {
   248  				return err
   249  			}
   250  			defer r.Close()
   251  			if _, err := io.Copy(w, r); err != nil {
   252  				return err
   253  			}
   254  		}
   255  	}
   256  
   257  	// TODO(hyangah): do we need to use aapt to create R.txt?
   258  	w, err = aarwcreate("R.txt")
   259  	if err != nil {
   260  		return err
   261  	}
   262  
   263  	w, err = aarwcreate("res/")
   264  	if err != nil {
   265  		return err
   266  	}
   267  
   268  	return aarw.Close()
   269  }
   270  
   271  const (
   272  	javacTargetVer = "1.7"
   273  	minAndroidAPI  = 15
   274  )
   275  
   276  func buildJar(w io.Writer, srcDir string) error {
   277  	var srcFiles []string
   278  	if buildN {
   279  		srcFiles = []string{"*.java"}
   280  	} else {
   281  		err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
   282  			if err != nil {
   283  				return err
   284  			}
   285  			if filepath.Ext(path) == ".java" {
   286  				srcFiles = append(srcFiles, filepath.Join(".", path[len(srcDir):]))
   287  			}
   288  			return nil
   289  		})
   290  		if err != nil {
   291  			return err
   292  		}
   293  	}
   294  
   295  	dst := filepath.Join(tmpdir, "javac-output")
   296  	if !buildN {
   297  		if err := os.MkdirAll(dst, 0700); err != nil {
   298  			return err
   299  		}
   300  	}
   301  
   302  	bClspath, err := bootClasspath()
   303  
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	args := []string{
   309  		"-d", dst,
   310  		"-source", javacTargetVer,
   311  		"-target", javacTargetVer,
   312  		"-bootclasspath", bClspath,
   313  	}
   314  	if bindClasspath != "" {
   315  		args = append(args, "-classpath", bindClasspath)
   316  	}
   317  
   318  	args = append(args, srcFiles...)
   319  
   320  	javac := exec.Command("javac", args...)
   321  	javac.Dir = srcDir
   322  	if err := runCmd(javac); err != nil {
   323  		return err
   324  	}
   325  
   326  	if buildX {
   327  		printcmd("jar c -C %s .", dst)
   328  	}
   329  	return writeJar(w, dst)
   330  }
   331  
   332  func writeJar(w io.Writer, dir string) error {
   333  	if buildN {
   334  		return nil
   335  	}
   336  	jarw := zip.NewWriter(w)
   337  	jarwcreate := func(name string) (io.Writer, error) {
   338  		if buildV {
   339  			fmt.Fprintf(os.Stderr, "jar: %s\n", name)
   340  		}
   341  		return jarw.Create(name)
   342  	}
   343  	f, err := jarwcreate("META-INF/MANIFEST.MF")
   344  	if err != nil {
   345  		return err
   346  	}
   347  	fmt.Fprintf(f, manifestHeader)
   348  
   349  	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
   350  		if err != nil {
   351  			return err
   352  		}
   353  		if info.IsDir() {
   354  			return nil
   355  		}
   356  		out, err := jarwcreate(filepath.ToSlash(path[len(dir)+1:]))
   357  		if err != nil {
   358  			return err
   359  		}
   360  		in, err := os.Open(path)
   361  		if err != nil {
   362  			return err
   363  		}
   364  		defer in.Close()
   365  		_, err = io.Copy(out, in)
   366  		return err
   367  	})
   368  	if err != nil {
   369  		return err
   370  	}
   371  	return jarw.Close()
   372  }
   373  
   374  // androidAPIPath returns an android SDK platform directory under ANDROID_HOME.
   375  // If there are multiple platforms that satisfy the minimum version requirement
   376  // androidAPIPath returns the latest one among them.
   377  func androidAPIPath() (string, error) {
   378  	sdk := os.Getenv("ANDROID_HOME")
   379  	if sdk == "" {
   380  		return "", fmt.Errorf("ANDROID_HOME environment var is not set")
   381  	}
   382  	sdkDir, err := os.Open(filepath.Join(sdk, "platforms"))
   383  	if err != nil {
   384  		return "", fmt.Errorf("failed to find android SDK platform: %v", err)
   385  	}
   386  	defer sdkDir.Close()
   387  	fis, err := sdkDir.Readdir(-1)
   388  	if err != nil {
   389  		return "", fmt.Errorf("failed to find android SDK platform (API level: %d): %v", buildAndroidAPI, err)
   390  	}
   391  
   392  	var apiPath string
   393  	var apiVer int
   394  	for _, fi := range fis {
   395  		name := fi.Name()
   396  		if !strings.HasPrefix(name, "android-") {
   397  			continue
   398  		}
   399  		n, err := strconv.Atoi(name[len("android-"):])
   400  		if err != nil || n < buildAndroidAPI {
   401  			continue
   402  		}
   403  		p := filepath.Join(sdkDir.Name(), name)
   404  		_, err = os.Stat(filepath.Join(p, "android.jar"))
   405  		if err == nil && apiVer < n {
   406  			apiPath = p
   407  			apiVer = n
   408  		}
   409  	}
   410  	if apiVer == 0 {
   411  		return "", fmt.Errorf("failed to find android SDK platform (API level: %d) in %s",
   412  			buildAndroidAPI, sdkDir.Name())
   413  	}
   414  	return apiPath, nil
   415  }