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 }