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  }