package tart import ( "context" "fmt" "io" "os" "path/filepath" "strings" "gitea.com/gitea/act_runner/pkg/common" "gitea.com/gitea/act_runner/pkg/container" "github.com/kballard/go-shellquote" ) type Environment struct { container.HostEnvironment vm *VM Config Config Env *Env Miscpath string } // "/Volumes/My Shared Files/act/" func (e *Environment) ToHostPath(path string) string { return e.translatePath(path, false) } func (e *Environment) ToContainerPath(path string) string { path = e.HostEnvironment.ToContainerPath(path) return e.translatePath(path, true) } func (e *Environment) translatePath(path string, reverse bool) string { mounts := map[string]string{ "/private/tmp/act": e.Miscpath, "/private/tmp/tool_cache": e.ToolCache, } altPath := filepath.Clean(path) for k, v := range mounts { if reverse { v, k = k, v } actPath := filepath.Clean(k) add := 0 if strings.HasPrefix(altPath, actPath+"/") { add = 1 } if altPath == actPath || add > 0 { return filepath.Join(v, altPath[len(actPath)+add:]) } } return altPath } func (e *Environment) Exec(command []string /*cmdline string, */, env map[string]string, user, workdir string) common.Executor { return e.ExecWithCmdLine(command, "", env, user, workdir) } func (e *Environment) ExecWithCmdLine(command []string, cmdline string, env map[string]string, user, workdir string) common.Executor { return func(ctx context.Context) error { if err := e.exec(ctx, command, cmdline, env, user, workdir); err != nil { select { case <-ctx.Done(): return fmt.Errorf("this step has been cancelled: %w", err) default: return err } } return nil } } func (e *Environment) Start(b bool) common.Executor { return e.HostEnvironment.Start(b).Then(func(ctx context.Context) error { return e.start(ctx) }) } func (e *Environment) start(ctx context.Context) error { actEnv := e.Env config := e.Config config.Writer = e.StdOut if config.AlwaysPull { common.Logger(ctx).Infof("Pulling the latest version of %s...\n", actEnv.JobImage) _, _, err := ExecWithEnv(ctx, nil, "pull", actEnv.JobImage) if err != nil { return err } } common.Logger(ctx).Info("Cloning and configuring a new VM...") vm, err := CreateNewVM(ctx, *actEnv, 0, 0) if err != nil { _ = e.Stop(ctx) return err } var customDirectoryMounts []string _ = os.MkdirAll(e.Miscpath, 0777) _ = os.MkdirAll(e.ToolCache, 0777) customDirectoryMounts = append(customDirectoryMounts, "act:"+e.Miscpath) customDirectoryMounts = append(customDirectoryMounts, "tool_cache:"+e.ToolCache) e.vm = vm err = vm.Start(ctx, config, actEnv, customDirectoryMounts) if err != nil { _ = e.Stop(ctx) return err } return e.execRaw(ctx, "ln -sf '/Volumes/My Shared Files/act' /private/tmp/act && ln -sf '/Volumes/My Shared Files/tool_cache' /private/tmp/tool_cache") } func (e *Environment) Stop(ctx context.Context) error { common.Logger(ctx).Debug("Preparing stopping VM") actEnv := e.Env var vm *VM if e.vm != nil { vm = e.vm } else { vm = ExistingVM(*actEnv) } if err := vm.Stop(ctx); err != nil { common.Logger(ctx).Errorf("failed to stop VM: %v", err) } if err := vm.Delete(ctx); err != nil { common.Logger(ctx).Error("failed to delete VM: %v", err) return err } return nil } func (e *Environment) Remove() common.Executor { return func(ctx context.Context) error { _ = e.Stop(ctx) common.Logger(ctx).Debug("Stopped VM, removing...") if e.CleanUp != nil { e.CleanUp() } _ = os.RemoveAll(e.Path) return e.Close()(ctx) } } func (e *Environment) exec(ctx context.Context, command []string, _ string, env map[string]string, _, workdir string) error { var wd string if workdir != "" { if filepath.IsAbs(workdir) { wd = filepath.Clean(workdir) } else { wd = filepath.Clean(filepath.Join(e.Path, workdir)) } } else { wd = e.ToContainerPath(e.Path) } envs := "" var envsSb164 strings.Builder for k, v := range env { envsSb164.WriteString(shellquote.Join(k) + "=" + shellquote.Join(v) + " ") } envs += envsSb164.String() return e.execRaw(ctx, "cd "+shellquote.Join(wd)+"\nenv "+envs+shellquote.Join(command...)+"\nexit $?") } func (e *Environment) execRaw(ctx context.Context, script string) error { actEnv := e.Env var vm *VM if e.vm != nil { vm = e.vm } else { vm = ExistingVM(*actEnv) } config := e.Config ssh, err := vm.OpenSSH(ctx, config) if err != nil { return err } defer ssh.Close() session, err := ssh.NewSession() if err != nil { return err } defer session.Close() common.Logger(ctx).Debug(script) session.Stdin = strings.NewReader( script, ) session.Stdout = e.StdOut session.Stderr = e.StdOut err = session.Shell() if err != nil { return err } return session.Wait() } func (e *Environment) GetActPath() string { return e.ToContainerPath(e.HostEnvironment.GetActPath()) } func (e *Environment) Copy(destPath string, files ...*container.FileEntry) common.Executor { return e.HostEnvironment.Copy(e.ToHostPath(destPath), files...) } func (e *Environment) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error { return e.HostEnvironment.CopyTarStream(ctx, e.ToHostPath(destPath), tarStream) } func (e *Environment) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor { return e.HostEnvironment.CopyDir(e.ToHostPath(destPath), srcPath, useGitIgnore) } func (e *Environment) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) { return e.HostEnvironment.GetContainerArchive(ctx, e.ToHostPath(srcPath)) } func (e *Environment) GetRunnerContext(ctx context.Context) map[string]any { rctx := e.HostEnvironment.GetRunnerContext(ctx) rctx["temp"] = e.ToContainerPath(e.TmpDir) rctx["tool_cache"] = e.ToContainerPath(e.ToolCache) return rctx }