github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/vcweb/script.go (about)

     1  // Copyright 2022 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  package vcweb
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"log"
    15  	"net/http"
    16  	"os"
    17  	"os/exec"
    18  	"path/filepath"
    19  	"runtime"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/go-asm/go/cmd/go/script"
    25  	"github.com/go-asm/go/txtar"
    26  
    27  	"golang.org/x/mod/module"
    28  	"golang.org/x/mod/zip"
    29  )
    30  
    31  // newScriptEngine returns a script engine augmented with commands for
    32  // reproducing version-control repositories by replaying commits.
    33  func newScriptEngine() *script.Engine {
    34  	conds := script.DefaultConds()
    35  
    36  	interrupt := func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
    37  	gracePeriod := 30 * time.Second // arbitrary
    38  
    39  	cmds := script.DefaultCmds()
    40  	cmds["at"] = scriptAt()
    41  	cmds["bzr"] = script.Program("bzr", interrupt, gracePeriod)
    42  	cmds["fossil"] = script.Program("fossil", interrupt, gracePeriod)
    43  	cmds["git"] = script.Program("git", interrupt, gracePeriod)
    44  	cmds["hg"] = script.Program("hg", interrupt, gracePeriod)
    45  	cmds["handle"] = scriptHandle()
    46  	cmds["modzip"] = scriptModzip()
    47  	cmds["svnadmin"] = script.Program("svnadmin", interrupt, gracePeriod)
    48  	cmds["svn"] = script.Program("svn", interrupt, gracePeriod)
    49  	cmds["unquote"] = scriptUnquote()
    50  
    51  	return &script.Engine{
    52  		Cmds:  cmds,
    53  		Conds: conds,
    54  	}
    55  }
    56  
    57  // loadScript interprets the given script content using the vcweb script engine.
    58  // loadScript always returns either a non-nil handler or a non-nil error.
    59  //
    60  // The script content must be a txtar archive with a comment containing a script
    61  // with exactly one "handle" command and zero or more VCS commands to prepare
    62  // the repository to be served.
    63  func (s *Server) loadScript(ctx context.Context, logger *log.Logger, scriptPath string, scriptContent []byte, workDir string) (http.Handler, error) {
    64  	ar := txtar.Parse(scriptContent)
    65  
    66  	if err := os.MkdirAll(workDir, 0755); err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	st, err := s.newState(ctx, workDir)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	if err := st.ExtractFiles(ar); err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	scriptName := filepath.Base(scriptPath)
    79  	scriptLog := new(strings.Builder)
    80  	err = s.engine.Execute(st, scriptName, bufio.NewReader(bytes.NewReader(ar.Comment)), scriptLog)
    81  	closeErr := st.CloseAndWait(scriptLog)
    82  	logger.Printf("%s:", scriptName)
    83  	io.WriteString(logger.Writer(), scriptLog.String())
    84  	io.WriteString(logger.Writer(), "\n")
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	if closeErr != nil {
    89  		return nil, err
    90  	}
    91  
    92  	sc, err := getScriptCtx(st)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	if sc.handler == nil {
    97  		return nil, errors.New("script completed without setting handler")
    98  	}
    99  	return sc.handler, nil
   100  }
   101  
   102  // newState returns a new script.State for executing scripts in workDir.
   103  func (s *Server) newState(ctx context.Context, workDir string) (*script.State, error) {
   104  	ctx = &scriptCtx{
   105  		Context: ctx,
   106  		server:  s,
   107  	}
   108  
   109  	st, err := script.NewState(ctx, workDir, s.env)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	return st, nil
   114  }
   115  
   116  // scriptEnviron returns a new environment that attempts to provide predictable
   117  // behavior for the supported version-control tools.
   118  func scriptEnviron(homeDir string) []string {
   119  	env := []string{
   120  		"USER=gopher",
   121  		homeEnvName() + "=" + homeDir,
   122  		"GIT_CONFIG_NOSYSTEM=1",
   123  		"HGRCPATH=" + filepath.Join(homeDir, ".hgrc"),
   124  		"HGENCODING=utf-8",
   125  	}
   126  	// Preserve additional environment variables that may be needed by VCS tools.
   127  	for _, k := range []string{
   128  		pathEnvName(),
   129  		tempEnvName(),
   130  		"SYSTEMROOT",        // must be preserved on Windows to find DLLs; golang.org/issue/25210
   131  		"WINDIR",            // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711
   132  		"ComSpec",           // must be preserved on Windows to be able to run Batch files; golang.org/issue/56555
   133  		"DYLD_LIBRARY_PATH", // must be preserved on macOS systems to find shared libraries
   134  		"LD_LIBRARY_PATH",   // must be preserved on Unix systems to find shared libraries
   135  		"LIBRARY_PATH",      // allow override of non-standard static library paths
   136  		"PYTHONPATH",        // may be needed by hg to find imported modules
   137  	} {
   138  		if v, ok := os.LookupEnv(k); ok {
   139  			env = append(env, k+"="+v)
   140  		}
   141  	}
   142  
   143  	if os.Getenv("GO_BUILDER_NAME") != "" || os.Getenv("GIT_TRACE_CURL") == "1" {
   144  		// To help diagnose https://go.dev/issue/52545,
   145  		// enable tracing for Git HTTPS requests.
   146  		env = append(env,
   147  			"GIT_TRACE_CURL=1",
   148  			"GIT_TRACE_CURL_NO_DATA=1",
   149  			"GIT_REDACT_COOKIES=o,SSO,GSSO_Uberproxy")
   150  	}
   151  
   152  	return env
   153  }
   154  
   155  // homeEnvName returns the environment variable used by os.UserHomeDir
   156  // to locate the user's home directory.
   157  func homeEnvName() string {
   158  	switch runtime.GOOS {
   159  	case "windows":
   160  		return "USERPROFILE"
   161  	case "plan9":
   162  		return "home"
   163  	default:
   164  		return "HOME"
   165  	}
   166  }
   167  
   168  // tempEnvName returns the environment variable used by os.TempDir
   169  // to locate the default directory for temporary files.
   170  func tempEnvName() string {
   171  	switch runtime.GOOS {
   172  	case "windows":
   173  		return "TMP"
   174  	case "plan9":
   175  		return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine
   176  	default:
   177  		return "TMPDIR"
   178  	}
   179  }
   180  
   181  // pathEnvName returns the environment variable used by exec.LookPath to
   182  // identify directories to search for executables.
   183  func pathEnvName() string {
   184  	switch runtime.GOOS {
   185  	case "plan9":
   186  		return "path"
   187  	default:
   188  		return "PATH"
   189  	}
   190  }
   191  
   192  // A scriptCtx is a context.Context that stores additional state for script
   193  // commands.
   194  type scriptCtx struct {
   195  	context.Context
   196  	server      *Server
   197  	commitTime  time.Time
   198  	handlerName string
   199  	handler     http.Handler
   200  }
   201  
   202  // scriptCtxKey is the key associating the *scriptCtx in a script's Context..
   203  type scriptCtxKey struct{}
   204  
   205  func (sc *scriptCtx) Value(key any) any {
   206  	if key == (scriptCtxKey{}) {
   207  		return sc
   208  	}
   209  	return sc.Context.Value(key)
   210  }
   211  
   212  func getScriptCtx(st *script.State) (*scriptCtx, error) {
   213  	sc, ok := st.Context().Value(scriptCtxKey{}).(*scriptCtx)
   214  	if !ok {
   215  		return nil, errors.New("scriptCtx not found in State.Context")
   216  	}
   217  	return sc, nil
   218  }
   219  
   220  func scriptAt() script.Cmd {
   221  	return script.Command(
   222  		script.CmdUsage{
   223  			Summary: "set the current commit time for all version control systems",
   224  			Args:    "time",
   225  			Detail: []string{
   226  				"The argument must be an absolute timestamp in RFC3339 format.",
   227  			},
   228  		},
   229  		func(st *script.State, args ...string) (script.WaitFunc, error) {
   230  			if len(args) != 1 {
   231  				return nil, script.ErrUsage
   232  			}
   233  
   234  			sc, err := getScriptCtx(st)
   235  			if err != nil {
   236  				return nil, err
   237  			}
   238  
   239  			sc.commitTime, err = time.ParseInLocation(time.RFC3339, args[0], time.UTC)
   240  			if err == nil {
   241  				st.Setenv("GIT_COMMITTER_DATE", args[0])
   242  				st.Setenv("GIT_AUTHOR_DATE", args[0])
   243  			}
   244  			return nil, err
   245  		})
   246  }
   247  
   248  func scriptHandle() script.Cmd {
   249  	return script.Command(
   250  		script.CmdUsage{
   251  			Summary: "set the HTTP handler that will serve the script's output",
   252  			Args:    "handler [dir]",
   253  			Detail: []string{
   254  				"The handler will be passed the script's current working directory and environment as arguments.",
   255  				"Valid handlers include 'dir' (for general http.Dir serving), 'bzr', 'fossil', 'git', and 'hg'",
   256  			},
   257  		},
   258  		func(st *script.State, args ...string) (script.WaitFunc, error) {
   259  			if len(args) == 0 || len(args) > 2 {
   260  				return nil, script.ErrUsage
   261  			}
   262  
   263  			sc, err := getScriptCtx(st)
   264  			if err != nil {
   265  				return nil, err
   266  			}
   267  
   268  			if sc.handler != nil {
   269  				return nil, fmt.Errorf("server handler already set to %s", sc.handlerName)
   270  			}
   271  
   272  			name := args[0]
   273  			h, ok := sc.server.vcsHandlers[name]
   274  			if !ok {
   275  				return nil, fmt.Errorf("unrecognized VCS %q", name)
   276  			}
   277  			sc.handlerName = name
   278  			if !h.Available() {
   279  				return nil, ServerNotInstalledError{name}
   280  			}
   281  
   282  			dir := st.Getwd()
   283  			if len(args) >= 2 {
   284  				dir = st.Path(args[1])
   285  			}
   286  			sc.handler, err = h.Handler(dir, st.Environ(), sc.server.logger)
   287  			return nil, err
   288  		})
   289  }
   290  
   291  func scriptModzip() script.Cmd {
   292  	return script.Command(
   293  		script.CmdUsage{
   294  			Summary: "create a Go module zip file from a directory",
   295  			Args:    "zipfile path@version dir",
   296  		},
   297  		func(st *script.State, args ...string) (wait script.WaitFunc, err error) {
   298  			if len(args) != 3 {
   299  				return nil, script.ErrUsage
   300  			}
   301  			zipPath := st.Path(args[0])
   302  			mPath, version, ok := strings.Cut(args[1], "@")
   303  			if !ok {
   304  				return nil, script.ErrUsage
   305  			}
   306  			dir := st.Path(args[2])
   307  
   308  			if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil {
   309  				return nil, err
   310  			}
   311  			f, err := os.Create(zipPath)
   312  			if err != nil {
   313  				return nil, err
   314  			}
   315  			defer func() {
   316  				if closeErr := f.Close(); err == nil {
   317  					err = closeErr
   318  				}
   319  			}()
   320  
   321  			return nil, zip.CreateFromDir(f, module.Version{Path: mPath, Version: version}, dir)
   322  		})
   323  }
   324  
   325  func scriptUnquote() script.Cmd {
   326  	return script.Command(
   327  		script.CmdUsage{
   328  			Summary: "unquote the argument as a Go string",
   329  			Args:    "string",
   330  		},
   331  		func(st *script.State, args ...string) (script.WaitFunc, error) {
   332  			if len(args) != 1 {
   333  				return nil, script.ErrUsage
   334  			}
   335  
   336  			s, err := strconv.Unquote(`"` + args[0] + `"`)
   337  			if err != nil {
   338  				return nil, err
   339  			}
   340  
   341  			wait := func(*script.State) (stdout, stderr string, err error) {
   342  				return s, "", nil
   343  			}
   344  			return wait, nil
   345  		})
   346  }