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  }