Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download, Install, Shim and Updating #12

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.exe
!/pkg/scoop/shim.exe
/spoon
*.pprof

.repl
47 changes: 27 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ relying on the existing community work in form of buckets.

* More thorough `scoop search`
* Better performance (Varies from command to command)
* Behaviour changes
* `spoon install app@version` will now use an old manifest and hold the app, instead
of generating a manifest (destined to be buggy)
* Additional features
* Tab completion for commands, flags and packages
* Common command aliases
Expand All @@ -21,8 +24,13 @@ below.

## Breaking Changes

* No automatic `spoon update` calls during `install`, `download`, ...
* The `--global` flag hasn't beren implemented anywhere and I am not planning to
do so as of now. If there's demand in the future, I will consider.
* Only `kiennq/shim.exe` is supported for shimming
> The older shim formats were included in scoop for backwards compatibility
> reasons. The solution is probably to simply reinstall all currently
> installed packages via `scoop export` and `scoop import`.

## Manual Installation

Expand All @@ -46,6 +54,16 @@ any harm to your OS or other packages.
Note that self-updating is *NOT YET* possible. To update, please use `scoop
update spoon` for now.

## Runtime dependencies

While spoon is written in Golang, it has runtime dependencies needed for
installing. Rewriting those would provide little to no value and cost a lot of
value.

