summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/vcweb/script.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/go/internal/vcweb/script.go')
-rw-r--r--src/cmd/go/internal/vcweb/script.go345
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
+ })
+}