github.com/tencent/goom@v1.0.1/test/version.go (about) 1 // Copyright 2016 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 test 6 7 import ( 8 "archive/tar" 9 "archive/zip" 10 "bufio" 11 "compress/gzip" 12 "crypto/sha256" 13 "errors" 14 "fmt" 15 "io" 16 "io/ioutil" 17 "log" 18 "net/http" 19 "os" 20 "os/exec" 21 "os/signal" 22 "os/user" 23 "path" 24 "path/filepath" 25 "runtime" 26 "strings" 27 "time" 28 29 "github.com/tencent/goom/internal/hack" 30 ) 31 32 const osWindows = "windows" 33 34 func init() { 35 http.DefaultTransport = &userAgentTransport{http.DefaultTransport} 36 } 37 38 // Run runs the "go" tool of the provided Go version. 39 func Run(version string, logHandler func(log string), args ...string) error { 40 log.SetFlags(0) 41 42 root, err := goroot(version) 43 if err != nil { 44 log.Fatalf("%s: %v", version, err) 45 } 46 47 if err := install(root, version); err != nil { 48 log.Fatalf("%s: download failed: %v", version, err) 49 } 50 51 if _, err := os.Stat(filepath.Join(root, unpackedOkay)); err != nil { 52 log.Fatalf("%s: not downloaded. Run '%s download' to install to %v", version, version, root) 53 } 54 55 return runGo(root, logHandler, args) 56 } 57 58 func runGo(root string, logHandler func(log string), args []string) error { 59 gobin := filepath.Join(root, "bin", "go"+exe()) 60 cmd := exec.Command(gobin, args...) 61 //cmd.Stdin = os.Stdin 62 //cmd.Stdout = os.Stdout 63 //cmd.Stderr = os.Stderr 64 newPath := filepath.Join(root, "bin") 65 if p := os.Getenv("PATH"); p != "" { 66 newPath += string(filepath.ListSeparator) + p 67 } 68 cmd.Env = dedupEnv(caseInsensitiveEnv, append(os.Environ(), "GOROOT="+root, "PATH="+newPath)) 69 70 handleSignals() 71 72 cmdReader, err := cmd.StdoutPipe() 73 if err != nil { 74 log.Fatalf("cmd exec failed: %v", err) 75 } 76 cmd.Stderr = cmd.Stdout 77 scanner := bufio.NewScanner(cmdReader) 78 done := make(chan bool) 79 go func() { 80 for scanner.Scan() { 81 logs := scanner.Text() 82 fmt.Println(logs) 83 if logHandler != nil { 84 logHandler(logs) 85 } 86 } 87 done <- true 88 }() 89 90 if err := cmd.Start(); err != nil { 91 log.Fatalf("cmd exec failed: %v", err) 92 } 93 if err := cmd.Wait(); err != nil { 94 return err 95 } 96 <-done 97 return nil 98 } 99 100 // install installs a version of Go to the named target directory, creating the 101 // directory as needed. 102 func install(targetDir, version string) error { 103 if _, err := os.Stat(filepath.Join(targetDir, unpackedOkay)); err == nil { 104 log.Printf("%s: already downloaded in %v", version, targetDir) 105 return nil 106 } 107 108 if err := os.MkdirAll(targetDir, 0755); err != nil { 109 return err 110 } 111 goURL := versionArchiveURL(version) 112 res, err := http.Head(goURL) 113 if err != nil { 114 return err 115 } 116 if res.StatusCode == http.StatusNotFound { 117 return fmt.Errorf("no binary release of %v for %v/%v at %v", version, getOS(), runtime.GOARCH, goURL) 118 } 119 if res.StatusCode != http.StatusOK { 120 return fmt.Errorf("server returned %v checking size of %v", http.StatusText(res.StatusCode), goURL) 121 } 122 base := path.Base(goURL) 123 archiveFile := filepath.Join(targetDir, base) 124 if fi, err2 := os.Stat(archiveFile); err2 != nil || fi.Size() != res.ContentLength { 125 if err2 != nil && !os.IsNotExist(err2) { 126 // Something weird. Don't try to download. 127 return err2 128 } 129 if err3 := copyFromURL(archiveFile, goURL); err3 != nil { 130 return fmt.Errorf("error downloading %v: %v", goURL, err3) 131 } 132 fi, err2 = os.Stat(archiveFile) 133 if err2 != nil { 134 return err2 135 } 136 if fi.Size() != res.ContentLength { 137 return fmt.Errorf("downloaded file %s size %v doesn't match server size %v", 138 archiveFile, fi.Size(), res.ContentLength) 139 } 140 } 141 wantSHA, err := slurpURLToString(goURL + ".sha256") 142 if err != nil { 143 return err 144 } 145 if err := verifySHA256(archiveFile, strings.TrimSpace(wantSHA)); err != nil { 146 return fmt.Errorf("error verifying SHA256 of %v: %v", archiveFile, err) 147 } 148 log.Printf("Unpacking %v ...", archiveFile) 149 if err := unpackArchive(targetDir, archiveFile); err != nil { 150 return fmt.Errorf("extracting archive %v: %v", archiveFile, err) 151 } 152 if err := ioutil.WriteFile(filepath.Join(targetDir, unpackedOkay), nil, 0644); err != nil { 153 return err 154 } 155 log.Printf("Success. You may now run '%v'", version) 156 return nil 157 } 158 159 // unpackArchive unpacks the provided archive zip or tar.gz file to targetDir, 160 // removing the "go/" prefix from file entries. 161 func unpackArchive(targetDir, archiveFile string) error { 162 switch { 163 case strings.HasSuffix(archiveFile, ".zip"): 164 return unpackZip(targetDir, archiveFile) 165 case strings.HasSuffix(archiveFile, ".tar.gz"): 166 return unpackTarGz(targetDir, archiveFile) 167 default: 168 return errors.New("unsupported archive file") 169 } 170 } 171 172 // unpackTarGz is the tar.gz implementation of unpackArchive. 173 func unpackTarGz(targetDir, archiveFile string) error { 174 r, err := os.Open(archiveFile) 175 if err != nil { 176 return err 177 } 178 defer func() { 179 _ = r.Close() 180 }() 181 madeDir := map[string]bool{} 182 zr, err := gzip.NewReader(r) 183 if err != nil { 184 return err 185 } 186 tr := tar.NewReader(zr) 187 for { 188 f, err := tr.Next() 189 if err == io.EOF { 190 break 191 } 192 if err != nil { 193 return err 194 } 195 if !validRelPath(f.Name) { 196 return fmt.Errorf("tar file contained invalid name %q", f.Name) 197 } 198 rel := filepath.FromSlash(strings.TrimPrefix(f.Name, "go/")) 199 abs := filepath.Join(targetDir, rel) 200 201 fi := f.FileInfo() 202 mode := fi.Mode() 203 switch { 204 case mode.IsRegular(): 205 // Make the directory. This is redundant because it should 206 // already be made by a directory entry in the tar 207 // beforehand. Thus, don't check for errors; the next 208 // write will fail with the same error. 209 dir := filepath.Dir(abs) 210 if !madeDir[dir] { 211 if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { 212 return err 213 } 214 madeDir[dir] = true 215 } 216 wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) 217 if err != nil { 218 return err 219 } 220 n, err := io.Copy(wf, tr) 221 if closeErr := wf.Close(); closeErr != nil && err == nil { 222 err = closeErr 223 } 224 if err != nil { 225 return fmt.Errorf("error writing to %s: %v", abs, err) 226 } 227 if n != f.Size { 228 return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size) 229 } 230 if !f.ModTime.IsZero() { 231 if err := os.Chtimes(abs, f.ModTime, f.ModTime); err != nil { 232 // benign error. Gerrit doesn't even set the 233 // modtime in these, and we don't end up relying 234 // on it anywhere (the gomote push command relies 235 // on digests only), so this is a little pointless 236 // for now. 237 log.Printf("error changing modtime: %v", err) 238 } 239 } 240 case mode.IsDir(): 241 if err := os.MkdirAll(abs, 0755); err != nil { 242 return err 243 } 244 madeDir[abs] = true 245 default: 246 return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode) 247 } 248 } 249 return nil 250 } 251 252 // unpackZip is the zip implementation of unpackArchive. 253 func unpackZip(targetDir, archiveFile string) error { 254 zr, err := zip.OpenReader(archiveFile) 255 if err != nil { 256 return err 257 } 258 defer func() { 259 _ = zr.Close() 260 }() 261 262 for _, f := range zr.File { 263 name := strings.TrimPrefix(f.Name, "go/") 264 265 outpath := filepath.Join(targetDir, name) 266 if f.FileInfo().IsDir() { 267 if err := os.MkdirAll(outpath, 0755); err != nil { 268 return err 269 } 270 continue 271 } 272 273 rc, err := f.Open() 274 if err != nil { 275 return err 276 } 277 278 // File 279 if err2 := os.MkdirAll(filepath.Dir(outpath), 0755); err2 != nil { 280 return err2 281 } 282 out, err := os.OpenFile(outpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 283 if err != nil { 284 return err 285 } 286 _, err = io.Copy(out, rc) 287 _ = rc.Close() 288 if err != nil { 289 _ = out.Close() 290 return err 291 } 292 if err := out.Close(); err != nil { 293 return err 294 } 295 } 296 return nil 297 } 298 299 // verifySHA256 reports whether the named file has contents with 300 // SHA-256 of the given wantHex value. 301 func verifySHA256(file, wantHex string) error { 302 f, err := os.Open(file) 303 if err != nil { 304 return err 305 } 306 defer func() { 307 _ = f.Close() 308 }() 309 hash := sha256.New() 310 if _, err := io.Copy(hash, f); err != nil { 311 return err 312 } 313 if fmt.Sprintf("%x", hash.Sum(nil)) != wantHex { 314 return fmt.Errorf("%s corrupt? does not have expected SHA-256 of %v", file, wantHex) 315 } 316 return nil 317 } 318 319 // slurpURLToString downloads the given URL and returns it as a string. 320 func slurpURLToString(uRL string) (string, error) { 321 res, err := http.Get(uRL) 322 if err != nil { 323 return "", err 324 } 325 defer func() { 326 _ = res.Body.Close() 327 }() 328 if res.StatusCode != http.StatusOK { 329 return "", fmt.Errorf("%s: %v", uRL, res.Status) 330 } 331 slurp, err := ioutil.ReadAll(res.Body) 332 if err != nil { 333 return "", fmt.Errorf("reading %s: %v", uRL, err) 334 } 335 return string(slurp), nil 336 } 337 338 // copyFromURL downloads srcURL to dstFile. 339 func copyFromURL(dstFile, srcURL string) (err error) { 340 f, err := os.Create(dstFile) 341 if err != nil { 342 return err 343 } 344 defer func() { 345 if err != nil { 346 _ = f.Close() 347 _ = os.Remove(dstFile) 348 } 349 }() 350 c := &http.Client{ 351 Transport: &userAgentTransport{&http.Transport{ 352 // It's already compressed. Prefer accurate ContentLength. 353 // (Not that GCS would try to compress it, though) 354 DisableCompression: true, 355 DisableKeepAlives: true, 356 Proxy: http.ProxyFromEnvironment, 357 }}, 358 } 359 res, err := c.Get(srcURL) 360 if err != nil { 361 return err 362 } 363 defer func() { 364 _ = res.Body.Close() 365 }() 366 if res.StatusCode != http.StatusOK { 367 return errors.New(res.Status) 368 } 369 pw := &progressWriter{w: f, total: res.ContentLength} 370 n, err := io.Copy(pw, res.Body) 371 if err != nil { 372 return err 373 } 374 if res.ContentLength != -1 && res.ContentLength != n { 375 return fmt.Errorf("copied %v bytes; expected %v", n, res.ContentLength) 376 } 377 pw.update() // 100% 378 return f.Close() 379 } 380 381 type progressWriter struct { 382 w io.Writer 383 n int64 384 total int64 385 last time.Time 386 } 387 388 func (p *progressWriter) update() { 389 end := " ..." 390 if p.n == p.total { 391 end = "" 392 } 393 _, _ = fmt.Fprintf(os.Stderr, "Downloaded %5.1f%% (%*d / %d bytes)%s\n", 394 (100.0*float64(p.n))/float64(p.total), 395 ndigits(p.total), p.n, p.total, end) 396 } 397 398 func ndigits(i int64) int { 399 var n int 400 for ; i != 0; i /= 10 { 401 n++ 402 } 403 return n 404 } 405 406 func (p *progressWriter) Write(buf []byte) (n int, err error) { 407 n, err = p.w.Write(buf) 408 p.n += int64(n) 409 if now := time.Now(); now.Unix() != p.last.Unix() { 410 p.update() 411 p.last = now 412 } 413 return 414 } 415 416 // getOS returns runtime.GOOS. It exists as a function just for lazy 417 // testing of the Windows zip path when running on Linux/Darwin. 418 func getOS() string { 419 return runtime.GOOS 420 } 421 422 // versionArchiveURL returns the zip or tar.gz URL of the given Go version. 423 func versionArchiveURL(version string) string { 424 goos := getOS() 425 426 ext := ".tar.gz" 427 if goos == osWindows { 428 ext = ".zip" 429 } 430 arch := runtime.GOARCH 431 if goos == "linux" && runtime.GOARCH == "arm" { 432 arch = "armv6l" 433 } 434 return "https://dl.google.com/go/" + version + "." + goos + "-" + arch + ext 435 } 436 437 const caseInsensitiveEnv = runtime.GOOS == osWindows 438 439 // unpackedOkay is a sentinel zero-byte file to indicate that the Go 440 // version was downloaded and unpacked successfully. 441 const unpackedOkay = ".unpacked-success" 442 443 func exe() string { 444 if runtime.GOOS == osWindows { 445 return ".exe" 446 } 447 return "" 448 } 449 450 func goroot(version string) (string, error) { 451 home, err := homedir() 452 if err != nil { 453 return "", fmt.Errorf("failed to get home directory: %v", err) 454 } 455 return filepath.Join(home, "sdk", version), nil 456 } 457 458 func homedir() (string, error) { 459 // This could be replaced with os.UserHomeDir, but it was introduced too 460 // recently, and we want this to work with go as packaged by Linux 461 // distributions. Note that user.Current is not enough as it does not 462 // prioritize $HOME. See also Issue 26463. 463 switch getOS() { 464 case "plan9": 465 return "", fmt.Errorf("%q not yet supported", runtime.GOOS) 466 case osWindows: 467 if dir := os.Getenv("USERPROFILE"); dir != "" { 468 return dir, nil 469 } 470 return "", errors.New("can't find user home directory; %USERPROFILE% is empty") 471 default: 472 if dir := os.Getenv("HOME"); dir != "" { 473 return dir, nil 474 } 475 if u, err := user.Current(); err == nil && u.HomeDir != "" { 476 return u.HomeDir, nil 477 } 478 return "", errors.New("can't find user home directory; $HOME is empty") 479 } 480 } 481 482 func validRelPath(p string) bool { 483 if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") { 484 return false 485 } 486 return true 487 } 488 489 type userAgentTransport struct { 490 rt http.RoundTripper 491 } 492 493 func (uat userAgentTransport) RoundTrip(r *http.Request) (*http.Response, error) { 494 version := runtime.Version() 495 if strings.Contains(version, "devel") { 496 // Strip the SHA hash and date. We don't want spaces or other tokens (see RFC2616 14.43) 497 version = "devel" 498 } 499 r.Header.Set("User-Agent", "golang-x-build-version/"+version) 500 return uat.rt.RoundTrip(r) 501 } 502 503 // dedupEnv returns a copy of env with any duplicates removed, in favor of 504 // later values. 505 // Items are expected to be on the normal environment "key=value" form. 506 // If caseInsensitive is true, the case of keys is ignored. 507 // 508 // This function is unnecessary when the binary is 509 // built with Go 1.9+, but keep it around for now until Go 1.8 510 // is no longer seen in the wild in common distros. 511 // 512 // This is copied verbatim from golang.org/x/build/envutil.Dedup at CL 10301 513 // (commit a91ae26). 514 func dedupEnv(caseInsensitive bool, env []string) []string { 515 out := make([]string, 0, len(env)) 516 saw := map[string]int{} // to index in the array 517 for _, kv := range env { 518 eq := strings.Index(kv, "=") 519 if eq < 1 { 520 out = append(out, kv) 521 continue 522 } 523 k := kv[:eq] 524 if caseInsensitive { 525 k = strings.ToLower(k) 526 } 527 if dupIdx, isDup := saw[k]; isDup { 528 out[dupIdx] = kv 529 } else { 530 saw[k] = len(out) 531 out = append(out, kv) 532 } 533 } 534 return out 535 } 536 537 // nolint 538 func handleSignals() { 539 // Ensure that signals intended for the child process are not handled by 540 // this process' runtime (e.g. SIGQUIT). See issue #36976. 541 signal.Notify(make(chan os.Signal), hack.SignalsToIgnore...) 542 }