Source file src/cmd/go/internal/workcmd/use.go

     1  // Copyright 2021 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 work use
     6  
     7  package workcmd
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"io/fs"
    13  	"os"
    14  	"path/filepath"
    15  
    16  	"cmd/go/internal/base"
    17  	"cmd/go/internal/fsys"
    18  	"cmd/go/internal/gover"
    19  	"cmd/go/internal/modload"
    20  	"cmd/go/internal/str"
    21  	"cmd/go/internal/toolchain"
    22  
    23  	"golang.org/x/mod/modfile"
    24  )
    25  
    26  var cmdUse = &base.Command{
    27  	UsageLine: "go work use [-r] [moddirs]",
    28  	Short:     "add modules to workspace file",
    29  	Long: `Use provides a command-line interface for adding
    30  directories, optionally recursively, to a go.work file.
    31  
    32  A use directive will be added to the go.work file for each argument
    33  directory listed on the command line go.work file, if it exists,
    34  or removed from the go.work file if it does not exist.
    35  Use fails if any remaining use directives refer to modules that
    36  do not exist.
    37  
    38  Use updates the go line in go.work to specify a version at least as
    39  new as all the go lines in the used modules, both preexisting ones
    40  and newly added ones. With no arguments, this update is the only
    41  thing that go work use does.
    42  
    43  The -r flag searches recursively for modules in the argument
    44  directories, and the use command operates as if each of the directories
    45  were specified as arguments.
    46  
    47  
    48  
    49  See the workspaces reference at https://go.dev/ref/mod#workspaces
    50  for more information.
    51  `,
    52  }
    53  
    54  var useR = cmdUse.Flag.Bool("r", false, "")
    55  
    56  func init() {
    57  	cmdUse.Run = runUse // break init cycle
    58  
    59  	base.AddChdirFlag(&cmdUse.Flag)
    60  	base.AddModCommonFlags(&cmdUse.Flag)
    61  }
    62  
    63  func runUse(ctx context.Context, cmd *base.Command, args []string) {
    64  	modload.ForceUseModules = true
    65  	modload.InitWorkfile()
    66  	gowork := modload.WorkFilePath()
    67  	if gowork == "" {
    68  		base.Fatalf("go: no go.work file found\n\t(run 'go work init' first or specify path using GOWORK environment variable)")
    69  	}
    70  	wf, err := modload.ReadWorkFile(gowork)
    71  	if err != nil {
    72  		base.Fatal(err)
    73  	}
    74  	workUse(ctx, gowork, wf, args)
    75  	modload.WriteWorkFile(gowork, wf)
    76  }
    77  
    78  func workUse(ctx context.Context, gowork string, wf *modfile.WorkFile, args []string) {
    79  	workDir := filepath.Dir(gowork) // absolute, since gowork itself is absolute
    80  
    81  	haveDirs := make(map[string][]string) // absolute → original(s)
    82  	for _, use := range wf.Use {
    83  		var abs string
    84  		if filepath.IsAbs(use.Path) {
    85  			abs = filepath.Clean(use.Path)
    86  		} else {
    87  			abs = filepath.Join(workDir, use.Path)
    88  		}
    89  		haveDirs[abs] = append(haveDirs[abs], use.Path)
    90  	}
    91  
    92  	// keepDirs maps each absolute path to keep to the literal string to use for
    93  	// that path (either an absolute or a relative path), or the empty string if
    94  	// all entries for the absolute path should be removed.
    95  	keepDirs := make(map[string]string)
    96  
    97  	var sw toolchain.Switcher
    98  
    99  	// lookDir updates the entry in keepDirs for the directory dir,
   100  	// which is either absolute or relative to the current working directory
   101  	// (not necessarily the directory containing the workfile).
   102  	lookDir := func(dir string) {
   103  		absDir, dir := pathRel(workDir, dir)
   104  
   105  		file := base.ShortPathConservative(filepath.Join(absDir, "go.mod"))
   106  		fi, err := fsys.Stat(file)
   107  		if err != nil {
   108  			if os.IsNotExist(err) {
   109  				keepDirs[absDir] = ""
   110  			} else {
   111  				sw.Error(err)
   112  			}
   113  			return
   114  		}
   115  
   116  		if !fi.Mode().IsRegular() {
   117  			sw.Error(fmt.Errorf("%v is not a regular file", file))
   118  			return
   119  		}
   120  
   121  		if dup := keepDirs[absDir]; dup != "" && dup != dir {
   122  			base.Errorf(`go: already added "%s" as "%s"`, dir, dup)
   123  		}
   124  		keepDirs[absDir] = dir
   125  	}
   126  
   127  	for _, useDir := range args {
   128  		absArg, _ := pathRel(workDir, useDir)
   129  		useDirShort := base.ShortPathConservative(absArg) // relative to the working directory rather than the workspace
   130  
   131  		info, err := fsys.Stat(useDirShort)
   132  		if err != nil {
   133  			// Errors raised from os.Stat are formatted to be more user-friendly.
   134  			if os.IsNotExist(err) {
   135  				err = fmt.Errorf("directory %v does not exist", useDirShort)
   136  			}
   137  			sw.Error(err)
   138  			continue
   139  		} else if !info.IsDir() {
   140  			sw.Error(fmt.Errorf("%s is not a directory", useDirShort))
   141  			continue
   142  		}
   143  
   144  		if !*useR {
   145  			lookDir(useDir)
   146  			continue
   147  		}
   148  
   149  		// Add or remove entries for any subdirectories that still exist.
   150  		// If the root itself is a symlink to a directory,
   151  		// we want to follow it (see https://go.dev/issue/50807).
   152  		// Add a trailing separator to force that to happen.
   153  		fsys.Walk(str.WithFilePathSeparator(useDir), func(path string, info fs.FileInfo, err error) error {
   154  			if err != nil {
   155  				return err
   156  			}
   157  
   158  			if !info.IsDir() {
   159  				if info.Mode()&fs.ModeSymlink != 0 {
   160  					if target, err := fsys.Stat(path); err == nil && target.IsDir() {
   161  						fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", base.ShortPathConservative(path))
   162  					}
   163  				}
   164  				return nil
   165  			}
   166  			lookDir(path)
   167  			return nil
   168  		})
   169  
   170  		// Remove entries for subdirectories that no longer exist.
   171  		// Because they don't exist, they will be skipped by Walk.
   172  		for absDir := range haveDirs {
   173  			if str.HasFilePathPrefix(absDir, absArg) {
   174  				if _, ok := keepDirs[absDir]; !ok {
   175  					keepDirs[absDir] = "" // Mark for deletion.
   176  				}
   177  			}
   178  		}
   179  	}
   180  
   181  	// Update the work file.
   182  	for absDir, keepDir := range keepDirs {
   183  		nKept := 0
   184  		for _, dir := range haveDirs[absDir] {
   185  			if dir == keepDir { // (note that dir is always non-empty)
   186  				nKept++
   187  			} else {
   188  				wf.DropUse(dir)
   189  			}
   190  		}
   191  		if keepDir != "" && nKept != 1 {
   192  			// If we kept more than one copy, delete them all.
   193  			// We'll recreate a unique copy with AddUse.
   194  			if nKept > 1 {
   195  				wf.DropUse(keepDir)
   196  			}
   197  			wf.AddUse(keepDir, "")
   198  		}
   199  	}
   200  
   201  	// Read the Go versions from all the use entries, old and new (but not dropped).
   202  	goV := gover.FromGoWork(wf)
   203  	for _, use := range wf.Use {
   204  		if use.Path == "" { // deleted
   205  			continue
   206  		}
   207  		var abs string
   208  		if filepath.IsAbs(use.Path) {
   209  			abs = filepath.Clean(use.Path)
   210  		} else {
   211  			abs = filepath.Join(workDir, use.Path)
   212  		}
   213  		_, mf, err := modload.ReadModFile(base.ShortPathConservative(filepath.Join(abs, "go.mod")), nil)
   214  		if err != nil {
   215  			sw.Error(err)
   216  			continue
   217  		}
   218  		goV = gover.Max(goV, gover.FromGoMod(mf))
   219  	}
   220  	sw.Switch(ctx)
   221  	base.ExitIfErrors()
   222  
   223  	modload.UpdateWorkGoVersion(wf, goV)
   224  	modload.UpdateWorkFile(wf)
   225  }
   226  
   227  // pathRel returns the absolute and canonical forms of dir for use in a
   228  // go.work file located in directory workDir.
   229  //
   230  // If dir is relative, it is interpreted relative to base.Cwd()
   231  // and its canonical form is relative to workDir if possible.
   232  // If dir is absolute or cannot be made relative to workDir,
   233  // its canonical form is absolute.
   234  //
   235  // Canonical absolute paths are clean.
   236  // Canonical relative paths are clean and slash-separated.
   237  func pathRel(workDir, dir string) (abs, canonical string) {
   238  	if filepath.IsAbs(dir) {
   239  		abs = filepath.Clean(dir)
   240  		return abs, abs
   241  	}
   242  
   243  	abs = filepath.Join(base.Cwd(), dir)
   244  	rel, err := filepath.Rel(workDir, abs)
   245  	if err != nil {
   246  		// The path can't be made relative to the go.work file,
   247  		// so it must be kept absolute instead.
   248  		return abs, abs
   249  	}
   250  
   251  	// Normalize relative paths to use slashes, so that checked-in go.work
   252  	// files with relative paths within the repo are platform-independent.
   253  	return abs, modload.ToDirectoryPath(rel)
   254  }
   255  

View as plain text