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 }