From 910843ac7cd9a9f23feb4ce1b5de4a2a214a9feb Mon Sep 17 00:00:00 2001 From: Marcel Schramm Date: Sun, 31 Mar 2024 16:52:57 +0200 Subject: [PATCH] Implements install, updating, uninstall and more --- .gitignore | 1 + README.md | 47 +- cmd/spoon/cat.go | 14 +- cmd/spoon/complete.go | 2 +- cmd/spoon/depends.go | 2 +- cmd/spoon/download.go | 103 +++ cmd/spoon/install.go | 35 +- cmd/spoon/main.go | 1 + cmd/spoon/search.go | 2 - cmd/spoon/shell.go | 8 +- cmd/spoon/uninstall.go | 55 +- cmd/spoon/versions.go | 2 +- go.mod | 6 +- go.sum | 9 +- internal/windows/env.go | 129 +++ internal/windows/env_test.go | 14 + internal/windows/windows.go | 6 +- pkg/scoop/manifest.go | 318 +++++++ pkg/scoop/scoop.go | 1258 +++++++++++++++++++++------ pkg/scoop/scoop_test.go | 50 +- pkg/scoop/shim.exe | Bin 0 -> 136192 bytes pkg/scoop/shim.go | 152 ++++ pkg/scoop/shim_bash.template | 0 pkg/scoop/shim_cmd_to_bash.template | 4 + pkg/scoop/shim_cmd_to_cmd.template | 3 + pkg/scoop/shim_ps1_to_ps1.template | 5 + pkg/scoop/shim_sh.template | 0 27 files changed, 1866 insertions(+), 360 deletions(-) create mode 100644 cmd/spoon/download.go create mode 100644 internal/windows/env_test.go create mode 100644 pkg/scoop/manifest.go create mode 100644 pkg/scoop/shim.exe create mode 100644 pkg/scoop/shim.go create mode 100644 pkg/scoop/shim_bash.template create mode 100644 pkg/scoop/shim_cmd_to_bash.template create mode 100644 pkg/scoop/shim_cmd_to_cmd.template create mode 100644 pkg/scoop/shim_ps1_to_ps1.template create mode 100644 pkg/scoop/shim_sh.template diff --git a/.gitignore b/.gitignore index f62343d..3590b74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.exe +!/pkg/scoop/shim.exe /spoon *.pprof diff --git a/README.md b/README.md index 0740167..294454d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -41,6 +49,16 @@ below. 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 @@ -60,24 +78,24 @@ There are basically three levels of implementations (and the states inbetween): | ---------- | ------------------- | ------------------------------------------------------------------------ | | help | Native | | | search | Native | * Performance improvements
* JSON output
* 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`
* Allow getting specific manifest versions | | status | Native | * `--local` has been deleted (It's always local now)
* Shows outdated / installed things scoop didn't (due to bugs) | -| info | Wrapper | | | depends | Native (WIP) | * Adds `--reverse/-r` flag
* 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 | | | @@ -87,14 +105,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 `. - -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`. - diff --git a/cmd/spoon/cat.go b/cmd/spoon/cat.go index 2f9f3db..b30604f 100644 --- a/cmd/spoon/cat.go +++ b/cmd/spoon/cat.go @@ -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) } @@ -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 { diff --git a/cmd/spoon/complete.go b/cmd/spoon/complete.go index e9a4e4b..f4385b8 100644 --- a/cmd/spoon/complete.go +++ b/cmd/spoon/complete.go @@ -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 } diff --git a/cmd/spoon/depends.go b/cmd/spoon/depends.go index b463ff0..8bf4179 100644 --- a/cmd/spoon/depends.go +++ b/cmd/spoon/depends.go @@ -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) } diff --git a/cmd/spoon/download.go b/cmd/spoon/download.go new file mode 100644 index 0000000..7ae9192 --- /dev/null +++ b/cmd/spoon/download.go @@ -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 +} diff --git a/cmd/spoon/install.go b/cmd/spoon/install.go index f7cb251..69d8c27 100644 --- a/cmd/spoon/install.go +++ b/cmd/spoon/install.go @@ -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") diff --git a/cmd/spoon/main.go b/cmd/spoon/main.go index cfb381b..02e81ef 100644 --- a/cmd/spoon/main.go +++ b/cmd/spoon/main.go @@ -34,6 +34,7 @@ func main() { } rootCmd.AddCommand(searchCmd()) + rootCmd.AddCommand(downloadCmd()) rootCmd.AddCommand(installCmd()) rootCmd.AddCommand(uninstallCmd()) rootCmd.AddCommand(updateCmd()) diff --git a/cmd/spoon/search.go b/cmd/spoon/search.go index 6936d15..364ef7c 100644 --- a/cmd/spoon/search.go +++ b/cmd/spoon/search.go @@ -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" diff --git a/cmd/spoon/shell.go b/cmd/spoon/shell.go index 148afe7..7822ae4 100644 --- a/cmd/spoon/shell.go +++ b/cmd/spoon/shell.go @@ -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) } @@ -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 } diff --git a/cmd/spoon/uninstall.go b/cmd/spoon/uninstall.go index 3c9910f..b8ab439 100644 --- a/cmd/spoon/uninstall.go +++ b/cmd/spoon/uninstall.go @@ -25,28 +25,57 @@ func uninstallCmd() *cobra.Command { }, Args: cobra.MinimumNArgs(1), ValidArgsFunction: autocompleteInstalled, - Run: func(cmd *cobra.Command, args []string) { + RunE: RunE(func(cmd *cobra.Command, args []string) error { yes, err := cmd.Flags().GetBool("yes") if err != nil { - fmt.Println("error getting yes flag:", err) - os.Exit(1) + return fmt.Errorf("error getting yes flag: %w", err) } defaultScoop, err := scoop.NewScoop() if err != nil { - fmt.Println("error getting default scoop:", err) - os.Exit(1) + return fmt.Errorf("error getting default scoop: %w", err) } + if err := checkRunningProcesses(defaultScoop, args, yes); err != nil { - fmt.Println(err) + return fmt.Errorf("error checking running processes: %w", err) } - redirectedFlags, err := getFlags(cmd, "global", "purge") - if err != nil { - fmt.Println(err) - os.Exit(1) + // FIXME 3 funcs: FindInstalledApp, FindInstalledApps, + // InstalledApps. The later returns all of them, returning + // everything instead of finding something. + for _, arg := range args { + app, err := defaultScoop.FindInstalledApp(args[0]) + if err != nil { + return err + } + + // FIXME Is this good? What does scoop do? + if app == nil { + fmt.Printf("App '%s' is not intalled.\n", arg) + continue + } + + // FIXME We need to make the loading stuff less annoying. Can we + // have a special optimisation path / package so that we can + // still cover stuff such as search? + if err := app.LoadDetails(scoop.DetailFieldsAll...); err != nil { + return fmt.Errorf("error loading app details: %w", err) + } + + // FIXME This currently only uninstalls a specific version. We + // need multiple versions current, specific all? + if err := defaultScoop.Uninstall(app, app.Architecture); err != nil { + return fmt.Errorf("error uninstalling '%s': %w", arg, err) + } } - os.Exit(execScoopCommand("uninstall", append(redirectedFlags, args...)...)) - }, + + // redirectedFlags, err := getFlags(cmd, "global", "purge") + // if err != nil { + // fmt.Println(err) + // os.Exit(1) + // } + // os.Exit(execScoopCommand("uninstall", append(redirectedFlags, args...)...)) + return nil + }), } cmd.Flags().BoolP("global", "g", false, "Uninstall a globally installed app") @@ -65,7 +94,7 @@ func checkRunningProcesses(scoop *scoop.Scoop, args []string, yes bool) error { var processPrefixes []string for _, arg := range args { processPrefixes = append(processPrefixes, - strings.ToLower(filepath.Join(scoop.GetAppsDir(), arg)+"\\")) + strings.ToLower(filepath.Join(scoop.AppDir(), arg)+"\\")) } var processesToKill []shared.Process diff --git a/cmd/spoon/versions.go b/cmd/spoon/versions.go index 21ef625..5eecff6 100644 --- a/cmd/spoon/versions.go +++ b/cmd/spoon/versions.go @@ -19,7 +19,7 @@ func versionsCmd() *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) } diff --git a/go.mod b/go.mod index 1536fcf..9b042f1 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/Bios-Marcel/spoon -go 1.21.1 +go 1.22.0 require ( github.com/Bios-Marcel/versioncmp v0.0.0-20240329201707-2bd36cfebbc9 + github.com/cavaliergopher/grab/v3 v3.0.1 github.com/fatih/color v1.16.0 github.com/go-git/go-git/v5 v5.11.0 github.com/iamacarpet/go-win64api v0.0.0-20230324134531-ef6dbdd6db97 @@ -39,6 +40,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/skeema/knownhosts v1.2.1 // indirect @@ -47,7 +49,7 @@ require ( golang.org/x/crypto v0.16.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/tools v0.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6e7ec9c..f7378e4 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/capnspacehook/taskmaster v0.0.0-20210519235353-1629df7c85e9/go.mod h1:257CYs3Wd/CTlLQ3c72jKv+fFE2MV3WPNnV5jiroYUU= +github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4= +github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -125,8 +127,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rickb777/date v1.14.2/go.mod h1:swmf05C+hN+m8/Xh7gEq3uB6QJDNc5pQBWojKdHetOs= github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= @@ -216,8 +219,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= diff --git a/internal/windows/env.go b/internal/windows/env.go index bd82618..a7790df 100644 --- a/internal/windows/env.go +++ b/internal/windows/env.go @@ -1,15 +1,95 @@ package windows import ( + "bytes" "encoding/json" "fmt" "os" "os/exec" + "slices" + "strings" ) +type Paths []string + +// ParsePath will break the path variable content down into separate paths. This +// also handles quoting. The order is preserved. +func ParsePath(value string) Paths { + // Technically we could also use strings.FieldFunc, but we got to manually + // cut of the quotes anyway then, so we'll just do the whole thing manually. + var values []string + var quoteOpen bool + var nextStart int + for index, char := range value { + if char == '"' { + if quoteOpen { + // +1 to skip the open quote + values = append(values, value[nextStart+1:index]) + // End quote means we'll have a separator next, so we start at + // the next path char. + nextStart = index + 2 + } + + quoteOpen = !quoteOpen + } else if char == ';' && index > nextStart { + if quoteOpen { + continue + } + + values = append(values, value[nextStart:index]) + nextStart = index + 1 + } + } + + // Last element if applicable, since the path could also end on a semicolon + // or quote. + if nextStart < len(value) { + values = append(values, value[nextStart:]) + } + + return Paths(values) +} + +// Remove returns a new path object that doesn't contain any of the specified +// paths. +func (p Paths) Remove(paths ...string) Paths { + p = slices.DeleteFunc(p, func(value string) bool { + // FIXME This should sanitize the path separators and such. We also need + // tests for this. + return slices.Contains(paths, value) + }) + return p +} + +// Preprend will create a new Paths object, adding the supplied paths infront, +// using the given order. +func (p Paths) Prepend(paths ...string) Paths { + newPath := make(Paths, 0, len(p)+len(paths)) + newPath = append(newPath, paths...) + newPath = append(newPath, p...) + return newPath +} + +// Creates a new path string, where all entries are quoted. +func (p Paths) String() string { + var buffer bytes.Buffer + for i := 0; i < len(p); i++ { + if i != 0 { + buffer.WriteRune(';') + } + + // FIXME Only quote if necessary? Only if contains semicolon? + buffer.WriteRune('"') + buffer.WriteString(p[i]) + buffer.WriteRune('"') + } + return buffer.String() +} + func GetPersistentEnvValues() (map[string]string, error) { cmd := exec.Command( "powershell", + "-NoLogo", "-NoProfile", "[Environment]::GetEnvironmentVariables('User') | ConvertTo-Json", ) @@ -36,12 +116,34 @@ func GetPersistentEnvValues() (map[string]string, error) { return result, nil } +// GetPersistentEnvValue retrieves a persistent user level environment variable. +// The first returned value is the key and the second the value. While the key +// is defined in the query, the casing might be different, which COULD matter. +// If nothing was found, we return empty strings without an error. +func GetPersistentEnvValue(key string) (string, string, error) { + // While we could directly call GetEnvironmentVariable, we want to find out + // he string, therefore we use the result of the GetAll call. + + allVars, err := GetPersistentEnvValues() + if err != nil { + return "", "", fmt.Errorf("error retrieving variables: %w", err) + } + + for keyPersisted, val := range allVars { + if strings.EqualFold(key, keyPersisted) { + return keyPersisted, val, nil + } + } + return "", "", nil +} + // Sets a User-Level Environment variable. An empty value will remove the key // completly. func SetPersistentEnvValue(key, value string) error { cmd := exec.Command( "powershell", "-NoProfile", + "-NoLogo", "-Command", "[Environment]::SetEnvironmentVariable('"+key+"','"+value+"','User')", ) @@ -50,3 +152,30 @@ func SetPersistentEnvValue(key, value string) error { cmd.Stdin = os.Stdin return cmd.Run() } + +func SetPersistentEnvValues(vars ...[2]string) error { + if len(vars) == 0 { + return nil + } + + var command bytes.Buffer + for _, pair := range vars { + command.WriteString("[Environment]::SetEnvironmentVariable('") + command.WriteString(pair[0]) + command.WriteString("','") + command.WriteString(pair[1]) + command.WriteString("','User');") + } + + cmd := exec.Command( + "powershell", + "-NoProfile", + "-NoLogo", + "-Command", + command.String(), + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd.Run() +} diff --git a/internal/windows/env_test.go b/internal/windows/env_test.go new file mode 100644 index 0000000..116b89b --- /dev/null +++ b/internal/windows/env_test.go @@ -0,0 +1,14 @@ +package windows_test + +import ( + "testing" + + "github.com/Bios-Marcel/spoon/internal/windows" + "github.com/stretchr/testify/require" +) + +func Test_ParsePath(t *testing.T) { + path := windows.ParsePath(`C:\path_a;"C:\path_b";"C:\path_;";C:\path_c`) + require.Equal(t, []string{`C:\path_a`, `C:\path_b`, `C:\path_;`, `C:\path_c`}, []string(path)) + require.Equal(t, `"C:\path_a";"C:\path_b";"C:\path_;";"C:\path_c"`, path.String()) +} diff --git a/internal/windows/windows.go b/internal/windows/windows.go index f10f6d9..e0064ed 100644 --- a/internal/windows/windows.go +++ b/internal/windows/windows.go @@ -67,8 +67,10 @@ func GetShellExecutable() (string, error) { } // Depending on whether we are shimmed or not, our parent might be - // a shim, so we'll try ignoring this and going deeper. - if lowered := strings.ToLower(name); lowered == "spoon.exe" || lowered == "spoon" { + // a shim, so we'll try ignoring this and going deeper. We'll + // additionally ignore go.exe, as this helps during dev, using `go run`. + if lowered := strings.ToLower(name); lowered == "spoon.exe" || + lowered == "spoon" || lowered == "go" || lowered == "go.exe" { parentId = id continue } diff --git a/pkg/scoop/manifest.go b/pkg/scoop/manifest.go new file mode 100644 index 0000000..1ab3764 --- /dev/null +++ b/pkg/scoop/manifest.go @@ -0,0 +1,318 @@ +package scoop + +import ( + "fmt" + "io" + "os" + "slices" + "strings" + + jsoniter "github.com/json-iterator/go" +) + +const ( + DetailFieldBin = "bin" + DetailFieldShortcuts = "shortcuts" + DetailFieldUrl = "url" + DetailFieldHash = "hash" + DetailFieldArchitecture = "architecture" + DetailFieldDescription = "description" + DetailFieldVersion = "version" + DetailFieldNotes = "notes" + DetailFieldDepends = "depends" + DetailFieldEnvSet = "env_set" + DetailFieldEnvAddPath = "env_add_path" + DetailFieldExtractDir = "extract_dir" + DetailFieldExtractTo = "extract_to" + DetailFieldPostInstall = "post_install" + DetailFieldPreInstall = "pre_install" + DetailFieldPreUninstall = "pre_uninstall" + DetailFieldPostUninstall = "post_uninstall" + DetailFieldInstaller = "installer" + DetailFieldUninstaller = "uninstaller" + DetailFieldInnoSetup = "innosetup" +) + +// DetailFieldsAll is a list of all available DetailFields to load during +// [App.LoadDetails]. Use these if you need all fields or don't care whether +// unneeded fields are being loaded. +var DetailFieldsAll = []string{ + DetailFieldBin, + DetailFieldShortcuts, + DetailFieldUrl, + DetailFieldHash, + DetailFieldArchitecture, + DetailFieldDescription, + DetailFieldVersion, + DetailFieldNotes, + DetailFieldDepends, + DetailFieldEnvSet, + DetailFieldEnvAddPath, + DetailFieldExtractDir, + DetailFieldExtractTo, + DetailFieldPostInstall, + DetailFieldPreInstall, + DetailFieldPreUninstall, + DetailFieldPostUninstall, + DetailFieldInstaller, + DetailFieldUninstaller, + DetailFieldInnoSetup, +} + +// manifestIter gives you an iterator with a big enough size to read any +// manifest without reallocations. +func manifestIter() *jsoniter.Iterator { + return jsoniter.Parse(jsoniter.ConfigFastest, nil, 1024*128) +} + +// LoadDetails will load additional data regarding the manifest, such as +// description and version information. This causes IO on your drive and +// therefore isn't done by default. +func (a *App) LoadDetails(fields ...string) error { + return a.LoadDetailsWithIter(manifestIter(), fields...) +} + +// LoadDetails will load additional data regarding the manifest, such as +// description and version information. This causes IO on your drive and +// therefore isn't done by default. +func (a *App) LoadDetailsWithIter(iter *jsoniter.Iterator, fields ...string) error { + file, err := os.Open(a.manifestPath) + if err != nil { + return fmt.Errorf("error opening manifest: %w", err) + } + defer file.Close() + + return a.loadDetailFromManifestWithIter(iter, file, fields...) +} + +func mergeIntoDownloadables(urls, hashes, extractDirs, extractTos []string) []Downloadable { + // It can happen that we have different extract_dirs, but only one archive, + // containing both architectures. This should also never be empty, but at + // least of size one, so we'll never allocate for naught. + downloadables := make([]Downloadable, max(len(urls), len(extractDirs), len(extractTos))) + + // We assume that we have the same length in each. While this + // hasn't been specified in the app manifests wiki page, it's + // the seemingly only sensible thing to me. + // If we are missing extract_dir or extract_to entries, it's fine, as we use + // nonpointer values anyway and simple default to empty, which means + // application directory. + for index, value := range urls { + downloadables[index].URL = value + } + for index, value := range hashes { + downloadables[index].Hash = value + } + for index, value := range extractDirs { + downloadables[index].ExtractDir = value + } + for index, value := range extractTos { + downloadables[index].ExtractTo = value + } + + return downloadables +} + +// LoadDetails will load additional data regarding the manifest, such as +// description and version information. This causes IO on your drive and +// therefore isn't done by default. +func (a *App) loadDetailFromManifestWithIter( + iter *jsoniter.Iterator, + manifest io.Reader, + fields ...string, +) error { + iter.Reset(manifest) + + var urls, hashes, extractDirs, extractTos []string + for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { + if !slices.Contains(fields, field) { + iter.Skip() + continue + } + + switch field { + case DetailFieldDescription: + a.Description = iter.ReadString() + case DetailFieldVersion: + a.Version = iter.ReadString() + case DetailFieldUrl: + urls = parseStringOrArray(iter) + case DetailFieldHash: + hashes = parseStringOrArray(iter) + case DetailFieldShortcuts: + a.Shortcuts = parseBin(iter) + case DetailFieldBin: + a.Bin = parseBin(iter) + case DetailFieldArchitecture: + // Preallocate to 3, as we support at max 3 architectures + a.Architecture = make(map[ArchitectureKey]*Architecture, 3) + for arch := iter.ReadObject(); arch != ""; arch = iter.ReadObject() { + var archValue Architecture + a.Architecture[ArchitectureKey(arch)] = &archValue + + var urls, hashes, extractDirs []string + for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { + switch field { + case "url": + urls = parseStringOrArray(iter) + case "hash": + hashes = parseStringOrArray(iter) + case "extract_dir": + extractDirs = parseStringOrArray(iter) + case "bin": + archValue.Bin = parseBin(iter) + case "shortcuts": + archValue.Shortcuts = parseBin(iter) + case "installer": + installer := parseInstaller(iter) + archValue.Installer = &installer + case "uninstaller": + uninstaller := Uninstaller(parseInstaller(iter)) + archValue.Uninstaller = &uninstaller + default: + iter.Skip() + } + } + + // extract_to is always on the root level, so we pass nil + archValue.Downloadables = mergeIntoDownloadables(urls, hashes, extractDirs, nil) + } + case DetailFieldDepends: + // Array at top level to create multiple entries + if iter.WhatIsNext() == jsoniter.ArrayValue { + for iter.ReadArray() { + a.Depends = append(a.Depends, a.parseDependency(iter.ReadString())) + } + } else { + a.Depends = []Dependency{a.parseDependency(iter.ReadString())} + } + case DetailFieldEnvAddPath: + a.EnvAddPath = parseStringOrArray(iter) + case DetailFieldEnvSet: + for key := iter.ReadObject(); key != ""; key = iter.ReadObject() { + a.EnvSet = append(a.EnvSet, EnvVar{Key: key, Value: iter.ReadString()}) + } + case DetailFieldInstaller: + installer := parseInstaller(iter) + a.Installer = &installer + case DetailFieldUninstaller: + uninstaller := Uninstaller(parseInstaller(iter)) + a.Uninstaller = &uninstaller + case DetailFieldInnoSetup: + a.InnoSetup = iter.ReadBool() + case DetailFieldPreInstall: + a.PreInstall = parseStringOrArray(iter) + case DetailFieldPostInstall: + a.PostInstall = parseStringOrArray(iter) + case DetailFieldPreUninstall: + a.PreUninstall = parseStringOrArray(iter) + case DetailFieldPostUninstall: + a.PostUninstall = parseStringOrArray(iter) + case DetailFieldExtractDir: + extractDirs = parseStringOrArray(iter) + case DetailFieldExtractTo: + extractTos = parseStringOrArray(iter) + case DetailFieldNotes: + if iter.WhatIsNext() == jsoniter.ArrayValue { + var lines []string + for iter.ReadArray() { + lines = append(lines, iter.ReadString()) + } + a.Notes = strings.Join(lines, "\n") + } else { + a.Notes = iter.ReadString() + } + default: + iter.Skip() + } + } + + if iter.Error != nil { + return fmt.Errorf("error parsing json: %w", iter.Error) + } + + // If there are no URLs at the root level, that means they are in the + // arch-specific instructions. In this case, we'll only access the + // ExtractTo / ExtractDir when resolving a certain arch. + if len(urls) > 0 { + a.Downloadables = mergeIntoDownloadables(urls, hashes, extractDirs, extractTos) + } + + return nil +} + +func parseInstaller(iter *jsoniter.Iterator) Installer { + installer := Installer{} + for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { + switch field { + case "file": + installer.File = iter.ReadString() + case "script": + installer.Script = parseStringOrArray(iter) + case "args": + installer.Args = parseStringOrArray(iter) + case "keep": + installer.Keep = iter.ReadBool() + default: + iter.Skip() + } + } + return installer +} + +func parseBin(iter *jsoniter.Iterator) []Bin { + // Array at top level to create multiple entries + if iter.WhatIsNext() == jsoniter.ArrayValue { + var bins []Bin + for iter.ReadArray() { + // There are nested arrays, for shim creation, with format: + // binary alias [args...] + if iter.WhatIsNext() == jsoniter.ArrayValue { + var bin Bin + if iter.ReadArray() { + bin.Name = iter.ReadString() + } + if iter.ReadArray() { + bin.Alias = iter.ReadString() + } + for iter.ReadArray() { + bin.Args = append(bin.Args, iter.ReadString()) + } + bins = append(bins, bin) + } else { + // String in the root level array to add to path + bins = append(bins, Bin{Name: iter.ReadString()}) + } + } + return bins + } + + // String value at root level to add to path. + return []Bin{{Name: iter.ReadString()}} +} + +func parseStringOrArray(iter *jsoniter.Iterator) []string { + if iter.WhatIsNext() == jsoniter.ArrayValue { + var val []string + for iter.ReadArray() { + val = append(val, iter.ReadString()) + } + return val + } + + return []string{iter.ReadString()} +} + +func (a App) parseDependency(value string) Dependency { + parts := strings.SplitN(value, "/", 1) + switch len(parts) { + case 0: + // Should be a broken manifest + return Dependency{} + case 1: + // No bucket means same bucket. + return Dependency{Bucket: a.Bucket.Name(), Name: parts[0]} + default: + return Dependency{Bucket: parts[0], Name: parts[1]} + } +} diff --git a/pkg/scoop/scoop.go b/pkg/scoop/scoop.go index 6295e9b..870ce3b 100644 --- a/pkg/scoop/scoop.go +++ b/pkg/scoop/scoop.go @@ -1,22 +1,31 @@ package scoop import ( + "archive/zip" "bytes" "context" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" "encoding/json" "errors" "fmt" + "hash" "io" "math" "os" + "os/exec" "path/filepath" "regexp" - "slices" + "strconv" "strings" "github.com/Bios-Marcel/spoon/internal/git" "github.com/Bios-Marcel/spoon/internal/windows" "github.com/Bios-Marcel/versioncmp" + "github.com/cavaliergopher/grab/v3" jsoniter "github.com/json-iterator/go" ) @@ -61,7 +70,7 @@ func (b *Bucket) ManifestDir() string { return b.manifestDir } -func (b *Bucket) GetApp(name string) *App { +func (b *Bucket) FindApp(name string) *App { potentialManifest := filepath.Join(b.ManifestDir(), name+".json") if _, err := os.Stat(potentialManifest); err == nil { return &App{ @@ -103,13 +112,13 @@ var ErrBucketNotFound = errors.New("bucket not found") // GetBucket constructs a new bucket object pointing at the given bucket. At // this point, the bucket might not necessarily exist. func (scoop *Scoop) GetBucket(name string) *Bucket { - return &Bucket{rootDir: filepath.Join(scoop.GetBucketsDir(), name)} + return &Bucket{rootDir: filepath.Join(scoop.BucketDir(), name)} } -func (scoop *Scoop) GetAvailableApp(name string) (*App, error) { +func (scoop *Scoop) FindAvailableApp(name string) (*App, error) { bucket, name, _ := ParseAppIdentifier(name) if bucket != "" { - return scoop.GetBucket(bucket).GetApp(name), nil + return scoop.GetBucket(bucket).FindApp(name), nil } buckets, err := scoop.GetLocalBuckets() @@ -117,23 +126,23 @@ func (scoop *Scoop) GetAvailableApp(name string) (*App, error) { return nil, fmt.Errorf("error getting local buckets: %w", err) } for _, bucket := range buckets { - if app := bucket.GetApp(name); app != nil { + if app := bucket.FindApp(name); app != nil { return app, nil } } return nil, nil } -func (scoop *Scoop) GetInstalledApp(name string) (*InstalledApp, error) { +func (scoop *Scoop) FindInstalledApp(name string) (*InstalledApp, error) { iter := jsoniter.Parse(jsoniter.ConfigFastest, nil, 256) - return scoop.getInstalledApp(iter, name) + return scoop.findInstalledApp(iter, name) } -func (scoop *Scoop) getInstalledApp(iter *jsoniter.Iterator, name string) (*InstalledApp, error) { +func (scoop *Scoop) findInstalledApp(iter *jsoniter.Iterator, name string) (*InstalledApp, error) { _, name, _ = ParseAppIdentifier(name) name = strings.ToLower(name) - appDir := filepath.Join(scoop.GetAppsDir(), name, "current") + appDir := filepath.Join(scoop.AppDir(), name, "current") installJson, err := os.Open(filepath.Join(appDir, "install.json")) if err != nil { @@ -147,11 +156,14 @@ func (scoop *Scoop) getInstalledApp(iter *jsoniter.Iterator, name string) (*Inst iter.Reset(installJson) var ( - bucketName string - hold bool + bucketName string + architecture string + hold bool ) for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { switch field { + case "architecture": + architecture = iter.ReadString() case "bucket": bucketName = iter.ReadString() case "hold": @@ -167,7 +179,8 @@ func (scoop *Scoop) getInstalledApp(iter *jsoniter.Iterator, name string) (*Inst } return &InstalledApp{ - Hold: hold, + Hold: hold, + Architecture: ArchitectureKey(architecture), App: &App{ Bucket: bucket, Name: name, @@ -213,7 +226,7 @@ type KnownBucket struct { // GetKnownBuckets returns the list of available "default" buckets that are // available, but might have not necessarily been installed locally. func (scoop *Scoop) GetKnownBuckets() ([]KnownBucket, error) { - file, err := os.Open(filepath.Join(scoop.GetScoopInstallationDir(), "buckets.json")) + file, err := os.Open(filepath.Join(scoop.ScoopInstallationDir(), "buckets.json")) if err != nil { return nil, fmt.Errorf("error opening buckets.json: %w", err) } @@ -234,7 +247,7 @@ func (scoop *Scoop) GetKnownBuckets() ([]KnownBucket, error) { // GetLocalBuckets is an API representation of locally installed buckets. func (scoop *Scoop) GetLocalBuckets() ([]*Bucket, error) { - potentialBuckets, err := windows.GetDirFilenames(scoop.GetBucketsDir()) + potentialBuckets, err := windows.GetDirFilenames(scoop.BucketDir()) if err != nil { return nil, fmt.Errorf("error reading bucket names: %w", err) } @@ -243,7 +256,7 @@ func (scoop *Scoop) GetLocalBuckets() ([]*Bucket, error) { for _, potentialBucket := range potentialBuckets { // While the bucket folder SHOULD only contain buckets, one could // accidentally place ANYTHING else in it, even textfiles. - absBucketPath := filepath.Join(scoop.GetBucketsDir(), potentialBucket) + absBucketPath := filepath.Join(scoop.BucketDir(), potentialBucket) file, err := os.Stat(absBucketPath) if err != nil { return nil, fmt.Errorf("error stat-ing potential bucket: %w", err) @@ -261,6 +274,9 @@ func (scoop *Scoop) GetLocalBuckets() ([]*Bucket, error) { // may not be part of a bucket. "Headless" manifests are also a thing, for // example when you are using an auto-generated manifest for a version that's // not available anymore. In that case, scoop will lose the bucket information. +// +// Note that this structure doesn't reflect the same schema as the scoop +// manifests, as we are trying to make usage easier, not just as hard. type App struct { Name string `json:"name"` Description string `json:"description"` @@ -272,17 +288,21 @@ type App struct { EnvAddPath []string `json:"env_add_path"` EnvSet []EnvVar `json:"env_set"` + Downloadables []Downloadable + Depends []Dependency `json:"depends"` - URL []string `json:"url"` Architecture map[ArchitectureKey]*Architecture `json:"architecture"` InnoSetup bool `json:"innosetup"` - Installer *Installer `json:"installer"` - PreInstall []string `json:"pre_install"` - PostInstall []string `json:"post_install"` - ExtractTo []string `json:"extract_to"` - // ExtractDir specifies which dir should be extracted from the downloaded - // archive. However, there might be more URLs than there are URLs. - ExtractDir []string `json:"extract_dir"` + // Installer deprecates msi + Installer *Installer `json:"installer"` + Uninstaller *Uninstaller `json:"uninstaller"` + PreInstall []string `json:"pre_install"` + PostInstall []string `json:"post_install"` + PreUninstall []string `json:"pre_uninstall"` + PostUninstall []string `json:"post_uninstall"` + ExtractTo []string `json:"extract_to"` + + // Spoon "internals" Bucket *Bucket `json:"-"` manifestPath string @@ -293,6 +313,9 @@ type InstalledApp struct { // Hold indicates whether the app should be kept on the currently installed // version. It's versioning pinning. Hold bool + // Archictecture defines which architecture was used for installation. On a + // 64Bit system for example, this could also be 32Bit, but not vice versa. + Architecture ArchitectureKey } type OutdatedApp struct { @@ -329,13 +352,14 @@ const ( ) type Architecture struct { - Items []ArchitectureItem `json:"items"` + Downloadables []Downloadable `json:"items"` Bin []Bin Shortcuts []Bin // Installer replaces MSI - Installer Installer + Installer *Installer + Uninstaller *Uninstaller // PreInstall contains a list of commands to execute before installation. // Note that PreUninstall isn't supported in ArchitectureItem, even though @@ -347,251 +371,72 @@ type Architecture struct { PostInstall []string } -type ArchitectureItem struct { - URL string - Hash string +type Downloadable struct { + URL string + Hash string + // ExtractDir specifies which dir should be extracted from the downloaded + // archive. However, there might be more URLs than there are ExtractDirs. ExtractDir string + ExtractTo string } type Installer struct { // File is the installer executable. If not specified, this will - // autoamtically be set to the last item of the URLs. + // automatically be set to the last item of the URLs. Note, that this will + // be looked up in the extracted dirs, if explicitly specified. File string Script []string Args []string Keep bool } -func (a App) ManifestPath() string { - return a.manifestPath -} - -const ( - DetailFieldBin = "bin" - DetailFieldShortcuts = "shortcuts" - DetailFieldUrl = "url" - DetailFieldArchitecture = "architecture" - DetailFieldDescription = "description" - DetailFieldVersion = "version" - DetailFieldNotes = "notes" - DetailFieldDepends = "depends" - DetailFieldEnvSet = "env_set" - DetailFieldEnvAddPath = "env_add_path" - DetailFieldExtractDir = "extract_dir" - DetailFieldExtractTo = "extract_to" - DetailFieldPostInstall = "post_install" - DetailFieldPreInstall = "pre_install" - DetailFieldInstaller = "installer" - DetailFieldInnoSetup = "innosetup" -) - -// LoadDetails will load additional data regarding the manifest, such as -// description and version information. This causes IO on your drive and -// therefore isn't done by default. -func (a *App) LoadDetails(fields ...string) error { - iter := jsoniter.Parse(jsoniter.ConfigFastest, nil, 1024*128) - return a.LoadDetailsWithIter(iter, fields...) -} - -// LoadDetails will load additional data regarding the manifest, such as -// description and version information. This causes IO on your drive and -// therefore isn't done by default. -func (a *App) LoadDetailsWithIter(iter *jsoniter.Iterator, fields ...string) error { - file, err := os.Open(a.manifestPath) - if err != nil { - return fmt.Errorf("error opening manifest: %w", err) - } - defer file.Close() - - iter.Reset(file) - - for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { - if !slices.Contains(fields, field) { - iter.Skip() - continue - } - - switch field { - case DetailFieldDescription: - a.Description = iter.ReadString() - case DetailFieldVersion: - a.Version = iter.ReadString() - case DetailFieldUrl: - a.URL = parseStringOrArray(iter) - case DetailFieldShortcuts: - a.Shortcuts = parseBin(iter) - case DetailFieldBin: - a.Bin = parseBin(iter) - case DetailFieldArchitecture: - // Preallocate to 3, as we support at max 3 architectures - a.Architecture = make(map[ArchitectureKey]*Architecture, 3) - for arch := iter.ReadObject(); arch != ""; arch = iter.ReadObject() { - var archValue Architecture - a.Architecture[ArchitectureKey(arch)] = &archValue - - var urls, hashes, extractDirs []string - for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { - switch field { - case "url": - urls = parseStringOrArray(iter) - case "hash": - hashes = parseStringOrArray(iter) - case "extract_dir": - extractDirs = parseStringOrArray(iter) - case "bin": - archValue.Bin = parseBin(iter) - case "shortcuts": - archValue.Shortcuts = parseBin(iter) - default: - iter.Skip() - } - } - - // We use non-pointers, as we'll have everything initiliased - // already then. It can happen that we have different - // extract_dirs, but only one archive, containing both - // architectures. - archValue.Items = make([]ArchitectureItem, max(len(urls), len(extractDirs))) - - // We assume that we have the same length in each. While this - // hasn't been specified in the app manifests wiki page, it's - // the seemingly only sensible thing to me. - for index, value := range urls { - archValue.Items[index].URL = value - } - for index, value := range hashes { - archValue.Items[index].Hash = value - } - for index, value := range extractDirs { - archValue.Items[index].ExtractDir = value - } - } - case DetailFieldDepends: - // Array at top level to create multiple entries - if iter.WhatIsNext() == jsoniter.ArrayValue { - for iter.ReadArray() { - a.Depends = append(a.Depends, a.parseDependency(iter.ReadString())) - } - } else { - a.Depends = []Dependency{a.parseDependency(iter.ReadString())} - } - case DetailFieldEnvAddPath: - a.EnvAddPath = parseStringOrArray(iter) - case DetailFieldEnvSet: - for key := iter.ReadObject(); key != ""; key = iter.ReadObject() { - a.EnvSet = append(a.EnvSet, EnvVar{Key: key, Value: iter.ReadString()}) - } - case DetailFieldInstaller: - a.Installer = &Installer{} - for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { - switch field { - case "file": - a.Installer.File = iter.ReadString() - case "script": - a.Installer.Script = parseStringOrArray(iter) - case "args": - a.Installer.Args = parseStringOrArray(iter) - case "keep": - a.Installer.Keep = iter.ReadBool() - default: - iter.Skip() - } - } - case DetailFieldInnoSetup: - a.InnoSetup = iter.ReadBool() - case DetailFieldPreInstall: - a.PreInstall = parseStringOrArray(iter) - case DetailFieldPostInstall: - a.PostInstall = parseStringOrArray(iter) - case DetailFieldExtractDir: - a.ExtractDir = parseStringOrArray(iter) - case DetailFieldExtractTo: - a.ExtractTo = parseStringOrArray(iter) - case DetailFieldNotes: - if iter.WhatIsNext() == jsoniter.ArrayValue { - var lines []string - for iter.ReadArray() { - lines = append(lines, iter.ReadString()) - } - a.Notes = strings.Join(lines, "\n") - } else { - a.Notes = iter.ReadString() - } - default: - iter.Skip() +type Uninstaller Installer + +// invoke will run the installer script or file. This method is implemented on a +// non-pointer as we manipulate the script. +func (installer Installer) invoke(scoop *Scoop, dir string, arch ArchitectureKey) error { + // File and Script are mutually exclusive and Keep is only used if script is + // not set. However, we automatically set file to the last downloaded file + // if none is set, we then pass this to the script if any is present. + if len(installer.Script) > 0 { + variableSubstitutions := map[string]string{ + "$fname": installer.File, + "$dir": dir, + "$architecture": string(arch), + // FIXME We don't intend to support writing back the manifest into + // our context for now, as it seems only 1 or 2 apps actually do + // this. Instead, we should try to prepend a line that parses the + // manifest inline and creates the variable locally. + "$manifest": "TODO", } - } - - if iter.Error != nil { - return fmt.Errorf("error parsing json: %w", iter.Error) - } - - if a.Installer != nil { - if len(a.Architecture) > 0 { - // FIXME Get Architecvhture - } else if len(a.URL) > 0 { - a.Installer.File = a.URL[len(a.URL)-1] + for index, line := range installer.Script { + installer.Script[index] = substituteVariables(line, variableSubstitutions) } - } - - return nil -} - -func parseBin(iter *jsoniter.Iterator) []Bin { - // Array at top level to create multiple entries - if iter.WhatIsNext() == jsoniter.ArrayValue { - var bins []Bin - for iter.ReadArray() { - // There are nested arrays, for shim creation, with format: - // binary alias [args...] - if iter.WhatIsNext() == jsoniter.ArrayValue { - var bin Bin - if iter.ReadArray() { - bin.Name = iter.ReadString() - } - if iter.ReadArray() { - bin.Alias = iter.ReadString() - } - for iter.ReadArray() { - bin.Args = append(bin.Args, iter.ReadString()) - } - bins = append(bins, bin) - } else { - // String in the root level array to add to path - bins = append(bins, Bin{Name: iter.ReadString()}) - } + if err := scoop.runScript(installer.Script); err != nil { + return fmt.Errorf("error running installer: %w", err) } - return bins - } - - // String value at root level to add to path. - return []Bin{{Name: iter.ReadString()}} -} - -func parseStringOrArray(iter *jsoniter.Iterator) []string { - if iter.WhatIsNext() == jsoniter.ArrayValue { - var val []string - for iter.ReadArray() { - val = append(val, iter.ReadString()) + } else if installer.File != "" { + // FIXME RUN! Not extract? + + if !installer.Keep { + // FIXME Okay ... it seems scoop downloads the files not only into + // cache, but also into the installation directory. This seems a bit + // wasteful to me. Instead, we should copy the files into the dir + // only if we actually want to keep them. This way we can prevent + // useless copy and remove actions. + // + // This implementation shouldn't be part of the download, but + // instead be done during installation, manually checking both + // uninstaller.keep and installer.keep, copying if necessary and + // correctly invoking with the resulting paths. } - return val } - return []string{iter.ReadString()} + return nil } -func (a App) parseDependency(value string) Dependency { - parts := strings.SplitN(value, "/", 1) - switch len(parts) { - case 0: - // Should be a broken manifest - return Dependency{} - case 1: - // No bucket means same bucket. - return Dependency{Bucket: a.Bucket.Name(), Name: parts[0]} - default: - return Dependency{Bucket: parts[0], Name: parts[1]} - } +func (a *App) ManifestPath() string { + return a.manifestPath } type Dependencies struct { @@ -603,7 +448,7 @@ func (scoop *Scoop) DependencyTree(a *App) (*Dependencies, error) { dependencies := Dependencies{App: a} for _, dependency := range a.Depends { bucket := scoop.GetBucket(dependency.Bucket) - dependencyApp := bucket.GetApp(dependency.Name) + dependencyApp := bucket.FindApp(dependency.Name) subTree, err := scoop.DependencyTree(dependencyApp) if err != nil { return nil, fmt.Errorf("error getting sub dependency tree: %w", err) @@ -628,7 +473,7 @@ func (scoop *Scoop) ReverseDependencyTree(apps []*App, app *App) *Dependencies { } func (scoop *Scoop) GetOutdatedApps() ([]*OutdatedApp, error) { - installJSONPaths, err := filepath.Glob(filepath.Join(scoop.GetAppsDir(), "*/current/install.json")) + installJSONPaths, err := filepath.Glob(filepath.Join(scoop.AppDir(), "*/current/install.json")) if err != nil { return nil, fmt.Errorf("error globbing manifests: %w", err) } @@ -668,12 +513,12 @@ func (scoop *Scoop) GetOutdatedApps() ([]*OutdatedApp, error) { // We don't access the bucket directly, as this function supports // searching with and without bucket. - app, err := scoop.GetAvailableApp(appName) + app, err := scoop.FindAvailableApp(appName) if err != nil { return nil, fmt.Errorf("error getting app '%s' from bucket: %w", appName, err) } - installedApp, err := scoop.getInstalledApp(iter, appName) + installedApp, err := scoop.findInstalledApp(iter, appName) if err != nil { return nil, fmt.Errorf("error getting installed app '%s': %w", appName, err) } @@ -709,8 +554,8 @@ func (scoop *Scoop) GetOutdatedApps() ([]*OutdatedApp, error) { return outdated, nil } -func (scoop *Scoop) GetInstalledApps() ([]*InstalledApp, error) { - manifestPaths, err := filepath.Glob(filepath.Join(scoop.GetAppsDir(), "*/current/manifest.json")) +func (scoop *Scoop) InstalledApps() ([]*InstalledApp, error) { + manifestPaths, err := filepath.Glob(filepath.Join(scoop.AppDir(), "*/current/manifest.json")) if err != nil { return nil, fmt.Errorf("error globbing manifests: %w", err) } @@ -742,12 +587,16 @@ func (scoop *Scoop) GetInstalledApps() ([]*InstalledApp, error) { return apps, nil } -func (scoop *Scoop) GetBucketsDir() string { +func (scoop *Scoop) BucketDir() string { return filepath.Join(scoop.scoopRoot, "buckets") } -func (scoop *Scoop) GetScoopInstallationDir() string { - return filepath.Join(scoop.GetAppsDir(), "scoop", "current") +func (scoop *Scoop) PersistDir() string { + return filepath.Join(scoop.scoopRoot, "persist") +} + +func (scoop *Scoop) ScoopInstallationDir() string { + return filepath.Join(scoop.AppDir(), "scoop", "current") } func GetDefaultScoopDir() (string, error) { @@ -766,31 +615,880 @@ func GetDefaultScoopDir() (string, error) { return filepath.Join(home, "scoop"), nil } -func (scoop *Scoop) Install(apps []string, arch ArchitectureKey) error { - for _, inputName := range apps { - app, err := scoop.GetAvailableApp(inputName) +func (scoop *Scoop) runScript(lines []string) error { + if len(lines) == 0 { + return nil + } + + shell, err := windows.GetShellExecutable() + if err != nil { + // FIXME Does this need to be terminal? + return fmt.Errorf("error getting shell") + } + shell = strings.ToLower(shell) + switch shell { + case "pwsh.exe", "powershell.exe": + default: + return fmt.Errorf("shell '%s' not supported right now", shell) + } + + cmd := exec.Command(shell, "-NoLogo") + stdin, err := cmd.StdinPipe() + if err != nil { + return fmt.Errorf("error opening stdin pipe: %w", err) + } + + // To slash, so we don't have to escape + bucketsDir := `"` + filepath.ToSlash(scoop.BucketDir()) + `"` + + // FIXME So ... it seems we also need to be able to pass a reference to + // $manifest, which the script CAN manipulate, which we then have to + // reparse. + + go func() { + defer stdin.Close() + for _, line := range lines { + // FIXME Improve implementation + line = strings.ReplaceAll(line, "$bucketsdir", bucketsDir) + fmt.Fprintln(stdin, line) + } + }() + return cmd.Run() +} + +// InstallAll will install the given application into userspace. If an app is +// already installed, it will be updated if applicable. +// +// One key difference to scoop however, is how installing a concrete version +// works. Instead of creating a dirty manifest, we will search for the old +// manifest, install it and hold the app. This will have the same effect for the +// user, but without the fact that the user will never again get update +// notifications. +func (scoop *Scoop) InstallAll(appNames []string, arch ArchitectureKey) []error { + iter := manifestIter() + + var errs []error + for _, inputName := range appNames { + if err := scoop.install(iter, inputName, arch); err != nil { + errs = append(errs, fmt.Errorf("error installing '%s': %w", inputName, err)) + } + } + + return errs +} + +type CacheHit struct { + Downloadable *Downloadable +} + +type FinishedDownload struct { + Downloadable *Downloadable +} + +type StartedDownload struct { + Downloadable *Downloadable +} + +type ChecksumMismatchError struct { + Expected string + Actual string + File string +} + +func (_ *ChecksumMismatchError) Error() string { + return "checksum mismatch" +} + +// Download will download all files for the desired architecture, skipping +// already cached files. The cache lookups happen before downloading and are +// synchronous, directly returning an error instead of using the error channel. +// As soon as download starts (chan, chan, nil) is returned. Both channels are +// closed upon completion (success / failure). +// FIXME Make single result chan with a types: +// (download_start, download_finished, cache_hit) +func (resolvedApp *AppResolved) Download( + cacheDir string, + arch ArchitectureKey, + verifyHashes, overwriteCache bool, +) (chan any, error) { + var download []Downloadable + + // We use a channel for this, as its gonna get more once we finish download + // packages. For downloads, this is not the case, so it is a slice. + results := make(chan any, len(resolvedApp.Downloadables)) + + if overwriteCache { + for _, item := range resolvedApp.Downloadables { + download = append(download, item) + } + } else { + // Parallelise extraction / download. We want to make installation as fast + // as possible. + for _, item := range resolvedApp.Downloadables { + path := filepath.Join( + cacheDir, + CachePath(resolvedApp.Name, resolvedApp.Version, item.URL), + ) + _, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + download = append(download, item) + continue + } + + close(results) + return nil, fmt.Errorf("error checking cached file: %w", err) + } + + if err := validateHash(path, item.Hash); err != nil { + // FIXME We have an error here, but we'll swallow and + // redownload. Should we possibly make a new type? + download = append(download, item) + } else { + results <- &CacheHit{&item} + } + } + } + + cachePath := func(downloadable Downloadable) string { + return filepath.Join(cacheDir, CachePath(resolvedApp.Name, resolvedApp.Version, downloadable.URL)) + } + var requests []*grab.Request + for index, item := range download { + request, err := grab.NewRequest(cachePath(item), item.URL) + if err != nil { + close(results) + return nil, fmt.Errorf("error preparing download: %w", err) + } + + // We attach the item as a context value, since we'll have to make a + // separate mapping otherwise. This is a bit un-nice, but it is stable. + request = request.WithContext(context.WithValue(context.Background(), "item", item)) + request.Label = strconv.FormatInt(int64(index), 10) + requests = append(requests, request) + } + + if len(requests) == 0 { + close(results) + return results, nil + } + + // FIXME Determine batchsize? + client := grab.NewClient() + responses := client.DoBatch(2, requests...) + + // We work on multiple requests at once, but only have one extraction + // routine, as extraction should already make use of many CPU cores. + go func() { + for response := range responses { + if err := response.Err(); err != nil { + results <- fmt.Errorf("error during download: %w", err) + continue + } + + downloadable := response.Request.Context().Value("item").(Downloadable) + results <- &StartedDownload{&downloadable} + + if hashVal := downloadable.Hash; hashVal != "" && verifyHashes { + if err := validateHash(cachePath(downloadable), hashVal); err != nil { + results <- err + continue + } + } + + results <- &FinishedDownload{&downloadable} + } + + close(results) + }() + + return results, nil +} + +func validateHash(path, hashVal string) error { + if hashVal == "" { + return nil + } + + var algo hash.Hash + if strings.HasPrefix(hashVal, "sha1") { + algo = sha1.New() + } else if strings.HasPrefix(hashVal, "sha512") { + algo = sha512.New() + } else if strings.HasPrefix(hashVal, "md5") { + algo = md5.New() + } else { + // sha256 is the default in scoop and has no prefix. This + // will most likely not break, due to the fact scoop goes + // hard on backwards compatibility / not having to migrate + // any of the existing manifests. + algo = sha256.New() + } + + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("error determining checksum: %w", err) + } + + if _, err := io.Copy(algo, file); err != nil { + return fmt.Errorf("error determining checksum: %w", err) + } + + hashVal = strings.ToLower(hashVal) + formattedHash := strings.ToLower(hex.EncodeToString(algo.Sum(nil))) + + if formattedHash != hashVal { + return &ChecksumMismatchError{ + Actual: formattedHash, + Expected: hashVal, + File: path, + } + } + + return nil +} + +func (scoop *Scoop) Install(appName string, arch ArchitectureKey) error { + return scoop.install(manifestIter(), appName, arch) +} + +func (scoop *Scoop) Uninstall(app *InstalledApp, arch ArchitectureKey) error { + resolvedApp := app.ForArch(arch) + + if err := scoop.runScript(resolvedApp.PreUninstall); err != nil { + return fmt.Errorf("error executing pre_uninstall script: %w", err) + } + + if uninstaller := resolvedApp.Uninstaller; uninstaller != nil { + dir := filepath.Join(scoop.AppDir(), app.Name, app.Version) + if err := Installer(*uninstaller).invoke(scoop, dir, arch); err != nil { + return fmt.Errorf("error invoking uninstaller: %w", err) + } + } + + var updatedEnvVars [][2]string + for _, envVar := range resolvedApp.EnvSet { + updatedEnvVars = append(updatedEnvVars, [2]string{envVar.Key, ""}) + } + + if len(resolvedApp.EnvAddPath) > 0 { + pathKey, pathVar, err := windows.GetPersistentEnvValue("User") + if err != nil { + return fmt.Errorf("error retrieving path variable: %w", err) + } + + newPath := windows.ParsePath(pathVar).Remove(resolvedApp.EnvAddPath...) + updatedEnvVars = append(updatedEnvVars, [2]string{pathKey, newPath.String()}) + } + + if err := windows.SetPersistentEnvValues(updatedEnvVars...); err != nil { + return fmt.Errorf("error restoring environment variables: %w", err) + } + + appDir := filepath.Join(scoop.AppDir(), app.Name) + currentDir := filepath.Join(appDir, "current") + + // Make sure installation dir isn't readonly anymore. Scoop does this for + // some reason. + // FIXME The files inside are writable anyway. Should figure out why. + if err := os.Chmod(currentDir, 0o600); err != nil { + return fmt.Errorf("error making current dir deletable: %w", err) + } + + if err := os.RemoveAll(currentDir); err != nil { + return fmt.Errorf("error deleting installation files: %w", err) + } + + if err := scoop.RemoveShims(resolvedApp.Bin...); err != nil { + return fmt.Errorf("error removing shim: %w", err) + } + + // FIXME Do rest of the uninstall here + // 2. Remove shortcuts + + if err := scoop.runScript(resolvedApp.PostUninstall); err != nil { + return fmt.Errorf("error executing post_uninstall script: %w", err) + } + return nil +} + +var ( + ErrAlreadyInstalled = errors.New("app already installed (same version)") + ErrAppNotFound = errors.New("app not found") + ErrAppNotAvailableInVersion = errors.New("app not available in desird version") +) + +func (scoop *Scoop) install(iter *jsoniter.Iterator, appName string, arch ArchitectureKey) error { + fmt.Printf("Installing '%s' ...\n", appName) + + // FIXME Should we check installed first? If it's already installed, we can + // just ignore if it doesn't exist in the bucket anymore. + + app, err := scoop.FindAvailableApp(appName) + if err != nil { + return err + } + + // FIXME Instead try to find it installed / history / workspace. + // Scoop doesnt do this, but we could do it with a "dangerous" flag. + if app == nil { + return ErrAppNotFound + } + + installedApp, err := scoop.FindInstalledApp(appName) + if err != nil { + return fmt.Errorf("error checking for installed version: %w", err) + } + + // FIXME Make force flag. + // FIXME Should this be part of the low level install? + if installedApp != nil && installedApp.Hold { + return fmt.Errorf("app is held: %w", err) + } + + // We might be trying to install a specific version of the given + // application. If this happens, we first look for the manifest in our + // git history. If that fails, we try to auto-generate it. The later is + // what scoop always does. + var manifestFile io.ReadSeeker + _, _, version := ParseAppIdentifier(appName) + if version != "" { + fmt.Printf("Search for manifest version '%s' ...\n", version) + manifestFile, err = app.ManifestForVersion(version) + if err != nil { + return fmt.Errorf("error finding app in version: %w", err) + } + if manifestFile == nil { + return ErrAppNotAvailableInVersion + } + + app = &App{ + Name: app.Name, + Bucket: app.Bucket, + } + if err := app.loadDetailFromManifestWithIter(iter, manifestFile, DetailFieldsAll...); err != nil { + return fmt.Errorf("error loading manifest: %w", err) + } + } else { + manifestFile, err = os.Open(app.ManifestPath()) + if err != nil { + return fmt.Errorf("error opening manifest for copying: %w", err) + } + if err := app.loadDetailFromManifestWithIter(iter, manifestFile, DetailFieldsAll...); err != nil { + return fmt.Errorf("error loading manifest: %w", err) + } + } + + if closer, ok := manifestFile.(io.Closer); ok { + defer closer.Close() + } + + // We reuse the handle. + if _, err := manifestFile.Seek(0, 0); err != nil { + return fmt.Errorf("error resetting manifest file handle: %w", err) + } + + if installedApp != nil { + if err := installedApp.LoadDetailsWithIter(iter, + DetailFieldVersion, + DetailFieldPreUninstall, + DetailFieldPostUninstall, + ); err != nil { + return fmt.Errorf("error determining installed version: %w", err) + } + + // The user should manually run uninstall and install to reinstall. + if installedApp.Version == app.Version { + return ErrAlreadyInstalled + } + + // FIXME Get arch of installed app? Technically we could be on a 64-bit + // system and have the 32-bit version. The same goes for the version + // check. Just because the versions are the same, doesn't mean the arch + // necessarily needs to be the same. + scoop.Uninstall(installedApp, arch) + } + + appDir := filepath.Join(scoop.AppDir(), app.Name) + currentDir := filepath.Join(appDir, "current") + if installedApp != nil { + if err := os.RemoveAll(currentDir); err != nil { + return fmt.Errorf("error removing old currentdir: %w", err) + } + + // FIXME Do rest of the uninstall here + // REmove shims bla bla bla + + scoop.runScript(installedApp.PostUninstall) + } + // FIXME Check if an old version is already installed and we can + // just-relink it. + + resolvedApp := app.ForArch(arch) + + scoop.runScript(resolvedApp.PreInstall) + + versionDir := filepath.Join(appDir, app.Version) + if err := os.MkdirAll(versionDir, os.ModeDir); err != nil { + return fmt.Errorf("error creating isntallation targer dir: %w", err) + } + + cacheDir := scoop.CacheDir() + donwloadResults, err := resolvedApp.Download(cacheDir, arch, true, false) + if err != nil { + return fmt.Errorf("error initialising download: %w", err) + } + + for result := range donwloadResults { + switch result := result.(type) { + case error: + return err + case *CacheHit: + fmt.Printf("Cache hit for '%s'", filepath.Base(result.Downloadable.URL)) + if err := scoop.extract(app, resolvedApp, cacheDir, versionDir, *result.Downloadable, arch); err != nil { + return fmt.Errorf("error extracting file '%s': %w", filepath.Base(result.Downloadable.URL), err) + } + case *FinishedDownload: + fmt.Printf("Downloaded '%s'\n", filepath.Base(result.Downloadable.URL)) + if err := scoop.extract(app, resolvedApp, cacheDir, versionDir, *result.Downloadable, arch); err != nil { + return fmt.Errorf("error extracting file '%s': %w", filepath.Base(result.Downloadable.URL), err) + } + } + } + + if installer := resolvedApp.Installer; installer != nil { + dir := filepath.Join(scoop.AppDir(), app.Name, app.Version) + if err := installer.invoke(scoop, dir, arch); err != nil { + return fmt.Errorf("error invoking installer: %w", err) + } + } + + // FIXME Make copy util? + // FIXME Read perms? + newManifestFile, err := os.OpenFile( + filepath.Join(versionDir, "manifest.json"), os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return fmt.Errorf("error creating new manifest: %w", err) + } + if _, err := io.Copy(newManifestFile, manifestFile); err != nil { + return fmt.Errorf("error copying manfiest: %w", err) + } + + fmt.Println("Linking to newly installed version.") + if err := windows.CreateJunctions([2]string{versionDir, currentDir}); err != nil { + return fmt.Errorf("error linking from new current dir: %w", err) + } + + // Shims are copies of a certain binary that uses a ".shim" file next to + // it to realise some type of symlink. + for _, bin := range resolvedApp.Bin { + fmt.Printf("Creating shim for '%s'\n", bin.Name) + if err := scoop.CreateShim(filepath.Join(currentDir, bin.Name), bin); err != nil { + return fmt.Errorf("error creating shim: %w", err) + } + } + + var envVars [][2]string + if len(resolvedApp.EnvAddPath) > 0 { + pathKey, oldPath, err := windows.GetPersistentEnvValue("Path") + if err != nil { + return fmt.Errorf("error attempt to add variables to path: %w", err) + } + parsedPath := windows.ParsePath(oldPath).Prepend(resolvedApp.EnvAddPath...) + envVars = append(envVars, [2]string{pathKey, parsedPath.String()}) + } + + for _, pathEntry := range resolvedApp.EnvSet { + value := substituteVariables(pathEntry.Value, map[string]string{ + "dir": currentDir, + "persist_dir": filepath.Join(scoop.PersistDir(), app.Name), + }) + envVars = append(envVars, [2]string{pathEntry.Key, value}) + } + + if err := windows.SetPersistentEnvValues(envVars...); err != nil { + return fmt.Errorf("error setting env values: %w", err) + } + + // FIXME Adjust arch value if we install anything else than is desired. + if err := os.WriteFile(filepath.Join(versionDir, "install.json"), []byte(fmt.Sprintf( + `{ + "bucket": "%s", + "architecture": "%s", + "hold": %v +}`, app.Bucket.Name(), arch, version != "")), 0o600); err != nil { + return fmt.Errorf("error writing installation information: %w", err) + } + + if err := scoop.runScript(resolvedApp.PostInstall); err != nil { + return fmt.Errorf("error running post install script: %w", err) + } + + return nil +} + +func substituteVariables(value string, variables map[string]string) string { + // It seems like scoop does it this way as well. Instead of somehow checking + // whether there's a variable such as $directory, we simply replace $dir, + // not paying attention to potential damage done. + // FIXME However, this is error prone and should change in the future. + for key, val := range variables { + value = strings.ReplaceAll(value, key, val) + } + + // FIXME Additionally, we need to substitute any $env:VARIABLE. The bullet + // proof way to do this, would be to simply invoke powershell, albeit a bit + // slow. This should happen before the in-code substitution. + + // This needs more investigation though, should probably read the docs on + // powershell env var substitution and see how easy it would be. + + return value +} + +// extract will extract the given item. It doesn't matter which type it has, as +// this function will call the correct function. For example, a `.msi` will +// cause invocation of `lessmesi`. Note however, that this function isn't +// thread-safe, as it might install additional tooling required for extraction. +func (scoop *Scoop) extract( + app *App, + resolvedApp *AppResolved, + cacheDir string, + appDir string, + item Downloadable, + arch ArchitectureKey, +) error { + baseName := filepath.Base(item.URL) + fmt.Printf("Extracting '%s' ...\n", baseName) + + fileToExtract := filepath.Join(cacheDir, CachePath(app.Name, app.Version, item.URL)) + targetPath := filepath.Join(appDir, item.ExtractTo) + + // Depending on metadata / filename, we decide how to extract the + // files that are to be installed. Note we don't care whether the + // dependency is installed via scoop, we just want it to be there. + + // We won't even bother testing the extension here, as it could + // technically be an installed not ending on `.exe`. While this is + // not true for the other formats, it is TECHNCIALLY possible here. + if resolvedApp.InnoSetup { + // If this flag is set, the installer.script might be set, but the + // installer.file never is, meaning extraction is always the correct + // thing to do. + + innounpPath, err := exec.LookPath("innounp") + if err == nil && innounpPath != "" { + goto INVOKE_INNOUNP + } + if err != nil { - return fmt.Errorf("error installing app '%s': %w", inputName, err) + return fmt.Errorf("error looking up innounp: %w", err) + } + + if err := scoop.Install("innounp", arch); err != nil { + return fmt.Errorf("error installing dependency innounp: %w", err) } - // FIXME Instead try to find it installed / history / workspace. - // Scoop doesnt do this, but we could do it with a "dangerous" flag. - if app == nil { - return fmt.Errorf("app '%s' not found", inputName) + INVOKE_INNOUNP: + args := []string{ + // Extract + "-x", + // Confirm questions + "-y", + // Destination + "-d" + targetPath, + fileToExtract, } - // We might be trying to install a specific version of the given - // application. If this happens, we first look for the manifest in our - // git history. If that fails, we try to auto-generate it. The later is - // what scoop always does. - _, _, version := ParseAppIdentifier(inputName) - if version != "" { + if strings.HasPrefix(item.ExtractDir, "{") { + args = append(args, "-c"+item.ExtractDir) + } else if item.ExtractDir != "" { + args = append(args, "-c{app}\\"+item.ExtractDir) + } else { + args = append(args, "-c{app}") } + + cmd := exec.Command("innounp", args...) + if err := cmd.Run(); err != nil { + return fmt.Errorf("error invoking innounp: %w", err) + } + + return nil } + ext := strings.ToLower(filepath.Ext(item.URL)) + // 7zip supports A TON of file formats, so we try to use it where we + // can. It's fast and known to work well. + if supportedBy7Zip(ext) { + sevenZipPath, err := exec.LookPath("7z") + // Path can be non-empty and still return an error. Read + // LookPath documentation. + if err == nil && sevenZipPath != "" { + goto INVOKE_7Z + } + + // Fallback for cases where we don't have 7zip installed, but still + // want to unpack a zip. Without this, we'd print an error instead. + if ext == ".zip" { + goto STD_ZIP + } + + if err != nil { + return fmt.Errorf("error doing path lookup: %w", err) + } + + if err := scoop.Install("7zip", arch); err != nil { + return fmt.Errorf("error installing dependency 7zip: %w", err) + } + + INVOKE_7Z: + args := []string{ + // Extract from file + "x", + fileToExtract, + // Target path + "-o" + targetPath, + // Overwrite all files + "-aoa", + // Confirm + "-y", + } + // FIXME: $IsTar = ((strip_ext $Path) -match '\.tar$') -or ($Path -match '\.t[abgpx]z2?$') + if ext != ".tar" && item.ExtractDir != "" { + args = append(args, "-ir!"+filepath.Join(item.ExtractDir, "*")) + } + cmd := exec.Command( + "7z", + args..., + ) + if err := cmd.Run(); err != nil { + return fmt.Errorf("error invoking 7z: %w", err) + } + } + + // TODO: dark, msi, inno, installer, zst + + switch ext { + case ".msi": + lessmsiPath, err := scoop.ensureExecutable("lessmsi", "lessmsi", arch) + if err != nil { + return fmt.Errorf("error installing lessmsi: %w", err) + } + fmt.Println(lessmsiPath) + + return nil + } + +STD_ZIP: + if ext == ".zip" { + zipReader, err := zip.OpenReader(fileToExtract) + if err != nil { + return fmt.Errorf("error opening zip reader: %w", err) + } + + for _, f := range zipReader.File { + // We create these anyway later. + if f.FileInfo().IsDir() { + continue + } + + // FIXME Prevent accidental mismatches + extractDir := filepath.ToSlash(item.ExtractDir) + fName := filepath.ToSlash(f.Name) + if extractDir != "" && !strings.HasPrefix(fName, extractDir) { + continue + } + + // Strip extract dir, as these aren't meant to be preserved, + // unless specified via extractTo + fName = strings.TrimLeft(strings.TrimPrefix(fName, extractDir), "/") + + filePath := filepath.Join(appDir, item.ExtractTo, fName) + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return fmt.Errorf("error creating dir: %w", err) + } + + dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return fmt.Errorf("error creating target file for zip entry: %w", err) + } + + fileInArchive, err := f.Open() + if err != nil { + return fmt.Errorf("error opening zip file entry: %w", err) + } + + if _, err := io.Copy(dstFile, fileInArchive); err != nil { + return fmt.Errorf("error copying zip file entry: %w", err) + } + + dstFile.Close() + fileInArchive.Close() + } + } else { + targetFile, err := os.OpenFile( + filepath.Join(appDir, item.ExtractTo, baseName), + os.O_CREATE|os.O_WRONLY|os.O_TRUNC, + 0o600, + ) + if err != nil { + return fmt.Errorf("error opening handle target file: %w", err) + } + defer targetFile.Close() + + sourceFile, err := os.Open(fileToExtract) + if err != nil { + return fmt.Errorf("error opening cache file: %w", err) + } + defer sourceFile.Close() + + if _, err := io.Copy(targetFile, sourceFile); err != nil { + return fmt.Errorf("error copying file: %w", err) + } + } + + // Mark RO afterwards? return nil } +// ensureExecutable will look for a given executable on the path. If not +// found, it will attempt installing the dependency using the given app +// information. +func (scoop *Scoop) ensureExecutable(executable, appName string, arch ArchitectureKey) (string, error) { + executablePath, err := exec.LookPath(executable) + if err != nil { + if !errors.Is(err, exec.ErrDot) && !errors.Is(err, exec.ErrNotFound) { + return "", fmt.Errorf("error locating '%s': %w", executable, err) + } + + // We'll treat a relative path binary as non-existent for now and + // install the dependency. + executablePath = "" + } + + if executablePath == "" { + if err := scoop.Install(appName, arch); err != nil { + return "", fmt.Errorf("error installing required dependency '%s': %w", appName, err) + } + + executablePath, err = exec.LookPath(executable) + if err != nil { + return "", fmt.Errorf("error locating '%s': %w", executable, err) + } + } + + // Might be empty if the second lookup failed. HOWEVER, it shouldn't as we + // simply add to the shims folder, which should already be on the path. + return executablePath, err +} + +var sevenZipFileFormatRegex = regexp.MustCompile(`\.((gz)|(tar)|(t[abgpx]z2?)|(lzma)|(bz2?)|(7z)|(001)|(rar)|(iso)|(xz)|(lzh)|(nupkg))(\.[^\d.]+)?$`) + +func supportedBy7Zip(extension string) bool { + return sevenZipFileFormatRegex.MatchString(extension) +} + +// AppResolved is a version of app forming the data into a way that it's ready +// for installation, deinstallation or update. +type AppResolved struct { + *App + + // TODO checkver, hash, extract_dir; + // TODO Merge url, hash and extract_dir? Like we did with bin, to give + // semantic meaning to it. + + Bin []Bin `json:"bin"` + Shortcuts []Bin `json:"shortcuts"` + + Downloadables []Downloadable `json:"downloadables"` + + // Installer deprecates msi; InnoSetup bool should be same for each + // architecture. The docs don't mention it. + Installer *Installer `json:"installer"` + PreInstall []string `json:"pre_install"` + PostInstall []string `json:"post_install"` +} + +// ForArch will create a merged version that includes all the relevant fields at +// root level. Access to architecture shouldn't be required anymore, it should +// be ready to use for installtion, update or uninstall. +func (a *App) ForArch(arch ArchitectureKey) *AppResolved { + resolved := &AppResolved{ + App: a, + } + + resolved.Bin = a.Bin + resolved.Shortcuts = a.Shortcuts + resolved.Downloadables = a.Downloadables + resolved.PreInstall = a.PreInstall + resolved.PostInstall = a.PostInstall + resolved.Installer = a.Installer + + if a.Architecture == nil { + return resolved + } + + archValue := a.Architecture[arch] + if archValue == nil && arch == ArchitectureKey64Bit { + // Fallbackt to 32bit. If we are on arm, there's no use to fallback + // though, since only arm64 is supported by scoop either way. + archValue = a.Architecture[ArchitectureKey32Bit] + } + if archValue != nil { + // nil-checking might be fragile, so this is safer. + if len(archValue.Bin) > len(resolved.Bin) { + resolved.Bin = archValue.Bin + } + if len(archValue.Shortcuts) > len(resolved.Shortcuts) { + resolved.Shortcuts = archValue.Shortcuts + } + if len(archValue.Downloadables) > len(resolved.Downloadables) { + // If we need to manipulate these, we do a copy, to prevent changing the + // opriginal app. + if len(a.ExtractTo) > 0 { + resolved.Downloadables = append([]Downloadable{}, archValue.Downloadables...) + } else { + resolved.Downloadables = archValue.Downloadables + } + } + if len(archValue.PreInstall) > len(resolved.PreInstall) { + resolved.PreInstall = archValue.PreInstall + } + if len(archValue.PostInstall) > len(resolved.PostInstall) { + resolved.PostInstall = archValue.PostInstall + } + } + + // architecture does not support extract_to, so we merge it with the root + // level value for ease of use. + switch len(a.ExtractTo) { + case 0: + // Do nothing, path inferred to app root dir (current). + case 1: + // Same path everywhere + for i := 0; i < len(resolved.Downloadables); i++ { + resolved.Downloadables[i].ExtractTo = a.ExtractTo[0] + } + default: + // Path per URL, but to be defensive, we'll infer if missing ones, by + // leaving it empty (current root dir). + for i := 0; i < len(resolved.Downloadables) && i < len(a.ExtractTo); i++ { + resolved.Downloadables[i].ExtractTo = a.ExtractTo[i] + } + } + + // If we have neither an installer file, nor a script, we reference the last + // items downloaded, as per scoop documentation. + // FIXME Find out if this is really necessary, this is jank. + if a.Installer != nil && a.Installer.File == "" && + len(a.Installer.Script) == 0 && len(a.Downloadables) > 0 { + lastURL := resolved.Downloadables[len(a.Downloadables)-1].URL + a.Installer.File = filepath.Base(lastURL) + } + + return resolved +} + var ErrBucketNoGitDir = errors.New(".git dir at path not found") func (a *App) AvailableVersions() ([]string, error) { @@ -847,7 +1545,7 @@ func readVersion(iter *jsoniter.Iterator, data []byte) string { // desired version is found. Note that we compare the versions and stop // searching if a lower version is encountered. This function is expected to // be very slow, be warned! -func (a *App) ManifestForVersion(targetVersion string) (io.ReadCloser, error) { +func (a *App) ManifestForVersion(targetVersion string) (io.ReadSeeker, error) { repoPath, relManifestPath := git.GitPaths(a.ManifestPath()) if repoPath == "" || relManifestPath == "" { return nil, ErrBucketNoGitDir @@ -874,7 +1572,7 @@ func (a *App) ManifestForVersion(targetVersion string) (io.ReadCloser, error) { version := readVersion(iter, result.Data) comparison := versioncmp.Compare(version, targetVersion, cmpRules) if comparison == "" { - return io.NopCloser(bytes.NewReader(result.Data)), nil + return bytes.NewReader(result.Data), nil } // The version we are looking for is greater than the one from history, @@ -896,7 +1594,7 @@ func (scoop *Scoop) LookupCache(app, version string) ([]string, error) { expectedPrefix += "#" + cachePathRegex.ReplaceAllString(version, "_") } - return filepath.Glob(filepath.Join(scoop.GetCacheDir(), expectedPrefix+"*")) + return filepath.Glob(filepath.Join(scoop.CacheDir(), expectedPrefix+"*")) } var cachePathRegex = regexp.MustCompile(`[^\w\.\-]+`) @@ -911,7 +1609,7 @@ func CachePath(app, version, url string) string { return strings.Join(parts, "#") } -func (scoop *Scoop) GetCacheDir() string { +func (scoop *Scoop) CacheDir() string { return filepath.Join(scoop.scoopRoot, "cache") } @@ -919,7 +1617,7 @@ type Scoop struct { scoopRoot string } -func (scoop *Scoop) GetAppsDir() string { +func (scoop *Scoop) AppDir() string { return filepath.Join(scoop.scoopRoot, "apps") } diff --git a/pkg/scoop/scoop_test.go b/pkg/scoop/scoop_test.go index 7cb11fd..db454d6 100644 --- a/pkg/scoop/scoop_test.go +++ b/pkg/scoop/scoop_test.go @@ -11,7 +11,7 @@ func app(t *testing.T, name string) *scoop.App { defaultScoop, err := scoop.NewScoop() require.NoError(t, err) - app, err := defaultScoop.GetAvailableApp(name) + app, err := defaultScoop.FindAvailableApp(name) require.NoError(t, err) return app @@ -21,7 +21,7 @@ func Test_ManifestForVersion(t *testing.T) { defaultScoop, err := scoop.NewScoop() require.NoError(t, err) - app, err := defaultScoop.GetAvailableApp("main/go") + app, err := defaultScoop.FindAvailableApp("main/go") require.NoError(t, err) t.Run("found", func(t *testing.T) { @@ -78,6 +78,26 @@ func Test_ParseBin(t *testing.T) { Args: []string{`-c "$dir\config\config.yml"`}, }) }) + t.Run("nested array that contains arrays and strings", func(t *testing.T) { + app := app(t, "main/python") + + err := app.LoadDetails(scoop.DetailFieldBin) + require.NoError(t, err) + + // Order doesnt matter + require.Len(t, app.Bin, 3) + require.Contains(t, app.Bin, scoop.Bin{ + Name: "python.exe", + Alias: "python3", + }) + require.Contains(t, app.Bin, scoop.Bin{ + Name: "Lib\\idlelib\\idle.bat", + }) + require.Contains(t, app.Bin, scoop.Bin{ + Name: "Lib\\idlelib\\idle.bat", + Alias: "idle3", + }) + }) } func Test_ParseArchitecture_Items(t *testing.T) { @@ -95,20 +115,20 @@ func Test_ParseArchitecture_Items(t *testing.T) { arm64 := arch[scoop.ArchitectureKeyARM64] require.NotNil(t, arm64) - require.Len(t, x386.Items, 1) - require.Len(t, x686.Items, 1) - require.Len(t, arm64.Items, 1) + require.Len(t, x386.Downloadables, 1) + require.Len(t, x686.Downloadables, 1) + require.Len(t, arm64.Downloadables, 1) - require.Contains(t, x386.Items[0].URL, "386") - require.NotEmpty(t, x386.Items[0].Hash) - require.Empty(t, x386.Items[0].ExtractDir) + require.Contains(t, x386.Downloadables[0].URL, "386") + require.NotEmpty(t, x386.Downloadables[0].Hash) + require.Empty(t, x386.Downloadables[0].ExtractDir) - require.Contains(t, x686.Items[0].URL, "amd64") - require.NotEmpty(t, x686.Items[0].Hash) - require.Empty(t, x686.Items[0].ExtractDir) + require.Contains(t, x686.Downloadables[0].URL, "amd64") + require.NotEmpty(t, x686.Downloadables[0].Hash) + require.Empty(t, x686.Downloadables[0].ExtractDir) - require.Contains(t, arm64.Items[0].URL, "arm64") - require.NotEmpty(t, arm64.Items[0].URL) - require.NotEmpty(t, arm64.Items[0].Hash) - require.Empty(t, arm64.Items[0].ExtractDir) + require.Contains(t, arm64.Downloadables[0].URL, "arm64") + require.NotEmpty(t, arm64.Downloadables[0].URL) + require.NotEmpty(t, arm64.Downloadables[0].Hash) + require.Empty(t, arm64.Downloadables[0].ExtractDir) } diff --git a/pkg/scoop/shim.exe b/pkg/scoop/shim.exe new file mode 100644 index 0000000000000000000000000000000000000000..3ab79dd5e6fdf80f120db47cf062c8246733feb4 GIT binary patch literal 136192 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

AiF;@eM8%~< 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 Z=YcSz2g9763M0P=M!PEy<^F#f{sSGQsHp$| literal 0 HcmV?d00001 diff --git a/pkg/scoop/shim.go b/pkg/scoop/shim.go new file mode 100644 index 0000000..cdfb8a4 --- /dev/null +++ b/pkg/scoop/shim.go @@ -0,0 +1,152 @@ +package scoop + +import ( + "bytes" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + _ "embed" +) + +//go:embed shim_cmd_to_cmd.template +var cmdToCmdTemplate string + +//go:embed shim_cmd_to_bash.template +var cmdToBashTemplate string + +//go:embed shim.exe +var shimExecutable []byte + +// FIXME Should this be a public helper function on Bin? If so, we should +// probably split bin and shortcut. At this point, they don't seem to be to +// compatible anymore. +func shimName(bin Bin) string { + shimName := bin.Alias + if shimName == "" { + shimName = filepath.Base(bin.Name) + shimName = strings.TrimSuffix(shimName, filepath.Ext(shimName)) + } + return shimName +} + +func (scoop *Scoop) RemoveShims(bins ...Bin) error { + return filepath.WalkDir(scoop.ShimDir(), func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + for _, bin := range bins { + // This will catch all file types, including the shims. + shimName := shimName(bin) + binWithoutExt := strings.TrimSuffix(shimName, filepath.Ext(shimName)) + nameWithoutExt := strings.TrimSuffix(d.Name(), filepath.Ext(d.Name())) + if !strings.EqualFold(nameWithoutExt, binWithoutExt) { + continue + } + + if err := os.Remove(path); err != nil { + return fmt.Errorf("error deleting shim '%s': %w", path, err) + } + } + + return nil + }) +} + +func (scoop *Scoop) CreateShim(path string, bin Bin) error { + /* + We got the following possible constructs: + + 0. + bin: [ + path/to/file + ] + 1. + bin: [ + [ + path/to/file + shim.type + ] + ] + 2. + bin: [ + [ + path/to/file.exe + shim + ] + ] + + In case 0. we simply create whatever extension the file had as a + shim, falling back to .cmd. + + In case 1. we create a shim given the desired extension, no matter + what extension the actual file has. The same goes for case 2. where we + haven't passed an explicit shim extension even though we know it's an + executable. + */ + + shimName := shimName(bin) + + switch filepath.Ext(bin.Name) { + case ".exe", ".com": + // FIXME Do we need to escape anything here? + argsJoined := strings.Join(bin.Args, " ") + + // The .shim and .exe files needs to be writable, as scoop fails to + // uninstall otherwise. + var shimConfig bytes.Buffer + shimConfig.WriteString(`path = "`) + shimConfig.WriteString(path) + shimConfig.WriteString("\"\n") + if argsJoined != "" { + shimConfig.WriteString(`args = `) + shimConfig.WriteString(argsJoined) + shimConfig.WriteRune('\n') + } + if err := os.WriteFile(filepath.Join(scoop.ShimDir(), shimName+".shim"), + shimConfig.Bytes(), 0o600); err != nil { + return fmt.Errorf("error writing shim file: %w", err) + } + + targetPath := filepath.Join(scoop.ShimDir(), shimName+".exe") + err := os.WriteFile(targetPath, shimExecutable, 0o700) + if err != nil { + return fmt.Errorf("error creating shim executable: %w", err) + } + case ".cmd", ".bat": + // FIXME Do we need to escape anything here? + argsJoined := strings.Join(bin.Args, " ") + + if err := os.WriteFile( + filepath.Join(scoop.ShimDir(), shimName+".cmd"), + []byte(fmt.Sprintf(cmdToCmdTemplate, path, path, argsJoined)), + 0o700, + ); err != nil { + return fmt.Errorf("error creating cmdShim: %w", err) + } + if err := os.WriteFile( + filepath.Join(scoop.ShimDir(), shimName), + []byte(fmt.Sprintf(cmdToBashTemplate, path, path, argsJoined)), + 0o700, + ); err != nil { + return fmt.Errorf("error creating cmdShim: %w", err) + } + case ".ps1": + case ".jar": + case ".py": + default: + } + + return nil +} + +func (scoop *Scoop) ShimDir() string { + return filepath.Join(scoop.scoopRoot, "shims") +} diff --git a/pkg/scoop/shim_bash.template b/pkg/scoop/shim_bash.template new file mode 100644 index 0000000..e69de29 diff --git a/pkg/scoop/shim_cmd_to_bash.template b/pkg/scoop/shim_cmd_to_bash.template new file mode 100644 index 0000000..66aeacc --- /dev/null +++ b/pkg/scoop/shim_cmd_to_bash.template @@ -0,0 +1,4 @@ +#!/bin/sh +# %s +echo "bashin" +MSYS2_ARG_CONV_EXCL=/C cmd.exe /C "%s" %s "$@" diff --git a/pkg/scoop/shim_cmd_to_cmd.template b/pkg/scoop/shim_cmd_to_cmd.template new file mode 100644 index 0000000..e80069c --- /dev/null +++ b/pkg/scoop/shim_cmd_to_cmd.template @@ -0,0 +1,3 @@ +@rem %s +@"%s" %* + diff --git a/pkg/scoop/shim_ps1_to_ps1.template b/pkg/scoop/shim_ps1_to_ps1.template new file mode 100644 index 0000000..d01c611 --- /dev/null +++ b/pkg/scoop/shim_ps1_to_ps1.template @@ -0,0 +1,5 @@ +# %s +$path = "%s" +if ($MyInvocation.ExpectingInput) { $input | & $path $arg @args } else { & $path $arg @args } +exit $LASTEXITCODE + diff --git a/pkg/scoop/shim_sh.template b/pkg/scoop/shim_sh.template new file mode 100644 index 0000000..e69de29