repos / dbin

📦 Poor man's package manager.
git clone https://github.com/xplshn/dbin.git

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}