github.com/adwpc/xmobile@v0.0.0-20231212131043-3f9720cf0e99/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/adwpc/xmobile/internal/sdkpath" 18 "golang.org/x/sync/errgroup" 19 "golang.org/x/tools/go/packages" 20 ) 21 22 func goAndroidBind(gobind string, 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 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.8" 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 modulesUsed, err := areGoModulesUsed() 362 if err != nil { 363 return err 364 } 365 366 srcDir := filepath.Join(tmpdir, "src") 367 368 if modulesUsed { 369 // Copy the source directory for each architecture for concurrent building. 370 newSrcDir := filepath.Join(tmpdir, "src-android-"+arch) 371 if !buildN { 372 if err := doCopyAll(newSrcDir, srcDir); err != nil { 373 return err 374 } 375 } 376 srcDir = newSrcDir 377 378 if err := writeGoMod(srcDir, "android", arch); err != nil { 379 return err 380 } 381 382 // Run `go mod tidy` to force to create go.sum. 383 // Without go.sum, `go build` fails as of Go 1.16. 384 if err := goModTidyAt(srcDir, env); err != nil { 385 return err 386 } 387 } 388 389 toolchain := ndk.Toolchain(arch) 390 if err := goBuildAt( 391 srcDir, 392 "./gobind", 393 env, 394 "-buildmode=c-shared", 395 "-o="+filepath.Join(outputDir, "src", "main", "jniLibs", toolchain.abi, "libgojni.so"), 396 ); err != nil { 397 return err 398 } 399 400 return nil 401 }