github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/modcmd/edit.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 // go mod edit 6 7 package modcmd 8 9 import ( 10 "bytes" 11 "context" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "os" 16 "strings" 17 18 "github.com/go-asm/go/cmd/go/base" 19 "github.com/go-asm/go/cmd/go/gover" 20 "github.com/go-asm/go/cmd/go/lockedfile" 21 "github.com/go-asm/go/cmd/go/modfetch" 22 "github.com/go-asm/go/cmd/go/modload" 23 24 "golang.org/x/mod/modfile" 25 "golang.org/x/mod/module" 26 ) 27 28 var cmdEdit = &base.Command{ 29 UsageLine: "go mod edit [editing flags] [-fmt|-print|-json] [go.mod]", 30 Short: "edit go.mod from tools or scripts", 31 Long: ` 32 Edit provides a command-line interface for editing go.mod, 33 for use primarily by tools or scripts. It reads only go.mod; 34 it does not look up information about the modules involved. 35 By default, edit reads and writes the go.mod file of the main module, 36 but a different target file can be specified after the editing flags. 37 38 The editing flags specify a sequence of editing operations. 39 40 The -fmt flag reformats the go.mod file without making other changes. 41 This reformatting is also implied by any other modifications that use or 42 rewrite the go.mod file. The only time this flag is needed is if no other 43 flags are specified, as in 'go mod edit -fmt'. 44 45 The -module flag changes the module's path (the go.mod file's module line). 46 47 The -require=path@version and -droprequire=path flags 48 add and drop a requirement on the given module path and version. 49 Note that -require overrides any existing requirements on path. 50 These flags are mainly for tools that understand the module graph. 51 Users should prefer 'go get path@version' or 'go get path@none', 52 which make other go.mod adjustments as needed to satisfy 53 constraints imposed by other modules. 54 55 The -exclude=path@version and -dropexclude=path@version flags 56 add and drop an exclusion for the given module path and version. 57 Note that -exclude=path@version is a no-op if that exclusion already exists. 58 59 The -replace=old[@v]=new[@v] flag adds a replacement of the given 60 module path and version pair. If the @v in old@v is omitted, a 61 replacement without a version on the left side is added, which applies 62 to all versions of the old module path. If the @v in new@v is omitted, 63 the new path should be a local module root directory, not a module 64 path. Note that -replace overrides any redundant replacements for old[@v], 65 so omitting @v will drop existing replacements for specific versions. 66 67 The -dropreplace=old[@v] flag drops a replacement of the given 68 module path and version pair. If the @v is omitted, a replacement without 69 a version on the left side is dropped. 70 71 The -retract=version and -dropretract=version flags add and drop a 72 retraction on the given version. The version may be a single version 73 like "v1.2.3" or a closed interval like "[v1.1.0,v1.1.9]". Note that 74 -retract=version is a no-op if that retraction already exists. 75 76 The -require, -droprequire, -exclude, -dropexclude, -replace, 77 -dropreplace, -retract, and -dropretract editing flags may be repeated, 78 and the changes are applied in the order given. 79 80 The -go=version flag sets the expected Go language version. 81 82 The -toolchain=name flag sets the Go toolchain to use. 83 84 The -print flag prints the final go.mod in its text format instead of 85 writing it back to go.mod. 86 87 The -json flag prints the final go.mod file in JSON format instead of 88 writing it back to go.mod. The JSON output corresponds to these Go types: 89 90 type Module struct { 91 Path string 92 Version string 93 } 94 95 type GoMod struct { 96 Module ModPath 97 Go string 98 Toolchain string 99 Require []Require 100 Exclude []Module 101 Replace []Replace 102 Retract []Retract 103 } 104 105 type ModPath struct { 106 Path string 107 Deprecated string 108 } 109 110 type Require struct { 111 Path string 112 Version string 113 Indirect bool 114 } 115 116 type Replace struct { 117 Old Module 118 New Module 119 } 120 121 type Retract struct { 122 Low string 123 High string 124 Rationale string 125 } 126 127 Retract entries representing a single version (not an interval) will have 128 the "Low" and "High" fields set to the same value. 129 130 Note that this only describes the go.mod file itself, not other modules 131 referred to indirectly. For the full set of modules available to a build, 132 use 'go list -m -json all'. 133 134 Edit also provides the -C, -n, and -x build flags. 135 136 See https://golang.org/ref/mod#go-mod-edit for more about 'go mod edit'. 137 `, 138 } 139 140 var ( 141 editFmt = cmdEdit.Flag.Bool("fmt", false, "") 142 editGo = cmdEdit.Flag.String("go", "", "") 143 editToolchain = cmdEdit.Flag.String("toolchain", "", "") 144 editJSON = cmdEdit.Flag.Bool("json", false, "") 145 editPrint = cmdEdit.Flag.Bool("print", false, "") 146 editModule = cmdEdit.Flag.String("module", "", "") 147 edits []func(*modfile.File) // edits specified in flags 148 ) 149 150 type flagFunc func(string) 151 152 func (f flagFunc) String() string { return "" } 153 func (f flagFunc) Set(s string) error { f(s); return nil } 154 155 func init() { 156 cmdEdit.Run = runEdit // break init cycle 157 158 cmdEdit.Flag.Var(flagFunc(flagRequire), "require", "") 159 cmdEdit.Flag.Var(flagFunc(flagDropRequire), "droprequire", "") 160 cmdEdit.Flag.Var(flagFunc(flagExclude), "exclude", "") 161 cmdEdit.Flag.Var(flagFunc(flagDropReplace), "dropreplace", "") 162 cmdEdit.Flag.Var(flagFunc(flagReplace), "replace", "") 163 cmdEdit.Flag.Var(flagFunc(flagDropExclude), "dropexclude", "") 164 cmdEdit.Flag.Var(flagFunc(flagRetract), "retract", "") 165 cmdEdit.Flag.Var(flagFunc(flagDropRetract), "dropretract", "") 166 167 base.AddBuildFlagsNX(&cmdEdit.Flag) 168 base.AddChdirFlag(&cmdEdit.Flag) 169 base.AddModCommonFlags(&cmdEdit.Flag) 170 } 171 172 func runEdit(ctx context.Context, cmd *base.Command, args []string) { 173 anyFlags := *editModule != "" || 174 *editGo != "" || 175 *editToolchain != "" || 176 *editJSON || 177 *editPrint || 178 *editFmt || 179 len(edits) > 0 180 181 if !anyFlags { 182 base.Fatalf("go: no flags specified (see 'go help mod edit').") 183 } 184 185 if *editJSON && *editPrint { 186 base.Fatalf("go: cannot use both -json and -print") 187 } 188 189 if len(args) > 1 { 190 base.Fatalf("go: too many arguments") 191 } 192 var gomod string 193 if len(args) == 1 { 194 gomod = args[0] 195 } else { 196 gomod = modload.ModFilePath() 197 } 198 199 if *editModule != "" { 200 if err := module.CheckImportPath(*editModule); err != nil { 201 base.Fatalf("go: invalid -module: %v", err) 202 } 203 } 204 205 if *editGo != "" && *editGo != "none" { 206 if !modfile.GoVersionRE.MatchString(*editGo) { 207 base.Fatalf(`go mod: invalid -go option; expecting something like "-go %s"`, gover.Local()) 208 } 209 } 210 if *editToolchain != "" && *editToolchain != "none" { 211 if !modfile.ToolchainRE.MatchString(*editToolchain) { 212 base.Fatalf(`go mod: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local()) 213 } 214 } 215 216 data, err := lockedfile.Read(gomod) 217 if err != nil { 218 base.Fatal(err) 219 } 220 221 modFile, err := modfile.Parse(gomod, data, nil) 222 if err != nil { 223 base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gomod), err) 224 } 225 226 if *editModule != "" { 227 modFile.AddModuleStmt(*editModule) 228 } 229 230 if *editGo == "none" { 231 modFile.DropGoStmt() 232 } else if *editGo != "" { 233 if err := modFile.AddGoStmt(*editGo); err != nil { 234 base.Fatalf("go: internal error: %v", err) 235 } 236 } 237 if *editToolchain == "none" { 238 modFile.DropToolchainStmt() 239 } else if *editToolchain != "" { 240 if err := modFile.AddToolchainStmt(*editToolchain); err != nil { 241 base.Fatalf("go: internal error: %v", err) 242 } 243 } 244 245 if len(edits) > 0 { 246 for _, edit := range edits { 247 edit(modFile) 248 } 249 } 250 modFile.SortBlocks() 251 modFile.Cleanup() // clean file after edits 252 253 if *editJSON { 254 editPrintJSON(modFile) 255 return 256 } 257 258 out, err := modFile.Format() 259 if err != nil { 260 base.Fatal(err) 261 } 262 263 if *editPrint { 264 os.Stdout.Write(out) 265 return 266 } 267 268 // Make a best-effort attempt to acquire the side lock, only to exclude 269 // previous versions of the 'go' command from making simultaneous edits. 270 if unlock, err := modfetch.SideLock(ctx); err == nil { 271 defer unlock() 272 } 273 274 err = lockedfile.Transform(gomod, func(lockedData []byte) ([]byte, error) { 275 if !bytes.Equal(lockedData, data) { 276 return nil, errors.New("go.mod changed during editing; not overwriting") 277 } 278 return out, nil 279 }) 280 if err != nil { 281 base.Fatal(err) 282 } 283 } 284 285 // parsePathVersion parses -flag=arg expecting arg to be path@version. 286 func parsePathVersion(flag, arg string) (path, version string) { 287 before, after, found := strings.Cut(arg, "@") 288 if !found { 289 base.Fatalf("go: -%s=%s: need path@version", flag, arg) 290 } 291 path, version = strings.TrimSpace(before), strings.TrimSpace(after) 292 if err := module.CheckImportPath(path); err != nil { 293 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err) 294 } 295 296 if !allowedVersionArg(version) { 297 base.Fatalf("go: -%s=%s: invalid version %q", flag, arg, version) 298 } 299 300 return path, version 301 } 302 303 // parsePath parses -flag=arg expecting arg to be path (not path@version). 304 func parsePath(flag, arg string) (path string) { 305 if strings.Contains(arg, "@") { 306 base.Fatalf("go: -%s=%s: need just path, not path@version", flag, arg) 307 } 308 path = arg 309 if err := module.CheckImportPath(path); err != nil { 310 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err) 311 } 312 return path 313 } 314 315 // parsePathVersionOptional parses path[@version], using adj to 316 // describe any errors. 317 func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) { 318 if allowDirPath && modfile.IsDirectoryPath(arg) { 319 return arg, "", nil 320 } 321 before, after, found := strings.Cut(arg, "@") 322 if !found { 323 path = arg 324 } else { 325 path, version = strings.TrimSpace(before), strings.TrimSpace(after) 326 } 327 if err := module.CheckImportPath(path); err != nil { 328 return path, version, fmt.Errorf("invalid %s path: %v", adj, err) 329 } 330 if path != arg && !allowedVersionArg(version) { 331 return path, version, fmt.Errorf("invalid %s version: %q", adj, version) 332 } 333 return path, version, nil 334 } 335 336 // parseVersionInterval parses a single version like "v1.2.3" or a closed 337 // interval like "[v1.2.3,v1.4.5]". Note that a single version has the same 338 // representation as an interval with equal upper and lower bounds: both 339 // Low and High are set. 340 func parseVersionInterval(arg string) (modfile.VersionInterval, error) { 341 if !strings.HasPrefix(arg, "[") { 342 if !allowedVersionArg(arg) { 343 return modfile.VersionInterval{}, fmt.Errorf("invalid version: %q", arg) 344 } 345 return modfile.VersionInterval{Low: arg, High: arg}, nil 346 } 347 if !strings.HasSuffix(arg, "]") { 348 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg) 349 } 350 s := arg[1 : len(arg)-1] 351 before, after, found := strings.Cut(s, ",") 352 if !found { 353 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg) 354 } 355 low := strings.TrimSpace(before) 356 high := strings.TrimSpace(after) 357 if !allowedVersionArg(low) || !allowedVersionArg(high) { 358 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg) 359 } 360 return modfile.VersionInterval{Low: low, High: high}, nil 361 } 362 363 // allowedVersionArg returns whether a token may be used as a version in go.mod. 364 // We don't call modfile.CheckPathVersion, because that insists on versions 365 // being in semver form, but here we want to allow versions like "master" or 366 // "1234abcdef", which the go command will resolve the next time it runs (or 367 // during -fix). Even so, we need to make sure the version is a valid token. 368 func allowedVersionArg(arg string) bool { 369 return !modfile.MustQuote(arg) 370 } 371 372 // flagRequire implements the -require flag. 373 func flagRequire(arg string) { 374 path, version := parsePathVersion("require", arg) 375 edits = append(edits, func(f *modfile.File) { 376 if err := f.AddRequire(path, version); err != nil { 377 base.Fatalf("go: -require=%s: %v", arg, err) 378 } 379 }) 380 } 381 382 // flagDropRequire implements the -droprequire flag. 383 func flagDropRequire(arg string) { 384 path := parsePath("droprequire", arg) 385 edits = append(edits, func(f *modfile.File) { 386 if err := f.DropRequire(path); err != nil { 387 base.Fatalf("go: -droprequire=%s: %v", arg, err) 388 } 389 }) 390 } 391 392 // flagExclude implements the -exclude flag. 393 func flagExclude(arg string) { 394 path, version := parsePathVersion("exclude", arg) 395 edits = append(edits, func(f *modfile.File) { 396 if err := f.AddExclude(path, version); err != nil { 397 base.Fatalf("go: -exclude=%s: %v", arg, err) 398 } 399 }) 400 } 401 402 // flagDropExclude implements the -dropexclude flag. 403 func flagDropExclude(arg string) { 404 path, version := parsePathVersion("dropexclude", arg) 405 edits = append(edits, func(f *modfile.File) { 406 if err := f.DropExclude(path, version); err != nil { 407 base.Fatalf("go: -dropexclude=%s: %v", arg, err) 408 } 409 }) 410 } 411 412 // flagReplace implements the -replace flag. 413 func flagReplace(arg string) { 414 before, after, found := strings.Cut(arg, "=") 415 if !found { 416 base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg) 417 } 418 old, new := strings.TrimSpace(before), strings.TrimSpace(after) 419 if strings.HasPrefix(new, ">") { 420 base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg) 421 } 422 oldPath, oldVersion, err := parsePathVersionOptional("old", old, false) 423 if err != nil { 424 base.Fatalf("go: -replace=%s: %v", arg, err) 425 } 426 newPath, newVersion, err := parsePathVersionOptional("new", new, true) 427 if err != nil { 428 base.Fatalf("go: -replace=%s: %v", arg, err) 429 } 430 if newPath == new && !modfile.IsDirectoryPath(new) { 431 base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg) 432 } 433 434 edits = append(edits, func(f *modfile.File) { 435 if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil { 436 base.Fatalf("go: -replace=%s: %v", arg, err) 437 } 438 }) 439 } 440 441 // flagDropReplace implements the -dropreplace flag. 442 func flagDropReplace(arg string) { 443 path, version, err := parsePathVersionOptional("old", arg, true) 444 if err != nil { 445 base.Fatalf("go: -dropreplace=%s: %v", arg, err) 446 } 447 edits = append(edits, func(f *modfile.File) { 448 if err := f.DropReplace(path, version); err != nil { 449 base.Fatalf("go: -dropreplace=%s: %v", arg, err) 450 } 451 }) 452 } 453 454 // flagRetract implements the -retract flag. 455 func flagRetract(arg string) { 456 vi, err := parseVersionInterval(arg) 457 if err != nil { 458 base.Fatalf("go: -retract=%s: %v", arg, err) 459 } 460 edits = append(edits, func(f *modfile.File) { 461 if err := f.AddRetract(vi, ""); err != nil { 462 base.Fatalf("go: -retract=%s: %v", arg, err) 463 } 464 }) 465 } 466 467 // flagDropRetract implements the -dropretract flag. 468 func flagDropRetract(arg string) { 469 vi, err := parseVersionInterval(arg) 470 if err != nil { 471 base.Fatalf("go: -dropretract=%s: %v", arg, err) 472 } 473 edits = append(edits, func(f *modfile.File) { 474 if err := f.DropRetract(vi); err != nil { 475 base.Fatalf("go: -dropretract=%s: %v", arg, err) 476 } 477 }) 478 } 479 480 // fileJSON is the -json output data structure. 481 type fileJSON struct { 482 Module editModuleJSON 483 Go string `json:",omitempty"` 484 Toolchain string `json:",omitempty"` 485 Require []requireJSON 486 Exclude []module.Version 487 Replace []replaceJSON 488 Retract []retractJSON 489 } 490 491 type editModuleJSON struct { 492 Path string 493 Deprecated string `json:",omitempty"` 494 } 495 496 type requireJSON struct { 497 Path string 498 Version string `json:",omitempty"` 499 Indirect bool `json:",omitempty"` 500 } 501 502 type replaceJSON struct { 503 Old module.Version 504 New module.Version 505 } 506 507 type retractJSON struct { 508 Low string `json:",omitempty"` 509 High string `json:",omitempty"` 510 Rationale string `json:",omitempty"` 511 } 512 513 // editPrintJSON prints the -json output. 514 func editPrintJSON(modFile *modfile.File) { 515 var f fileJSON 516 if modFile.Module != nil { 517 f.Module = editModuleJSON{ 518 Path: modFile.Module.Mod.Path, 519 Deprecated: modFile.Module.Deprecated, 520 } 521 } 522 if modFile.Go != nil { 523 f.Go = modFile.Go.Version 524 } 525 if modFile.Toolchain != nil { 526 f.Toolchain = modFile.Toolchain.Name 527 } 528 for _, r := range modFile.Require { 529 f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect}) 530 } 531 for _, x := range modFile.Exclude { 532 f.Exclude = append(f.Exclude, x.Mod) 533 } 534 for _, r := range modFile.Replace { 535 f.Replace = append(f.Replace, replaceJSON{r.Old, r.New}) 536 } 537 for _, r := range modFile.Retract { 538 f.Retract = append(f.Retract, retractJSON{r.Low, r.High, r.Rationale}) 539 } 540 data, err := json.MarshalIndent(&f, "", "\t") 541 if err != nil { 542 base.Fatalf("go: internal error: %v", err) 543 } 544 data = append(data, '\n') 545 os.Stdout.Write(data) 546 }