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

View as plain text