Skip to content

Commit

Permalink
Test whether a backend can be run at all (#190)
Browse files Browse the repository at this point in the history
* Dart "pub" command is now a subcommand of "dart"

* Adding IsAvailable checks

* Providing basic implementations of IsAvailable for known binaries
  • Loading branch information
blast-hardcheese authored Dec 19, 2023
1 parent c7d056e commit 87fc1f3
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 10 deletions.
4 changes: 4 additions & 0 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()),
Expand Down
11 changes: 8 additions & 3 deletions internal/backends/backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
11 changes: 9 additions & 2 deletions internal/backends/dart/dart.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path"
"runtime"

Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions internal/backends/dotnet/dotnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions internal/backends/elisp/elisp.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
Expand All @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions internal/backends/java/java.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/xml"
"fmt"
"os"
"os/exec"
"regexp"

"github.com/replit/upm/internal/api"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 24 additions & 0 deletions internal/backends/nodejs/nodejs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down
7 changes: 7 additions & 0 deletions internal/backends/php/php.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"net/url"
"os"
"os/exec"
"strings"

"github.com/replit/upm/internal/api"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions internal/backends/python/python.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions internal/backends/rlang/rlang.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rlang
import (
"context"
"os"
"os/exec"
"path"
"regexp"
"strings"
Expand All @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions internal/backends/ruby/ruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net/url"
"os"
"os/exec"
"strings"

"github.com/replit/upm/internal/api"
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions internal/backends/rust/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net/url"
"os"
"os/exec"

"github.com/BurntSushi/toml"
"github.com/replit/upm/internal/api"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 6 additions & 2 deletions internal/cli/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}
}

Expand Down
Loading

0 comments on commit 87fc1f3

Please sign in to comment.