github.com/adwpc/xmobile@v0.0.0-20231212131043-3f9720cf0e99/cmd/gomobile/build.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 //go:generate go run gendex.go -o dex.go 6 7 package main 8 9 import ( 10 "bufio" 11 "errors" 12 "fmt" 13 "io" 14 "os" 15 "os/exec" 16 "regexp" 17 "strconv" 18 "strings" 19 20 "github.com/adwpc/xmobile/internal/sdkpath" 21 "golang.org/x/tools/go/packages" 22 ) 23 24 var tmpdir string 25 26 var cmdBuild = &command{ 27 run: runBuild, 28 Name: "build", 29 Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-o output] [-bundleid bundleID] [build flags] [package]", 30 Short: "compile android APK and iOS app", 31 Long: ` 32 Build compiles and encodes the app named by the import path. 33 34 The named package must define a main function. 35 36 The -target flag takes either android (the default), or one or more 37 comma-delimited Apple platforms (` + strings.Join(applePlatforms, ", ") + `). 38 39 For -target android, if an AndroidManifest.xml is defined in the 40 package directory, it is added to the APK output. Otherwise, a default 41 manifest is generated. By default, this builds a fat APK for all supported 42 instruction sets (arm, 386, amd64, arm64). A subset of instruction sets can 43 be selected by specifying target type with the architecture name. E.g. 44 -target=android/arm,android/386. 45 46 For Apple -target platforms, gomobile must be run on an OS X machine with 47 Xcode installed. 48 49 By default, -target ios will generate an XCFramework for both ios 50 and iossimulator. Multiple Apple targets can be specified, creating a "fat" 51 XCFramework with each slice. To generate a fat XCFramework that supports 52 iOS, macOS, and macCatalyst for all supportec architectures (amd64 and arm64), 53 specify -target ios,macos,maccatalyst. A subset of instruction sets can be 54 selectged by specifying the platform with an architecture name. E.g. 55 -target=ios/arm64,maccatalyst/arm64. 56 57 If the package directory contains an assets subdirectory, its contents 58 are copied into the output. 59 60 Flag -iosversion sets the minimal version of the iOS SDK to compile against. 61 The default version is 13.0. 62 63 Flag -androidapi sets the Android API version to compile against. 64 The default and minimum is 16. 65 66 The -bundleid flag is required for -target ios and sets the bundle ID to use 67 with the app. 68 69 The -o flag specifies the output file name. If not specified, the 70 output file name depends on the package built. 71 72 The -v flag provides verbose output, including the list of packages built. 73 74 The build flags -a, -i, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work are 75 shared with the build command. For documentation, see 'go help build'. 76 `, 77 } 78 79 func runBuild(cmd *command) (err error) { 80 _, err = runBuildImpl(cmd) 81 return 82 } 83 84 // runBuildImpl builds a package for mobiles based on the given commands. 85 // runBuildImpl returns a built package information and an error if exists. 86 func runBuildImpl(cmd *command) (*packages.Package, error) { 87 cleanup, err := buildEnvInit() 88 if err != nil { 89 return nil, err 90 } 91 defer cleanup() 92 93 args := cmd.flag.Args() 94 95 targets, err := parseBuildTarget(buildTarget) 96 if err != nil { 97 return nil, fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err) 98 } 99 100 var buildPath string 101 switch len(args) { 102 case 0: 103 buildPath = "." 104 case 1: 105 buildPath = args[0] 106 default: 107 cmd.usage() 108 os.Exit(1) 109 } 110 111 // TODO(ydnar): this should work, unless build tags affect loading a single package. 112 // Should we try to import packages with different build tags per platform? 113 pkgs, err := packages.Load(packagesConfig(targets[0]), buildPath) 114 if err != nil { 115 return nil, err 116 } 117 118 // len(pkgs) can be more than 1 e.g., when the specified path includes `...`. 119 if len(pkgs) != 1 { 120 cmd.usage() 121 os.Exit(1) 122 } 123 124 pkg := pkgs[0] 125 126 if pkg.Name != "main" && buildO != "" { 127 return nil, fmt.Errorf("cannot set -o when building non-main package") 128 } 129 130 var nmpkgs map[string]bool 131 switch { 132 case isAndroidPlatform(targets[0].platform): 133 if pkg.Name != "main" { 134 for _, t := range targets { 135 if err := goBuild(pkg.PkgPath, androidEnv[t.arch]); err != nil { 136 return nil, err 137 } 138 } 139 return pkg, nil 140 } 141 nmpkgs, err = goAndroidBuild(pkg, targets) 142 if err != nil { 143 return nil, err 144 } 145 case isApplePlatform(targets[0].platform): 146 if !xcodeAvailable() { 147 return nil, fmt.Errorf("-target=%s requires XCode", buildTarget) 148 } 149 if pkg.Name != "main" { 150 for _, t := range targets { 151 // Catalyst support requires iOS 13+ 152 v, _ := strconv.ParseFloat(buildIOSVersion, 64) 153 if t.platform == "maccatalyst" && v < 13.0 { 154 return nil, errors.New("catalyst requires -iosversion=13 or higher") 155 } 156 if err := goBuild(pkg.PkgPath, appleEnv[t.String()]); err != nil { 157 return nil, err 158 } 159 } 160 return pkg, nil 161 } 162 if buildBundleID == "" { 163 return nil, fmt.Errorf("-target=ios requires -bundleid set") 164 } 165 nmpkgs, err = goAppleBuild(pkg, buildBundleID, targets) 166 if err != nil { 167 return nil, err 168 } 169 } 170 171 if !nmpkgs["github.com/adwpc/xmobile/app"] { 172 return nil, fmt.Errorf(`%s does not import "github.com/adwpc/xmobile/app"`, pkg.PkgPath) 173 } 174 175 return pkg, nil 176 } 177 178 var nmRE = regexp.MustCompile(`[0-9a-f]{8} t _?(?:.*/vendor/)?(golang.org/x.*/[^.]*)`) 179 180 func extractPkgs(nm string, path string) (map[string]bool, error) { 181 if buildN { 182 return map[string]bool{"github.com/adwpc/xmobile/app": true}, nil 183 } 184 r, w := io.Pipe() 185 cmd := exec.Command(nm, path) 186 cmd.Stdout = w 187 cmd.Stderr = os.Stderr 188 189 nmpkgs := make(map[string]bool) 190 errc := make(chan error, 1) 191 go func() { 192 s := bufio.NewScanner(r) 193 for s.Scan() { 194 if res := nmRE.FindStringSubmatch(s.Text()); res != nil { 195 nmpkgs[res[1]] = true 196 } 197 } 198 errc <- s.Err() 199 }() 200 201 err := cmd.Run() 202 w.Close() 203 if err != nil { 204 return nil, fmt.Errorf("%s %s: %v", nm, path, err) 205 } 206 if err := <-errc; err != nil { 207 return nil, fmt.Errorf("%s %s: %v", nm, path, err) 208 } 209 return nmpkgs, nil 210 } 211 212 var xout io.Writer = os.Stderr 213 214 func printcmd(format string, args ...interface{}) { 215 cmd := fmt.Sprintf(format+"\n", args...) 216 if tmpdir != "" { 217 cmd = strings.Replace(cmd, tmpdir, "$WORK", -1) 218 } 219 if androidHome, err := sdkpath.AndroidHome(); err == nil { 220 cmd = strings.Replace(cmd, androidHome, "$ANDROID_HOME", -1) 221 } 222 if gomobilepath != "" { 223 cmd = strings.Replace(cmd, gomobilepath, "$GOMOBILE", -1) 224 } 225 if gopath := goEnv("GOPATH"); gopath != "" { 226 cmd = strings.Replace(cmd, gopath, "$GOPATH", -1) 227 } 228 if env := os.Getenv("HOMEPATH"); env != "" { 229 cmd = strings.Replace(cmd, env, "$HOMEPATH", -1) 230 } 231 fmt.Fprint(xout, cmd) 232 } 233 234 // "Build flags", used by multiple commands. 235 var ( 236 buildA bool // -a 237 buildI bool // -i 238 buildN bool // -n 239 buildV bool // -v 240 buildX bool // -x 241 buildO string // -o 242 buildGcflags string // -gcflags 243 buildLdflags string // -ldflags 244 buildTarget string // -target 245 buildTrimpath bool // -trimpath 246 buildWork bool // -work 247 buildBundleID string // -bundleid 248 buildIOSVersion string // -iosversion 249 buildAndroidAPI int // -androidapi 250 buildTags stringsFlag // -tags 251 ) 252 253 func addBuildFlags(cmd *command) { 254 cmd.flag.StringVar(&buildO, "o", "", "") 255 cmd.flag.StringVar(&buildGcflags, "gcflags", "", "") 256 cmd.flag.StringVar(&buildLdflags, "ldflags", "", "") 257 cmd.flag.StringVar(&buildTarget, "target", "android", "") 258 cmd.flag.StringVar(&buildBundleID, "bundleid", "", "") 259 cmd.flag.StringVar(&buildIOSVersion, "iosversion", "13.0", "") 260 cmd.flag.IntVar(&buildAndroidAPI, "androidapi", minAndroidAPI, "") 261 262 cmd.flag.BoolVar(&buildA, "a", false, "") 263 cmd.flag.BoolVar(&buildI, "i", false, "") 264 cmd.flag.BoolVar(&buildTrimpath, "trimpath", false, "") 265 cmd.flag.Var(&buildTags, "tags", "") 266 } 267 268 func addBuildFlagsNVXWork(cmd *command) { 269 cmd.flag.BoolVar(&buildN, "n", false, "") 270 cmd.flag.BoolVar(&buildV, "v", false, "") 271 cmd.flag.BoolVar(&buildX, "x", false, "") 272 cmd.flag.BoolVar(&buildWork, "work", false, "") 273 } 274 275 func init() { 276 addBuildFlags(cmdBuild) 277 addBuildFlagsNVXWork(cmdBuild) 278 279 addBuildFlags(cmdInstall) 280 addBuildFlagsNVXWork(cmdInstall) 281 282 addBuildFlagsNVXWork(cmdInit) 283 284 addBuildFlags(cmdBind) 285 addBuildFlagsNVXWork(cmdBind) 286 287 addBuildFlagsNVXWork(cmdClean) 288 } 289 290 func goBuild(src string, env []string, args ...string) error { 291 return goCmd("build", []string{src}, env, args...) 292 } 293 294 func goBuildAt(at string, src string, env []string, args ...string) error { 295 return goCmdAt(at, "build", []string{src}, env, args...) 296 } 297 298 func goInstall(srcs []string, env []string, args ...string) error { 299 return goCmd("install", srcs, env, args...) 300 } 301 302 func goCmd(subcmd string, srcs []string, env []string, args ...string) error { 303 return goCmdAt("", subcmd, srcs, env, args...) 304 } 305 306 func goCmdAt(at string, subcmd string, srcs []string, env []string, args ...string) error { 307 cmd := exec.Command("go", subcmd) 308 tags := buildTags 309 if len(tags) > 0 { 310 cmd.Args = append(cmd.Args, "-tags", strings.Join(tags, ",")) 311 } 312 if buildV { 313 cmd.Args = append(cmd.Args, "-v") 314 } 315 if subcmd != "install" && buildI { 316 cmd.Args = append(cmd.Args, "-i") 317 } 318 if buildX { 319 cmd.Args = append(cmd.Args, "-x") 320 } 321 if buildGcflags != "" { 322 cmd.Args = append(cmd.Args, "-gcflags", buildGcflags) 323 } 324 if buildLdflags != "" { 325 cmd.Args = append(cmd.Args, "-ldflags", buildLdflags) 326 } 327 if buildTrimpath { 328 cmd.Args = append(cmd.Args, "-trimpath") 329 } 330 if buildWork { 331 cmd.Args = append(cmd.Args, "-work") 332 } 333 cmd.Args = append(cmd.Args, args...) 334 cmd.Args = append(cmd.Args, srcs...) 335 336 // Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod, 337 // but the path varies when GOPATH is specified at env, which results in cold cache. 338 if gmc, err := goModCachePath(); err == nil { 339 env = append([]string{"GOMODCACHE=" + gmc}, env...) 340 } else { 341 env = append([]string{}, env...) 342 } 343 cmd.Env = env 344 cmd.Dir = at 345 return runCmd(cmd) 346 } 347 348 func goModTidyAt(at string, env []string) error { 349 cmd := exec.Command("go", "mod", "tidy") 350 if buildV { 351 cmd.Args = append(cmd.Args, "-v") 352 } 353 354 // Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod, 355 // but the path varies when GOPATH is specified at env, which results in cold cache. 356 if gmc, err := goModCachePath(); err == nil { 357 env = append([]string{"GOMODCACHE=" + gmc}, env...) 358 } else { 359 env = append([]string{}, env...) 360 } 361 cmd.Env = env 362 cmd.Dir = at 363 return runCmd(cmd) 364 } 365 366 // parseBuildTarget parses buildTarget into 1 or more platforms and architectures. 367 // Returns an error if buildTarget contains invalid input. 368 // Example valid target strings: 369 // 370 // android 371 // android/arm64,android/386,android/amd64 372 // ios,iossimulator,maccatalyst 373 // macos/amd64 374 func parseBuildTarget(buildTarget string) ([]targetInfo, error) { 375 if buildTarget == "" { 376 return nil, fmt.Errorf(`invalid target ""`) 377 } 378 379 targets := []targetInfo{} 380 targetsAdded := make(map[targetInfo]bool) 381 382 addTarget := func(platform, arch string) { 383 t := targetInfo{platform, arch} 384 if targetsAdded[t] { 385 return 386 } 387 targets = append(targets, t) 388 targetsAdded[t] = true 389 } 390 391 addPlatform := func(platform string) { 392 for _, arch := range platformArchs(platform) { 393 addTarget(platform, arch) 394 } 395 } 396 397 var isAndroid, isApple bool 398 for _, target := range strings.Split(buildTarget, ",") { 399 tuple := strings.SplitN(target, "/", 2) 400 platform := tuple[0] 401 hasArch := len(tuple) == 2 402 403 if isAndroidPlatform(platform) { 404 isAndroid = true 405 } else if isApplePlatform(platform) { 406 isApple = true 407 } else { 408 return nil, fmt.Errorf("unsupported platform: %q", platform) 409 } 410 if isAndroid && isApple { 411 return nil, fmt.Errorf(`cannot mix android and Apple platforms`) 412 } 413 414 if hasArch { 415 arch := tuple[1] 416 if !isSupportedArch(platform, arch) { 417 return nil, fmt.Errorf(`unsupported platform/arch: %q`, target) 418 } 419 addTarget(platform, arch) 420 } else { 421 addPlatform(platform) 422 } 423 } 424 425 // Special case to build iossimulator if -target=ios 426 if buildTarget == "ios" { 427 addPlatform("iossimulator") 428 } 429 430 return targets, nil 431 } 432 433 type targetInfo struct { 434 platform string 435 arch string 436 } 437 438 func (t targetInfo) String() string { 439 return t.platform + "/" + t.arch 440 } 441 442 func goModCachePath() (string, error) { 443 out, err := exec.Command("go", "env", "GOMODCACHE").Output() 444 if err != nil { 445 return "", err 446 } 447 return strings.TrimSpace(string(out)), nil 448 }