aboutsummaryrefslogtreecommitdiff
path: root/internal/lsp/fake/sandbox.go
blob: f628f2d5409f740a26a97524013cbe9c7cb4eb1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package fake

import (
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/tools/internal/gocommand"
	"golang.org/x/tools/internal/testenv"
	"golang.org/x/tools/txtar"
	errors "golang.org/x/xerrors"
)

// Sandbox holds a collection of temporary resources to use for working with Go
// code in tests.
type Sandbox struct {
	gopath  string
	rootdir string
	goproxy string
	Workdir *Workdir
}

// SandboxConfig controls the behavior of a test sandbox. The zero value
// defines a reasonable default.
type SandboxConfig struct {
	// RootDir sets the base directory to use when creating temporary
	// directories. If not specified, defaults to a new temporary directory.
	RootDir string
	// Files holds a txtar-encoded archive of files to populate the initial state
	// of the working directory.
	//
	// For convenience, the special substring "$SANDBOX_WORKDIR" is replaced with
	// the sandbox's resolved working directory before writing files.
	Files map[string][]byte
	// InGoPath specifies that the working directory should be within the
	// temporary GOPATH.
	InGoPath bool
	// Workdir configures the working directory of the Sandbox. It behaves as
	// follows:
	//  - if set to an absolute path, use that path as the working directory.
	//  - if set to a relative path, create and use that path relative to the
	//    sandbox.
	//  - if unset, default to a the 'work' subdirectory of the sandbox.
	//
	// This option is incompatible with InGoPath or Files.
	Workdir string
	// ProxyFiles holds a txtar-encoded archive of files to populate a file-based
	// Go proxy.
	ProxyFiles map[string][]byte
	// GOPROXY is the explicit GOPROXY value that should be used for the sandbox.
	//
	// This option is incompatible with ProxyFiles.
	GOPROXY string
}

// NewSandbox creates a collection of named temporary resources, with a
// working directory populated by the txtar-encoded content in srctxt, and a
// file-based module proxy populated with the txtar-encoded content in
// proxytxt.
//
// If rootDir is non-empty, it will be used as the root of temporary
// directories created for the sandbox. Otherwise, a new temporary directory
// will be used as root.
func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
	if config == nil {
		config = new(SandboxConfig)
	}
	if err := validateConfig(*config); err != nil {
		return nil, fmt.Errorf("invalid SandboxConfig: %v", err)
	}

	sb := &Sandbox{}
	defer func() {
		// Clean up if we fail at any point in this constructor.
		if err != nil {
			sb.Close()
		}
	}()

	rootDir := config.RootDir
	if rootDir == "" {
		rootDir, err = ioutil.TempDir(config.RootDir, "gopls-sandbox-")
		if err != nil {
			return nil, fmt.Errorf("creating temporary workdir: %v", err)
		}
	}
	sb.rootdir = rootDir
	sb.gopath = filepath.Join(sb.rootdir, "gopath")
	if err := os.Mkdir(sb.gopath, 0755); err != nil {
		return nil, err
	}
	if config.GOPROXY != "" {
		sb.goproxy = config.GOPROXY
	} else {
		proxydir := filepath.Join(sb.rootdir, "proxy")
		if err := os.Mkdir(proxydir, 0755); err != nil {
			return nil, err
		}
		sb.goproxy, err = WriteProxy(proxydir, config.ProxyFiles)
		if err != nil {
			return nil, err
		}
	}
	// Short-circuit writing the workdir if we're given an absolute path, since
	// this is used for running in an existing directory.
	// TODO(findleyr): refactor this to be less of a workaround.
	if filepath.IsAbs(config.Workdir) {
		sb.Workdir = NewWorkdir(config.Workdir)
		return sb, nil
	}
	var workdir string
	if config.Workdir == "" {
		if config.InGoPath {
			// Set the working directory as $GOPATH/src.
			workdir = filepath.Join(sb.gopath, "src")
		} else if workdir == "" {
			workdir = filepath.Join(sb.rootdir, "work")
		}
	} else {
		// relative path
		workdir = filepath.Join(sb.rootdir, config.Workdir)
	}
	if err := os.MkdirAll(workdir, 0755); err != nil {
		return nil, err
	}
	sb.Workdir = NewWorkdir(workdir)
	if err := sb.Workdir.writeInitialFiles(config.Files); err != nil {
		return nil, err
	}
	return sb, nil
}

