From 7337d230c3d0434d80c761f99d705d1b0c906d46 Mon Sep 17 00:00:00 2001 From: Will Roden Date: Tue, 28 Nov 2023 13:52:12 -0600 Subject: [PATCH] split out bindown wrap command --- README.md | 1 + cmd/bindown/cli.go | 188 ++++++++++++++++------------------ cmd/bindown/completion.go | 26 ++--- cmd/bindown/dependency.go | 2 +- cmd/bindown/testutil_test.go | 4 +- docs/clihelp.txt | 1 + internal/bindown/config.go | 190 +++++++++++++++++++++-------------- 7 files changed, 215 insertions(+), 197 deletions(-) diff --git a/README.md b/README.md index 459905f..daea666 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,7 @@ Commands: download download a dependency but don't extract or install it extract download and extract a dependency but don't install it install download, extract and install a dependency + wrap create a wrapper script for a dependency format formats the config file dependency list list configured dependencies dependency add add a template-based dependency diff --git a/cmd/bindown/cli.go b/cmd/bindown/cli.go index 26e0a86..d144fd4 100644 --- a/cmd/bindown/cli.go +++ b/cmd/bindown/cli.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "slices" "time" "github.com/alecthomas/kong" @@ -16,6 +17,7 @@ var kongVars = kong.Vars{ "configfile_help": `file with bindown config. default is the first one of bindown.yml, bindown.yaml, bindown.json, .bindown.yml, .bindown.yaml or .bindown.json`, "cache_help": `directory downloads will be cached`, "install_help": `download, extract and install a dependency`, + "wrap_help": `create a wrapper script for a dependency`, "system_default": string(bindown.CurrentSystem), "system_help": `target system in the format of /`, "systems_help": `target systems in the format of /`, @@ -27,13 +29,11 @@ var kongVars = kong.Vars{ "config_install_completions_help": `install shell completions`, "config_extract_path_help": `output path to directory where the downloaded archive is extracted`, "install_force_help": `force install even if it already exists`, - "install_target_file_help": `where to write the file. when multiple dependencies are selected, this is the directory to write to.`, + "output_help": `where to write the file. when multiple dependencies are selected, this is the directory to write to.`, "download_force_help": `force download even if the file already exists`, - "download_target_file_help": `filename and path for the downloaded file. Default downloads to cache.`, "allow_missing_checksum": `allow missing checksums`, "download_help": `download a dependency but don't extract or install it`, "extract_help": `download and extract a dependency but don't install it`, - "extract_target_dir_help": `path to extract to. Default extracts to cache.`, "checksums_dep_help": `name of the dependency to update`, "all_deps_help": `select all dependencies`, "dependency_help": `name of dependency`, @@ -52,6 +52,7 @@ type rootCmd struct { Download downloadCmd `kong:"cmd,help=${download_help}"` Extract extractCmd `kong:"cmd,help=${extract_help}"` Install installCmd `kong:"cmd,help=${install_help}"` + Wrap wrapCmd `kong:"cmd,help=${wrap_help}"` Format fmtCmd `kong:"cmd,help=${config_format_help}"` Dependency dependencyCmd `kong:"cmd,help='manage dependencies'"` Template templateCmd `kong:"cmd,help='manage templates'"` @@ -66,6 +67,20 @@ type rootCmd struct { InstallCompletions kongplete.InstallCompletions `kong:"cmd,help=${config_install_completions_help}"` } +func (r *rootCmd) BeforeApply(k *kong.Context) error { + // set dependency positional to optional for install, wrap, download and extract. + // We do this because we want to allow --all to be equivalent to specifying all + // dependencies but want the help output to indicate that a dependency is required. + if slices.Contains([]string{"install", "wrap", "download", "extract"}, k.Selected().Name) { + for _, pos := range k.Selected().Positional { + if pos.Name == "dependency" { + pos.Required = false + } + } + } + return nil +} + var defaultConfigFilenames = []string{ "bindown.yml", "bindown.yaml", @@ -120,6 +135,7 @@ type runContext struct { parent context.Context stdin fileReader stdout fileWriter + stderr fileWriter rootCmd *rootCmd } @@ -148,7 +164,7 @@ func (r *runContext) Value(key any) any { type runOpts struct { stdin fileReader stdout fileWriter - stderr io.Writer + stderr fileWriter cmdName string exitHandler func(int) } @@ -169,9 +185,9 @@ func Run(ctx context.Context, args []string, opts *runOpts) { if runCtx.stdout == nil { runCtx.stdout = os.Stdout } - stderr := opts.stderr - if stderr == nil { - stderr = os.Stderr + runCtx.stderr = opts.stderr + if runCtx.stderr == nil { + runCtx.stderr = os.Stderr } kongOptions := []kong.Option{ @@ -179,7 +195,7 @@ func Run(ctx context.Context, args []string, opts *runOpts) { kong.BindTo(runCtx, &runCtx), kongVars, kong.UsageOnError(), - kong.Writers(runCtx.stdout, stderr), + kong.Writers(runCtx.stdout, runCtx.stderr), } if opts.exitHandler != nil { kongOptions = append(kongOptions, kong.Exit(opts.exitHandler)) @@ -206,6 +222,7 @@ func runCompletion(ctx context.Context, parser *kong.Kong) { defer cancel() kongplete.Complete(parser, kongplete.WithPredictor("bin", binCompleter(ctx)), + kongplete.WithPredictor("wrap_bin", binCompleter(ctx)), kongplete.WithPredictor("allSystems", allSystemsCompleter), kongplete.WithPredictor("templateSource", templateSourceCompleter(ctx)), kongplete.WithPredictor("system", systemCompleter(ctx)), @@ -254,55 +271,77 @@ func (c fmtCmd) Run(ctx *runContext, cli *rootCmd) error { } type installCmd struct { - dependencySelector + Dependency []string `kong:"arg,name=dependency,help=${dependency_help},predictor=bin"` + All bool `kong:"help=${all_deps_help}"` Force bool `kong:"help=${install_force_help}"` - TargetFile string `kong:"type=path,name=output,type=file,help=${install_target_file_help}"` + Output string `kong:"type=path,name=output,type=file,help=${output_help}"` System bindown.System `kong:"name=system,default=${system_default},help=${system_help},predictor=allSystems"` AllowMissingChecksum bool `kong:"name=allow-missing-checksum,help=${allow_missing_checksum}"` ToCache bool `kong:"name=to-cache,help=${install_to_cache_help}"` - Wrapper bool `kong:"name=wrapper,help=${install_wrapper_help}"` - BindownExec string `kong:"name=bindown,help=${install_bindown_help}"` + + // hidden options to be removed + Wrapper bool `kong:"hidden,name=wrapper"` + BindownExec string `kong:"hidden,name=bindown"` } func (d *installCmd) Run(ctx *runContext) error { + if d.Wrapper { + fmt.Fprintln(ctx.stderr, `--wrapper is deprecated and will be removed in a future version. Use "bindown wrap" instead.`) + if d.ToCache { + return fmt.Errorf("cannot use --to-cache and --wrapper together") + } + if d.Force { + return fmt.Errorf("cannot use --force and --wrapper together") + } + cmd := &wrapCmd{ + Dependency: d.Dependency, + All: d.All, + Output: d.Output, + AllowMissingChecksum: d.AllowMissingChecksum, + BindownExec: d.BindownExec, + } + return cmd.Run(ctx) + } config, err := loadConfigFile(ctx, false) if err != nil { return err } - err = d.setDependencies(config) + + return config.InstallDependencies(d.Dependency, d.System, &bindown.ConfigInstallDependenciesOpts{ + Output: d.Output, + Force: d.Force, + AllowMissingChecksum: d.AllowMissingChecksum, + ToCache: d.ToCache, + Stdout: ctx.stdout, + AllDeps: d.All, + }) +} + +type wrapCmd struct { + Dependency []string `kong:"arg,name=dependency,help=${dependency_help},predictor=bin"` + All bool `kong:"help=${all_deps_help}"` + Output string `kong:"type=path,name=output,type=file,help=${output_help}"` + AllowMissingChecksum bool `kong:"name=allow-missing-checksum,help=${allow_missing_checksum}"` + BindownExec string `kong:"name=bindown,help=${install_bindown_help}"` +} + +func (d *wrapCmd) Run(ctx *runContext) error { + config, err := loadConfigFile(ctx, false) if err != nil { return err } - if d.ToCache && d.Wrapper { - return fmt.Errorf("cannot use --to-cache and --wrapper together") - } - if d.BindownExec != "" && !d.Wrapper { - return fmt.Errorf("--bindown can only be used with --wrapper") - } - if d.Force && d.Wrapper { - return fmt.Errorf("cannot use --force and --wrapper together") - } - opts := bindown.ConfigInstallDependenciesOpts{ - TargetFile: d.TargetFile, - Force: d.Force, + return config.WrapDependencies(d.Dependency, &bindown.ConfigWrapDependenciesOpts{ + Output: d.Output, AllowMissingChecksum: d.AllowMissingChecksum, - ToCache: d.ToCache, - Wrapper: d.Wrapper, BindownPath: d.BindownExec, Stdout: ctx.stdout, - } - if d.All || len(d.Dependency) > 1 { - opts.TargetFile = "" - opts.TargetDir = d.TargetFile - if opts.TargetDir == "" { - opts.TargetDir = config.InstallDir - } - } - return config.InstallDependencies(d.Dependency, d.System, &opts) + AllDeps: d.All, + }) } type downloadCmd struct { - dependencySelector + Dependency []string `kong:"arg,name=dependency,help=${dependency_help},predictor=bin"` + All bool `kong:"help=${all_deps_help}"` Force bool `kong:"help=${download_force_help}"` System bindown.System `kong:"name=system,default=${system_default},help=${system_help},predictor=allSystems"` AllowMissingChecksum bool `kong:"name=allow-missing-checksum,help=${allow_missing_checksum}"` @@ -313,26 +352,17 @@ func (d *downloadCmd) Run(ctx *runContext) error { if err != nil { return err } - err = d.setDependencies(config) - if err != nil { - return err - } - for _, dep := range d.Dependency { - var pth string - pth, err = config.DownloadDependency(dep, d.System, &bindown.ConfigDownloadDependencyOpts{ - Force: d.Force, - AllowMissingChecksum: d.AllowMissingChecksum, - }) - if err != nil { - return err - } - fmt.Fprintf(ctx.stdout, "downloaded %s to %s\n", dep, pth) - } - return nil + return config.DownloadDependencies(d.Dependency, d.System, &bindown.ConfigDownloadDependenciesOpts{ + Force: d.Force, + AllowMissingChecksum: d.AllowMissingChecksum, + AllDeps: d.All, + Stdout: ctx.stdout, + }) } type extractCmd struct { - dependencySelector + Dependency []string `kong:"arg,name=dependency,help=${dependency_help},predictor=bin"` + All bool `kong:"help=${all_deps_help}"` System bindown.System `kong:"name=system,default=${system_default},help=${system_help},predictor=allSystems"` AllowMissingChecksum bool `kong:"name=allow-missing-checksum,help=${allow_missing_checksum}"` } @@ -342,49 +372,9 @@ func (d *extractCmd) Run(ctx *runContext) error { if err != nil { return err } - err = d.setDependencies(config) - if err != nil { - return err - } - for _, dep := range d.Dependency { - var pth string - pth, err = config.ExtractDependency(dep, d.System, &bindown.ConfigExtractDependencyOpts{ - AllowMissingChecksum: d.AllowMissingChecksum, - }) - if err != nil { - return err - } - fmt.Fprintf(ctx.stdout, "extracted %s to %s\n", dep, pth) - } - return nil -} - -type dependencySelector struct { - Dependency []string `kong:"arg,name=dependency,help=${dependency_help},predictor=bin"` - All bool `kong:"help=${all_deps_help}"` -} - -func (d *dependencySelector) BeforeApply(k *kong.Context) error { - // sets dependency positional to optional. We do this because we want to allow --all to be - // equivalent to specifying all dependencies but want the help output to indicate that a - // dependency is required. - for _, pos := range k.Selected().Positional { - if pos.Name == "dependency" { - pos.Required = false - } - } - return nil -} - -func (d *dependencySelector) setDependencies(config *bindown.Config) error { - if d.All { - if len(d.Dependency) > 0 { - return fmt.Errorf("cannot specify dependencies when using --all") - } - d.Dependency = allDependencies(config) - } - if len(d.Dependency) == 0 { - return fmt.Errorf("must specify at least one dependency") - } - return nil + return config.ExtractDependencies(d.Dependency, d.System, &bindown.ConfigExtractDependenciesOpts{ + AllowMissingChecksum: d.AllowMissingChecksum, + AllDeps: d.All, + Stdout: ctx.stdout, + }) } diff --git a/cmd/bindown/completion.go b/cmd/bindown/completion.go index 35d0642..2d476da 100644 --- a/cmd/bindown/completion.go +++ b/cmd/bindown/completion.go @@ -3,7 +3,6 @@ package main import ( "context" "os" - "slices" "strings" "github.com/alecthomas/kong" @@ -71,23 +70,6 @@ func completionConfig(ctx context.Context, args []string) *bindown.Config { return configFile } -func allDependencies(cfg *bindown.Config) []string { - if cfg == nil { - return []string{} - } - system := bindown.CurrentSystem - dependencies := make([]string, 0, len(cfg.Dependencies)) - for depName := range cfg.Dependencies { - bn, err := cfg.BinName(depName, system) - if err != nil { - return []string{} - } - dependencies = append(dependencies, bn) - } - slices.Sort(dependencies) - return dependencies -} - func templateSourceCompleter(ctx context.Context) complete.PredictFunc { return func(a complete.Args) []string { cfg := completionConfig(ctx, a.Completed) @@ -164,13 +146,19 @@ func localTemplateFromSourceCompleter(ctx context.Context) complete.PredictFunc func binCompleter(ctx context.Context) complete.PredictFunc { return func(a complete.Args) []string { cfg := completionConfig(ctx, a.Completed) - return complete.PredictSet(allDependencies(cfg)...).Predict(a) + if cfg == nil { + return []string{} + } + return complete.PredictSet(cfg.DependencyNames()...).Predict(a) } } func systemCompleter(ctx context.Context) complete.PredictFunc { return func(a complete.Args) []string { cfg := completionConfig(ctx, a.Completed) + if cfg == nil { + return []string{} + } opts := make([]string, 0, len(cfg.Systems)) for _, system := range cfg.Systems { opts = append(opts, string(system)) diff --git a/cmd/bindown/dependency.go b/cmd/bindown/dependency.go index 68ac08c..c43f784 100644 --- a/cmd/bindown/dependency.go +++ b/cmd/bindown/dependency.go @@ -132,7 +132,7 @@ func (c *dependencyListCmd) Run(ctx *runContext) error { if err != nil { return err } - fmt.Fprintln(ctx.stdout, strings.Join(allDependencies(cfg), "\n")) + fmt.Fprintln(ctx.stdout, strings.Join(cfg.DependencyNames(), "\n")) return nil } diff --git a/cmd/bindown/testutil_test.go b/cmd/bindown/testutil_test.go index d6052d6..7b0fadc 100644 --- a/cmd/bindown/testutil_test.go +++ b/cmd/bindown/testutil_test.go @@ -70,7 +70,7 @@ func (c *cmdRunner) run(commandLine ...string) *runCmdResult { &runOpts{ stdin: simpleFileReader{c.stdin}, stdout: SimpleFileWriter{&result.stdOut}, - stderr: &result.stdErr, + stderr: SimpleFileWriter{&result.stdErr}, cmdName: "cmd", exitHandler: func(i int) { result.exited = true @@ -102,7 +102,7 @@ func (c *cmdRunner) runExpect(expectFunc func(*expect.Console), commandLine ...s &runOpts{ stdin: console.Tty(), stdout: console.Tty(), - stderr: &result.stdErr, + stderr: console.Tty(), cmdName: "cmd", exitHandler: func(i int) { result.exited = true diff --git a/docs/clihelp.txt b/docs/clihelp.txt index d889b77..5baa3a7 100644 --- a/docs/clihelp.txt +++ b/docs/clihelp.txt @@ -13,6 +13,7 @@ Commands: download download a dependency but don't extract or install it extract download and extract a dependency but don't install it install download, extract and install a dependency + wrap create a wrapper script for a dependency format formats the config file dependency list list configured dependencies dependency add add a template-based dependency diff --git a/internal/bindown/config.go b/internal/bindown/config.go index e221f46..30e6409 100644 --- a/internal/bindown/config.go +++ b/internal/bindown/config.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "fmt" "hash/fnv" "io" @@ -48,6 +49,15 @@ type Config struct { Filename string `json:"-" yaml:"-"` } +func (c *Config) DependencyNames() []string { + var result []string + for name := range c.Dependencies { + result = append(result, name) + } + slices.Sort(result) + return result +} + // UnsetDependencyVars removes a dependency var. Noop if the var doesn't exist. func (c *Config) UnsetDependencyVars(depName string, vars []string) error { dep := c.Dependencies[depName] @@ -317,12 +327,6 @@ func (c *Config) ClearCache() error { return os.RemoveAll(c.Cache) } -// ConfigDownloadDependencyOpts options for Config.DownloadDependency -type ConfigDownloadDependencyOpts struct { - Force bool - AllowMissingChecksum bool -} - func (c *Config) downloadsCache() *cache.Cache { return &cache.Cache{ Root: filepath.Join(c.Cache, "downloads"), @@ -341,28 +345,42 @@ func cacheKey(hashMaterial string) string { return hex.EncodeToString(hasher.Sum(nil)) } -// DownloadDependency downloads a dependency -func (c *Config) DownloadDependency( - name string, - system System, - opts *ConfigDownloadDependencyOpts, -) (_ string, errOut error) { +type ConfigDownloadDependenciesOpts struct { + Force bool + AllowMissingChecksum bool + AllDeps bool + Stdout io.Writer +} + +func (c *Config) DownloadDependencies(deps []string, system System, opts *ConfigDownloadDependenciesOpts) error { if opts == nil { - opts = &ConfigDownloadDependencyOpts{} + opts = &ConfigDownloadDependenciesOpts{} } - dep, err := c.BuildDependency(name, system) - if err != nil { - return "", err + if opts.AllDeps { + deps = c.DependencyNames() } - dlFile, _, unlock, err := downloadDependency(dep, c.downloadsCache(), opts.AllowMissingChecksum, opts.Force) - if err != nil { - return "", err - } - err = unlock() - if err != nil { - return "", err + for _, name := range deps { + dep, err := c.BuildDependency(name, system) + if err != nil { + return err + } + dlFile, _, unlock, err := downloadDependency(dep, c.downloadsCache(), opts.AllowMissingChecksum, opts.Force) + if err != nil { + return err + } + err = unlock() + if err != nil { + return err + } + if opts.Stdout == nil { + continue + } + _, err = fmt.Fprintf(opts.Stdout, "downloaded %s to %s\n", dep.name, dlFile) + if err != nil { + return err + } } - return dlFile, nil + return nil } func urlFilename(dlURL string) (string, error) { @@ -373,36 +391,45 @@ func urlFilename(dlURL string) (string, error) { return path.Base(u.EscapedPath()), nil } -// ConfigExtractDependencyOpts options for Config.ExtractDependency -type ConfigExtractDependencyOpts struct { - Force bool +type ConfigExtractDependenciesOpts struct { AllowMissingChecksum bool + AllDeps bool + Stdout io.Writer } -// ExtractDependency downloads and extracts a dependency -func (c *Config) ExtractDependency(dependencyName string, system System, opts *ConfigExtractDependencyOpts) (_ string, errOut error) { +func (c *Config) ExtractDependencies(deps []string, system System, opts *ConfigExtractDependenciesOpts) error { if opts == nil { - opts = &ConfigExtractDependencyOpts{} - } - dep, err := c.BuildDependency(dependencyName, system) - if err != nil { - return "", err + opts = &ConfigExtractDependenciesOpts{} } - dlFile, key, dlUnlock, err := downloadDependency(dep, c.downloadsCache(), opts.AllowMissingChecksum, opts.Force) - if err != nil { - return "", err - } - defer deferErr(&errOut, dlUnlock) - - outDir, unlock, err := extractDependencyToCache(dlFile, c.Cache, key, c.extractsCache(), opts.Force) - if err != nil { - return "", err + if opts.AllDeps { + deps = c.DependencyNames() } - err = unlock() - if err != nil { - return "", err + for _, name := range deps { + dep, err := c.BuildDependency(name, system) + if err != nil { + return err + } + dlFile, key, dlUnlock, err := downloadDependency(dep, c.downloadsCache(), opts.AllowMissingChecksum, false) + if err != nil { + return err + } + outDir, unlock, err := extractDependencyToCache(dlFile, c.Cache, key, c.extractsCache(), false) + if err != nil { + return errors.Join(dlUnlock(), err) + } + err = errors.Join(dlUnlock(), unlock()) + if err != nil { + return err + } + if opts.Stdout == nil { + continue + } + _, err = fmt.Fprintf(opts.Stdout, "extracted %s to %s\n", dep.name, outDir) + if err != nil { + return err + } } - return outDir, nil + return nil } // ConfigInstallDependencyOpts provides options for Config.InstallDependency @@ -419,41 +446,36 @@ type ConfigInstallDependencyOpts struct { // ConfigInstallDependenciesOpts provides options for Config.InstallDependencies type ConfigInstallDependenciesOpts struct { - TargetFile string + Output string TargetDir string - BindownPath string Stdout io.Writer Force bool AllowMissingChecksum bool ToCache bool - Wrapper bool + AllDeps bool } -func (c *Config) InstallDependencies(deps []string, system System, opts *ConfigInstallDependenciesOpts) (errOut error) { +func (c *Config) InstallDependencies(deps []string, system System, opts *ConfigInstallDependenciesOpts) error { if opts == nil { opts = &ConfigInstallDependenciesOpts{} } - targetDir := opts.TargetDir - if targetDir == "" { - targetDir = c.InstallDir + if opts.AllDeps { + deps = c.DependencyNames() } - if opts.Wrapper { - cacheDir := c.Cache - configFile := c.Filename - err := createWrappers(deps, opts.TargetFile, targetDir, opts.BindownPath, cacheDir, configFile, opts.AllowMissingChecksum, opts.Stdout) - if err != nil { - return err - } - return nil + output := opts.Output + outputIsDir := opts.AllDeps || len(deps) > 1 + if output == "" { + output = c.InstallDir + outputIsDir = true } for _, name := range deps { dep, err := c.BuildDependency(name, system) if err != nil { return err } - target := opts.TargetFile - if target == "" { - target = filepath.Join(targetDir, dep.binName()) + target := output + if outputIsDir { + target = filepath.Join(output, dep.binName()) } out, err := install(dep, target, c.Cache, opts.Force, opts.ToCache, opts.AllowMissingChecksum) if err != nil { @@ -473,24 +495,40 @@ func (c *Config) InstallDependencies(deps []string, system System, opts *ConfigI return nil } -func createWrappers( - deps []string, targetFile, targetDir, bindownExec, cacheDir, configFile string, - missingSums bool, - stdout io.Writer, -) error { +type ConfigWrapDependenciesOpts struct { + Output string + BindownPath string + AllowMissingChecksum bool + AllDeps bool + Stdout io.Writer +} + +func (c *Config) WrapDependencies(deps []string, opts *ConfigWrapDependenciesOpts) error { + if opts == nil { + opts = &ConfigWrapDependenciesOpts{} + } + if opts.AllDeps { + deps = c.DependencyNames() + } + output := opts.Output + outputIsDir := opts.AllDeps || len(deps) > 1 + if output == "" { + output = c.InstallDir + outputIsDir = true + } for _, name := range deps { - target := targetFile - if target == "" { - target = filepath.Join(targetDir, name) + target := output + if outputIsDir { + target = filepath.Join(output, name) } - out, err := createWrapper(name, target, bindownExec, cacheDir, configFile, missingSums) + out, err := createWrapper(name, target, opts.BindownPath, c.Cache, c.Filename, opts.AllowMissingChecksum) if err != nil { return err } - if stdout == nil { + if opts.Stdout == nil { continue } - _, err = fmt.Fprintln(stdout, out) + _, err = fmt.Fprintln(opts.Stdout, out) if err != nil { return err }