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