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