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 7package workcmd 8 9import ( 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 26var 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 30directories, optionally recursively, to a go.work file. 31 32A use directive will be added to the go.work file for each argument 33directory listed on the command line go.work file, if it exists, 34or removed from the go.work file if it does not exist. 35Use fails if any remaining use directives refer to modules that 36do not exist. 37 38Use updates the go line in go.work to specify a version at least as 39new as all the go lines in the used modules, both preexisting ones 40and newly added ones. With no arguments, this update is the only 41thing that go work use does. 42 43The -r flag searches recursively for modules in the argument 44directories, and the use command operates as if each of the directories 45were specified as arguments. 46 47 48 49See the workspaces reference at https://go.dev/ref/mod#workspaces 50for more information. 51`, 52} 53 54var useR = cmdUse.Flag.Bool("r", false, "") 55 56func init() { 57 cmdUse.Run = runUse // break init cycle 58 59 base.AddChdirFlag(&cmdUse.Flag) 60 base.AddModCommonFlags(&cmdUse.Flag) 61} 62 63func 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 78func 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.ShortPath(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 130 info, err := fsys.Stat(base.ShortPath(absArg)) 131 if err != nil { 132 // Errors raised from os.Stat are formatted to be more user-friendly. 133 if os.IsNotExist(err) { 134 err = fmt.Errorf("directory %v does not exist", base.ShortPath(absArg)) 135 } 136 sw.Error(err) 137 continue 138 } else if !info.IsDir() { 139 sw.Error(fmt.Errorf("%s is not a directory", base.ShortPath(absArg))) 140 continue 141 } 142 143 if !*useR { 144 lookDir(useDir) 145 continue 146 } 147 148 // Add or remove entries for any subdirectories that still exist. 149 // If the root itself is a symlink to a directory, 150 // we want to follow it (see https://go.dev/issue/50807). 151 // Add a trailing separator to force that to happen. 152 fsys.Walk(str.WithFilePathSeparator(useDir), func(path string, info fs.FileInfo, err error) error { 153 if err != nil { 154 return err 155 } 156 157 if !info.IsDir() { 158 if info.Mode()&fs.ModeSymlink != 0 { 159 if target, err := fsys.Stat(path); err == nil && target.IsDir() { 160 fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", base.ShortPath(path)) 161 } 162 } 163 return nil 164 } 165 lookDir(path) 166 return nil 167 }) 168 169 // Remove entries for subdirectories that no longer exist. 170 // Because they don't exist, they will be skipped by Walk. 171 for absDir := range haveDirs { 172 if str.HasFilePathPrefix(absDir, absArg) { 173 if _, ok := keepDirs[absDir]; !ok { 174 keepDirs[absDir] = "" // Mark for deletion. 175 } 176 } 177 } 178 } 179 180 // Update the work file. 181 for absDir, keepDir := range keepDirs { 182 nKept := 0 183 for _, dir := range haveDirs[absDir] { 184 if dir == keepDir { // (note that dir is always non-empty) 185 nKept++ 186 } else { 187 wf.DropUse(dir) 188 } 189 } 190 if keepDir != "" && nKept != 1 { 191 // If we kept more than one copy, delete them all. 192 // We'll recreate a unique copy with AddUse. 193 if nKept > 1 { 194 wf.DropUse(keepDir) 195 } 196 wf.AddUse(keepDir, "") 197 } 198 } 199 200 // Read the Go versions from all the use entries, old and new (but not dropped). 201 goV := gover.FromGoWork(wf) 202 for _, use := range wf.Use { 203 if use.Path == "" { // deleted 204 continue 205 } 206 var abs string 207 if filepath.IsAbs(use.Path) { 208 abs = filepath.Clean(use.Path) 209 } else { 210 abs = filepath.Join(workDir, use.Path) 211 } 212 _, mf, err := modload.ReadModFile(base.ShortPath(filepath.Join(abs, "go.mod")), nil) 213 if err != nil { 214 sw.Error(err) 215 continue 216 } 217 goV = gover.Max(goV, gover.FromGoMod(mf)) 218 } 219 sw.Switch(ctx) 220 base.ExitIfErrors() 221 222 modload.UpdateWorkGoVersion(wf, goV) 223 modload.UpdateWorkFile(wf) 224} 225 226// pathRel returns the absolute and canonical forms of dir for use in a 227// go.work file located in directory workDir. 228// 229// If dir is relative, it is interpreted relative to base.Cwd() 230// and its canonical form is relative to workDir if possible. 231// If dir is absolute or cannot be made relative to workDir, 232// its canonical form is absolute. 233// 234// Canonical absolute paths are clean. 235// Canonical relative paths are clean and slash-separated. 236func pathRel(workDir, dir string) (abs, canonical string) { 237 if filepath.IsAbs(dir) { 238 abs = filepath.Clean(dir) 239 return abs, abs 240 } 241 242 abs = filepath.Join(base.Cwd(), dir) 243 rel, err := filepath.Rel(workDir, abs) 244 if err != nil { 245 // The path can't be made relative to the go.work file, 246 // so it must be kept absolute instead. 247 return abs, abs 248 } 249 250 // Normalize relative paths to use slashes, so that checked-in go.work 251 // files with relative paths within the repo are platform-independent. 252 return abs, modload.ToDirectoryPath(rel) 253} 254