github.com/replit/upm@v0.0.0-20240423230255-9ce4fc3ea24c/internal/backends/backends.go (about)

     1  // Package backends contains the language-specific UPM backends, and
     2  // logic for selecting amongst them.
     3  package backends
     4  
     5  import (
     6  	"context"
     7  	"strings"
     8  
     9  	"github.com/replit/upm/internal/api"
    10  	"github.com/replit/upm/internal/backends/dart"
    11  	"github.com/replit/upm/internal/backends/dotnet"
    12  	"github.com/replit/upm/internal/backends/elisp"
    13  	"github.com/replit/upm/internal/backends/java"
    14  	"github.com/replit/upm/internal/backends/nodejs"
    15  	"github.com/replit/upm/internal/backends/php"
    16  	"github.com/replit/upm/internal/backends/python"
    17  	"github.com/replit/upm/internal/backends/rlang"
    18  	"github.com/replit/upm/internal/backends/ruby"
    19  	"github.com/replit/upm/internal/backends/rust"
    20  	"github.com/replit/upm/internal/util"
    21  	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
    22  )
    23  
    24  // languageBackends is a slice of language backends which may be used
    25  // from the command line.
    26  //
    27  // If more than one backend might match the same project, then one
    28  // that comes first in this list will be used.
    29  var languageBackends = []api.LanguageBackend{
    30  	python.PythonPoetryBackend,
    31  	python.PythonPipBackend,
    32  	nodejs.BunBackend,
    33  	nodejs.NodejsNPMBackend,
    34  	nodejs.NodejsPNPMBackend,
    35  	nodejs.NodejsYarnBackend,
    36  	ruby.RubyBackend,
    37  	elisp.ElispBackend,
    38  	dart.DartPubBackend,
    39  	java.JavaBackend,
    40  	rlang.RlangBackend,
    41  	dotnet.DotNetBackend,
    42  	rust.RustBackend,
    43  	php.PhpComposerBackend,
    44  }
    45  
    46  // matchesLanguage checks if a language backend matches a value for
    47  // the --lang argument. For example, the python-python3-poetry backend
    48  // will match --lang=python-poetry and --lang=python3 but not
    49  // --lang=python2. This is used to filter the available language
    50  // backends before trying to guess which one should be used.
    51  func matchesLanguage(b api.LanguageBackend, language string) bool {
    52  	bParts := map[string]bool{}
    53  	for _, bPart := range strings.Split(b.Name, "-") {
    54  		bParts[bPart] = true
    55  	}
    56  	checkAlias := false
    57  	for _, lPart := range strings.Split(language, "-") {
    58  		if !bParts[lPart] {
    59  			checkAlias = true
    60  			break
    61  		}
    62  	}
    63  	if checkAlias {
    64  		bParts = map[string]bool{}
    65  		for _, bPart := range strings.Split(b.Alias, "-") {
    66  			bParts[bPart] = true
    67  		}
    68  		for _, lPart := range strings.Split(language, "-") {
    69  			if !bParts[lPart] {
    70  				return false
    71  			}
    72  		}
    73  	}
    74  	return true
    75  }
    76  
    77  // GetBackend returns the language backend for a given --lang argument
    78  // value. If none is applicable, it exits the process.
    79  func GetBackend(ctx context.Context, language string) api.LanguageBackend {
    80  	//nolint:ineffassign,wastedassign,staticcheck
    81  	span, ctx := tracer.StartSpanFromContext(ctx, "GetBackend")
    82  	defer span.Finish()
    83  	backends := languageBackends
    84  	if language != "" {
    85  		filteredBackends := []api.LanguageBackend{}
    86  		for _, b := range backends {
    87  			if matchesLanguage(b, language) {
    88  				filteredBackends = append(filteredBackends, b)
    89  			}
    90  		}
    91  		switch len(filteredBackends) {
    92  		case 0:
    93  			util.DieConsistency("no such language: %s", language)
    94  		case 1:
    95  			return filteredBackends[0]
    96  		default:
    97  			backends = filteredBackends
    98  		}
    99  
   100  	}
   101  	for _, b := range backends {
   102  		if util.Exists(b.Specfile) &&
   103  			util.Exists(b.Lockfile) {
   104  			if b.IsSpecfileCompatible != nil {
   105  				isValid, err := b.IsSpecfileCompatible(b.Specfile)
   106  				if err != nil {
   107  					panic(err)
   108  				}
   109  				if !isValid {
   110  					continue
   111  				}
   112  			}
   113  			return b
   114  		}
   115  	}
   116  	for _, b := range backends {
   117  		if util.Exists(b.Specfile) {
   118  			if b.IsSpecfileCompatible != nil {
   119  				isValid, err := b.IsSpecfileCompatible(b.Specfile)
   120  				if err != nil {
   121  					panic(err)
   122  				}
   123  				if !isValid {
   124  					continue
   125  				}
   126  			}
   127  
   128  			return b
   129  		}
   130  		if util.Exists(b.Lockfile) {
   131  			return b
   132  		}
   133  	}
   134  	for _, b := range backends {
   135  		for _, p := range b.FilenamePatterns {
   136  			if util.PatternExists(p) {
   137  				return b
   138  			}
   139  		}
   140  	}
   141  	if language == "" {
   142  		util.DieInitializationError("could not autodetect a language for your project")
   143  	}
   144  	return backends[0]
   145  }
   146  
   147  type BackendInfo struct {
   148  	Name      string
   149  	Available bool
   150  }
   151  
   152  // GetBackendNames returns a slice of the canonical names (e.g.
   153  // python-python3-poetry, not just python3) for all the backends
   154  // listed in languageBackends.
   155  func GetBackendNames() []BackendInfo {
   156  	var backendNames []BackendInfo
   157  	for _, b := range languageBackends {
   158  		backendNames = append(backendNames, BackendInfo{Name: b.Name, Available: b.IsAvailable()})
   159  	}
   160  	return backendNames
   161  }
   162  
   163  // SetupAll panics if any registered language backend does not
   164  // implement its mandatory fields. It also assigns defaults for all
   165  // registered language backends.
   166  func SetupAll() {
   167  	for i := range languageBackends {
   168  		// Make sure that the Setup function can make changes
   169  		// to the struct.
   170  		(&languageBackends[i]).Setup()
   171  	}
   172  }