github.com/gernest/nezuko@v0.1.2/internal/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  // z mod edit
     6  
     7  package modcmd
     8  
     9  import (
    10  	"bytes"
    11  	"encoding/json"
    12  	"fmt"
    13  	"io/ioutil"
    14  	"os"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	"github.com/gernest/nezuko/internal/base"
    19  	"github.com/gernest/nezuko/internal/modfetch"
    20  	"github.com/gernest/nezuko/internal/modfile"
    21  	"github.com/gernest/nezuko/internal/modload"
    22  	"github.com/gernest/nezuko/internal/module"
    23  )
    24  
    25  var cmdEdit = &base.Command{
    26  	UsageLine: "z mod edit [editing flags] [z.mod]",
    27  	Short:     "edit z.mod from tools or scripts",
    28  	Long: `
    29  Edit provides a command-line interface for editing z.mod,
    30  for use primarily by tools or scripts. It reads only z.mod;
    31  it does not look up information about the modules involved.
    32  By default, edit reads and writes the z.mod file of the main module,
    33  but a different target file can be specified after the editing flags.
    34  
    35  The editing flags specify a sequence of editing operations.
    36  
    37  The -fmt flag reformats the z.mod file without making other changes.
    38  This reformatting is also implied by any other modifications that use or
    39  rewrite the z.mod file. The only time this flag is needed is if no other
    40  flags are specified, as in 'z mod edit -fmt'.
    41  
    42  The -module flag changes the module's path (the z.mod file's module line).
    43  
    44  The -require=path@version and -droprequire=path flags
    45  add and drop a requirement on the given module path and version.
    46  Note that -require overrides any existing requirements on path.
    47  These flags are mainly for tools that understand the module graph.
    48  Users should prefer 'go get path@version' or 'go get path@none',
    49  which make other z.mod adjustments as needed to satisfy
    50  constraints imposed by other modules.
    51  
    52  The -exclude=path@version and -dropexclude=path@version flags
    53  add and drop an exclusion for the given module path and version.
    54  Note that -exclude=path@version is a no-op if that exclusion already exists.
    55  
    56  The -replace=old[@v]=new[@v] and -dropreplace=old[@v] flags
    57  add and drop a replacement of the given module path and version pair.
    58  If the @v in old@v is omitted, the replacement applies to all versions
    59  with the old module path. If the @v in new@v is omitted, the new path
    60  should be a local module root directory, not a module path.
    61  Note that -replace overrides any existing replacements for old[@v].
    62  
    63  The -require, -droprequire, -exclude, -dropexclude, -replace,
    64  and -dropreplace editing flags may be repeated, and the changes
    65  are applied in the order given.
    66  
    67  The -go=version flag sets the expected Go language version.
    68  
    69  The -print flag prints the final z.mod in its text format instead of
    70  writing it back to z.mod.
    71  
    72  The -json flag prints the final z.mod file in JSON format instead of
    73  writing it back to z.mod. The JSON output corresponds to these Go types:
    74  
    75  	type Module struct {
    76  		Path string
    77  		Version string
    78  	}
    79  
    80  	type GoMod struct {
    81  		Module  Module
    82  		Go      string
    83  		Require []Require
    84  		Exclude []Module
    85  		Replace []Replace
    86  	}
    87  
    88  	type Require struct {
    89  		Path string
    90  		Version string
    91  		Indirect bool
    92  	}
    93  
    94  	type Replace struct {
    95  		Old Module
    96  		New Module
    97  	}
    98  
    99  Note that this only describes the z.mod file itself, not other modules
   100  referred to indirectly. For the full set of modules available to a build,
   101  use 'go list -m -json all'.
   102  
   103  For example, a tool can obtain the z.mod as a data structure by
   104  parsing the output of 'z mod edit -json' and can then make changes
   105  by invoking 'z mod edit' with -require, -exclude, and so on.
   106  	`,
   107  }
   108  
   109  var (
   110  	editFmt     = cmdEdit.Flag.Bool("fmt", false, "")
   111  	editExports = cmdEdit.Flag.String("exports", "", "")
   112  	editJSON    = cmdEdit.Flag.Bool("json", false, "")
   113  	editPrint   = cmdEdit.Flag.Bool("print", false, "")
   114  	editModule  = cmdEdit.Flag.String("module", "", "")
   115  	edits       []func(*modfile.File) // edits specified in flags
   116  )
   117  
   118  type flagFunc func(string)
   119  
   120  func (f flagFunc) String() string     { return "" }
   121  func (f flagFunc) Set(s string) error { f(s); return nil }
   122  
   123  func init() {
   124  	cmdEdit.Run = runEdit // break init cycle
   125  
   126  	cmdEdit.Flag.Var(flagFunc(flagRequire), "require", "")
   127  	cmdEdit.Flag.Var(flagFunc(flagDropRequire), "droprequire", "")
   128  	cmdEdit.Flag.Var(flagFunc(flagExclude), "exclude", "")
   129  	cmdEdit.Flag.Var(flagFunc(flagDropReplace), "dropreplace", "")
   130  	cmdEdit.Flag.Var(flagFunc(flagReplace), "replace", "")
   131  	cmdEdit.Flag.Var(flagFunc(flagDropExclude), "dropexclude", "")
   132  
   133  	base.AddBuildFlagsNX(&cmdEdit.Flag)
   134  }
   135  
   136  func runEdit(cmd *base.Command, args []string) {
   137  	anyFlags :=
   138  		*editModule != "" ||
   139  			*editExports != "" ||
   140  			*editJSON ||
   141  			*editPrint ||
   142  			*editFmt ||
   143  			len(edits) > 0
   144  
   145  	if !anyFlags {
   146  		base.Fatalf("z mod edit: no flags specified (see 'go help mod edit').")
   147  	}
   148  
   149  	if *editJSON && *editPrint {
   150  		base.Fatalf("z mod edit: cannot use both -json and -print")
   151  	}
   152  
   153  	if len(args) > 1 {
   154  		base.Fatalf("z mod edit: too many arguments")
   155  	}
   156  	var gomod string
   157  	if len(args) == 1 {
   158  		gomod = args[0]
   159  	} else {
   160  		gomod = filepath.Join(modload.ModRoot(), "z.mod")
   161  	}
   162  
   163  	if *editModule != "" {
   164  		if err := module.CheckPath(*editModule); err != nil {
   165  			base.Fatalf("z mod: invalid -module: %v", err)
   166  		}
   167  	}
   168  
   169  	if *editExports != "" {
   170  		if !modfile.IsValidExport(*editExports) {
   171  			base.Fatalf(`z mod: invalid -exports option; expecting something like "-exports foo"`)
   172  		}
   173  	}
   174  
   175  	data, err := ioutil.ReadFile(gomod)
   176  	if err != nil {
   177  		base.Fatalf("z: %v", err)
   178  	}
   179  
   180  	modFile, err := modfile.Parse(gomod, data, nil)
   181  	if err != nil {
   182  		base.Fatalf("z: errors parsing %s:\n%s", base.ShortPath(gomod), err)
   183  	}
   184  
   185  	if *editModule != "" {
   186  		modFile.AddModuleStmt(*editModule)
   187  	}
   188  
   189  	if *editExports != "" {
   190  		if err := modFile.AddExportsStmt(*editExports); err != nil {
   191  			base.Fatalf("z: internal error: %v", err)
   192  		}
   193  	}
   194  
   195  	if len(edits) > 0 {
   196  		for _, edit := range edits {
   197  			edit(modFile)
   198  		}
   199  	}
   200  	modFile.SortBlocks()
   201  	modFile.Cleanup() // clean file after edits
   202  
   203  	if *editJSON {
   204  		editPrintJSON(modFile)
   205  		return
   206  	}
   207  
   208  	out, err := modFile.Format()
   209  	if err != nil {
   210  		base.Fatalf("z: %v", err)
   211  	}
   212  
   213  	if *editPrint {
   214  		os.Stdout.Write(out)
   215  		return
   216  	}
   217  
   218  	unlock := modfetch.SideLock()
   219  	defer unlock()
   220  	lockedData, err := ioutil.ReadFile(gomod)
   221  	if err == nil && !bytes.Equal(lockedData, data) {
   222  		base.Fatalf("z: z.mod changed during editing; not overwriting")
   223  	}
   224  	if err := ioutil.WriteFile(gomod, out, 0666); err != nil {
   225  		base.Fatalf("z: %v", err)
   226  	}
   227  }
   228  
   229  // parsePathVersion parses -flag=arg expecting arg to be path@version.
   230  func parsePathVersion(flag, arg string) (path, version string) {
   231  	i := strings.Index(arg, "@")
   232  	if i < 0 {
   233  		base.Fatalf("z mod: -%s=%s: need path@version", flag, arg)
   234  	}
   235  	path, version = strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+1:])
   236  	if err := module.CheckPath(path); err != nil {
   237  		base.Fatalf("z mod: -%s=%s: invalid path: %v", flag, arg, err)
   238  	}
   239  
   240  	// We don't call modfile.CheckPathVersion, because that insists
   241  	// on versions being in semver form, but here we want to allow
   242  	// versions like "master" or "1234abcdef", which the go command will resolve
   243  	// the next time it runs (or during -fix).
   244  	// Even so, we need to make sure the version is a valid token.
   245  	if modfile.MustQuote(version) {
   246  		base.Fatalf("z mod: -%s=%s: invalid version %q", flag, arg, version)
   247  	}
   248  
   249  	return path, version
   250  }
   251  
   252  // parsePath parses -flag=arg expecting arg to be path (not path@version).
   253  func parsePath(flag, arg string) (path string) {
   254  	if strings.Contains(arg, "@") {
   255  		base.Fatalf("z mod: -%s=%s: need just path, not path@version", flag, arg)
   256  	}
   257  	path = arg
   258  	if err := module.CheckPath(path); err != nil {
   259  		base.Fatalf("z mod: -%s=%s: invalid path: %v", flag, arg, err)
   260  	}
   261  	return path
   262  }
   263  
   264  // parsePathVersionOptional parses path[@version], using adj to
   265  // describe any errors.
   266  func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
   267  	if i := strings.Index(arg, "@"); i < 0 {
   268  		path = arg
   269  	} else {
   270  		path, version = strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+1:])
   271  	}
   272  	if err := module.CheckPath(path); err != nil {
   273  		if !allowDirPath || !modfile.IsDirectoryPath(path) {
   274  			return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
   275  		}
   276  	}
   277  	if path != arg && modfile.MustQuote(version) {
   278  		return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
   279  	}
   280  	return path, version, nil
   281  }
   282  
   283  // flagRequire implements the -require flag.
   284  func flagRequire(arg string) {
   285  	path, version := parsePathVersion("require", arg)
   286  	edits = append(edits, func(f *modfile.File) {
   287  		if err := f.AddRequire(path, version); err != nil {
   288  			base.Fatalf("z mod: -require=%s: %v", arg, err)
   289  		}
   290  	})
   291  }
   292  
   293  // flagDropRequire implements the -droprequire flag.
   294  func flagDropRequire(arg string) {
   295  	path := parsePath("droprequire", arg)
   296  	edits = append(edits, func(f *modfile.File) {
   297  		if err := f.DropRequire(path); err != nil {
   298  			base.Fatalf("z mod: -droprequire=%s: %v", arg, err)
   299  		}
   300  	})
   301  }
   302  
   303  // flagExclude implements the -exclude flag.
   304  func flagExclude(arg string) {
   305  	path, version := parsePathVersion("exclude", arg)
   306  	edits = append(edits, func(f *modfile.File) {
   307  		if err := f.AddExclude(path, version); err != nil {
   308  			base.Fatalf("z mod: -exclude=%s: %v", arg, err)
   309  		}
   310  	})
   311  }
   312  
   313  // flagDropExclude implements the -dropexclude flag.
   314  func flagDropExclude(arg string) {
   315  	path, version := parsePathVersion("dropexclude", arg)
   316  	edits = append(edits, func(f *modfile.File) {
   317  		if err := f.DropExclude(path, version); err != nil {
   318  			base.Fatalf("z mod: -dropexclude=%s: %v", arg, err)
   319  		}
   320  	})
   321  }
   322  
   323  // flagReplace implements the -replace flag.
   324  func flagReplace(arg string) {
   325  	var i int
   326  	if i = strings.Index(arg, "="); i < 0 {
   327  		base.Fatalf("z mod: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
   328  	}
   329  	old, new := strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+1:])
   330  	if strings.HasPrefix(new, ">") {
   331  		base.Fatalf("z mod: -replace=%s: separator between old and new is =, not =>", arg)
   332  	}
   333  	oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
   334  	if err != nil {
   335  		base.Fatalf("z mod: -replace=%s: %v", arg, err)
   336  	}
   337  	newPath, newVersion, err := parsePathVersionOptional("new", new, true)
   338  	if err != nil {
   339  		base.Fatalf("z mod: -replace=%s: %v", arg, err)
   340  	}
   341  	if newPath == new && !modfile.IsDirectoryPath(new) {
   342  		base.Fatalf("z mod: -replace=%s: unversioned new path must be local directory", arg)
   343  	}
   344  
   345  	edits = append(edits, func(f *modfile.File) {
   346  		if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
   347  			base.Fatalf("z mod: -replace=%s: %v", arg, err)
   348  		}
   349  	})
   350  }
   351  
   352  // flagDropReplace implements the -dropreplace flag.
   353  func flagDropReplace(arg string) {
   354  	path, version, err := parsePathVersionOptional("old", arg, true)
   355  	if err != nil {
   356  		base.Fatalf("z mod: -dropreplace=%s: %v", arg, err)
   357  	}
   358  	edits = append(edits, func(f *modfile.File) {
   359  		if err := f.DropReplace(path, version); err != nil {
   360  			base.Fatalf("z mod: -dropreplace=%s: %v", arg, err)
   361  		}
   362  	})
   363  }
   364  
   365  // fileJSON is the -json output data structure.
   366  type fileJSON struct {
   367  	Module  module.Version
   368  	Exports string `json:",omitempty"`
   369  	Require []requireJSON
   370  	Exclude []module.Version
   371  	Replace []replaceJSON
   372  }
   373  
   374  type requireJSON struct {
   375  	Path     string
   376  	Version  string `json:",omitempty"`
   377  	Indirect bool   `json:",omitempty"`
   378  }
   379  
   380  type replaceJSON struct {
   381  	Old module.Version
   382  	New module.Version
   383  }
   384  
   385  // editPrintJSON prints the -json output.
   386  func editPrintJSON(modFile *modfile.File) {
   387  	var f fileJSON
   388  	f.Module = modFile.Module.Mod
   389  	if modFile.Exports != nil {
   390  		f.Exports = modFile.Exports.Name
   391  	}
   392  	for _, r := range modFile.Require {
   393  		f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect})
   394  	}
   395  	for _, x := range modFile.Exclude {
   396  		f.Exclude = append(f.Exclude, x.Mod)
   397  	}
   398  	for _, r := range modFile.Replace {
   399  		f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
   400  	}
   401  	data, err := json.MarshalIndent(&f, "", "\t")
   402  	if err != nil {
   403  		base.Fatalf("z: internal error: %v", err)
   404  	}
   405  	data = append(data, '\n')
   406  	os.Stdout.Write(data)
   407  }