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

View as plain text