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 }