github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/workcmd/use.go (about) 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 "github.com/go-asm/go/cmd/go/base" 17 "github.com/go-asm/go/cmd/go/fsys" 18 "github.com/go-asm/go/cmd/go/gover" 19 "github.com/go-asm/go/cmd/go/modload" 20 "github.com/go-asm/go/cmd/go/str" 21 "github.com/go-asm/go/cmd/go/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: namely, use directives will be added for 46 directories that exist, and removed for directories that do not exist. 47 48 49 50 See the workspaces reference at https://go.dev/ref/mod#workspaces 51 for more information. 52 `, 53 } 54 55 var useR = cmdUse.Flag.Bool("r", false, "") 56 57 func init() { 58 cmdUse.Run = runUse // break init cycle 59 60 base.AddChdirFlag(&cmdUse.Flag) 61 base.AddModCommonFlags(&cmdUse.Flag) 62 } 63 64 func runUse(ctx context.Context, cmd *base.Command, args []string) { 65 modload.ForceUseModules = true 66 modload.InitWorkfile() 67 gowork := modload.WorkFilePath() 68 if gowork == "" { 69 base.Fatalf("go: no go.work file found\n\t(run 'go work init' first or specify path using GOWORK environment variable)") 70 } 71 wf, err := modload.ReadWorkFile(gowork) 72 if err != nil { 73 base.Fatal(err) 74 } 75 workUse(ctx, gowork, wf, args) 76 modload.WriteWorkFile(gowork, wf) 77 } 78 79 func workUse(ctx context.Context, gowork string, wf *modfile.WorkFile, args []string) { 80 workDir := filepath.Dir(gowork) // absolute, since gowork itself is absolute 81 82 haveDirs := make(map[string][]string) // absolute → original(s) 83 for _, use := range wf.Use { 84 var abs string 85 if filepath.IsAbs(use.Path) { 86 abs = filepath.Clean(use.Path) 87 } else { 88 abs = filepath.Join(workDir, use.Path) 89 } 90 haveDirs[abs] = append(haveDirs[abs], use.Path) 91 } 92 93 // keepDirs maps each absolute path to keep to the literal string to use for 94 // that path (either an absolute or a relative path), or the empty string if 95 // all entries for the absolute path should be removed. 96 keepDirs := make(map[string]string) 97 98 var sw toolchain.Switcher 99 100 // lookDir updates the entry in keepDirs for the directory dir, 101 // which is either absolute or relative to the current working directory 102 // (not necessarily the directory containing the workfile). 103 lookDir := func(dir string) { 104 absDir, dir := pathRel(workDir, dir) 105 106 file := base.ShortPath(filepath.Join(absDir, "go.mod")) 107 fi, err := fsys.Stat(file) 108 if err != nil { 109 if os.IsNotExist(err) { 110 keepDirs[absDir] = "" 111 } else { 112 sw.Error(err) 113 } 114 return 115 } 116 117 if !fi.Mode().IsRegular() { 118 sw.Error(fmt.Errorf("%v is not a regular file", file)) 119 return 120 } 121 122 if dup := keepDirs[absDir]; dup != "" && dup != dir { 123 base.Errorf(`go: already added "%s" as "%s"`, dir, dup) 124 } 125 keepDirs[absDir] = dir 126 } 127 128 for _, useDir := range args { 129 absArg, _ := pathRel(workDir, useDir) 130 131 info, err := fsys.Stat(base.ShortPath(absArg)) 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", base.ShortPath(absArg)) 136 } 137 sw.Error(err) 138 continue 139 } else if !info.IsDir() { 140 sw.Error(fmt.Errorf("%s is not a directory", base.ShortPath(absArg))) 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.ShortPath(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.ShortPath(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 }