github.com/adwpc/xmobile@v0.0.0-20231212131043-3f9720cf0e99/cmd/gomobile/build_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  	"bytes"
     9  	"crypto/x509"
    10  	"encoding/base64"
    11  	"encoding/pem"
    12  	"encoding/xml"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"io/ioutil"
    17  	"log"
    18  	"os"
    19  	"path"
    20  	"path/filepath"
    21  	"strings"
    22  
    23  	"github.com/adwpc/xmobile/internal/binres"
    24  	"golang.org/x/tools/go/packages"
    25  )
    26  
    27  func goAndroidBuild(pkg *packages.Package, targets []targetInfo) (map[string]bool, error) {
    28  	ndkRoot, err := ndkRoot(targets...)
    29  	if err != nil {
    30  		return nil, err
    31  	}
    32  	appName := path.Base(pkg.PkgPath)
    33  	libName := androidPkgName(appName)
    34  
    35  	// TODO(hajimehoshi): This works only with Go tools that assume all source files are in one directory.
    36  	// Fix this to work with other Go tools.
    37  	dir := filepath.Dir(pkg.GoFiles[0])
    38  
    39  	manifestPath := filepath.Join(dir, "AndroidManifest.xml")
    40  	manifestData, err := ioutil.ReadFile(manifestPath)
    41  	if err != nil {
    42  		if !os.IsNotExist(err) {
    43  			return nil, err
    44  		}
    45  
    46  		buf := new(bytes.Buffer)
    47  		buf.WriteString(`<?xml version="1.0" encoding="utf-8"?>`)
    48  		err := manifestTmpl.Execute(buf, manifestTmplData{
    49  			// TODO(crawshaw): a better package path.
    50  			JavaPkgPath: "org.golang.todo." + libName,
    51  			Name:        strings.Title(appName),
    52  			LibName:     libName,
    53  		})
    54  		if err != nil {
    55  			return nil, err
    56  		}
    57  		manifestData = buf.Bytes()
    58  		if buildV {
    59  			fmt.Fprintf(os.Stderr, "generated AndroidManifest.xml:\n%s\n", manifestData)
    60  		}
    61  	} else {
    62  		libName, err = manifestLibName(manifestData)
    63  		if err != nil {
    64  			return nil, fmt.Errorf("error parsing %s: %v", manifestPath, err)
    65  		}
    66  	}
    67  
    68  	libFiles := []string{}
    69  	nmpkgs := make(map[string]map[string]bool) // map: arch -> extractPkgs' output
    70  
    71  	for _, t := range targets {
    72  		toolchain := ndk.Toolchain(t.arch)
    73  		libPath := "lib/" + toolchain.abi + "/lib" + libName + ".so"
    74  		libAbsPath := filepath.Join(tmpdir, libPath)
    75  		if err := mkdir(filepath.Dir(libAbsPath)); err != nil {
    76  			return nil, err
    77  		}
    78  		err = goBuild(
    79  			pkg.PkgPath,
    80  			androidEnv[t.arch],
    81  			"-buildmode=c-shared",
    82  			"-o", libAbsPath,
    83  		)
    84  		if err != nil {
    85  			return nil, err
    86  		}
    87  		nmpkgs[t.arch], err = extractPkgs(toolchain.Path(ndkRoot, "nm"), libAbsPath)
    88  		if err != nil {
    89  			return nil, err
    90  		}
    91  		libFiles = append(libFiles, libPath)
    92  	}
    93  
    94  	block, _ := pem.Decode([]byte(debugCert))
    95  	if block == nil {
    96  		return nil, errors.New("no debug cert")
    97  	}
    98  	privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	if buildO == "" {
   104  		buildO = androidPkgName(path.Base(pkg.PkgPath)) + ".apk"
   105  	}
   106  	if !strings.HasSuffix(buildO, ".apk") {
   107  		return nil, fmt.Errorf("output file name %q does not end in '.apk'", buildO)
   108  	}
   109  	var out io.Writer
   110  	if !buildN {
   111  		f, err := os.Create(buildO)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  		defer func() {
   116  			if cerr := f.Close(); err == nil {
   117  				err = cerr
   118  			}
   119  		}()
   120  		out = f
   121  	}
   122  
   123  	var apkw *Writer
   124  	if !buildN {
   125  		apkw = NewWriter(out, privKey)
   126  	}
   127  	apkwCreate := func(name string) (io.Writer, error) {
   128  		if buildV {
   129  			fmt.Fprintf(os.Stderr, "apk: %s\n", name)
   130  		}
   131  		if buildN {
   132  			return ioutil.Discard, nil
   133  		}
   134  		return apkw.Create(name)
   135  	}
   136  	apkwWriteFile := func(dst, src string) error {
   137  		w, err := apkwCreate(dst)
   138  		if err != nil {
   139  			return err
   140  		}
   141  		if !buildN {
   142  			f, err := os.Open(src)
   143  			if err != nil {
   144  				return err
   145  			}
   146  			defer f.Close()
   147  			if _, err := io.Copy(w, f); err != nil {
   148  				return err
   149  			}
   150  		}
   151  		return nil
   152  	}
   153  
   154  	w, err := apkwCreate("classes.dex")
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	dexData, err := base64.StdEncoding.DecodeString(dexStr)
   159  	if err != nil {
   160  		log.Fatalf("internal error bad dexStr: %v", err)
   161  	}
   162  	if _, err := w.Write(dexData); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	for _, libFile := range libFiles {
   167  		if err := apkwWriteFile(libFile, filepath.Join(tmpdir, libFile)); err != nil {
   168  			return nil, err
   169  		}
   170  	}
   171  
   172  	for _, t := range targets {
   173  		toolchain := ndk.Toolchain(t.arch)
   174  		if nmpkgs[t.arch]["github.com/adwpc/xmobile/exp/audio/al"] {
   175  			dst := "lib/" + toolchain.abi + "/libopenal.so"
   176  			src := filepath.Join(gomobilepath, dst)
   177  			if _, err := os.Stat(src); err != nil {
   178  				return nil, errors.New("the Android requires the github.com/adwpc/xmobile/exp/audio/al, but the OpenAL libraries was not found. Please run gomobile init with the -openal flag pointing to an OpenAL source directory.")
   179  			}
   180  			if err := apkwWriteFile(dst, src); err != nil {
   181  				return nil, err
   182  			}
   183  		}
   184  	}
   185  
   186  	// Add any assets.
   187  	var arsc struct {
   188  		iconPath string
   189  	}
   190  	assetsDir := filepath.Join(dir, "assets")
   191  	assetsDirExists := true
   192  	fi, err := os.Stat(assetsDir)
   193  	if err != nil {
   194  		if os.IsNotExist(err) {
   195  			assetsDirExists = false
   196  		} else {
   197  			return nil, err
   198  		}
   199  	} else {
   200  		assetsDirExists = fi.IsDir()
   201  	}
   202  	if assetsDirExists {
   203  		// if assets is a symlink, follow the symlink.
   204  		assetsDir, err = filepath.EvalSymlinks(assetsDir)
   205  		if err != nil {
   206  			return nil, err
   207  		}
   208  		err = filepath.Walk(assetsDir, func(path string, info os.FileInfo, err error) error {
   209  			if err != nil {
   210  				return err
   211  			}
   212  			if name := filepath.Base(path); strings.HasPrefix(name, ".") {
   213  				// Do not include the hidden files.
   214  				return nil
   215  			}
   216  			if info.IsDir() {
   217  				return nil
   218  			}
   219  
   220  			if rel, err := filepath.Rel(assetsDir, path); rel == "icon.png" && err == nil {
   221  				arsc.iconPath = path
   222  				// TODO returning here does not write the assets/icon.png to the final assets output,
   223  				// making it unavailable via the assets API. Should the file be duplicated into assets
   224  				// or should assets API be able to retrieve files from the generated resource table?
   225  				return nil
   226  			}
   227  
   228  			name := "assets/" + path[len(assetsDir)+1:]
   229  			return apkwWriteFile(name, path)
   230  		})
   231  		if err != nil {
   232  			return nil, fmt.Errorf("asset %v", err)
   233  		}
   234  	}
   235  
   236  	bxml, err := binres.UnmarshalXML(bytes.NewReader(manifestData), arsc.iconPath != "")
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  
   241  	// generate resources.arsc identifying single xxxhdpi icon resource.
   242  	if arsc.iconPath != "" {
   243  		pkgname, err := bxml.RawValueByName("manifest", xml.Name{Local: "package"})
   244  		if err != nil {
   245  			return nil, err
   246  		}
   247  		tbl, name := binres.NewMipmapTable(pkgname)
   248  		if err := apkwWriteFile(name, arsc.iconPath); err != nil {
   249  			return nil, err
   250  		}
   251  		w, err := apkwCreate("resources.arsc")
   252  		if err != nil {
   253  			return nil, err
   254  		}
   255  		bin, err := tbl.MarshalBinary()
   256  		if err != nil {
   257  			return nil, err
   258  		}
   259  		if _, err := w.Write(bin); err != nil {
   260  			return nil, err
   261  		}
   262  	}
   263  
   264  	w, err = apkwCreate("AndroidManifest.xml")
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	bin, err := bxml.MarshalBinary()
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  	if _, err := w.Write(bin); err != nil {
   273  		return nil, err
   274  	}
   275  
   276  	// TODO: add gdbserver to apk?
   277  
   278  	if !buildN {
   279  		if err := apkw.Close(); err != nil {
   280  			return nil, err
   281  		}
   282  	}
   283  
   284  	// TODO: return nmpkgs
   285  	return nmpkgs[targets[0].arch], nil
   286  }
   287  
   288  // androidPkgName sanitizes the go package name to be acceptable as a android
   289  // package name part. The android package name convention is similar to the
   290  // java package name convention described in
   291  // https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.5.3.1
   292  // but not exactly same.
   293  func androidPkgName(name string) string {
   294  	var res []rune
   295  	for _, r := range name {
   296  		switch {
   297  		case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z', '0' <= r && r <= '9':
   298  			res = append(res, r)
   299  		default:
   300  			res = append(res, '_')
   301  		}
   302  	}
   303  	if len(res) == 0 || res[0] == '_' || ('0' <= res[0] && res[0] <= '9') {
   304  		// Android does not seem to allow the package part starting with _.
   305  		res = append([]rune{'g', 'o'}, res...)
   306  	}
   307  	s := string(res)
   308  	// Look for Java keywords that are not Go keywords, and avoid using
   309  	// them as a package name.
   310  	//
   311  	// This is not a problem for normal Go identifiers as we only expose
   312  	// exported symbols. The upper case first letter saves everything
   313  	// from accidentally matching except for the package name.
   314  	//
   315  	// Note that basic type names (like int) are not keywords in Go.
   316  	switch s {
   317  	case "abstract", "assert", "boolean", "byte", "catch", "char", "class",
   318  		"do", "double", "enum", "extends", "final", "finally", "float",
   319  		"implements", "instanceof", "int", "long", "native", "private",
   320  		"protected", "public", "short", "static", "strictfp", "super",
   321  		"synchronized", "this", "throw", "throws", "transient", "try",
   322  		"void", "volatile", "while":
   323  		s += "_"
   324  	}
   325  	return s
   326  }
   327  
   328  // A random uninteresting private key.
   329  // Must be consistent across builds so newer app versions can be installed.
   330  const debugCert = `
   331  -----BEGIN RSA PRIVATE KEY-----
   332  MIIEowIBAAKCAQEAy6ItnWZJ8DpX9R5FdWbS9Kr1U8Z7mKgqNByGU7No99JUnmyu
   333  NQ6Uy6Nj0Gz3o3c0BXESECblOC13WdzjsH1Pi7/L9QV8jXOXX8cvkG5SJAyj6hcO
   334  LOapjDiN89NXjXtyv206JWYvRtpexyVrmHJgRAw3fiFI+m4g4Qop1CxcIF/EgYh7
   335  rYrqh4wbCM1OGaCleQWaOCXxZGm+J5YNKQcWpjZRrDrb35IZmlT0bK46CXUKvCqK
   336  x7YXHgfhC8ZsXCtsScKJVHs7gEsNxz7A0XoibFw6DoxtjKzUCktnT0w3wxdY7OTj
   337  9AR8mobFlM9W3yirX8TtwekWhDNTYEu8dwwykwIDAQABAoIBAA2hjpIhvcNR9H9Z
   338  BmdEecydAQ0ZlT5zy1dvrWI++UDVmIp+Ve8BSd6T0mOqV61elmHi3sWsBN4M1Rdz
   339  3N38lW2SajG9q0fAvBpSOBHgAKmfGv3Ziz5gNmtHgeEXfZ3f7J95zVGhlHqWtY95
   340  JsmuplkHxFMyITN6WcMWrhQg4A3enKLhJLlaGLJf9PeBrvVxHR1/txrfENd2iJBH
   341  FmxVGILL09fIIktJvoScbzVOneeWXj5vJGzWVhB17DHBbANGvVPdD5f+k/s5aooh
   342  hWAy/yLKocr294C4J+gkO5h2zjjjSGcmVHfrhlXQoEPX+iW1TGoF8BMtl4Llc+jw
   343  lKWKfpECgYEA9C428Z6CvAn+KJ2yhbAtuRo41kkOVoiQPtlPeRYs91Pq4+NBlfKO
   344  2nWLkyavVrLx4YQeCeaEU2Xoieo9msfLZGTVxgRlztylOUR+zz2FzDBYGicuUD3s
   345  EqC0Wv7tiX6dumpWyOcVVLmR9aKlOUzA9xemzIsWUwL3PpyONhKSq7kCgYEA1X2F
   346  f2jKjoOVzglhtuX4/SP9GxS4gRf9rOQ1Q8DzZhyH2LZ6Dnb1uEQvGhiqJTU8CXxb
   347  7odI0fgyNXq425Nlxc1Tu0G38TtJhwrx7HWHuFcbI/QpRtDYLWil8Zr7Q3BT9rdh
   348  moo4m937hLMvqOG9pyIbyjOEPK2WBCtKW5yabqsCgYEAu9DkUBr1Qf+Jr+IEU9I8
   349  iRkDSMeusJ6gHMd32pJVCfRRQvIlG1oTyTMKpafmzBAd/rFpjYHynFdRcutqcShm
   350  aJUq3QG68U9EAvWNeIhA5tr0mUEz3WKTt4xGzYsyWES8u4tZr3QXMzD9dOuinJ1N
   351  +4EEumXtSPKKDG3M8Qh+KnkCgYBUEVSTYmF5EynXc2xOCGsuy5AsrNEmzJqxDUBI
   352  SN/P0uZPmTOhJIkIIZlmrlW5xye4GIde+1jajeC/nG7U0EsgRAV31J4pWQ5QJigz
   353  0+g419wxIUFryGuIHhBSfpP472+w1G+T2mAGSLh1fdYDq7jx6oWE7xpghn5vb9id
   354  EKLjdwKBgBtz9mzbzutIfAW0Y8F23T60nKvQ0gibE92rnUbjPnw8HjL3AZLU05N+
   355  cSL5bhq0N5XHK77sscxW9vXjG0LJMXmFZPp9F6aV6ejkMIXyJ/Yz/EqeaJFwilTq
   356  Mc6xR47qkdzu0dQ1aPm4XD7AWDtIvPo/GG2DKOucLBbQc2cOWtKS
   357  -----END RSA PRIVATE KEY-----
   358  `