golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/updatestd/updatestd.go (about)

     1  // Copyright 2020 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  // updatestd is an experimental program that has been used to update
     6  // the standard library modules as part of golang.org/issue/36905 in
     7  // CL 255860 and CL 266898. It's expected to be modified to meet the
     8  // ongoing needs of that recurring maintenance work.
     9  package main
    10  
    11  import (
    12  	"bytes"
    13  	"context"
    14  	"debug/buildinfo"
    15  	"encoding/json"
    16  	"errors"
    17  	"flag"
    18  	"fmt"
    19  	"go/ast"
    20  	"go/parser"
    21  	"go/token"
    22  	"io"
    23  	"log"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"strconv"
    28  	"strings"
    29  
    30  	"golang.org/x/build/gerrit"
    31  	"golang.org/x/build/internal/envutil"
    32  )
    33  
    34  var goCmd string // the go command
    35  
    36  func main() {
    37  	log.SetFlags(0)
    38  
    39  	flag.Usage = func() {
    40  		fmt.Fprintln(os.Stderr, "Usage: updatestd -goroot=<goroot> -branch=<branch>")
    41  		flag.PrintDefaults()
    42  	}
    43  	goroot := flag.String("goroot", "", "path to a working copy of https://go.googlesource.com/go (required)")
    44  	branch := flag.String("branch", "", "branch to target, such as master or release-branch.go1.Y (required)")
    45  	flag.Parse()
    46  	if flag.NArg() != 0 || *goroot == "" || *branch == "" {
    47  		flag.Usage()
    48  		os.Exit(2)
    49  	}
    50  
    51  	// Determine the Go version from the GOROOT source tree.
    52  	goVersion, err := gorootVersion(*goroot)
    53  	if err != nil {
    54  		log.Fatalln(err)
    55  	}
    56  
    57  	goCmd = filepath.Join(*goroot, "bin", "go")
    58  
    59  	// Confirm that bundle is in PATH.
    60  	// It's needed for a go generate step later.
    61  	bundlePath, err := exec.LookPath("bundle")
    62  	if err != nil {
    63  		log.Fatalln("can't find bundle in PATH; did you run 'go install golang.org/x/tools/cmd/bundle@latest' and add it to PATH?")
    64  	}
    65  	if bi, err := buildinfo.ReadFile(bundlePath); err != nil || bi.Path != "golang.org/x/tools/cmd/bundle" {
    66  		// Not the bundle command we want.
    67  		log.Fatalln("unexpected bundle command in PATH; did you run 'go install golang.org/x/tools/cmd/bundle@latest' and add it to PATH?")
    68  	}
    69  
    70  	// Fetch latest hashes of Go projects from Gerrit,
    71  	// using the specified branch name.
    72  	//
    73  	// We get a fairly consistent snapshot of all golang.org/x module versions
    74  	// at a given point in time. This ensures selection of latest available
    75  	// pseudo-versions is done without being subject to module mirror caching,
    76  	// and that selected pseudo-versions can be re-used across multiple modules.
    77  	//
    78  	// TODO: Consider a future enhancement of fetching build status for all
    79  	// commits that are selected and reporting if any of them have a failure.
    80  	//
    81  	cl := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
    82  	projs, err := cl.ListProjects(context.Background())
    83  	if err != nil {
    84  		log.Fatalln("failed to get a list of Gerrit projects:", err)
    85  	}
    86  	hashes := map[string]string{}
    87  	for _, p := range projs {
    88  		b, err := cl.GetBranch(context.Background(), p.Name, *branch)
    89  		if errors.Is(err, gerrit.ErrResourceNotExist) {
    90  			continue
    91  		} else if err != nil {
    92  			log.Fatalf("failed to get the %q branch of Gerrit project %q: %v\n", *branch, p.Name, err)
    93  		}
    94  		hashes[p.Name] = b.Revision
    95  	}
    96  
    97  	w := Work{
    98  		Branch:        *branch,
    99  		GoVersion:     fmt.Sprintf("1.%d", goVersion),
   100  		ProjectHashes: hashes,
   101  	}
   102  
   103  	// Print environment information.
   104  	r := runner{filepath.Join(*goroot, "src")}
   105  	r.run(goCmd, "version")
   106  	r.run(goCmd, "env", "GOROOT")
   107  	r.run(goCmd, "version", "-m", bundlePath)
   108  	log.Println()
   109  
   110  	// Walk the standard library source tree (GOROOT/src),
   111  	// skipping directories that the Go command ignores (see go help packages)
   112  	// and update modules that are found.
   113  	err = filepath.Walk(filepath.Join(*goroot, "src"), func(path string, fi os.FileInfo, err error) error {
   114  		if err != nil {
   115  			return err
   116  		}
   117  		if fi.IsDir() && (strings.HasPrefix(fi.Name(), ".") || strings.HasPrefix(fi.Name(), "_") || fi.Name() == "testdata" || fi.Name() == "vendor") {
   118  			return filepath.SkipDir
   119  		}
   120  		goModFile := fi.Name() == "go.mod" && !fi.IsDir()
   121  		if goModFile {
   122  			moduleDir := filepath.Dir(path)
   123  			err := w.UpdateModule(moduleDir)
   124  			if err != nil {
   125  				return fmt.Errorf("failed to update module in %s: %v", moduleDir, err)
   126  			}
   127  			return filepath.SkipDir // Skip the remaining files in this directory.
   128  		}
   129  		return nil
   130  	})
   131  	if err != nil {
   132  		log.Fatalln(err)
   133  	}
   134  
   135  	// Re-bundle packages in the standard library.
   136  	//
   137  	// TODO: Maybe do GOBIN=$(mktemp -d) go install golang.org/x/tools/cmd/bundle@version or so,
   138  	// and add it to PATH to eliminate variance in bundle tool version. Can be considered later.
   139  	//
   140  	log.Println("updating bundles in", r.dir)
   141  	r.run(goCmd, "generate", "-run=bundle", "std", "cmd")
   142  }
   143  
   144  type Work struct {
   145  	Branch        string            // Target branch name.
   146  	GoVersion     string            // Major Go version, like "1.x".
   147  	ProjectHashes map[string]string // Gerrit project name → commit hash.
   148  }
   149  
   150  // UpdateModule updates the standard library module found in dir:
   151  //
   152  //  1. Set the expected Go version in go.mod file to w.GoVersion.
   153  //  2. For modules in the build list with "golang.org/x/" prefix,
   154  //     update to pseudo-version corresponding to w.ProjectHashes.
   155  //  3. Run go mod tidy.
   156  //  4. Run go mod vendor.
   157  //
   158  // The logic in this method needs to serve the dependency update
   159  // policy for the purpose of golang.org/issue/36905, although it
   160  // does not directly define said policy.
   161  func (w Work) UpdateModule(dir string) error {
   162  	// Determine the build list.
   163  	main, deps := buildList(dir)
   164  
   165  	// Determine module versions to get.
   166  	goGet := []string{goCmd, "get", "-d"}
   167  	for _, m := range deps {
   168  		if !strings.HasPrefix(m.Path, "golang.org/x/") {
   169  			log.Printf("skipping %s (out of scope, it's not a golang.org/x dependency)\n", m.Path)
   170  			continue
   171  		}
   172  		gerritProj := m.Path[len("golang.org/x/"):]
   173  		hash, ok := w.ProjectHashes[gerritProj]
   174  		if !ok {
   175  			if m.Indirect {
   176  				log.Printf("skipping %s because branch %s doesn't exist and it's indirect\n", m.Path, w.Branch)
   177  				continue
   178  			}
   179  			return fmt.Errorf("no hash for Gerrit project %q", gerritProj)
   180  		}
   181  		goGet = append(goGet, m.Path+"@"+hash)
   182  	}
   183  
   184  	// Run all the commands.
   185  	log.Println("updating module", main.Path, "in", dir)
   186  	r := runner{dir}
   187  	gowork := strings.TrimSpace(string(r.runOut(goCmd, "env", "GOWORK")))
   188  	if gowork != "" && gowork != "off" {
   189  		log.Printf("warning: GOWORK=%q, things may go wrong?", gowork)
   190  	}
   191  	r.run(goCmd, "mod", "edit", "-go="+w.GoVersion)
   192  	r.run(goGet...)
   193  	r.run(goCmd, "mod", "tidy")
   194  	r.run(goCmd, "mod", "vendor")
   195  	log.Println()
   196  	return nil
   197  }
   198  
   199  // buildList determines the build list in the directory dir
   200  // by invoking the go command. It uses -mod=readonly mode.
   201  // It returns the main module and other modules separately
   202  // for convenience to the UpdateModule caller.
   203  //
   204  // See https://go.dev/ref/mod#go-list-m and https://go.dev/ref/mod#glos-build-list.
   205  func buildList(dir string) (main module, deps []module) {
   206  	out := runner{dir}.runOut(goCmd, "list", "-mod=readonly", "-m", "-json", "all")
   207  	for dec := json.NewDecoder(bytes.NewReader(out)); ; {
   208  		var m module
   209  		err := dec.Decode(&m)
   210  		if err == io.EOF {
   211  			break
   212  		} else if err != nil {
   213  			log.Fatalf("internal error: unexpected problem decoding JSON returned by go list -json: %v", err)
   214  		}
   215  		if m.Main {
   216  			main = m
   217  			continue
   218  		}
   219  		deps = append(deps, m)
   220  	}
   221  	return main, deps
   222  }
   223  
   224  type module struct {
   225  	Path     string // Module path.
   226  	Main     bool   // Is this the main module?
   227  	Indirect bool   // Is this module only an indirect dependency of main module?
   228  }
   229  
   230  // gorootVersion reads the GOROOT/src/internal/goversion/goversion.go
   231  // file and reports the Version declaration value found therein.
   232  func gorootVersion(goroot string) (int, error) {
   233  	// Parse the goversion.go file, extract the declaration from the AST.
   234  	//
   235  	// This is a pragmatic approach that relies on the trajectory of the
   236  	// internal/goversion package being predictable and unlikely to change.
   237  	// If that stops being true, this small helper is easy to re-write.
   238  	//
   239  	fset := token.NewFileSet()
   240  	f, err := parser.ParseFile(fset, filepath.Join(goroot, "src", "internal", "goversion", "goversion.go"), nil, 0)
   241  	if os.IsNotExist(err) {
   242  		return 0, fmt.Errorf("did not find goversion.go file (%v); wrong goroot or did internal/goversion package change?", err)
   243  	} else if err != nil {
   244  		return 0, err
   245  	}
   246  	for _, d := range f.Decls {
   247  		g, ok := d.(*ast.GenDecl)
   248  		if !ok {
   249  			continue
   250  		}
   251  		for _, s := range g.Specs {
   252  			v, ok := s.(*ast.ValueSpec)
   253  			if !ok || len(v.Names) != 1 || v.Names[0].String() != "Version" || len(v.Values) != 1 {
   254  				continue
   255  			}
   256  			l, ok := v.Values[0].(*ast.BasicLit)
   257  			if !ok || l.Kind != token.INT {
   258  				continue
   259  			}
   260  			return strconv.Atoi(l.Value)
   261  		}
   262  	}
   263  	return 0, fmt.Errorf("did not find Version declaration in %s; wrong goroot or did internal/goversion package change?", fset.File(f.Pos()).Name())
   264  }
   265  
   266  type runner struct{ dir string }
   267  
   268  // run runs the command and requires that it succeeds.
   269  // It logs the command's combined output.
   270  func (r runner) run(args ...string) {
   271  	log.Printf("> %s\n", strings.Join(args, " "))
   272  	cmd := exec.Command(args[0], args[1:]...)
   273  	envutil.SetDir(cmd, r.dir)
   274  	out, err := cmd.CombinedOutput()
   275  	if err != nil {
   276  		log.Fatalf("command failed: %s\n%s", err, out)
   277  	}
   278  	if len(out) != 0 {
   279  		log.Print(string(out))
   280  	}
   281  }
   282  
   283  // runOut runs the command, requires that it succeeds,
   284  // and returns the command's standard output.
   285  func (r runner) runOut(args ...string) []byte {
   286  	cmd := exec.Command(args[0], args[1:]...)
   287  	envutil.SetDir(cmd, r.dir)
   288  	out, err := cmd.Output()
   289  	if err != nil {
   290  		log.Printf("> %s\n", strings.Join(args, " "))
   291  		if ee := (*exec.ExitError)(nil); errors.As(err, &ee) {
   292  			out = append(out, ee.Stderr...)
   293  		}
   294  		log.Fatalf("command failed: %s\n%s", err, out)
   295  	}
   296  	return out
   297  }