github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/modcmd/download.go (about) 1 // Copyright 2018 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 modcmd 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "os" 12 "runtime" 13 "sync" 14 15 "github.com/go-asm/go/cmd/go/base" 16 "github.com/go-asm/go/cmd/go/cfg" 17 "github.com/go-asm/go/cmd/go/gover" 18 "github.com/go-asm/go/cmd/go/modfetch" 19 "github.com/go-asm/go/cmd/go/modfetch/codehost" 20 "github.com/go-asm/go/cmd/go/modload" 21 "github.com/go-asm/go/cmd/go/toolchain" 22 23 "golang.org/x/mod/module" 24 ) 25 26 var cmdDownload = &base.Command{ 27 UsageLine: "go mod download [-x] [-json] [-reuse=old.json] [modules]", 28 Short: "download modules to local cache", 29 Long: ` 30 Download downloads the named modules, which can be module patterns selecting 31 dependencies of the main module or module queries of the form path@version. 32 33 With no arguments, download applies to the modules needed to build and test 34 the packages in the main module: the modules explicitly required by the main 35 module if it is at 'go 1.17' or higher, or all transitively-required modules 36 if at 'go 1.16' or lower. 37 38 The go command will automatically download modules as needed during ordinary 39 execution. The "go mod download" command is useful mainly for pre-filling 40 the local cache or to compute the answers for a Go module proxy. 41 42 By default, download writes nothing to standard output. It may print progress 43 messages and errors to standard error. 44 45 The -json flag causes download to print a sequence of JSON objects 46 to standard output, describing each downloaded module (or failure), 47 corresponding to this Go struct: 48 49 type Module struct { 50 Path string // module path 51 Query string // version query corresponding to this version 52 Version string // module version 53 Error string // error loading module 54 Info string // absolute path to cached .info file 55 GoMod string // absolute path to cached .mod file 56 Zip string // absolute path to cached .zip file 57 Dir string // absolute path to cached source root directory 58 Sum string // checksum for path, version (as in go.sum) 59 GoModSum string // checksum for go.mod (as in go.sum) 60 Origin any // provenance of module 61 Reuse bool // reuse of old module info is safe 62 } 63 64 The -reuse flag accepts the name of file containing the JSON output of a 65 previous 'go mod download -json' invocation. The go command may use this 66 file to determine that a module is unchanged since the previous invocation 67 and avoid redownloading it. Modules that are not redownloaded will be marked 68 in the new output by setting the Reuse field to true. Normally the module 69 cache provides this kind of reuse automatically; the -reuse flag can be 70 useful on systems that do not preserve the module cache. 71 72 The -x flag causes download to print the commands download executes. 73 74 See https://golang.org/ref/mod#go-mod-download for more about 'go mod download'. 75 76 See https://golang.org/ref/mod#version-queries for more about version queries. 77 `, 78 } 79 80 var ( 81 downloadJSON = cmdDownload.Flag.Bool("json", false, "") 82 downloadReuse = cmdDownload.Flag.String("reuse", "", "") 83 ) 84 85 func init() { 86 cmdDownload.Run = runDownload // break init cycle 87 88 // TODO(jayconrod): https://golang.org/issue/35849 Apply -x to other 'go mod' commands. 89 cmdDownload.Flag.BoolVar(&cfg.BuildX, "x", false, "") 90 base.AddChdirFlag(&cmdDownload.Flag) 91 base.AddModCommonFlags(&cmdDownload.Flag) 92 } 93 94 // A ModuleJSON describes the result of go mod download. 95 type ModuleJSON struct { 96 Path string `json:",omitempty"` 97 Version string `json:",omitempty"` 98 Query string `json:",omitempty"` 99 Error string `json:",omitempty"` 100 Info string `json:",omitempty"` 101 GoMod string `json:",omitempty"` 102 Zip string `json:",omitempty"` 103 Dir string `json:",omitempty"` 104 Sum string `json:",omitempty"` 105 GoModSum string `json:",omitempty"` 106 107 Origin *codehost.Origin `json:",omitempty"` 108 Reuse bool `json:",omitempty"` 109 } 110 111 func runDownload(ctx context.Context, cmd *base.Command, args []string) { 112 modload.InitWorkfile() 113 114 // Check whether modules are enabled and whether we're in a module. 115 modload.ForceUseModules = true 116 modload.ExplicitWriteGoMod = true 117 haveExplicitArgs := len(args) > 0 118 119 if modload.HasModRoot() || modload.WorkFilePath() != "" { 120 modload.LoadModFile(ctx) // to fill MainModules 121 122 if haveExplicitArgs { 123 for _, mainModule := range modload.MainModules.Versions() { 124 targetAtUpgrade := mainModule.Path + "@upgrade" 125 targetAtPatch := mainModule.Path + "@patch" 126 for _, arg := range args { 127 switch arg { 128 case mainModule.Path, targetAtUpgrade, targetAtPatch: 129 os.Stderr.WriteString("go: skipping download of " + arg + " that resolves to the main module\n") 130 } 131 } 132 } 133 } else if modload.WorkFilePath() != "" { 134 // TODO(#44435): Think about what the correct query is to download the 135 // right set of modules. Also see code review comment at 136 // https://go-review.googlesource.com/c/go/+/359794/comments/ce946a80_6cf53992. 137 args = []string{"all"} 138 } else { 139 mainModule := modload.MainModules.Versions()[0] 140 modFile := modload.MainModules.ModFile(mainModule) 141 if modFile.Go == nil || gover.Compare(modFile.Go.Version, gover.ExplicitIndirectVersion) < 0 { 142 if len(modFile.Require) > 0 { 143 args = []string{"all"} 144 } 145 } else { 146 // As of Go 1.17, the go.mod file explicitly requires every module 147 // that provides any package imported by the main module. 148 // 'go mod download' is typically run before testing packages in the 149 // main module, so by default we shouldn't download the others 150 // (which are presumed irrelevant to the packages in the main module). 151 // See https://golang.org/issue/44435. 152 // 153 // However, we also need to load the full module graph, to ensure that 154 // we have downloaded enough of the module graph to run 'go list all', 155 // 'go mod graph', and similar commands. 156 _, err := modload.LoadModGraph(ctx, "") 157 if err != nil { 158 // TODO(#64008): call base.Fatalf instead of toolchain.SwitchOrFatal 159 // here, since we can only reach this point with an outdated toolchain 160 // if the go.mod file is inconsistent. 161 toolchain.SwitchOrFatal(ctx, err) 162 } 163 164 for _, m := range modFile.Require { 165 args = append(args, m.Mod.Path) 166 } 167 } 168 } 169 } 170 171 if len(args) == 0 { 172 if modload.HasModRoot() { 173 os.Stderr.WriteString("go: no module dependencies to download\n") 174 } else { 175 base.Errorf("go: no modules specified (see 'go help mod download')") 176 } 177 base.Exit() 178 } 179 180 if *downloadReuse != "" && modload.HasModRoot() { 181 base.Fatalf("go mod download -reuse cannot be used inside a module") 182 } 183 184 var mods []*ModuleJSON 185 type token struct{} 186 sem := make(chan token, runtime.GOMAXPROCS(0)) 187 infos, infosErr := modload.ListModules(ctx, args, 0, *downloadReuse) 188 189 // There is a bit of a chicken-and-egg problem here: ideally we need to know 190 // which Go version to switch to to download the requested modules, but if we 191 // haven't downloaded the module's go.mod file yet the GoVersion field of its 192 // info struct is not yet populated. 193 // 194 // We also need to be careful to only print the info for each module once 195 // if the -json flag is set. 196 // 197 // In theory we could go through each module in the list, attempt to download 198 // its go.mod file, and record the maximum version (either from the file or 199 // from the resulting TooNewError), all before we try the actual full download 200 // of each module. 201 // 202 // For now, we go ahead and try all the downloads and collect the errors, and 203 // if any download failed due to a TooNewError, we switch toolchains and try 204 // again. Any downloads that already succeeded will still be in cache. 205 // That won't give optimal concurrency (we'll do two batches of concurrent 206 // downloads instead of all in one batch), and it might add a little overhead 207 // to look up the downloads from the first batch in the module cache when 208 // we see them again in the second batch. On the other hand, it's way simpler 209 // to implement, and not really any more expensive if the user is requesting 210 // no explicit arguments (their go.mod file should already list an appropriate 211 // toolchain version) or only one module (as is used by the Go Module Proxy). 212 213 if infosErr != nil { 214 var sw toolchain.Switcher 215 sw.Error(infosErr) 216 if sw.NeedSwitch() { 217 sw.Switch(ctx) 218 } 219 // Otherwise, wait to report infosErr after we have downloaded 220 // when we can. 221 } 222 223 if !haveExplicitArgs && modload.WorkFilePath() == "" { 224 // 'go mod download' is sometimes run without arguments to pre-populate the 225 // module cache. In modules that aren't at go 1.17 or higher, it may fetch 226 // modules that aren't needed to build packages in the main module. This is 227 // usually not intended, so don't save sums for downloaded modules 228 // (golang.org/issue/45332). We do still fix inconsistencies in go.mod 229 // though. 230 // 231 // TODO(#64008): In the future, report an error if go.mod or go.sum need to 232 // be updated after loading the build list. This may require setting 233 // the mode to "mod" or "readonly" depending on haveExplicitArgs. 234 if err := modload.WriteGoMod(ctx, modload.WriteOpts{}); err != nil { 235 base.Fatal(err) 236 } 237 } 238 239 var downloadErrs sync.Map 240 for _, info := range infos { 241 if info.Replace != nil { 242 info = info.Replace 243 } 244 if info.Version == "" && info.Error == nil { 245 // main module or module replaced with file path. 246 // Nothing to download. 247 continue 248 } 249 m := &ModuleJSON{ 250 Path: info.Path, 251 Version: info.Version, 252 Query: info.Query, 253 Reuse: info.Reuse, 254 Origin: info.Origin, 255 } 256 mods = append(mods, m) 257 if info.Error != nil { 258 m.Error = info.Error.Err 259 continue 260 } 261 if m.Reuse { 262 continue 263 } 264 sem <- token{} 265 go func() { 266 err := DownloadModule(ctx, m) 267 if err != nil { 268 downloadErrs.Store(m, err) 269 m.Error = err.Error() 270 } 271 <-sem 272 }() 273 } 274 275 // Fill semaphore channel to wait for goroutines to finish. 276 for n := cap(sem); n > 0; n-- { 277 sem <- token{} 278 } 279 280 // If there were explicit arguments 281 // (like 'go mod download golang.org/x/tools@latest'), 282 // check whether we need to upgrade the toolchain in order to download them. 283 // 284 // (If invoked without arguments, we expect the module graph to already 285 // be tidy and the go.mod file to declare a 'go' version that satisfies 286 // transitive requirements. If that invariant holds, then we should have 287 // already upgraded when we loaded the module graph, and should not need 288 // an additional check here. See https://go.dev/issue/45551.) 289 // 290 // We also allow upgrades if in a workspace because in workspace mode 291 // with no arguments we download the module pattern "all", 292 // which may include dependencies that are normally pruned out 293 // of the individual modules in the workspace. 294 if haveExplicitArgs || modload.WorkFilePath() != "" { 295 var sw toolchain.Switcher 296 // Add errors to the Switcher in deterministic order so that they will be 297 // logged deterministically. 298 for _, m := range mods { 299 if erri, ok := downloadErrs.Load(m); ok { 300 sw.Error(erri.(error)) 301 } 302 } 303 // Only call sw.Switch if it will actually switch. 304 // Otherwise, we may want to write the errors as JSON 305 // (instead of using base.Error as sw.Switch would), 306 // and we may also have other errors to report from the 307 // initial infos returned by ListModules. 308 if sw.NeedSwitch() { 309 sw.Switch(ctx) 310 } 311 } 312 313 if *downloadJSON { 314 for _, m := range mods { 315 b, err := json.MarshalIndent(m, "", "\t") 316 if err != nil { 317 base.Fatal(err) 318 } 319 os.Stdout.Write(append(b, '\n')) 320 if m.Error != "" { 321 base.SetExitStatus(1) 322 } 323 } 324 } else { 325 for _, m := range mods { 326 if m.Error != "" { 327 base.Error(errors.New(m.Error)) 328 } 329 } 330 base.ExitIfErrors() 331 } 332 333 // If there were explicit arguments, update go.mod and especially go.sum. 334 // 'go mod download mod@version' is a useful way to add a sum without using 335 // 'go get mod@version', which may have other side effects. We print this in 336 // some error message hints. 337 // 338 // If we're in workspace mode, update go.work.sum with checksums for all of 339 // the modules we downloaded that aren't already recorded. Since a requirement 340 // in one module may upgrade a dependency of another, we can't be sure that 341 // the import graph matches the import graph of any given module in isolation, 342 // so we may end up needing to load packages from modules that wouldn't 343 // otherwise be relevant. 344 // 345 // TODO(#44435): If we adjust the set of modules downloaded in workspace mode, 346 // we may also need to adjust the logic for saving checksums here. 347 // 348 // Don't save sums for 'go mod download' without arguments unless we're in 349 // workspace mode; see comment above. 350 if haveExplicitArgs || modload.WorkFilePath() != "" { 351 if err := modload.WriteGoMod(ctx, modload.WriteOpts{}); err != nil { 352 base.Error(err) 353 } 354 } 355 356 // If there was an error matching some of the requested packages, emit it now 357 // (after we've written the checksums for the modules that were downloaded 358 // successfully). 359 if infosErr != nil { 360 base.Error(infosErr) 361 } 362 } 363 364 // DownloadModule runs 'go mod download' for m.Path@m.Version, 365 // leaving the results (including any error) in m itself. 366 func DownloadModule(ctx context.Context, m *ModuleJSON) error { 367 var err error 368 _, file, err := modfetch.InfoFile(ctx, m.Path, m.Version) 369 if err != nil { 370 return err 371 } 372 m.Info = file 373 m.GoMod, err = modfetch.GoModFile(ctx, m.Path, m.Version) 374 if err != nil { 375 return err 376 } 377 m.GoModSum, err = modfetch.GoModSum(ctx, m.Path, m.Version) 378 if err != nil { 379 return err 380 } 381 mod := module.Version{Path: m.Path, Version: m.Version} 382 m.Zip, err = modfetch.DownloadZip(ctx, mod) 383 if err != nil { 384 return err 385 } 386 m.Sum = modfetch.Sum(ctx, mod) 387 m.Dir, err = modfetch.Download(ctx, mod) 388 return err 389 }