github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/gover/gover.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  // Command gover manages saved Go build trees.
     6  //
     7  // gover saves builds of the Go source tree and runs commands using
     8  // these saved Go versions. For example,
     9  //
    10  //     cd $GOROOT
    11  //     git checkout go1.5.1
    12  //     gover build 1.5.1
    13  //
    14  // will checkout Go 1.5.1, build the source tree, and save it under
    15  // the name "1.5.1", as well as its commit hash (f2e4c8b). You can
    16  // then later run commands with Go 1.5.1. For example, the following
    17  // will run "go install" using Go 1.5.1:
    18  //
    19  //     gover 1.5.1 install
    20  //
    21  //
    22  // Usage
    23  //
    24  //     gover [flags] save [name]
    25  //
    26  // Save current build under it's commit hash and, optionally, as
    27  // "name".
    28  //
    29  //     gover [flags] build [name]
    30  //
    31  // Like "save", but first run make.bash in the current tree.
    32  //
    33  //     gover [flags] <name> <args>...
    34  //
    35  // Run "go <args>..." using saved build <name>. <name> may be an
    36  // unambiguous commit hash or an explicit build name.
    37  //
    38  //     gover [flags] with <name> <command>...
    39  //
    40  // Run <command> with PATH and GOROOT for build <name>.
    41  //
    42  //     gover [flags] env <name>
    43  //
    44  // Print the environment for running commands in build <name>. This is
    45  // printed as shell code appropriate for eval.
    46  //
    47  //     gover [flags] list
    48  //
    49  // List saved builds.
    50  //
    51  //     gover [flags] gc
    52  //
    53  // Clean the deduplication cache. This is useful after removing saved
    54  // builds to free up space.
    55  //
    56  //
    57  // Recipies
    58  //
    59  // To build and save all versions of Go:
    60  //
    61  //     git clone https://go.googlesource.com/go && cd go
    62  //     for tag in $(git tag | grep '^go[0-9.]*$'); do
    63  //       git checkout $tag && git clean -df && gover build ${tag##go}
    64  //     done
    65  package main
    66  
    67  // TODO: Should untagged saved commits be treated like a cache and
    68  // deleted automatically?
    69  
    70  import (
    71  	"bytes"
    72  	"crypto/sha1"
    73  	"flag"
    74  	"fmt"
    75  	"io/ioutil"
    76  	"log"
    77  	"os"
    78  	"os/exec"
    79  	"os/user"
    80  	"path/filepath"
    81  	"regexp"
    82  	"runtime"
    83  	"sort"
    84  	"strings"
    85  	"syscall"
    86  )
    87  
    88  // TODO: Consider also accepting a path for name, which could let this
    89  // replace rego.
    90  
    91  // TODO: Half of these global flags only apply to save and build.
    92  
    93  // TODO: The hash and diff hash aren't everything. Environment
    94  // variables like GOEXPERIMENT also affect the build, but right now
    95  // goenv save will complain that the hash already exists.
    96  
    97  var (
    98  	verbose    = flag.Bool("v", false, "print commands being run")
    99  	verDir     = flag.String("dir", defaultVerDir(), "`directory` of saved Go roots")
   100  	noDedup    = flag.Bool("no-dedup", false, "disable deduplication of saved trees")
   101  	gorootFlag = flag.String("C", defaultGoroot(), "use `dir` as the root of the Go tree for save and build")
   102  )
   103  
   104  var binTools = []string{"go", "godoc", "gofmt"}
   105  
   106  func defaultVerDir() string {
   107  	cache := os.Getenv("XDG_CACHE_HOME")
   108  	if cache == "" {
   109  		home := os.Getenv("HOME")
   110  		if home == "" {
   111  			u, err := user.Current()
   112  			if err != nil {
   113  				home = u.HomeDir
   114  			}
   115  		}
   116  		cache = filepath.Join(home, ".cache")
   117  	}
   118  	return filepath.Join(cache, "gover")
   119  }
   120  
   121  func defaultGoroot() string {
   122  	c := exec.Command("git", "rev-parse", "--show-cdup")
   123  	output, err := c.Output()
   124  	if err != nil {
   125  		return ""
   126  	}
   127  	goroot := strings.TrimSpace(string(output))
   128  	if goroot == "" {
   129  		// The empty string is --show-cdup's helpful way of
   130  		// saying "the current directory".
   131  		goroot = "."
   132  	}
   133  	if !isGoroot(goroot) {
   134  		return ""
   135  	}
   136  	return goroot
   137  }
   138  
   139  // isGoroot returns true if path is the root of a Go tree. It is
   140  // somewhat heuristic.
   141  func isGoroot(path string) bool {
   142  	st, err := os.Stat(filepath.Join(path, "src", "cmd", "go"))
   143  	return err == nil && st.IsDir()
   144  }
   145  
   146  func main() {
   147  	log.SetFlags(0)
   148  
   149  	flag.Usage = func() {
   150  		fmt.Fprintf(os.Stderr, "Usage:\n")
   151  		fmt.Fprintf(os.Stderr, "  %s [flags] save [name] - save Go build tree\n", os.Args[0])
   152  		fmt.Fprintf(os.Stderr, "  %s [flags] build [name] - build and save current tree\n", os.Args[0])
   153  		fmt.Fprintf(os.Stderr, "  %s [flags] <name> <args>... - run go <args> using build <name>\n", os.Args[0])
   154  		fmt.Fprintf(os.Stderr, "  %s [flags] with <name> <command>... - run <command> using build <name>\n", os.Args[0])
   155  		fmt.Fprintf(os.Stderr, "  %s [flags] env <name> - print the environment for build <name> as shell code\n", os.Args[0])
   156  		fmt.Fprintf(os.Stderr, "  %s [flags] list - list saved builds\n", os.Args[0])
   157  		fmt.Fprintf(os.Stderr, "  %s [flags] gc [-rm-unlabeled] - clean the deduplication cache", os.Args[0])
   158  		fmt.Fprintf(os.Stderr, "\n\n")
   159  		fmt.Fprintf(os.Stderr, "<name> may be an unambiguous commit hash or a string name.\n\n")
   160  		fmt.Fprintf(os.Stderr, "Flags:\n")
   161  		flag.PrintDefaults()
   162  	}
   163  
   164  	flag.Parse()
   165  	if flag.NArg() < 1 {
   166  		flag.Usage()
   167  		os.Exit(2)
   168  	}
   169  
   170  	// Make gorootFlag absolute.
   171  	if *gorootFlag != "" {
   172  		abs, err := filepath.Abs(*gorootFlag)
   173  		if err != nil {
   174  			*gorootFlag = abs
   175  		}
   176  	}
   177  
   178  	switch flag.Arg(0) {
   179  	case "save", "build":
   180  		// TODO: Annoying: if gover save has already saved a
   181  		// commit by its hash, you can't then "gover save x"
   182  		// to name it. You have to "gover build x", but you're
   183  		// not building at all.
   184  
   185  		if flag.NArg() > 2 {
   186  			flag.Usage()
   187  			os.Exit(2)
   188  		}
   189  		hash, diff := getHash()
   190  		name := ""
   191  		if flag.NArg() >= 2 {
   192  			name = flag.Arg(1)
   193  			if name == hash {
   194  				name = ""
   195  			}
   196  		}
   197  
   198  		// Validate paths.
   199  		savePath, hashExists := resolveName(hash)
   200  
   201  		namePath, nameExists, nameRight := "", false, true
   202  		if name != "" && name != hash {
   203  			namePath, nameExists = resolveName(name)
   204  			if nameExists {
   205  				st1, _ := os.Stat(savePath)
   206  				st2, _ := os.Stat(namePath)
   207  				nameRight = os.SameFile(st1, st2)
   208  			}
   209  		}
   210  
   211  		if flag.Arg(0) == "build" {
   212  			if hashExists {
   213  				if !nameRight {
   214  					log.Fatalf("name `%s' exists and refers to another build", name)
   215  				}
   216  				msg := fmt.Sprintf("saved build `%s' already exists", hash)
   217  				if namePath != "" && !nameExists {
   218  					doLink(hash, namePath)
   219  					msg += fmt.Sprintf("; added name `%s'", name)
   220  				}
   221  				fmt.Fprintln(os.Stderr, msg)
   222  				os.Exit(0)
   223  			}
   224  
   225  			doBuild()
   226  		} else {
   227  			if hashExists {
   228  				log.Fatalf("saved build `%s' already exists", hash)
   229  			}
   230  			if nameExists {
   231  				log.Fatalf("saved build `%s' already exists", name)
   232  			}
   233  		}
   234  		doSave(hash, diff)
   235  		if namePath != "" {
   236  			doLink(hash, namePath)
   237  		}
   238  		if name == "" {
   239  			fmt.Fprintf(os.Stderr, "saved build as `%s'\n", hash)
   240  		} else {
   241  			fmt.Fprintf(os.Stderr, "saved build as `%s' and `%s'\n", hash, name)
   242  		}
   243  
   244  	case "list":
   245  		if flag.NArg() > 1 {
   246  			flag.Usage()
   247  			os.Exit(2)
   248  		}
   249  		doList()
   250  
   251  	case "with":
   252  		if flag.NArg() < 3 {
   253  			flag.Usage()
   254  			os.Exit(2)
   255  		}
   256  		doWith(flag.Arg(1), flag.Args()[2:])
   257  
   258  	case "env":
   259  		if flag.NArg() != 2 {
   260  			flag.Usage()
   261  			os.Exit(2)
   262  		}
   263  		doEnv(flag.Arg(1))
   264  
   265  	case "gc":
   266  		if flag.NArg() == 2 && flag.Arg(1) == "-rm-unlabeled" {
   267  			doRemoveUnlabeled()
   268  		} else if flag.NArg() > 1 {
   269  			flag.Usage()
   270  			os.Exit(2)
   271  		}
   272  		doGC()
   273  
   274  	default:
   275  		if flag.NArg() < 2 {
   276  			flag.Usage()
   277  			os.Exit(2)
   278  		}
   279  		if _, ok := resolveName(flag.Arg(0)); !ok {
   280  			log.Fatalf("unknown name or subcommand `%s'", flag.Arg(0))
   281  		}
   282  		doWith(flag.Arg(0), append([]string{"go"}, flag.Args()[1:]...))
   283  	}
   284  }
   285  
   286  func goroot() string {
   287  	if *gorootFlag == "" {
   288  		log.Fatal("not a git repository")
   289  	}
   290  	return *gorootFlag
   291  }
   292  
   293  func gitCmd(cmd string, args ...string) string {
   294  	args = append([]string{"-C", goroot(), cmd}, args...)
   295  	c := exec.Command("git", args...)
   296  	c.Stderr = os.Stderr
   297  	output, err := c.Output()
   298  	if err != nil {
   299  		log.Fatalf("error executing git %s: %s", strings.Join(args, " "), err)
   300  	}
   301  	return string(output)
   302  }
   303  
   304  func getHash() (string, []byte) {
   305  	rev := strings.TrimSpace(string(gitCmd("rev-parse", "HEAD")))
   306  
   307  	diff := []byte(gitCmd("diff", "HEAD"))
   308  
   309  	if len(bytes.TrimSpace(diff)) > 0 {
   310  		diffHash := fmt.Sprintf("%x", sha1.Sum(diff))
   311  		return rev + "+" + diffHash[:10], diff
   312  	}
   313  	return rev, nil
   314  }
   315  
   316  func doBuild() {
   317  	c := exec.Command("./make.bash")
   318  	c.Dir = filepath.Join(goroot(), "src")
   319  	c.Stdout = os.Stdout
   320  	c.Stderr = os.Stderr
   321  	if err := c.Run(); err != nil {
   322  		log.Fatalf("error executing make.bash: %s", err)
   323  		os.Exit(1)
   324  	}
   325  }
   326  
   327  func doSave(hash string, diff []byte) {
   328  	// Create a minimal GOROOT at $GOROOT/gover/hash.
   329  	savePath, _ := resolveName(hash)
   330  	goos, goarch := runtime.GOOS, runtime.GOARCH
   331  	if x := os.Getenv("GOOS"); x != "" {
   332  		goos = x
   333  	}
   334  	if x := os.Getenv("GOARCH"); x != "" {
   335  		goarch = x
   336  	}
   337  	osArch := goos + "_" + goarch
   338  
   339  	goroot := goroot()
   340  	for _, binTool := range binTools {
   341  		src := filepath.Join(goroot, "bin", binTool)
   342  		if _, err := os.Stat(src); err == nil {
   343  			cp(src, filepath.Join(savePath, "bin", binTool))
   344  		}
   345  	}
   346  	cpR(filepath.Join(goroot, "pkg", osArch), filepath.Join(savePath, "pkg", osArch))
   347  	cpR(filepath.Join(goroot, "pkg", "tool", osArch), filepath.Join(savePath, "pkg", "tool", osArch))
   348  	cpR(filepath.Join(goroot, "pkg", "include"), filepath.Join(savePath, "pkg", "include"))
   349  	// TODO: Use "go list" and save only the stuff depended on? Or
   350  	// maybe just save the types of files go list can return, plus
   351  	// "testdata" directories?
   352  	cpR(filepath.Join(goroot, "src"), filepath.Join(savePath, "src"))
   353  	// Copy tracer static resources.
   354  	cpR(filepath.Join(goroot, "misc", "trace"), filepath.Join(savePath, "misc", "trace"))
   355  
   356  	if diff != nil {
   357  		if err := ioutil.WriteFile(filepath.Join(savePath, "diff"), diff, 0666); err != nil {
   358  			log.Fatal(err)
   359  		}
   360  	}
   361  
   362  	// Save commit object.
   363  	commit := gitCmd("cat-file", "commit", "HEAD")
   364  	if err := ioutil.WriteFile(filepath.Join(savePath, "commit"), []byte(commit), 0666); err != nil {
   365  		log.Fatal(err)
   366  	}
   367  }
   368  
   369  func doLink(hash, namePath string) {
   370  	err := os.Symlink(hash, namePath)
   371  	if err != nil {
   372  		log.Fatal(err)
   373  	}
   374  }
   375  
   376  type buildInfoSorter []*buildInfo
   377  
   378  func (s buildInfoSorter) Len() int {
   379  	return len(s)
   380  }
   381  
   382  func (s buildInfoSorter) Less(i, j int) bool {
   383  	return s[i].commit.authorDate.Before(s[j].commit.authorDate)
   384  }
   385  
   386  func (s buildInfoSorter) Swap(i, j int) {
   387  	s[i], s[j] = s[j], s[i]
   388  }
   389  
   390  func doList() {
   391  	builds, err := listBuilds(listNames | listCommit)
   392  	if err != nil {
   393  		log.Fatal(err)
   394  	}
   395  
   396  	sort.Sort(buildInfoSorter(builds))
   397  
   398  	for _, info := range builds {
   399  		fmt.Print(info.shortName())
   400  		if !info.commit.authorDate.IsZero() {
   401  			fmt.Printf(" %s", info.commit.authorDate.Local().Format("2006-01-02T15:04:05"))
   402  		}
   403  		if len(info.names) > 0 {
   404  			fmt.Printf(" %s", info.names)
   405  		}
   406  		if info.commit.topLine != "" {
   407  			fmt.Printf(" %s", info.commit.topLine)
   408  		}
   409  		fmt.Println()
   410  	}
   411  }
   412  
   413  func doWith(name string, cmd []string) {
   414  	savePath, ok := resolveName(name)
   415  	if !ok {
   416  		log.Fatalf("unknown name `%s'", name)
   417  	}
   418  	goroot, path := getEnv(savePath)
   419  
   420  	// exec.Command looks up the command in this process' PATH.
   421  	// Unfortunately, this is a rather complex process and there's
   422  	// no way to provide a different PATH, so set the process'
   423  	// PATH.
   424  	os.Setenv("PATH", path)
   425  	c := exec.Command(cmd[0], cmd[1:]...)
   426  
   427  	// Build the rest of the command environment.
   428  	for _, env := range os.Environ() {
   429  		if strings.HasPrefix(env, "GOROOT=") {
   430  			continue
   431  		}
   432  		c.Env = append(c.Env, env)
   433  	}
   434  	c.Env = append(c.Env, "GOROOT="+goroot)
   435  
   436  	// Run command.
   437  	c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr
   438  	if err := c.Run(); err != nil {
   439  		fmt.Printf("command failed: %s\n", err)
   440  		os.Exit(1)
   441  	}
   442  }
   443  
   444  func doEnv(name string) {
   445  	savePath, ok := resolveName(name)
   446  	if !ok {
   447  		log.Fatalf("unknown name `%s'", name)
   448  	}
   449  
   450  	goroot, path := getEnv(savePath)
   451  	fmt.Printf("PATH=%s;\n", shellEscape(path))
   452  	fmt.Printf("GOROOT=%s;\n", shellEscape(goroot))
   453  	fmt.Printf("export GOROOT;\n")
   454  }
   455  
   456  // getEnv returns the GOROOT and PATH for the Go tree rooted at savePath.
   457  func getEnv(savePath string) (goroot, path string) {
   458  	p := []string{filepath.Join(savePath, "bin")}
   459  	// Strip existing Go tree from PATH.
   460  	for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
   461  		if isGoroot(filepath.Join(dir, "..")) {
   462  			continue
   463  		}
   464  		p = append(p, dir)
   465  	}
   466  
   467  	return savePath, strings.Join(p, string(filepath.ListSeparator))
   468  }
   469  
   470  func doRemoveUnlabeled() {
   471  	builds, err := listBuilds(listNames)
   472  	if err != nil {
   473  		log.Fatal(err)
   474  	}
   475  
   476  	rms := 0
   477  	for _, build := range builds {
   478  		if len(build.names) != 0 {
   479  			continue
   480  		}
   481  		if err := os.RemoveAll(build.path); err != nil {
   482  			// Not fatal.
   483  			log.Println(err)
   484  		} else {
   485  			rms++
   486  		}
   487  	}
   488  	fmt.Printf("removed %d unlabeled saved build(s)\n", rms)
   489  }
   490  
   491  var goodDedupPath = regexp.MustCompile("/[0-9a-f]{2}/[0-9a-f]{38}$")
   492  
   493  func doGC() {
   494  	removed, space := 0, int64(0)
   495  	filepath.Walk(filepath.Join(*verDir, "_dedup"), func(path string, info os.FileInfo, err error) error {
   496  		if info.IsDir() {
   497  			return nil
   498  		}
   499  		st, ok := info.Sys().(*syscall.Stat_t)
   500  		if !ok || st.Nlink != 1 {
   501  			return nil
   502  		}
   503  		if !goodDedupPath.MatchString(path) {
   504  			// Be paranoid about removing files.
   505  			log.Printf("unexpected file in dedup cache: %s\n", path)
   506  			return nil
   507  		}
   508  		if err := os.Remove(path); err != nil {
   509  			log.Printf("failed to remove %s: %v", path, err)
   510  		} else {
   511  			space += info.Size()
   512  			removed++
   513  		}
   514  		return nil
   515  	})
   516  	fmt.Printf("removed %d MB in %d unused file(s)\n", space>>20, removed)
   517  }
   518  
   519  func cp(src, dst string) {
   520  	data, err := ioutil.ReadFile(src)
   521  	if err != nil {
   522  		log.Fatal(err)
   523  	}
   524  
   525  	writeFile, xdst := true, dst
   526  	if !*noDedup {
   527  		hash := fmt.Sprintf("%x", sha1.Sum(data))
   528  		xdst = filepath.Join(*verDir, "_dedup", hash[:2], hash[2:])
   529  		if _, err := os.Stat(xdst); err == nil {
   530  			writeFile = false
   531  		}
   532  	}
   533  	if writeFile {
   534  		if *verbose {
   535  			fmt.Printf("cp %s %s\n", src, xdst)
   536  		}
   537  		st, err := os.Stat(src)
   538  		if err != nil {
   539  			log.Fatal(err)
   540  		}
   541  		if err := os.MkdirAll(filepath.Dir(xdst), 0777); err != nil {
   542  			log.Fatal(err)
   543  		}
   544  		if err := ioutil.WriteFile(xdst, data, st.Mode()); err != nil {
   545  			log.Fatal(err)
   546  		}
   547  		if err := os.Chtimes(xdst, st.ModTime(), st.ModTime()); err != nil {
   548  			log.Fatal(err)
   549  		}
   550  	}
   551  
   552  	if dst != xdst {
   553  		if *verbose {
   554  			fmt.Printf("ln %s %s\n", xdst, dst)
   555  		}
   556  		if err := os.MkdirAll(filepath.Dir(dst), 0777); err != nil {
   557  			log.Fatal(err)
   558  		}
   559  		if err := os.Link(xdst, dst); err != nil {
   560  			log.Fatal(err)
   561  		}
   562  	}
   563  }
   564  
   565  func cpR(src, dst string) {
   566  	filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
   567  		if err != nil || info.IsDir() {
   568  			return nil
   569  		}
   570  		base := filepath.Base(path)
   571  		if base == "core" || strings.HasSuffix(base, ".test") {
   572  			return nil
   573  		}
   574  
   575  		cp(path, dst+path[len(src):])
   576  		return nil
   577  	})
   578  }