package main import ( "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "strings" ) func main() { if err := run(); err != nil { fmt.Fprintf(os.Stderr, "build_android_apk: %v\n", err) os.Exit(1) } } func run() error { workDir, err := os.Getwd() if err != nil { return err } gogioDir, err := commandOutput("go", "list", "-m", "-f", "{{.Dir}}", "gioui.org/cmd") if err != nil { return err } tempDir, err := os.MkdirTemp("", "keepassgo-gogio-") if err != nil { return err } defer os.RemoveAll(tempDir) tempModule := filepath.Join(tempDir, "gioui-cmd") if err := copyDir(gogioDir, tempModule); err != nil { return err } targetFile := filepath.Join(tempModule, "gogio", "androidbuild.go") if err := patchAndroidBuild(targetFile); err != nil { return err } if err := runCommand(tempModule, "gofmt", "-w", targetFile); err != nil { return err } if err := refreshTempModuleDeps(tempModule); err != nil { return err } javaHome := os.Getenv("JAVA_HOME") wrappedJavaHome := javaHome var cleanup func() if major := javaMajor(javaHome); major >= 26 { wrappedJavaHome, cleanup, err = wrapJavaHome(javaHome) if err != nil { return err } defer cleanup() } patchedArgs := append([]string(nil), os.Args[1:]...) if len(patchedArgs) != 0 { last := patchedArgs[len(patchedArgs)-1] if strings.HasPrefix(last, ".") { patchedArgs[len(patchedArgs)-1] = filepath.Join(workDir, last) } } gogioFiles, err := filepath.Glob(filepath.Join(tempModule, "gogio", "*.go")) if err != nil { return err } filteredFiles := make([]string, 0, len(gogioFiles)) for _, file := range gogioFiles { if strings.HasSuffix(file, "_test.go") { continue } filteredFiles = append(filteredFiles, file) } if len(filteredFiles) == 0 { return fmt.Errorf("no gogio go files found in %s", tempModule) } args := append([]string{"run"}, filteredFiles...) args = append(args, patchedArgs...) cmd := exec.Command("go", args...) cmd.Dir = workDir cmd.Env = append(os.Environ(), "JAVA_HOME="+wrappedJavaHome) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func patchAndroidBuild(path string) error { data, err := os.ReadFile(path) if err != nil { return err } content := string(data) content = strings.Replace(content, "\tAppName string\n}", "\tAppName string\n\tApplicationSnippets string\n}", 1, ) content = strings.Replace(content, "\tif err := visitPkg(pkgs[0]); err != nil {\n\t\treturn err\n\t}\n\n\tif err := compileAndroid(tmpDir, tools, bi); err != nil {\n", "\tif err := visitPkg(pkgs[0]); err != nil {\n\t\treturn err\n\t}\n\tmoduleRoot := androidModuleRoot(bi.pkgDir)\n\tmoduleAndroidJars, err := filepath.Glob(filepath.Join(moduleRoot, \"android\", \"*.jar\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\textraJars = append(extraJars, moduleAndroidJars...)\n\n\tif err := compileAndroid(tmpDir, tools, bi); err != nil {\n", 1, ) content = strings.Replace(content, "\terr = os.WriteFile(filepath.Join(v21Dir, \"themes.xml\"), []byte(themesV21), 0660)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresZip := filepath.Join(tmpDir, \"resources.zip\")\n", "\terr = os.WriteFile(filepath.Join(v21Dir, \"themes.xml\"), []byte(themesV21), 0660)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmoduleRoot := androidModuleRoot(bi.pkgDir)\n\tcustomResDir := filepath.Join(moduleRoot, \"android\", \"res\")\n\tif _, err := os.Stat(customResDir); err == nil {\n\t\tif err := copyAndroidDir(customResDir, resDir); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tresZip := filepath.Join(tmpDir, \"resources.zip\")\n", 1, ) content = strings.Replace(content, "\t\tIconSnip: iconSnip,\n\t\tAppName: appName,\n\t}\n", "\t\tIconSnip: iconSnip,\n\t\tAppName: appName,\n\t\tApplicationSnippets: loadAndroidSnippet(filepath.Join(moduleRoot, \"android\", \"application_snippets.xml\")),\n\t}\n", 1, ) content = strings.Replace(content, "\t\t\n\t\n`)\n", "\t\t\n{{.ApplicationSnippets}}\n\t\n`)\n", 1, ) insertPos := strings.Index(content, "func findNDK(") if insertPos < 0 { return fmt.Errorf("findNDK not found") } helpers := ` func loadAndroidSnippet(path string) string { data, err := os.ReadFile(path) if err != nil { return "" } content := strings.TrimSpace(string(data)) if content == "" { return "" } return "\n" + content } func copyAndroidDir(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, path) if err != nil { return err } target := filepath.Join(dst, rel) if info.IsDir() { return os.MkdirAll(target, 0755) } data, err := os.ReadFile(path) if err != nil { return err } return os.WriteFile(target, data, 0660) }) } func androidModuleRoot(start string) string { dir := start for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { return start } dir = parent } } ` content = content[:insertPos] + helpers + content[insertPos:] return os.WriteFile(path, []byte(content), 0o644) } func commandOutput(name string, args ...string) (string, error) { cmd := exec.Command(name, args...) out, err := cmd.Output() if err != nil { var stderr []byte if exitErr, ok := err.(*exec.ExitError); ok { stderr = exitErr.Stderr } return "", fmt.Errorf("%s %s failed: %s%s", name, strings.Join(args, " "), out, stderr) } return strings.TrimSpace(string(out)), nil } func runCommand(dir, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func refreshTempModuleDeps(dir string) error { overrides := []string{ "golang.org/x/image@v0.37.0", "golang.org/x/mod@v0.33.0", "golang.org/x/sync@v0.20.0", "golang.org/x/text@v0.35.0", "golang.org/x/tools@v0.42.0", } args := append([]string{"get"}, overrides...) return runCommand(dir, "go", args...) } func javaMajor(javaHome string) int { if strings.TrimSpace(javaHome) == "" { return 0 } cmd := exec.Command(filepath.Join(javaHome, "bin", "java"), "-version") output, err := cmd.CombinedOutput() if err != nil { return 0 } matches := regexp.MustCompile(`version "([0-9]+)`).FindStringSubmatch(string(output)) if len(matches) != 2 { return 0 } var major int if _, err := fmt.Sscanf(matches[1], "%d", &major); err != nil { return 0 } return major } func wrapJavaHome(javaHome string) (string, func(), error) { tempDir, err := os.MkdirTemp("", "keepassgo-java-home-") if err != nil { return "", nil, err } binDir := filepath.Join(tempDir, "bin") if err := os.MkdirAll(binDir, 0o755); err != nil { os.RemoveAll(tempDir) return "", nil, err } realBin := filepath.Join(javaHome, "bin") if err := os.WriteFile(filepath.Join(binDir, "javac"), []byte("#!/bin/sh\nexport JAVA_TOOL_OPTIONS=-Xint\nexec "+shellQuote(filepath.Join(realBin, "javac"))+" \"$@\"\n"), 0o755); err != nil { os.RemoveAll(tempDir) return "", nil, err } for _, name := range []string{"java", "jar"} { if err := os.WriteFile(filepath.Join(binDir, name), []byte("#!/bin/sh\nexec "+shellQuote(filepath.Join(realBin, name))+" \"$@\"\n"), 0o755); err != nil { os.RemoveAll(tempDir) return "", nil, err } } return tempDir, func() { os.RemoveAll(tempDir) }, nil } func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" } func copyDir(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, path) if err != nil { return err } target := filepath.Join(dst, rel) if info.IsDir() { return os.MkdirAll(target, 0o755) } in, err := os.Open(path) if err != nil { return err } defer in.Close() out, err := os.Create(target) if err != nil { return err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return err } return out.Close() }) }