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