github.com/SpiderOak/mobile@v0.0.0-20221129182558-6f541b59af45/cmd/gomobile/bind.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 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "strings" 17 "sync" 18 19 "github.com/SpiderOak/mobile/internal/sdkpath" 20 "golang.org/x/mod/modfile" 21 "golang.org/x/tools/go/packages" 22 ) 23 24 var cmdBind = &command{ 25 run: runBind, 26 Name: "bind", 27 Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]", 28 Short: "build a library for Android and iOS", 29 Long: ` 30 Bind generates language bindings for the package named by the import 31 path, and compiles a library for the named target system. 32 33 The -target flag takes either android (the default), or one or more 34 comma-delimited Apple platforms (` + strings.Join(applePlatforms, ", ") + `). 35 36 For -target android, the bind command produces an AAR (Android ARchive) 37 file that archives the precompiled Java API stub classes, the compiled 38 shared libraries, and all asset files in the /assets subdirectory under 39 the package directory. The output is named '<package_name>.aar' by 40 default. This AAR file is commonly used for binary distribution of an 41 Android library project and most Android IDEs support AAR import. For 42 example, in Android Studio (1.2+), an AAR file can be imported using 43 the module import wizard (File > New > New Module > Import .JAR or 44 .AAR package), and setting it as a new dependency 45 (File > Project Structure > Dependencies). This requires 'javac' 46 (version 1.7+) and Android SDK (API level 16 or newer) to build the 47 library for Android. The ANDROID_HOME and ANDROID_NDK_HOME environment 48 variables can be used to specify the Android SDK and NDK if they are 49 not in the default locations. Use the -javapkg flag to specify the Java 50 package prefix for the generated classes. 51 52 By default, -target=android builds shared libraries for all supported 53 instruction sets (arm, arm64, 386, amd64). A subset of instruction sets 54 can be selected by specifying target type with the architecture name. E.g., 55 -target=android/arm,android/386. 56 57 For Apple -target platforms, gomobile must be run on an OS X machine with 58 Xcode installed. The generated Objective-C types can be prefixed with the 59 -prefix flag. 60 61 For -target android, the -bootclasspath and -classpath flags are used to 62 control the bootstrap classpath and the classpath for Go wrappers to Java 63 classes. 64 65 The -v flag provides verbose output, including the list of packages built. 66 67 The build flags -a, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work 68 are shared with the build command. For documentation, see 'go help build'. 69 `, 70 } 71 72 func runBind(cmd *command) error { 73 cleanup, err := buildEnvInit() 74 if err != nil { 75 return err 76 } 77 defer cleanup() 78 79 args := cmd.flag.Args() 80 81 targets, err := parseBuildTarget(buildTarget) 82 if err != nil { 83 return fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err) 84 } 85 86 if isAndroidPlatform(targets[0].platform) { 87 if bindPrefix != "" { 88 return fmt.Errorf("-prefix is supported only for Apple targets") 89 } 90 if _, err := ndkRoot(targets[0]); err != nil { 91 return err 92 } 93 } else { 94 if bindJavaPkg != "" { 95 return fmt.Errorf("-javapkg is supported only for android target") 96 } 97 } 98 99 if len(args) == 0 { 100 args = append(args, ".") 101 } 102 103 // TODO(ydnar): this should work, unless build tags affect loading a single package. 104 // Should we try to import packages with different build tags per platform? 105 pkgs, err := packages.Load(packagesConfig(targets[0]), args...) 106 if err != nil { 107 return err 108 } 109 110 // check if any of the package is main 111 for _, pkg := range pkgs { 112 if pkg.Name == "main" { 113 return fmt.Errorf(`binding "main" package (%s) is not supported`, pkg.PkgPath) 114 } 115 } 116 117 switch { 118 case isAndroidPlatform(targets[0].platform): 119 return goAndroidBind(pkgs, targets) 120 case isApplePlatform(targets[0].platform): 121 if !xcodeAvailable() { 122 return fmt.Errorf("-target=%q requires Xcode", buildTarget) 123 } 124 return goAppleBind(pkgs, targets) 125 default: 126 return fmt.Errorf(`invalid -target=%q`, buildTarget) 127 } 128 } 129 130 var ( 131 bindPrefix string // -prefix 132 bindJavaPkg string // -javapkg 133 bindClasspath string // -classpath 134 bindBootClasspath string // -bootclasspath 135 ) 136 137 func init() { 138 // bind command specific commands. 139 cmdBind.flag.StringVar(&bindJavaPkg, "javapkg", "", 140 "specifies custom Java package path prefix. Valid only with -target=android.") 141 cmdBind.flag.StringVar(&bindPrefix, "prefix", "", 142 "custom Objective-C name prefix. Valid only with -target=ios.") 143 cmdBind.flag.StringVar(&bindClasspath, "classpath", "", "The classpath for imported Java classes. Valid only with -target=android.") 144 cmdBind.flag.StringVar(&bindBootClasspath, "bootclasspath", "", "The bootstrap classpath for imported Java classes. Valid only with -target=android.") 145 } 146 147 func bootClasspath() (string, error) { 148 if bindBootClasspath != "" { 149 return bindBootClasspath, nil 150 } 151 apiPath, err := sdkpath.AndroidAPIPath(buildAndroidAPI) 152 if err != nil { 153 return "", err 154 } 155 return filepath.Join(apiPath, "android.jar"), nil 156 } 157 158 func copyFile(dst, src string) error { 159 if buildX { 160 printcmd("cp %s %s", src, dst) 161 } 162 return writeFile(dst, func(w io.Writer) error { 163 if buildN { 164 return nil 165 } 166 f, err := os.Open(src) 167 if err != nil { 168 return err 169 } 170 defer f.Close() 171 172 if _, err := io.Copy(w, f); err != nil { 173 return fmt.Errorf("cp %s %s failed: %v", src, dst, err) 174 } 175 return nil 176 }) 177 } 178 179 func writeFile(filename string, generate func(io.Writer) error) error { 180 if buildV { 181 fmt.Fprintf(os.Stderr, "write %s\n", filename) 182 } 183 184 if err := mkdir(filepath.Dir(filename)); err != nil { 185 return err 186 } 187 188 if buildN { 189 return generate(ioutil.Discard) 190 } 191 192 f, err := os.Create(filename) 193 if err != nil { 194 return err 195 } 196 defer func() { 197 if cerr := f.Close(); err == nil { 198 err = cerr 199 } 200 }() 201 202 return generate(f) 203 } 204 205 func packagesConfig(t targetInfo) *packages.Config { 206 config := &packages.Config{} 207 // Add CGO_ENABLED=1 explicitly since Cgo is disabled when GOOS is different from host OS. 208 config.Env = append(os.Environ(), "GOARCH="+t.arch, "GOOS="+platformOS(t.platform), "CGO_ENABLED=1") 209 tags := append(buildTags[:], platformTags(t.platform)...) 210 211 if len(tags) > 0 { 212 config.BuildFlags = []string{"-tags=" + strings.Join(tags, ",")} 213 } 214 return config 215 } 216 217 // getModuleVersions returns a module information at the directory src. 218 func getModuleVersions(targetPlatform string, targetArch string, src string) (*modfile.File, error) { 219 cmd := exec.Command("go", "list") 220 cmd.Env = append(os.Environ(), "GOOS="+platformOS(targetPlatform), "GOARCH="+targetArch) 221 222 tags := append(buildTags[:], platformTags(targetPlatform)...) 223 224 // TODO(hyangah): probably we don't need to add all the dependencies. 225 cmd.Args = append(cmd.Args, "-m", "-json", "-tags="+strings.Join(tags, ","), "all") 226 cmd.Dir = src 227 if buildX || buildN { 228 printcmd("PWD=%s GOOS=%s GOARCH=%s %s", 229 src, platformOS(targetPlatform), targetArch, cmd) 230 } 231 232 output, err := cmd.Output() 233 if err != nil { 234 // Module information is not available at src. 235 return nil, nil 236 } 237 238 type Module struct { 239 Main bool 240 Path string 241 Version string 242 Dir string 243 Replace *Module 244 } 245 246 f := &modfile.File{} 247 f.AddModuleStmt("gobind") 248 e := json.NewDecoder(bytes.NewReader(output)) 249 for { 250 var mod *Module 251 err := e.Decode(&mod) 252 if err != nil && err != io.EOF { 253 return nil, err 254 } 255 if mod != nil { 256 if mod.Replace != nil { 257 p, v := mod.Replace.Path, mod.Replace.Version 258 if modfile.IsDirectoryPath(p) { 259 // replaced by a local directory 260 p = mod.Replace.Dir 261 } 262 f.AddReplace(mod.Path, mod.Version, p, v) 263 } else { 264 // When the version part is empty, the module is local and mod.Dir represents the location. 265 if v := mod.Version; v == "" { 266 f.AddReplace(mod.Path, mod.Version, mod.Dir, "") 267 } else { 268 f.AddRequire(mod.Path, v) 269 } 270 } 271 } 272 if err == io.EOF { 273 break 274 } 275 } 276 return f, nil 277 } 278 279 // writeGoMod writes go.mod file at dir when Go modules are used. 280 func writeGoMod(pkgModPath string, workSrc string) error { 281 m, err := areGoModulesUsed() 282 if err != nil { 283 return err 284 } 285 // If Go modules are not used, go.mod should not be created because the dependencies might not be compatible with Go modules. 286 if !m { 287 return nil 288 } 289 290 buf, err := os.ReadFile(pkgModPath) 291 if err != nil { 292 return err 293 } 294 f, err := modfile.Parse(pkgModPath, buf, nil) 295 if err != nil { 296 return err 297 } 298 299 pkgDir := filepath.Dir(pkgModPath) 300 301 // Update existing replace directives to use absolute paths. 302 for _, r := range f.Replace { 303 newPath := r.New.Path 304 if r.New.Version == "" && 305 (!filepath.IsAbs(newPath) || 306 strings.HasPrefix(newPath, "./") || 307 strings.HasPrefix(newPath, "../")) { 308 newPath = filepath.Join(pkgDir, newPath) 309 } 310 err := f.AddReplace(r.Old.Path, r.Old.Version, newPath, r.New.Version) 311 if err != nil { 312 return err 313 } 314 } 315 316 // Add a replace directive for module itself. 317 v := f.Module.Mod 318 err = f.AddReplace(v.Path, v.Version, pkgDir, "") 319 if err != nil { 320 return err 321 } 322 323 // Replace the existing "module ..." statement with "module 324 // gobind". 325 err = f.AddModuleStmt("gobind") 326 if err != nil { 327 return err 328 } 329 330 buf, err = f.Format() 331 if err != nil { 332 return err 333 } 334 newModPath := filepath.Join(workSrc, "go.mod") 335 return os.WriteFile(newModPath, buf, 0644) 336 } 337 338 var ( 339 goModPathCache struct { 340 path string 341 err error 342 } 343 goModPathOnce sync.Once 344 ) 345 346 func goModPath() (string, error) { 347 goModPathOnce.Do(func() { 348 out, err := exec.Command("go", "env", "GOMOD").Output() 349 if err != nil { 350 goModPathCache.err = err 351 return 352 } 353 goModPathCache.path = strings.TrimSpace(string(out)) 354 }) 355 return goModPathCache.path, goModPathCache.err 356 } 357 358 func areGoModulesUsed() (bool, error) { 359 path, err := goModPath() 360 if err != nil { 361 return false, err 362 } 363 return path != "", nil 364 }