From 87fc1f34435e995ab8588bb683d880e0e6e3e519 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Tue, 19 Dec 2023 15:24:57 -0800 Subject: [PATCH] Test whether a backend can be run at all (#190) * Dart "pub" command is now a subcommand of "dart" * Adding IsAvailable checks * Providing basic implementations of IsAvailable for known binaries --- internal/api/types.go | 4 ++++ internal/backends/backends.go | 11 ++++++++--- internal/backends/dart/dart.go | 11 +++++++++-- internal/backends/dotnet/dotnet.go | 7 +++++++ internal/backends/elisp/elisp.go | 10 ++++++++++ internal/backends/java/java.go | 7 +++++++ internal/backends/nodejs/nodejs.go | 24 ++++++++++++++++++++++++ internal/backends/php/php.go | 7 +++++++ internal/backends/python/python.go | 13 +++++++++++++ internal/backends/rlang/rlang.go | 7 +++++++ internal/backends/ruby/ruby.go | 7 +++++++ internal/backends/rust/rust.go | 7 +++++++ internal/cli/cmds.go | 8 ++++++-- test-suite/Main_test.go | 6 +++--- 14 files changed, 119 insertions(+), 10 deletions(-) diff --git a/internal/api/types.go b/internal/api/types.go index bf4d4b5e..85de59dd 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -171,6 +171,9 @@ type LanguageBackend struct { // Poetry. Lockfile string + // Check to see if we think we can run at all + IsAvailable func() bool + // List of filename globs that match against files written in // this programming language, e.g. "*.py" for Python. These // should not include any slashes, because they may be matched @@ -350,6 +353,7 @@ func (b *LanguageBackend) Setup() { "missing Info": b.Info == nil, "missing Add": b.Add == nil, "missing Remove": b.Remove == nil, + "missing IsAvailable": b.IsAvailable == nil, // The lock method should be unimplemented if // and only if builds are not reproducible. "either implement Lock or mark QuirksIsNotReproducible": ((b.Lock == nil) != b.QuirksIsNotReproducible()), diff --git a/internal/backends/backends.go b/internal/backends/backends.go index 6de54062..f7a81f3d 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -123,13 +123,18 @@ func GetBackend(ctx context.Context, language string) api.LanguageBackend { return backends[0] } +type BackendInfo struct { + Name string + Available bool +} + // GetBackendNames returns a slice of the canonical names (e.g. // python-python3-poetry, not just python3) for all the backends // listed in languageBackends. -func GetBackendNames() []string { - backendNames := []string{} +func GetBackendNames() []BackendInfo { + var backendNames []BackendInfo for _, b := range languageBackends { - backendNames = append(backendNames, b.Name) + backendNames = append(backendNames, BackendInfo{Name: b.Name, Available: b.IsAvailable()}) } return backendNames } diff --git a/internal/backends/dart/dart.go b/internal/backends/dart/dart.go index 65c987c9..6f456656 100644 --- a/internal/backends/dart/dart.go +++ b/internal/backends/dart/dart.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "os" + "os/exec" "path" "runtime" @@ -20,6 +21,11 @@ import ( "gopkg.in/yaml.v2" ) +func dartIsAvailable() bool { + _, err := exec.LookPath("dart") + return err == nil +} + // getPubBaseUrl returns pub.dartlang.org (the primary API endpoint for pub.dev) // or a local override if set. func getPubBaseURL() string { @@ -298,6 +304,7 @@ var DartPubBackend = api.LanguageBackend{ Name: "dart-pub", Specfile: "pubspec.yaml", Lockfile: "pubspec.lock", + IsAvailable: dartIsAvailable, FilenamePatterns: []string{"*.dart"}, Quirks: api.QuirksLockAlsoInstalls, GetPackageDir: dartGetPackageDir, @@ -309,13 +316,13 @@ var DartPubBackend = api.LanguageBackend{ //nolint:ineffassign,wastedassign,staticcheck span, ctx := tracer.StartSpanFromContext(ctx, "pub get") defer span.Finish() - util.RunCmd([]string{"pub", "get"}) + util.RunCmd([]string{"dart", "pub", "get"}) }, Install: func(ctx context.Context) { //nolint:ineffassign,wastedassign,staticcheck span, ctx := tracer.StartSpanFromContext(ctx, "pub get") defer span.Finish() - util.RunCmd([]string{"pub", "get"}) + util.RunCmd([]string{"dart", "pub", "get"}) }, ListSpecfile: dartListPubspecYaml, ListLockfile: dartListPubspecLock, diff --git a/internal/backends/dotnet/dotnet.go b/internal/backends/dotnet/dotnet.go index 43f5aa12..b4de458e 100644 --- a/internal/backends/dotnet/dotnet.go +++ b/internal/backends/dotnet/dotnet.go @@ -3,17 +3,24 @@ package dotnet import ( "context" + "os/exec" "github.com/replit/upm/internal/api" "github.com/replit/upm/internal/nix" "github.com/replit/upm/internal/util" ) +func dotnetIsAvailable() bool { + _, err := exec.LookPath("dotnet") + return err == nil +} + // DotNetBackend is the UPM language backend .NET languages with support for C# var DotNetBackend = api.LanguageBackend{ Name: "dotnet", Specfile: findSpecFile(), Lockfile: lockFileName, + IsAvailable: dotnetIsAvailable, FilenamePatterns: []string{"*.cs", "*.csproj", "*.fs", "*.fsproj"}, Remove: func(ctx context.Context, pkgs map[api.PkgName]bool) { removePackages(ctx, pkgs, findSpecFile(), util.RunCmd) diff --git a/internal/backends/elisp/elisp.go b/internal/backends/elisp/elisp.go index 62afd1a8..a14e98b2 100755 --- a/internal/backends/elisp/elisp.go +++ b/internal/backends/elisp/elisp.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "path/filepath" "regexp" "strings" @@ -19,11 +20,20 @@ import ( // elispPatterns is the FilenamePatterns value for ElispBackend. var elispPatterns = []string{"*.el"} +func elispCaskIsAvailable() bool { + _, err := exec.LookPath("emacs") + if err == nil { + _, err = exec.LookPath("cask") + } + return err == nil +} + // ElispBackend is the UPM language backend for Emacs Lisp using Cask. var ElispBackend = api.LanguageBackend{ Name: "elisp-cask", Specfile: "Cask", Lockfile: "packages.txt", + IsAvailable: elispCaskIsAvailable, FilenamePatterns: elispPatterns, Quirks: api.QuirksNotReproducible, GetPackageDir: func() string { diff --git a/internal/backends/java/java.go b/internal/backends/java/java.go index ea5d7f1d..229d62fa 100755 --- a/internal/backends/java/java.go +++ b/internal/backends/java/java.go @@ -6,6 +6,7 @@ import ( "encoding/xml" "fmt" "os" + "os/exec" "regexp" "github.com/replit/upm/internal/api" @@ -115,6 +116,11 @@ func readProjectOrMakeEmpty(path string) Project { const pomdotxml = "pom.xml" +func isAvailable() bool { + _, err := exec.LookPath("mvn") + return err == nil +} + func addPackages(ctx context.Context, pkgs map[api.PkgName]api.PkgSpec, projectName string) { //nolint:ineffassign,wastedassign,staticcheck span, ctx := tracer.StartSpanFromContext(ctx, "Java add package") @@ -302,6 +308,7 @@ var JavaBackend = api.LanguageBackend{ Name: "java-maven", Specfile: pomdotxml, Lockfile: pomdotxml, + IsAvailable: isAvailable, FilenamePatterns: javaPatterns, Quirks: api.QuirksAddRemoveAlsoLocks, GetPackageDir: func() string { diff --git a/internal/backends/nodejs/nodejs.go b/internal/backends/nodejs/nodejs.go index 8f32e751..c7669def 100644 --- a/internal/backends/nodejs/nodejs.go +++ b/internal/backends/nodejs/nodejs.go @@ -195,6 +195,26 @@ type packageLockJSON struct { // nodejsPatterns is the FilenamePatterns value for NodejsBackend. var nodejsPatterns = []string{"*.js", "*.ts", "*.jsx", "*.tsx", "*.mjs", "*.cjs"} +func bunIsAvailable() bool { + _, err := exec.LookPath("bun") + return err == nil +} + +func pnpmIsAvailable() bool { + _, err := exec.LookPath("pnpm") + return err == nil +} + +func yarnIsAvailable() bool { + _, err := exec.LookPath("yarn") + return err == nil +} + +func npmIsAvailable() bool { + _, err := exec.LookPath("npm") + return err == nil +} + // nodejsSearch implements Search for nodejs-yarn, nodejs-pnpm and nodejs-npm. func nodejsSearch(query string) []api.PkgInfo { // Special case: if search query is only one character, the @@ -372,6 +392,7 @@ var NodejsYarnBackend = api.LanguageBackend{ Name: "nodejs-yarn", Specfile: "package.json", Lockfile: "yarn.lock", + IsAvailable: yarnIsAvailable, FilenamePatterns: nodejsPatterns, Quirks: api.QuirksAddRemoveAlsoLocks | api.QuirksAddRemoveAlsoInstalls | @@ -449,6 +470,7 @@ var NodejsPNPMBackend = api.LanguageBackend{ Name: "nodejs-pnpm", Specfile: "package.json", Lockfile: "pnpm-lock.yaml", + IsAvailable: pnpmIsAvailable, FilenamePatterns: nodejsPatterns, Quirks: api.QuirksAddRemoveAlsoLocks | api.QuirksAddRemoveAlsoInstalls | @@ -542,6 +564,7 @@ var NodejsNPMBackend = api.LanguageBackend{ Name: "nodejs-npm", Specfile: "package.json", Lockfile: "package-lock.json", + IsAvailable: npmIsAvailable, FilenamePatterns: nodejsPatterns, Quirks: api.QuirksAddRemoveAlsoLocks | api.QuirksAddRemoveAlsoInstalls | @@ -624,6 +647,7 @@ var BunBackend = api.LanguageBackend{ Name: "bun", Specfile: "package.json", Lockfile: "bun.lockb", + IsAvailable: bunIsAvailable, FilenamePatterns: nodejsPatterns, Quirks: api.QuirksAddRemoveAlsoLocks | api.QuirksAddRemoveAlsoInstalls | diff --git a/internal/backends/php/php.go b/internal/backends/php/php.go index 5db942b7..ffd3061d 100644 --- a/internal/backends/php/php.go +++ b/internal/backends/php/php.go @@ -8,6 +8,7 @@ import ( "io" "net/url" "os" + "os/exec" "strings" "github.com/replit/upm/internal/api" @@ -63,6 +64,11 @@ type authors struct { Email string `json:"email"` } +func composerIsAvailable() bool { + _, err := exec.LookPath("composer") + return err == nil +} + func search(query string) []api.PkgInfo { endpoint := "https://packagist.org/search.json?q=" + url.QueryEscape(query) resp, err := api.HttpClient.Get(endpoint) @@ -227,6 +233,7 @@ var PhpComposerBackend = api.LanguageBackend{ Name: "php-composer", Specfile: "composer.json", Lockfile: "composer.lock", + IsAvailable: composerIsAvailable, FilenamePatterns: []string{"*.php"}, Quirks: api.QuirksAddRemoveAlsoLocks | api.QuirksAddRemoveAlsoInstalls, GetPackageDir: func() string { diff --git a/internal/backends/python/python.go b/internal/backends/python/python.go index c964465c..e2e275a7 100644 --- a/internal/backends/python/python.go +++ b/internal/backends/python/python.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "os/exec" "regexp" "strings" @@ -68,6 +69,16 @@ type poetryLock struct { } `json:"package"` } +func pipIsAvailable() bool { + _, err := exec.LookPath("pip") + return err == nil +} + +func poetryIsAvailable() bool { + _, err := exec.LookPath("poetry") + return err == nil +} + // normalizeSpec returns the version string from a Poetry spec, or the // empty string. The Poetry spec may be either a string or a // map[string]interface{} with a "version" key that is a string. If @@ -199,6 +210,7 @@ func makePythonPoetryBackend(python string) api.LanguageBackend { Alias: "python-python3-poetry", Specfile: "pyproject.toml", Lockfile: "poetry.lock", + IsAvailable: poetryIsAvailable, FilenamePatterns: []string{"*.py"}, Quirks: api.QuirksAddRemoveAlsoLocks | api.QuirksAddRemoveAlsoInstalls, @@ -323,6 +335,7 @@ func makePythonPipBackend(python string) api.LanguageBackend { b := api.LanguageBackend{ Name: "python3-pip", Specfile: "requirements.txt", + IsAvailable: pipIsAvailable, Alias: "python-python3-pip", FilenamePatterns: []string{"*.py"}, Quirks: api.QuirksAddRemoveAlsoInstalls | api.QuirksNotReproducible, diff --git a/internal/backends/rlang/rlang.go b/internal/backends/rlang/rlang.go index 6d12a3e6..a999cecd 100644 --- a/internal/backends/rlang/rlang.go +++ b/internal/backends/rlang/rlang.go @@ -3,6 +3,7 @@ package rlang import ( "context" "os" + "os/exec" "path" "regexp" "strings" @@ -12,6 +13,11 @@ import ( "github.com/replit/upm/internal/util" ) +func rIsAvailable() bool { + _, err := exec.LookPath("R") + return err == nil +} + func getImports(imports string) []string { return regexp.MustCompile(`[a-zA-Z_]\w*`).FindAllString(imports, -1) } @@ -81,6 +87,7 @@ var RlangBackend = api.LanguageBackend{ Name: "rlang", Specfile: "Rconfig.json", Lockfile: "Rconfig.lock.json", + IsAvailable: rIsAvailable, FilenamePatterns: []string{"*.r", "*.R"}, Quirks: api.QuirksNone, GetPackageDir: getRPkgDir, diff --git a/internal/backends/ruby/ruby.go b/internal/backends/ruby/ruby.go index 0cb31451..89b26274 100644 --- a/internal/backends/ruby/ruby.go +++ b/internal/backends/ruby/ruby.go @@ -7,6 +7,7 @@ import ( "io" "net/url" "os" + "os/exec" "strings" "github.com/replit/upm/internal/api" @@ -33,6 +34,11 @@ type rubygemsInfo struct { Version string `json:"version"` } +func bundlerIsAvailable() bool { + _, err := exec.LookPath("bundle") + return err == nil +} + // getPath returns the appropriate --path for 'bundle install'. This // will normally be '.bundle' (in the current directory), but may // instead be the empty string, indicating that no --path argument @@ -67,6 +73,7 @@ var RubyBackend = api.LanguageBackend{ Name: "ruby-bundler", Specfile: "Gemfile", Lockfile: "Gemfile.lock", + IsAvailable: bundlerIsAvailable, FilenamePatterns: []string{"*.rb"}, Quirks: api.QuirksAddRemoveAlsoLocks, GetPackageDir: func() string { diff --git a/internal/backends/rust/rust.go b/internal/backends/rust/rust.go index 0ca91184..97168fa1 100644 --- a/internal/backends/rust/rust.go +++ b/internal/backends/rust/rust.go @@ -7,6 +7,7 @@ import ( "io" "net/url" "os" + "os/exec" "github.com/BurntSushi/toml" "github.com/replit/upm/internal/api" @@ -81,6 +82,11 @@ func (c *crateInfoResult) toPkgInfo() api.PkgInfo { } } +func cargoIsAvailable() bool { + _, err := exec.LookPath("cargo") + return err == nil +} + func search(query string) []api.PkgInfo { endpoint := "https://crates.io/api/v1/crates" path := "?q=" + url.QueryEscape(query) @@ -224,6 +230,7 @@ var RustBackend = api.LanguageBackend{ Name: "rust", Specfile: "Cargo.toml", Lockfile: "Cargo.lock", + IsAvailable: cargoIsAvailable, FilenamePatterns: []string{"*.rs"}, GetPackageDir: func() string { return "target" diff --git a/internal/cli/cmds.go b/internal/cli/cmds.go index bad1d6a6..ec4be565 100644 --- a/internal/cli/cmds.go +++ b/internal/cli/cmds.go @@ -49,8 +49,12 @@ func runWhichLanguage(language string) { // runListLanguages implements 'upm list-languages'. func runListLanguages() { - for _, backendName := range backends.GetBackendNames() { - fmt.Println(backendName) + for _, info := range backends.GetBackendNames() { + if info.Available { + fmt.Println(info.Name) + } else { + fmt.Println(info.Name + " (unavailable)") + } } } diff --git a/test-suite/Main_test.go b/test-suite/Main_test.go index 48b8a0b3..5b2316f6 100644 --- a/test-suite/Main_test.go +++ b/test-suite/Main_test.go @@ -25,11 +25,11 @@ func init() { fmt.Println("Preparing test suites:") for _, bn := range backends.GetBackendNames() { prefix := os.Getenv("UPM_SUITE_PREFIX") - if !strings.HasPrefix(bn, prefix) { + if !strings.HasPrefix(bn.Name, prefix) { continue } - fmt.Println("- " + bn) - bt := testUtils.InitBackendT(backends.GetBackend(context.Background(), bn), &templates.FS) + fmt.Println("- " + bn.Name) + bt := testUtils.InitBackendT(backends.GetBackend(context.Background(), bn.Name), &templates.FS) languageBackends = append(languageBackends, bt) } fmt.Println()