From b7dc407dbef838c100eaa182265c16a43b46da48 Mon Sep 17 00:00:00 2001 From: Inkeliz Date: Wed, 24 Mar 2021 14:54:10 +0000 Subject: [PATCH] cmd/gogio: [android] add support for AAB That patch makes possible to generate Android App Bundle (`.aab`) instead of APK. In order to generate AAB use `-o outputfile.aab`. Signed-off-by: Inkeliz --- cmd/gogio/androidbuild.go | 156 +++++++++++++++++++++++++++++++------- cmd/gogio/help.go | 2 +- 2 files changed, 128 insertions(+), 30 deletions(-) diff --git a/cmd/gogio/androidbuild.go b/cmd/gogio/androidbuild.go index 937d30af..c1181377 100644 --- a/cmd/gogio/androidbuild.go +++ b/cmd/gogio/androidbuild.go @@ -153,10 +153,27 @@ func buildAndroid(tmpDir string, bi *buildInfo) error { case "archive": return archiveAndroid(tmpDir, bi, perms) case "exe": - if err := exeAndroid(tmpDir, tools, bi, extraJars, perms); err != nil { + file := *destPath + if file == "" { + file = fmt.Sprintf("%s.apk", bi.name) + } + + isBundle := false + switch filepath.Ext(file) { + case ".apk": + case ".aab": + isBundle = true + default: + return fmt.Errorf("the specified output %q does not end in '.apk' or '.aab'", file) + } + + if err := exeAndroid(tmpDir, tools, bi, extraJars, perms, isBundle); err != nil { return err } - return signAPK(tmpDir, tools, bi) + if isBundle { + return signAAB(tmpDir, file, tools, bi) + } + return signAPK(tmpDir, file, tools, bi) default: panic("unreachable") } @@ -316,7 +333,7 @@ func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) { return aarw.Close() } -func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, perms []string) (err error) { +func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, perms []string, isBundle bool) (err error) { classes := filepath.Join(tmpDir, "classes") var classFiles []string err = filepath.Walk(classes, func(path string, f os.FileInfo, err error) error { @@ -441,20 +458,25 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe } linkAPK := filepath.Join(tmpDir, "link.apk") - link := exec.Command( - aapt2, + + args := []string{ "link", "--manifest", manifest, "-I", tools.androidjar, "-o", linkAPK, - resZip, - ) - if _, err := runCmd(link); err != nil { + } + if isBundle { + args = append(args, "--proto-format") + } + args = append(args, resZip) + + if _, err := runCmd(exec.Command(aapt2, args...)); err != nil { return err } + // The Go standard library archive/zip doesn't support appending to zip // files. Copy files from `link.apk` (generated by aapt2) along with classes.dex and - // the Go libraries to a new `app.ap_` file. + // the Go libraries to a new `app.zip` file. // Load link.apk as zip. linkAPKZip, err := zip.OpenReader(linkAPK) @@ -464,7 +486,7 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe defer linkAPKZip.Close() // Create new "APK". - unsignedAPK := filepath.Join(tmpDir, "app.ap_") + unsignedAPK := filepath.Join(tmpDir, "app.zip") unsignedAPKFile, err := os.Create(unsignedAPK) if err != nil { return err @@ -484,6 +506,14 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe Method: f.FileHeader.Method, } + if isBundle { + // AAB have pre-defined folders. + switch header.Name { + case "AndroidManifest.xml": + header.Name = "manifest/AndroidManifest.xml" + } + } + w, err := unsignedAPKZip.CreateHeader(&header) if err != nil { return err @@ -525,28 +555,19 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe } // Append classes.dex. - if err := appendToZip("classes.dex", filepath.Join(dexDir, "classes.dex")); err != nil { + classesFolder := "classes.dex" + if isBundle { + classesFolder = "dex/classes.dex" + } + if err := appendToZip(classesFolder, filepath.Join(dexDir, "classes.dex")); err != nil { return err } + return unsignedAPKZip.Close() } -func signAPK(tmpDir string, tools *androidTools, bi *buildInfo) error { - apkFile := *destPath - if apkFile == "" { - apkFile = fmt.Sprintf("%s.apk", bi.name) - } - if filepath.Ext(apkFile) != ".apk" { - return fmt.Errorf("the specified output %q does not end in '.apk'", apkFile) - } - _, err := runCmd(exec.Command( - filepath.Join(tools.buildtools, "zipalign"), - "-f", - "4", // 32-bit alignment. - filepath.Join(tmpDir, "app.ap_"), - apkFile, - )) - if err != nil { +func signAPK(tmpDir string, apkFile string, tools *androidTools, bi *buildInfo) error { + if err := zipalign(tools, filepath.Join(tmpDir, "app.zip"), apkFile); err != nil { return err } @@ -556,17 +577,94 @@ func signAPK(tmpDir string, tools *androidTools, bi *buildInfo) error { } } - _, err = runCmd(exec.Command( + _, err := runCmd(exec.Command( filepath.Join(tools.buildtools, "apksigner"), "sign", "--ks-pass", "pass:"+bi.password, "--ks", bi.key, apkFile, )) + + return err +} + +func signAAB(tmpDir string, aabFile string, tools *androidTools, bi *buildInfo) error { + allBundleTools, err := filepath.Glob(filepath.Join(tools.buildtools, "bundletool*.jar")) if err != nil { return err } - return nil + + bundletool := "" + for _, v := range allBundleTools { + bundletool = v + break + } + + if bundletool == "" { + return fmt.Errorf("bundletool was not found at %s. Download it from https://github.com/google/bundletool/releases and move to the respective folder", tools.buildtools) + } + + _, err = runCmd(exec.Command( + "java", + "-jar", bundletool, + "build-bundle", + "--modules="+filepath.Join(tmpDir, "app.zip"), + "--output="+filepath.Join(tmpDir, "app.aab"), + )) + if err != nil { + return err + } + + if err := zipalign(tools, filepath.Join(tmpDir, "app.aab"), aabFile); err != nil { + return err + } + + if bi.key == "" { + if err := defaultAndroidKeystore(tmpDir, bi); err != nil { + return err + } + } + + keytoolList, err := runCmd(exec.Command( + "keytool", + "-keystore", bi.key, + "-list", + "-keypass", bi.password, + "-v", + )) + if err != nil { + return err + } + + var alias string + for _, t := range strings.Split(keytoolList, "\n") { + if i, _ := fmt.Sscanf(t, "Alias name: %s", &alias); i > 0 { + break + } + } + + _, err = runCmd(exec.Command( + filepath.Join("jarsigner"), + "-sigalg", "SHA256withRSA", + "-digestalg", "SHA-256", + "-keystore", bi.key, + "-storepass", bi.password, + aabFile, + strings.TrimSpace(alias), + )) + + return err +} + +func zipalign(tools *androidTools, input, output string) error { + _, err := runCmd(exec.Command( + filepath.Join(tools.buildtools, "zipalign"), + "-f", + "4", // 32-bit alignment. + input, + output, + )) + return err } func defaultAndroidKeystore(tmpDir string, bi *buildInfo) error { diff --git a/cmd/gogio/help.go b/cmd/gogio/help.go index 11990412..ec9be8a2 100644 --- a/cmd/gogio/help.go +++ b/cmd/gogio/help.go @@ -60,7 +60,7 @@ its deletion. The -x flag will print all the external commands executed by the gogio tool. -The -signkey flag specifies the path of the keystore, used for signing Android apk files. +The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files. The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided. `