// Tempdir creates a new temp directory with the given txtar-encoded files. It
// is the responsibility of the caller to call os.RemoveAll on the returned
// file path when it is no longer needed.
func Tempdir(files map[string][]byte) (string, error) {
	dir, err := ioutil.TempDir("", "gopls-tempdir-")
	if err != nil {
		return "", err
	}
	for name, data := range files {
		if err := WriteFileData(name, data, RelativeTo(dir)); err != nil {
			return "", errors.Errorf("writing to tempdir: %w", err)
		}
	}
	return dir, nil
}

func UnpackTxt(txt string) map[string][]byte {
	dataMap := make(map[string][]byte)
	archive := txtar.Parse([]byte(txt))
	for _, f := range archive.Files {
		dataMap[f.Name] = f.Data
	}
	return dataMap
}

func validateConfig(config SandboxConfig) error {
	if filepath.IsAbs(config.Workdir) && (len(config.Files) > 0 || config.InGoPath) {
		return errors.New("absolute Workdir cannot be set in conjunction with Files or InGoPath")
	}
	if config.Workdir != "" && config.InGoPath {
		return errors.New("Workdir cannot be set in conjunction with InGoPath")
	}
	if config.GOPROXY != "" && config.ProxyFiles != nil {
		return errors.New("GOPROXY cannot be set in conjunction with ProxyFiles")
	}
	return nil
}

// splitModuleVersionPath extracts module information from files stored in the
// directory structure modulePath@version/suffix.
// For example:
//  splitModuleVersionPath("mod.com@v1.2.3/package") = ("mod.com", "v1.2.3", "package")
func splitModuleVersionPath(path string) (modulePath, version, suffix string) {
	parts := strings.Split(path, "/")
	var modulePathParts []string
	for i, p := range parts {
		if strings.Contains(p, "@") {
			mv := strings.SplitN(p, "@", 2)
			modulePathParts = append(modulePathParts, mv[0])
			return strings.Join(modulePathParts, "/"), mv[1], strings.Join(parts[i+1:], "/")
		}
		modulePathParts = append(modulePathParts, p)
	}
	// Default behavior: this is just a module path.
	return path, "", ""
}

func (sb *Sandbox) RootDir() string {
	return sb.rootdir
}

// GOPATH returns the value of the Sandbox GOPATH.
func (sb *Sandbox) GOPATH() string {
	return sb.gopath
}

// GoEnv returns the default environment variables that can be used for
// invoking Go commands in the sandbox.
func (sb *Sandbox) GoEnv() map[string]string {
	vars := map[string]string{
		"GOPATH":           sb.GOPATH(),
		"GOPROXY":          sb.goproxy,
		"GO111MODULE":      "",
		"GOSUMDB":          "off",
		"GOPACKAGESDRIVER": "off",
	}
	if testenv.Go1Point() >= 5 {
		vars["GOMODCACHE"] = ""
	}
	return vars
}

// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is
// true, the sandbox scans the working directory and emits file change events
// for any file changes it finds.
func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error {
	var vars []string
	for k, v := range sb.GoEnv() {
		vars = append(vars, fmt.Sprintf("%s=%s", k, v))
	}
	inv := gocommand.Invocation{
		Verb: verb,
		Args: args,
		Env:  vars,
	}
	// Use the provided directory for the working directory, if available.
	// sb.Workdir may be nil if we exited the constructor with errors (we call
	// Close to clean up any partial state from the constructor, which calls
	// RunGoCommand).
	if dir != "" {
		inv.WorkingDir = sb.Workdir.AbsPath(dir)
	} else if sb.Workdir != nil {
		inv.WorkingDir = string(sb.Workdir.RelativeTo)
	}
	gocmdRunner := &gocommand.Runner{}
	stdout, stderr, _, err := gocmdRunner.RunRaw(ctx, inv)
	if err != nil {
		return errors.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err)
	}
	// Since running a go command may result in changes to workspace files,
	// check if we need to send any any "watched" file events.
	//
	// TODO(rFindley): this side-effect can impact the usability of the sandbox
	//                 for benchmarks. Consider refactoring.
	if sb.Workdir != nil && checkForFileChanges {
		if err := sb.Workdir.CheckForFileChanges(ctx); err != nil {
			return errors.Errorf("checking for file changes: %w", err)
		}
	}
	return nil
}

// Close removes all state associated with the sandbox.
func (sb *Sandbox) Close() error {
	var goCleanErr error
	if sb.gopath != "" {
		goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"}, false)
	}
	err := os.RemoveAll(sb.rootdir)
	if err != nil || goCleanErr != nil {
		return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err)
	}
	return nil
}