* [shim.exe](https://github.com/kiennq/scoop-better-shimexe) - Included in
Binary - MIT/Unlicense
* ... TODO

## CLI Progress

Progress overview for scoop command implementations. This does NOT include spoon
Expand All @@ -65,24 +83,24 @@ There are basically three levels of implementations (and the states inbetween):
| ---------- | ------------------- | ------------------------------------------------------------------------ |
| help | Native | |
| search | Native | * Performance improvements<br/>* JSON output<br/> * Search configuration |
| install | Wrapper | |
| uninstall | Wrapper | * Terminate running processes |
| update | Partially Native | * Now invokes `status` after updating buckets |
| bucket | Partially Native | * `bucket rm` now supports multiple buckets to delete at once |
| download | Native | * Support for multiple apps to download at once |
| cat | Native | * Alias `manifest`<br/>* Allow getting specific manifest versions |
| status | Native | * `--local` has been deleted (It's always local now)<br/>* Shows outdated / installed things scoop didn't (due to bugs) |
| info | Wrapper | |
| depends | Native (WIP) | * Adds `--reverse/-r` flag<br/>* Prints an ASCII tree by default |
| update | Partially Native | * Now invokes `status` after updating buckets |
| bucket | Partially Native | * `bucket rm` now supports multiple buckets to delete at once |
| install | Native (WIP) | * Installing a specific version doesn't generate manifests anymore, but uses an old existing manifest and sets the installed app to `held`. |
| uninstall | Native (WIP) | * Terminate running processes |
| info | Wrapper | |
| shim | Planned Next | |
| unhold | Planned Next | |
| hold | Planned Next | |
| list | | |
| hold | | |
| unhold | | |
| reset | | |
| cleanup | | |
| create | | |
| shim | | |
| which | | |
| config | | |
| download | | |
| cache | | |
| prefix | | |
| home | | |
Expand All @@ -92,14 +110,3 @@ There are basically three levels of implementations (and the states inbetween):
| virustotal | | |
| alias | | |

## Search

The search here does nothing fancy, it simply does an offline search of
buckets, just like what scoop does, but faster. Online search is not supported
as I deem it unnecessary. If you want to search the latest, simply run
`scoop update; spoon search <app>`.

The search command allows plain output and JSON output. This allows use with
tools such as `jq` or direct use in powershell via Powershells builtin
`ConvertFrom-Json`.

14 changes: 10 additions & 4 deletions cmd/spoon/cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ func catCmd() *cobra.Command {
return fmt.Errorf("error getting default scoop: %w", err)
}

app, err := defaultScoop.GetAvailableApp(args[0])
app, err := defaultScoop.FindAvailableApp(args[0])
if err != nil {
return fmt.Errorf("error finding app: %w", err)
}

if app == nil {
installedApp, err := defaultScoop.GetInstalledApp(args[0])
installedApp, err := defaultScoop.FindInstalledApp(args[0])
if err != nil {
return fmt.Errorf("error finding app: %w", err)
}
Expand All @@ -44,12 +44,18 @@ func catCmd() *cobra.Command {
app = installedApp.App
}

var reader io.ReadCloser
var reader io.Reader
_, _, version := scoop.ParseAppIdentifier(args[0])
if version != "" {
reader, err = app.ManifestForVersion(version)
} else {
reader, err = os.Open(app.ManifestPath())
fileReader, tempErr := os.Open(app.ManifestPath())
if fileReader != nil {
defer fileReader.Close()
reader = fileReader
} else {
err = tempErr
}
}

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/spoon/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func autocompleteInstalled(
return nil, cobra.ShellCompDirectiveNoFileComp
}

apps, err := defaultScoop.GetInstalledApps()
apps, err := defaultScoop.InstalledApps()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/spoon/depends.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func dependsCmd() *cobra.Command {
if err != nil {
return fmt.Errorf("error getting default scoop: %w", err)
}
app, err := defaultScoop.GetAvailableApp(args[0])
app, err := defaultScoop.FindAvailableApp(args[0])
if err != nil {
return fmt.Errorf("error looking up app: %w", err)
}
Expand Down
103 changes: 103 additions & 0 deletions cmd/spoon/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"errors"
"fmt"
"os"
"path/filepath"

"github.com/Bios-Marcel/spoon/pkg/scoop"
"github.com/spf13/cobra"
)

func downloadCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "download",
Short: "Download all files required for a package",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: autocompleteAvailable,
RunE: RunE(func(cmd *cobra.Command, args []string) error {
arch := scoop.ArchitectureKey(must(cmd.Flags().GetString("arch")))
force := must(cmd.Flags().GetBool("force"))
noHashCheck := must(cmd.Flags().GetBool("no-hash-check"))

defaultScoop, err := scoop.NewScoop()
if err != nil {
return fmt.Errorf("error retrieving scoop instance: %w", err)
}

for _, arg := range args {
app, err := defaultScoop.FindAvailableApp(arg)
if err != nil {
return fmt.Errorf("error looking up app: %w", err)
}
if app == nil {
return fmt.Errorf("app '%s' not found", arg)
}

if err := app.LoadDetails(
scoop.DetailFieldArchitecture,
scoop.DetailFieldUrl,
scoop.DetailFieldHash,
); err != nil {
return fmt.Errorf("error loading app details: %w", err)
}

resolvedApp := app.ForArch(arch)
resultChan, err := resolvedApp.Download(
defaultScoop.CacheDir(), arch, !noHashCheck, force,
)
if err != nil {
return err
}

for result := range resultChan {
switch result := result.(type) {
case *scoop.CacheHit:
name := filepath.Base(result.Downloadable.URL)
fmt.Printf("Cache hit for '%s'\n", name)
case *scoop.FinishedDownload:
name := filepath.Base(result.Downloadable.URL)
fmt.Printf("Downloaded '%s'\n", name)
case error:
var checksumErr *scoop.ChecksumMismatchError
if errors.As(result, &checksumErr) {
fmt.Printf(
"Checksum mismatch:\n\rFile: '%s'\n\tExpected: '%s'\n\tActual: '%s'\n",
checksumErr.File,
checksumErr.Expected,
checksumErr.Actual,
)

// FIXME Find a better way to do this via
// returnvalue?
os.Exit(1)
}
if result != nil {
return result
}
}
}
}

return nil
}),
}

cmd.Flags().BoolP("force", "f", false, "Force download (overwrite cache)")
// FIXME No shorthand for now, since --h is help and seems to clash.
cmd.Flags().Bool("no-hash-check", false, "Skip hash verification (use with caution!)")
// We default to our system architecture here. If scoop encounters an
// unsupported arch, it is ignored. We'll do the same.
cmd.Flags().StringP("arch", "a", string(SystemArchitecture),
"use specified architecture, if app supports it")
cmd.RegisterFlagCompletionFunc("arch", cobra.FixedCompletions(
[]string{
string(scoop.ArchitectureKey32Bit),
string(scoop.ArchitectureKey64Bit),
string(scoop.ArchitectureKeyARM64),
},
cobra.ShellCompDirectiveDefault))

return cmd
}
35 changes: 23 additions & 12 deletions cmd/spoon/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,34 @@ func installCmd() *cobra.Command {
Short: "Install a package",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: autocompleteAvailable,
Run: func(cmd *cobra.Command, args []string) {
flags, err := getFlags(cmd, "global", "independent", "no-cache", "no-update-scoop", "skip", "arch")
RunE: RunE(func(cmd *cobra.Command, args []string) error {
// Flags we currently do not support
if must(cmd.Flags().GetBool("global")) {
flags, err := getFlags(cmd, "global", "independent", "no-cache",
"no-update-scoop", "skip", "arch")
if err != nil {
return err
}
os.Exit(execScoopCommand("install", append(flags, args...)...))
}

arch := must(cmd.Flags().GetString("arch"))

defaultScoop, err := scoop.NewScoop()
if err != nil {
fmt.Println(err)
os.Exit(1)
return fmt.Errorf("error retrieving scoop instance: %w", err)
}

// Default path, where we can't do our simple optimisation of
// parallelising install and download, as we only have one package.
if len(args) == 1 {
os.Exit(execScoopCommand("install", append(flags, args...)...))
return
installErrors := defaultScoop.InstallAll(args, scoop.ArchitectureKey(arch))
for _, err := range installErrors {
fmt.Println(err)
}

// FIXME Parallelise.
os.Exit(execScoopCommand("install", append(flags, args...)...))
},
if len(installErrors) > 0 {
os.Exit(1)
}
return nil
}),
}

cmd.Flags().BoolP("global", "g", false, "Install an app globally")
Expand Down
1 change: 1 addition & 0 deletions cmd/spoon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func main() {
}

rootCmd.AddCommand(searchCmd())
rootCmd.AddCommand(downloadCmd())
rootCmd.AddCommand(installCmd())
rootCmd.AddCommand(uninstallCmd())
rootCmd.AddCommand(updateCmd())
Expand Down
28 changes: 22 additions & 6 deletions cmd/spoon/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"strings"
"sync"

_ "runtime/pprof"

"github.com/Bios-Marcel/spoon/internal/cli"
"github.com/Bios-Marcel/spoon/pkg/scoop"
jsoniter "github.com/json-iterator/go"
Expand Down Expand Up @@ -191,11 +189,16 @@ LOOP:
for job := range queue {
if err := job.app.LoadDetailsWithIter(
iter,
scoop.DetailFieldBin,
// Required for printing
scoop.DetailFieldDescription,
scoop.DetailFieldVersion,

// Requied for search
scoop.DetailFieldBin,
scoop.DetailFieldShortcuts,
scoop.DetailFieldArchitecture,

// FIXME Why do we need this again?
scoop.DetailFieldVersion,
); err != nil {
fmt.Printf("Error loading details for '%s': %s\n", job.app.ManifestPath(), err)
os.Exit(1)
Expand All @@ -207,16 +210,19 @@ LOOP:
continue LOOP
}

// FIXME Make search shortcuts flag? Or should we just keep it in bin
// for now, as it is a somewhat similar meaning.
if searchBin {
if matchBin(app.Bin, search, caseInsensitive) ||
matchBin(app.Shortcuts, search, caseInsensitive) {
matchShortcut(app.Shortcuts, search, caseInsensitive) {
continue LOOP
}

if app.Architecture != nil {
// FIXME Don't we need to search all compatible architechtures?
if arch := app.Architecture[SystemArchitecture]; arch != nil {
if matchBin(arch.Bin, search, caseInsensitive) ||
matchBin(arch.Shortcuts, search, caseInsensitive) {
matchShortcut(arch.Shortcuts, search, caseInsensitive) {
continue LOOP
}
}
Expand All @@ -232,6 +238,16 @@ LOOP:
return matches
}

func matchShortcut(shortcuts []scoop.Shortcut, search string, caseInsensitive bool) bool {
for _, shortcut := range shortcuts {
if contains(filepath.Base(shortcut.Name), search, caseInsensitive) ||
contains(filepath.Base(shortcut.ShortcutName), search, caseInsensitive) {
return true
}
}
return false
}

func matchBin(bins []scoop.Bin, search string, caseInsensitive bool) bool {
for _, bin := range bins {
if contains(filepath.Base(bin.Name), search, caseInsensitive) ||
Expand Down
8 changes: 4 additions & 4 deletions cmd/spoon/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ func shellCmd() *cobra.Command {
}

if err := windows.CreateJunctions([][2]string{
{defaultScoop.GetCacheDir(), tempScoop.GetCacheDir()},
{defaultScoop.GetScoopInstallationDir(), tempScoop.GetScoopInstallationDir()},
{defaultScoop.GetBucketsDir(), tempScoop.GetBucketsDir()},
{defaultScoop.CacheDir(), tempScoop.CacheDir()},
{defaultScoop.ScoopInstallationDir(), tempScoop.ScoopInstallationDir()},
{defaultScoop.BucketDir(), tempScoop.BucketDir()},
}...); err != nil {
return fmt.Errorf("error creating junctions: %w", err)
}
Expand Down Expand Up @@ -211,7 +211,7 @@ func shellCmd() *cobra.Command {
// environment variables and some apps use env_add_path instead
// of specifying shims.
var app *scoop.InstalledApp
app, err = tempScoop.GetInstalledApp(dependency)
app, err = tempScoop.FindInstalledApp(dependency)
if err != nil {
break
}
Expand Down
Loading