// 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 basedir 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 string // InGoPath specifies that the working directory should be within the // temporary GOPATH. InGoPath bool // Workdir configures the working directory of the Sandbox, for running in a // pre-existing directory. If unset, a new working directory will be created // under RootDir. // // 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 string // 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 config.Workdir != "" && (config.Files != "" || config.InGoPath) { return nil, fmt.Errorf("invalid SandboxConfig: Workdir cannot be used in conjunction with Files or InGoPath. Got %+v", config) } if config.GOPROXY != "" && config.ProxyFiles != "" { return nil, fmt.Errorf("invalid SandboxConfig: GOPROXY cannot be set in conjunction with ProxyFiles. Got %+v", config) } sb := &Sandbox{} defer func() { // Clean up if we fail at any point in this constructor. if err != nil { sb.Close() } }() baseDir, err := ioutil.TempDir(config.RootDir, "gopls-sandbox-") if err != nil { return nil, fmt.Errorf("creating temporary workdir: %v", err) } sb.basedir = baseDir sb.gopath = filepath.Join(sb.basedir, "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.basedir, "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 } } if config.Workdir != "" { sb.Workdir = NewWorkdir(config.Workdir) } else { workdir := config.Workdir // If we don't have a pre-existing work dir, we want to create either // $GOPATH/src or . if config.InGoPath { // Set the working directory as $GOPATH/src. workdir = filepath.Join(sb.gopath, "src") } else if workdir == "" { workdir = filepath.Join(sb.basedir, "work") } if err := os.Mkdir(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(txt string) (string, error) { dir, err := ioutil.TempDir("", "gopls-tempdir-") if err != nil { return "", err } if err := writeTxtar(txt, RelativeTo(dir)); err != nil { return "", 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 } // 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, "", "" } // 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. func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string) 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{} _, _, _, err := gocmdRunner.RunRaw(ctx, inv) if err != nil { return err } // Since running a go command may result in changes to workspace files, // check if we need to send any any "watched" file events. if sb.Workdir != nil { 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"}) } err := os.RemoveAll(sb.basedir) if err != nil || goCleanErr != nil { return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err) } return nil }