diff options
Diffstat (limited to 'src/cmd/go/internal/vcweb/script.go')
-rw-r--r-- | src/cmd/go/internal/vcweb/script.go | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/src/cmd/go/internal/vcweb/script.go b/src/cmd/go/internal/vcweb/script.go new file mode 100644 index 0000000..c35b46f --- /dev/null +++ b/src/cmd/go/internal/vcweb/script.go @@ -0,0 +1,345 @@ +// Copyright 2022 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 vcweb + +import ( + "bufio" + "bytes" + "cmd/go/internal/script" + "context" + "errors" + "fmt" + "internal/txtar" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "golang.org/x/mod/module" + "golang.org/x/mod/zip" +) + +// newScriptEngine returns a script engine augmented with commands for +// reproducing version-control repositories by replaying commits. +func newScriptEngine() *script.Engine { + conds := script.DefaultConds() + + interrupt := func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) } + gracePeriod := 30 * time.Second // arbitrary + + cmds := script.DefaultCmds() + cmds["at"] = scriptAt() + cmds["bzr"] = script.Program("bzr", interrupt, gracePeriod) + cmds["fossil"] = script.Program("fossil", interrupt, gracePeriod) + cmds["git"] = script.Program("git", interrupt, gracePeriod) + cmds["hg"] = script.Program("hg", interrupt, gracePeriod) + cmds["handle"] = scriptHandle() + cmds["modzip"] = scriptModzip() + cmds["svnadmin"] = script.Program("svnadmin", interrupt, gracePeriod) + cmds["svn"] = script.Program("svn", interrupt, gracePeriod) + cmds["unquote"] = scriptUnquote() + + return &script.Engine{ + Cmds: cmds, + Conds: conds, + } +} + +// loadScript interprets the given script content using the vcweb script engine. +// loadScript always returns either a non-nil handler or a non-nil error. +// +// The script content must be a txtar archive with a comment containing a script +// with exactly one "handle" command and zero or more VCS commands to prepare +// the repository to be served. +func (s *Server) loadScript(ctx context.Context, logger *log.Logger, scriptPath string, scriptContent []byte, workDir string) (http.Handler, error) { + ar := txtar.Parse(scriptContent) + + if err := os.MkdirAll(workDir, 0755); err != nil { + return nil, err + } + + st, err := s.newState(ctx, workDir) + if err != nil { + return nil, err + } + if err := st.ExtractFiles(ar); err != nil { + return nil, err + } + + scriptName := filepath.Base(scriptPath) + scriptLog := new(strings.Builder) + err = s.engine.Execute(st, scriptName, bufio.NewReader(bytes.NewReader(ar.Comment)), scriptLog) + closeErr := st.CloseAndWait(scriptLog) + logger.Printf("%s:", scriptName) + io.WriteString(logger.Writer(), scriptLog.String()) + io.WriteString(logger.Writer(), "\n") + if err != nil { + return nil, err + } + if closeErr != nil { + return nil, err + } + + sc, err := getScriptCtx(st) + if err != nil { + return nil, err + } + if sc.handler == nil { + return nil, errors.New("script completed without setting handler") + } + return sc.handler, nil +} + +// newState returns a new script.State for executing scripts in workDir. +func (s *Server) newState(ctx context.Context, workDir string) (*script.State, error) { + ctx = &scriptCtx{ + Context: ctx, + server: s, + } + + st, err := script.NewState(ctx, workDir, s.env) + if err != nil { + return nil, err + } + return st, nil +} + +// scriptEnviron returns a new environment that attempts to provide predictable +// behavior for the supported version-control tools. +func scriptEnviron(homeDir string) []string { + env := []string{ + "USER=gopher", + homeEnvName() + "=" + homeDir, + "GIT_CONFIG_NOSYSTEM=1", + "HGRCPATH=" + filepath.Join(homeDir, ".hgrc"), + "HGENCODING=utf-8", + } + // Preserve additional environment variables that may be needed by VCS tools. + for _, k := range []string{ + pathEnvName(), + tempEnvName(), + "SYSTEMROOT", // must be preserved on Windows to find DLLs; golang.org/issue/25210 + "WINDIR", // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711 + "ComSpec", // must be preserved on Windows to be able to run Batch files; golang.org/issue/56555 + "DYLD_LIBRARY_PATH", // must be preserved on macOS systems to find shared libraries + "LD_LIBRARY_PATH", // must be preserved on Unix systems to find shared libraries + "LIBRARY_PATH", // allow override of non-standard static library paths + "PYTHONPATH", // may be needed by hg to find imported modules + } { + if v, ok := os.LookupEnv(k); ok { + env = append(env, k+"="+v) + } + } + + if os.Getenv("GO_BUILDER_NAME") != "" || os.Getenv("GIT_TRACE_CURL") == "1" { + // To help diagnose https://go.dev/issue/52545, + // enable tracing for Git HTTPS requests. + env = append(env, + "GIT_TRACE_CURL=1", + "GIT_TRACE_CURL_NO_DATA=1", + "GIT_REDACT_COOKIES=o,SSO,GSSO_Uberproxy") + } + + return env +} + +// homeEnvName returns the environment variable used by os.UserHomeDir +// to locate the user's home directory. +func homeEnvName() string { + switch runtime.GOOS { + case "windows": + return "USERPROFILE" + case "plan9": + return "home" + default: + return "HOME" + } +} + +// tempEnvName returns the environment variable used by os.TempDir +// to locate the default directory for temporary files. +func tempEnvName() string { + switch runtime.GOOS { + case "windows": + return "TMP" + case "plan9": + return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine + default: + return "TMPDIR" + } +} + +// pathEnvName returns the environment variable used by exec.LookPath to +// identify directories to search for executables. +func pathEnvName() string { + switch runtime.GOOS { + case "plan9": + return "path" + default: + return "PATH" + } +} + +// A scriptCtx is a context.Context that stores additional state for script +// commands. +type scriptCtx struct { + context.Context + server *Server + commitTime time.Time + handlerName string + handler http.Handler +} + +// scriptCtxKey is the key associating the *scriptCtx in a script's Context.. +type scriptCtxKey struct{} + +func (sc *scriptCtx) Value(key any) any { + if key == (scriptCtxKey{}) { + return sc + } + return sc.Context.Value(key) +} + +func getScriptCtx(st *script.State) (*scriptCtx, error) { + sc, ok := st.Context().Value(scriptCtxKey{}).(*scriptCtx) + if !ok { + return nil, errors.New("scriptCtx not found in State.Context") + } + return sc, nil +} + +func scriptAt() script.Cmd { + return script.Command( + script.CmdUsage{ + Summary: "set the current commit time for all version control systems", + Args: "time", + Detail: []string{ + "The argument must be an absolute timestamp in RFC3339 format.", + }, + }, + func(st *script.State, args ...string) (script.WaitFunc, error) { + if len(args) != 1 { + return nil, script.ErrUsage + } + + sc, err := getScriptCtx(st) + if err != nil { + return nil, err + } + + sc.commitTime, err = time.ParseInLocation(time.RFC3339, args[0], time.UTC) + if err == nil { + st.Setenv("GIT_COMMITTER_DATE", args[0]) + st.Setenv("GIT_AUTHOR_DATE", args[0]) + } + return nil, err + }) +} + +func scriptHandle() script.Cmd { + return script.Command( + script.CmdUsage{ + Summary: "set the HTTP handler that will serve the script's output", + Args: "handler [dir]", + Detail: []string{ + "The handler will be passed the script's current working directory and environment as arguments.", + "Valid handlers include 'dir' (for general http.Dir serving), 'bzr', 'fossil', 'git', and 'hg'", + }, + }, + func(st *script.State, args ...string) (script.WaitFunc, error) { + if len(args) == 0 || len(args) > 2 { + return nil, script.ErrUsage + } + + sc, err := getScriptCtx(st) + if err != nil { + return nil, err + } + + if sc.handler != nil { + return nil, fmt.Errorf("server handler already set to %s", sc.handlerName) + } + + name := args[0] + h, ok := sc.server.vcsHandlers[name] + if !ok { + return nil, fmt.Errorf("unrecognized VCS %q", name) + } + sc.handlerName = name + if !h.Available() { + return nil, ServerNotInstalledError{name} + } + + dir := st.Getwd() + if len(args) >= 2 { + dir = st.Path(args[1]) + } + sc.handler, err = h.Handler(dir, st.Environ(), sc.server.logger) + return nil, err + }) +} + +func scriptModzip() script.Cmd { + return script.Command( + script.CmdUsage{ + Summary: "create a Go module zip file from a directory", + Args: "zipfile path@version dir", + }, + func(st *script.State, args ...string) (wait script.WaitFunc, err error) { + if len(args) != 3 { + return nil, script.ErrUsage + } + zipPath := st.Path(args[0]) + mPath, version, ok := strings.Cut(args[1], "@") + if !ok { + return nil, script.ErrUsage + } + dir := st.Path(args[2]) + + if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil { + return nil, err + } + f, err := os.Create(zipPath) + if err != nil { + return nil, err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + return nil, zip.CreateFromDir(f, module.Version{Path: mPath, Version: version}, dir) + }) +} + +func scriptUnquote() script.Cmd { + return script.Command( + script.CmdUsage{ + Summary: "unquote the argument as a Go string", + Args: "string", + }, + func(st *script.State, args ...string) (script.WaitFunc, error) { + if len(args) != 1 { + return nil, script.ErrUsage + } + + s, err := strconv.Unquote(`"` + args[0] + `"`) + if err != nil { + return nil, err + } + + wait := func(*script.State) (stdout, stderr string, err error) { + return s, "", nil + } + return wait, nil + }) +} |