github.com/c-darwin/mobile@v0.0.0-20160313183840-ff625c46f7c9/cmd/gomobile/init.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 // TODO(crawshaw): android/{386,arm64} 8 9 import ( 10 "archive/tar" 11 "bytes" 12 "compress/gzip" 13 "crypto/sha256" 14 // "encoding/hex" 15 "fmt" 16 "io" 17 "io/ioutil" 18 "net/http" 19 "os" 20 "os/exec" 21 "path" 22 "path/filepath" 23 "runtime" 24 "strings" 25 "time" 26 ) 27 28 // useStrippedNDK determines whether the init subcommand fetches the GCC 29 // toolchain from the original Android NDK, or from the stripped-down NDK 30 // hosted specifically for the gomobile tool. 31 // 32 // There is a significant size different (400MB compared to 30MB). 33 var useStrippedNDK = true 34 35 const ndkVersion = "ndk-r10e" 36 const openALVersion = "openal-soft-1.16.0.1" 37 38 var ( 39 goos = runtime.GOOS 40 goarch = runtime.GOARCH 41 ndkarch string 42 ) 43 44 func init() { 45 switch runtime.GOARCH { 46 case "amd64": 47 ndkarch = "x86_64" 48 case "386": 49 ndkarch = "x86" 50 default: 51 ndkarch = runtime.GOARCH 52 } 53 } 54 55 var cmdInit = &command{ 56 run: runInit, 57 Name: "init", 58 Usage: "[-u]", 59 Short: "install android compiler toolchain", 60 Long: ` 61 Init installs the Android C++ compiler toolchain and builds copies 62 of the Go standard library for mobile devices. 63 64 When first run, it downloads part of the Android NDK. 65 The toolchain is installed in $GOPATH/pkg/gomobile. 66 67 The -u option forces download and installation of the new toolchain 68 even when the toolchain exists. 69 `, 70 } 71 72 var initU bool // -u 73 74 func init() { 75 cmdInit.flag.BoolVar(&initU, "u", false, "force toolchain download") 76 } 77 78 func runInit(cmd *command) error { 79 version, err := goVersion() 80 if err != nil { 81 return fmt.Errorf("%v: %s", err, version) 82 } 83 84 gopaths := filepath.SplitList(goEnv("GOPATH")) 85 if len(gopaths) == 0 { 86 return fmt.Errorf("GOPATH is not set") 87 } 88 gomobilepath = filepath.Join(gopaths[0], "pkg/gomobile") 89 ndkccpath = filepath.Join(gopaths[0], "pkg/gomobile/android-"+ndkVersion) 90 verpath := filepath.Join(gopaths[0], "pkg/gomobile/version") 91 if buildX || buildN { 92 fmt.Fprintln(xout, "GOMOBILE="+gomobilepath) 93 } 94 removeGomobilepkg() 95 96 if err := mkdir(ndkccpath); err != nil { 97 return err 98 } 99 100 if buildN { 101 tmpdir = filepath.Join(gomobilepath, "work") 102 } else { 103 var err error 104 tmpdir, err = ioutil.TempDir(gomobilepath, "work-") 105 if err != nil { 106 return err 107 } 108 } 109 if buildX || buildN { 110 fmt.Fprintln(xout, "WORK="+tmpdir) 111 } 112 defer func() { 113 if buildWork { 114 fmt.Printf("WORK=%s\n", tmpdir) 115 return 116 } 117 removeAll(tmpdir) 118 }() 119 120 if err := fetchNDK(); err != nil { 121 return err 122 } 123 if err := fetchOpenAL(); err != nil { 124 return err 125 } 126 127 if err := envInit(); err != nil { 128 return err 129 } 130 131 if runtime.GOOS == "darwin" { 132 // Install common x/mobile packages for local development. 133 // These are often slow to compile (due to cgo) and easy to forget. 134 // 135 // Limited to darwin for now as it is common for linux to 136 // not have GLES installed. 137 // 138 // TODO: consider testing GLES installation and suggesting it here 139 for _, pkg := range commonPkgs { 140 if err := installPkg(pkg, nil); err != nil { 141 return err 142 } 143 } 144 } 145 146 // Install standard libraries for cross compilers. 147 start := time.Now() 148 if err := installStd(androidArmEnv); err != nil { 149 return err 150 } 151 if err := installDarwin(); err != nil { 152 return err 153 } 154 155 if buildX || buildN { 156 printcmd("go version > %s", verpath) 157 } 158 if !buildN { 159 if err := ioutil.WriteFile(verpath, version, 0644); err != nil { 160 return err 161 } 162 } 163 if buildV { 164 took := time.Since(start) / time.Second * time.Second 165 fmt.Fprintf(os.Stderr, "\nDone, build took %s.\n", took) 166 } 167 return nil 168 } 169 170 var commonPkgs = []string{ 171 "github.com/c-darwin/mobile/gl", 172 "github.com/c-darwin/mobile/app", 173 "github.com/c-darwin/mobile/exp/app/debug", 174 } 175 176 func installDarwin() error { 177 if goos != "darwin" { 178 return nil // Only build iOS compilers on OS X. 179 } 180 if err := installStd(darwinArmEnv); err != nil { 181 return err 182 } 183 if err := installStd(darwinArm64Env); err != nil { 184 return err 185 } 186 // TODO(crawshaw): darwin/386 for the iOS simulator? 187 if err := installStd(darwinAmd64Env, "-tags=ios"); err != nil { 188 return err 189 } 190 return nil 191 } 192 193 func installStd(env []string, args ...string) error { 194 return installPkg("std", env, args...) 195 } 196 197 func installPkg(pkg string, env []string, args ...string) error { 198 tOS, tArch, pd := getenv(env, "GOOS"), getenv(env, "GOARCH"), pkgdir(env) 199 if tOS != "" && tArch != "" { 200 if buildV { 201 fmt.Fprintf(os.Stderr, "\n# Installing %s for %s/%s.\n", pkg, tOS, tArch) 202 } 203 args = append(args, "-pkgdir="+pd) 204 } else { 205 if buildV { 206 fmt.Fprintf(os.Stderr, "\n# Installing %s.\n", pkg) 207 } 208 } 209 210 // The -p flag is to speed up darwin/arm builds. 211 // Remove when golang.org/issue/10477 is resolved. 212 cmd := exec.Command("go", "install", fmt.Sprintf("-p=%d", runtime.NumCPU())) 213 cmd.Args = append(cmd.Args, args...) 214 if buildV { 215 cmd.Args = append(cmd.Args, "-v") 216 } 217 if buildX { 218 cmd.Args = append(cmd.Args, "-x") 219 } 220 if buildWork { 221 cmd.Args = append(cmd.Args, "-work") 222 } 223 cmd.Args = append(cmd.Args, pkg) 224 cmd.Env = append([]string{}, env...) 225 return runCmd(cmd) 226 } 227 228 func removeGomobilepkg() { 229 dir, err := os.Open(gomobilepath) 230 if err != nil { 231 return 232 } 233 names, err := dir.Readdirnames(-1) 234 if err != nil { 235 return 236 } 237 for _, name := range names { 238 if name == "dl" { 239 continue 240 } 241 removeAll(filepath.Join(gomobilepath, name)) 242 } 243 } 244 245 func move(dst, src string, names ...string) error { 246 for _, name := range names { 247 srcf := filepath.Join(src, name) 248 dstf := filepath.Join(dst, name) 249 if buildX || buildN { 250 printcmd("mv %s %s", srcf, dstf) 251 } 252 if buildN { 253 continue 254 } 255 if goos == "windows" { 256 // os.Rename fails if dstf already exists. 257 removeAll(dstf) 258 } 259 if err := os.Rename(srcf, dstf); err != nil { 260 return err 261 } 262 } 263 return nil 264 } 265 266 func mkdir(dir string) error { 267 if buildX || buildN { 268 printcmd("mkdir -p %s", dir) 269 } 270 if buildN { 271 return nil 272 } 273 return os.MkdirAll(dir, 0755) 274 } 275 276 func symlink(src, dst string) error { 277 if buildX || buildN { 278 printcmd("ln -s %s %s", src, dst) 279 } 280 if buildN { 281 return nil 282 } 283 if goos == "windows" { 284 return doCopyAll(dst, src) 285 } 286 return os.Symlink(src, dst) 287 } 288 289 func rm(name string) error { 290 if buildX || buildN { 291 printcmd("rm %s", name) 292 } 293 if buildN { 294 return nil 295 } 296 return os.Remove(name) 297 } 298 299 func goVersion() ([]byte, error) { 300 gobin, err := exec.LookPath("go") 301 if err != nil { 302 return nil, fmt.Errorf(`no Go tool on $PATH`) 303 } 304 buildHelp, err := exec.Command(gobin, "help", "build").CombinedOutput() 305 if err != nil { 306 return nil, fmt.Errorf("bad Go tool: %v (%s)", err, buildHelp) 307 } 308 // TODO(crawshaw): this is a crude test for Go 1.5. After release, 309 // remove this and check it is not an old release version. 310 if !bytes.Contains(buildHelp, []byte("-pkgdir")) { 311 return nil, fmt.Errorf("installed Go tool does not support -pkgdir") 312 } 313 return exec.Command(gobin, "version").CombinedOutput() 314 } 315 316 func fetchOpenAL() error { 317 url := "https://dl.google.com/go/mobile/gomobile-" + openALVersion + ".tar.gz" 318 archive, err := fetch(url) 319 if err != nil { 320 return err 321 } 322 if err := extract("openal", archive); err != nil { 323 return err 324 } 325 if goos == "windows" { 326 resetReadOnlyFlagAll(filepath.Join(tmpdir, "openal")) 327 } 328 dst := filepath.Join(ndkccpath, "arm", "sysroot", "usr", "include") 329 src := filepath.Join(tmpdir, "openal", "include") 330 if err := move(dst, src, "AL"); err != nil { 331 return err 332 } 333 libDst := filepath.Join(ndkccpath, "openal") 334 libSrc := filepath.Join(tmpdir, "openal") 335 if err := mkdir(libDst); err != nil { 336 return nil 337 } 338 if err := move(libDst, libSrc, "lib"); err != nil { 339 return err 340 } 341 return nil 342 } 343 344 func extract(dst, src string) error { 345 if buildX || buildN { 346 printcmd("tar xfz %s", src) 347 } 348 if buildN { 349 return nil 350 } 351 tf, err := os.Open(src) 352 if err != nil { 353 return err 354 } 355 defer tf.Close() 356 zr, err := gzip.NewReader(tf) 357 if err != nil { 358 return err 359 } 360 tr := tar.NewReader(zr) 361 for { 362 hdr, err := tr.Next() 363 if err == io.EOF { 364 break 365 } 366 if err != nil { 367 return err 368 } 369 dst := filepath.Join(tmpdir, dst+"/"+hdr.Name) 370 if hdr.Typeflag == tar.TypeSymlink { 371 if err := symlink(hdr.Linkname, dst); err != nil { 372 return err 373 } 374 continue 375 } 376 if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { 377 return err 378 } 379 f, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.FileMode(hdr.Mode)&0777) 380 if err != nil { 381 return err 382 } 383 if _, err := io.Copy(f, tr); err != nil { 384 return err 385 } 386 if err := f.Close(); err != nil { 387 return err 388 } 389 } 390 return nil 391 } 392 393 func fetchNDK() error { 394 if useStrippedNDK { 395 if err := fetchStrippedNDK(); err != nil { 396 return err 397 } 398 } else { 399 if err := fetchFullNDK(); err != nil { 400 return err 401 } 402 } 403 if goos == "windows" { 404 resetReadOnlyFlagAll(filepath.Join(tmpdir, "android-"+ndkVersion)) 405 } 406 407 dst := filepath.Join(ndkccpath, "arm") 408 dstSysroot := filepath.Join(dst, "sysroot/usr") 409 if err := mkdir(dstSysroot); err != nil { 410 return err 411 } 412 413 srcSysroot := filepath.Join(tmpdir, "android-"+ndkVersion+"/platforms/android-15/arch-arm/usr") 414 if err := move(dstSysroot, srcSysroot, "include", "lib"); err != nil { 415 return err 416 } 417 418 ndkpath := filepath.Join(tmpdir, "android-"+ndkVersion+"/toolchains/arm-linux-androideabi-4.8/prebuilt") 419 if goos == "windows" && ndkarch == "x86" { 420 ndkpath = filepath.Join(ndkpath, "windows") 421 } else { 422 ndkpath = filepath.Join(ndkpath, goos+"-"+ndkarch) 423 } 424 if err := move(dst, ndkpath, "bin", "lib", "libexec"); err != nil { 425 return err 426 } 427 428 linkpath := filepath.Join(dst, "arm-linux-androideabi/bin") 429 if err := mkdir(linkpath); err != nil { 430 return err 431 } 432 for _, name := range []string{"ld", "as", "gcc", "g++"} { 433 if goos == "windows" { 434 name += ".exe" 435 } 436 if err := symlink(filepath.Join(dst, "bin", "arm-linux-androideabi-"+name), filepath.Join(linkpath, name)); err != nil { 437 return err 438 } 439 } 440 return nil 441 } 442 443 func fetchStrippedNDK() error { 444 url := "https://dl.google.com/go/mobile/gomobile-" + ndkVersion + "-" + goos + "-" + ndkarch + ".tar.gz" 445 archive, err := fetch(url) 446 if err != nil { 447 return err 448 } 449 return extract("", archive) 450 } 451 452 func fetchFullNDK() error { 453 url := "https://dl.google.com/android/ndk/android-" + ndkVersion + "-" + goos + "-" + ndkarch + "." 454 if goos == "windows" { 455 url += "exe" 456 } else { 457 url += "bin" 458 } 459 archive, err := fetch(url) 460 if err != nil { 461 return err 462 } 463 464 // The self-extracting ndk dist file for Windows terminates 465 // with an error (error code 2 - corrupted or incomplete file) 466 // but there are no details on what caused this. 467 // 468 // Strangely, if the file is launched from file browser or 469 // unzipped with 7z.exe no error is reported. 470 // 471 // In general we use the stripped NDK, so this code path 472 // is not used, and 7z.exe is not a normal dependency. 473 var inflate *exec.Cmd 474 if goos != "windows" { 475 // The downloaded archive is executed on linux and os x to unarchive. 476 // To do this execute permissions are needed. 477 os.Chmod(archive, 0755) 478 479 inflate = exec.Command(archive) 480 } else { 481 inflate = exec.Command("7z.exe", "x", archive) 482 } 483 inflate.Dir = tmpdir 484 return runCmd(inflate) 485 } 486 487 // fetch reads a URL into $GOPATH/pkg/gomobile/dl and returns the path 488 // to the downloaded file. Downloading is skipped if the file is 489 // already present. 490 func fetch(url string) (dst string, err error) { 491 if err := mkdir(filepath.Join(gomobilepath, "dl")); err != nil { 492 return "", err 493 } 494 name := path.Base(url) 495 dst = filepath.Join(gomobilepath, "dl", name) 496 497 // Use what's in the cache if force update is not required. 498 if !initU { 499 if buildX { 500 printcmd("stat %s", dst) 501 } 502 if _, err = os.Stat(dst); err == nil { 503 return dst, nil 504 } 505 } 506 if buildX { 507 printcmd("curl -o%s %s", dst, url) 508 } 509 if buildN { 510 return dst, nil 511 } 512 513 if buildV { 514 fmt.Fprintf(os.Stderr, "Downloading %s.\n", url) 515 } 516 517 f, err := ioutil.TempFile(tmpdir, "partial-"+name) 518 if err != nil { 519 return "", err 520 } 521 defer func() { 522 if err != nil { 523 f.Close() 524 os.Remove(f.Name()) 525 } 526 }() 527 hashw := sha256.New() 528 529 resp, err := http.Get(url) 530 if err != nil { 531 return "", err 532 } 533 if resp.StatusCode != http.StatusOK { 534 err = fmt.Errorf("error fetching %v, status: %v", url, resp.Status) 535 } else { 536 _, err = io.Copy(io.MultiWriter(hashw, f), resp.Body) 537 } 538 if err2 := resp.Body.Close(); err == nil { 539 err = err2 540 } 541 if err != nil { 542 return "", err 543 } 544 if err = f.Close(); err != nil { 545 return "", err 546 } 547 /*hash := hex.EncodeToString(hashw.Sum(nil)) 548 if fetchHashes[name] != hash { 549 return "", fmt.Errorf("sha256 for %q: %s %v, want %v", name, f.Name(), hash, fetchHashes[name]) 550 }*/ 551 if err = os.Rename(f.Name(), dst); err != nil { 552 return "", err 553 } 554 return dst, nil 555 } 556 557 func doCopyAll(dst, src string) error { 558 return filepath.Walk(src, func(path string, info os.FileInfo, errin error) (err error) { 559 if errin != nil { 560 return errin 561 } 562 prefixLen := len(src) 563 if len(path) > prefixLen { 564 prefixLen++ // file separator 565 } 566 outpath := filepath.Join(dst, path[prefixLen:]) 567 if info.IsDir() { 568 return os.Mkdir(outpath, 0755) 569 } 570 in, err := os.Open(path) 571 if err != nil { 572 return err 573 } 574 defer in.Close() 575 out, err := os.OpenFile(outpath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode()) 576 if err != nil { 577 return err 578 } 579 defer func() { 580 if errc := out.Close(); err == nil { 581 err = errc 582 } 583 }() 584 _, err = io.Copy(out, in) 585 return err 586 }) 587 } 588 589 func removeAll(path string) error { 590 if buildX || buildN { 591 printcmd(`rm -r -f "%s"`, path) 592 } 593 if buildN { 594 return nil 595 } 596 597 // os.RemoveAll behaves differently in windows. 598 // http://golang.org/issues/9606 599 if goos == "windows" { 600 resetReadOnlyFlagAll(path) 601 } 602 if goos == "darwin" { 603 return nil 604 } 605 return os.RemoveAll(path) 606 } 607 608 func resetReadOnlyFlagAll(path string) error { 609 fi, err := os.Stat(path) 610 if err != nil { 611 return err 612 } 613 if !fi.IsDir() { 614 return os.Chmod(path, 0666) 615 } 616 fd, err := os.Open(path) 617 if err != nil { 618 return err 619 } 620 defer fd.Close() 621 622 names, _ := fd.Readdirnames(-1) 623 for _, name := range names { 624 resetReadOnlyFlagAll(path + string(filepath.Separator) + name) 625 } 626 return nil 627 } 628 629 func goEnv(name string) string { 630 if val := os.Getenv(name); val != "" { 631 return val 632 } 633 val, err := exec.Command("go", "env", name).Output() 634 if err != nil { 635 panic(err) // the Go tool was tested to work earlier 636 } 637 return strings.TrimSpace(string(val)) 638 } 639 640 func runCmd(cmd *exec.Cmd) error { 641 if buildX || buildN { 642 dir := "" 643 if cmd.Dir != "" { 644 dir = "PWD=" + cmd.Dir + " " 645 } 646 env := strings.Join(cmd.Env, " ") 647 if env != "" { 648 env += " " 649 } 650 printcmd("%s%s%s", dir, env, strings.Join(cmd.Args, " ")) 651 } 652 653 buf := new(bytes.Buffer) 654 buf.WriteByte('\n') 655 if buildV { 656 cmd.Stdout = os.Stdout 657 cmd.Stderr = os.Stderr 658 } else { 659 cmd.Stdout = buf 660 cmd.Stderr = buf 661 } 662 663 if buildWork { 664 if goos == "windows" { 665 cmd.Env = append(cmd.Env, `TEMP=`+tmpdir) 666 cmd.Env = append(cmd.Env, `TMP=`+tmpdir) 667 } else { 668 cmd.Env = append(cmd.Env, `TMPDIR=`+tmpdir) 669 } 670 } 671 672 if !buildN { 673 cmd.Env = environ(cmd.Env) 674 if err := cmd.Run(); err != nil { 675 return fmt.Errorf("%s failed: %v%s", strings.Join(cmd.Args, " "), err, buf) 676 } 677 } 678 return nil 679 }