xplshn
·
2025-08-13
run.go
Go
1package main
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "sort"
10 "strings"
11 "time"
12
13 "github.com/urfave/cli/v3"
14 "github.com/zeebo/errs"
15)
16
17var (
18 errRunFailed = errs.Class("run failed")
19)
20
21func runCommand() *cli.Command {
22 return &cli.Command{
23 Name: "run",
24 Usage: "Run a specified binary from cache",
25 Flags: []cli.Flag{
26 &cli.BoolFlag{
27 Name: "transparent",
28 Usage: "Run the binary from PATH if found",
29 },
30 },
31 SkipFlagParsing: true,
32 Action: func(_ context.Context, c *cli.Command) error {
33 if c.NArg() == 0 {
34 return errRunFailed.New("no binary name provided for run command")
35 }
36
37 config, err := loadConfig()
38 if err != nil {
39 return errRunFailed.Wrap(err)
40 }
41
42 bEntry := stringToBinaryEntry(c.Args().First())
43 return runFromCache(config, bEntry, c.Args().Tail(), c.Bool("transparent"), nil)
44 },
45 }
46}
47
48func runFromCache(config *config, bEntry binaryEntry, args []string, transparentMode bool, env []string) error {
49 if transparentMode {
50 binaryPath, err := exec.LookPath(bEntry.Name)
51 if err == nil {
52 if verbosityLevel >= normalVerbosity {
53 fmt.Printf("Running '%s' from PATH...\n", bEntry.Name)
54 }
55 return runBinary(binaryPath, args, env)
56 }
57 }
58
59 cachedFile, err := isCached(config, bEntry)
60 if err == nil {
61 if verbosityLevel >= normalVerbosity {
62 fmt.Printf("Running '%s' from cache...\n", parseBinaryEntry(bEntry, true))
63 }
64 if err := runBinary(cachedFile, args, env); err != nil {
65 return errRunFailed.Wrap(err)
66 }
67 return cleanRunCache(config.CacheDir)
68 }
69
70 if verbosityLevel >= normalVerbosity {
71 fmt.Printf("Couldn't find '%s' in the cache. Fetching a new one...\n", parseBinaryEntry(bEntry, true))
72 }
73
74 cacheConfig := *config
75 cacheConfig.UseIntegrationHooks = false
76 cacheConfig.InstallDir = config.CacheDir
77
78 uRepoIndex, err := fetchRepoIndex(&cacheConfig)
79 if err != nil {
80 return errRunFailed.Wrap(err)
81 }
82
83 verbosityLevel = silentVerbosityWithErrors
84
85 if err := installBinaries(context.Background(), &cacheConfig, []binaryEntry{bEntry}, uRepoIndex); err != nil {
86 return errRunFailed.Wrap(err)
87 }
88
89 cachedFile, err = isCached(config, bEntry)
90 if err != nil {
91 return errRunFailed.New("failed to find binary after installation: %v", err)
92 }
93
94 if err := runBinary(cachedFile, args, env); err != nil {
95 return errRunFailed.Wrap(err)
96 }
97 return cleanRunCache(config.CacheDir)
98}
99
100func isCached(config *config, bEntry binaryEntry) (string, error) {
101 cachedFile := filepath.Join(config.CacheDir, filepath.Base(bEntry.Name))
102
103 if fileExists(cachedFile) && isExecutable(cachedFile) {
104 trackedBEntry, err := readEmbeddedBEntry(cachedFile)
105 if err == nil && (trackedBEntry.PkgID == bEntry.PkgID || bEntry.PkgID == "") {
106 return cachedFile, nil
107 }
108 fmt.Println(trackedBEntry)
109 }
110
111 return "", errRunFailed.New("binary '%s' not found in cache or does not match the requested version", bEntry.Name)
112}
113
114func runBinary(binaryPath string, args []string, env []string) error {
115 cmd := exec.Command(binaryPath, args...)
116 if env == nil {
117 cmd.Env = os.Environ()
118 } else {
119 cmd.Env = env
120 }
121 cmd.Stdout = os.Stdout
122 cmd.Stderr = os.Stderr
123 cmd.Stdin = os.Stdin
124
125 err := cmd.Run()
126 if err != nil && verbosityLevel == extraVerbose {
127 fmt.Printf("The program (%s) errored out with a non-zero exit code (%d).\n", binaryPath, cmd.ProcessState.ExitCode())
128 }
129 return errRunFailed.Wrap(err)
130}
131
132func cleanRunCache(cacheDir string) error {
133 files, err := os.ReadDir(cacheDir)
134 if err != nil {
135 return errRunFailed.Wrap(err)
136 }
137
138 if len(files) <= maxCacheSize {
139 return nil
140 }
141
142 type fileWithAtime struct {
143 info os.DirEntry
144 atime time.Time
145 }
146
147 var filesWithAtime []fileWithAtime
148 for _, entry := range files {
149 // Skip files that start with "."
150 if strings.HasPrefix(entry.Name(), ".") {
151 continue
152 }
153
154 filePath := filepath.Join(cacheDir, entry.Name())
155
156 if !isExecutable(filePath) {
157 continue
158 }
159
160 fileInfo, err := os.Stat(filePath)
161 if err != nil {
162 if verbosityLevel >= silentVerbosityWithErrors {
163 fmt.Fprintf(os.Stderr, "failed to read file info: %v\n", err)
164 }
165 continue
166 }
167
168 filesWithAtime = append(filesWithAtime, fileWithAtime{info: entry, atime: fileInfo.ModTime()})
169 }
170
171 sort.Slice(filesWithAtime, func(i, j int) bool {
172 return filesWithAtime[i].atime.Before(filesWithAtime[j].atime)
173 })
174
175 for i := 0; i < binariesToDelete && i < len(filesWithAtime); i++ {
176 filePath := filepath.Join(cacheDir, filesWithAtime[i].info.Name())
177 if err := os.Remove(filePath); err != nil {
178 if verbosityLevel >= silentVerbosityWithErrors {
179 fmt.Fprintf(os.Stderr, "error removing old cached binary: %v\n", err)
180 }
181 } else if verbosityLevel >= extraVerbose {
182 fmt.Printf("Removed old cached binary: %s\n", filePath)
183 }
184 }
185
186 return nil
187}