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

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/replit/upm/internal/util"
     9  )
    10  
    11  // PkgName represents the name of a package, e.g. "flask" for Python.
    12  type PkgName string
    13  
    14  func (n PkgName) Contains(other PkgName) bool {
    15  	return strings.Contains(string(n), string(other))
    16  }
    17  
    18  func (n PkgName) HasPrefix(other PkgName) bool {
    19  	return strings.HasPrefix(string(n), string(other))
    20  }
    21  
    22  type PkgCoordinates struct {
    23  	Name string
    24  	Spec PkgSpec
    25  }
    26  
    27  // PkgSpec represents a package version constraint, e.g. "^1.1" or ">=
    28  // 1.1, <2.0" for Python.
    29  type PkgSpec string
    30  
    31  // PkgVersion represents an exact package version, e.g. "1.1" or
    32  // "1.0b2.post345.dev456" for Python.
    33  type PkgVersion string
    34  
    35  // PkgInfo is a general-purpose struct for representing package
    36  // metadata. Any of the fields may be zeroed except for Name. Which
    37  // fields are nonzero depends on the context and language backend.
    38  //
    39  // Note: the PkgInfo struct is parsed with reflection in several
    40  // places. It must have "json" and "pretty" tags, and the only allowed
    41  // types are string and []string.
    42  type PkgInfo struct {
    43  
    44  	// The name of the package, e.g. "flask". Package names cannot
    45  	// contain spaces.
    46  	Name string `json:"name,omitempty" pretty:"Name"`
    47  
    48  	// Brief description of the package, e.g. "A simple framework
    49  	// for building complex web applications.".
    50  	Description string `json:"description,omitempty" pretty:"Description"`
    51  
    52  	// Version number of the package, e.g. "1.1.1". No particular
    53  	// format is enforced.
    54  	Version string `json:"version,omitempty" pretty:"Version"`
    55  
    56  	// URL for the package's home page, e.g.
    57  	// "https://palletsprojects.com/p/flask/".
    58  	HomepageURL string `json:"homepageURL,omitempty" pretty:"Homepage"`
    59  
    60  	// URL for the package's documentation, e.g.
    61  	// "https://flask.palletsprojects.com/".
    62  	DocumentationURL string `json:"documentationURL,omitempty" pretty:"Documentation"`
    63  
    64  	// URL for the package's source code, e.g.
    65  	// "https://github.com/pallets/flask".
    66  	SourceCodeURL string `json:"sourceCodeURL,omitempty" pretty:"Source code"`
    67  
    68  	// URL for the package's bug tracker, e.g.
    69  	// "https://github.com/pallets/flask/issues".
    70  	BugTrackerURL string `json:"bugTrackerURL,omitempty" pretty:"Bug tracker"`
    71  
    72  	// Author of the package. Only one author is supported; if
    73  	// there are multiple, we either pick one or simply
    74  	// concatenate them into a single Author.
    75  	Author string `json:"author,omitempty" pretty:"Author"`
    76  
    77  	// License of the package. No particular format is enforced.
    78  	// If the package has multiple licenses, we just concatenate
    79  	// them into one string.
    80  	License string `json:"license,omitempty" pretty:"License"`
    81  
    82  	// Names of packages which are dependencies of this package.
    83  	// There is no way to distinguish between a package that has
    84  	// no dependencies and a package whose language backend did
    85  	// not provide dependency information.
    86  	Dependencies []string `json:"dependencies,omitempty" pretty:"Dependencies"`
    87  }
    88  
    89  // Quirks is a bitmask enum used to indicate how specific language
    90  // backends behave differently from the core abstractions of UPM, and
    91  // therefore require some different treatment by the command-line
    92  // interface layer. See the constants of this type for more
    93  // information.
    94  type Quirks uint8
    95  
    96  // Constants of type Quirks, used to denote whether a language backend
    97  // follows the expected abstractions of UPM or if it needs special
    98  // treatment.
    99  const (
   100  	// By default, UPM assumes that each language backend
   101  	// implements the add/remove, lock, and install operations as
   102  	// totally separate steps: add/remove only modifies the
   103  	// specfile, lock only updates the lockfile from the specfile,
   104  	// and install only installs packages from the lockfile.
   105  	QuirksNone Quirks = 0
   106  
   107  	// This constant indicates that the package manager doesn't
   108  	// have any concept of a lockfile, so the backend implements
   109  	// its own. In this case, the functioning of add/remove is
   110  	// unchanged. However, lock does nothing (and the backend must
   111  	// not implement it), while install installs packages directly
   112  	// from the specfile and then generates a lockfile from what
   113  	// was installed.
   114  	QuirksNotReproducible = 1 << iota
   115  
   116  	// This constant indicates that add/remove also executes lock
   117  	// subsequently, so it doesn't need to be run afterwards.
   118  	QuirksAddRemoveAlsoLocks
   119  
   120  	// This constant indicates that add/remove also executes lock
   121  	// and install subsequently, so they don't need to be run
   122  	// afterwards. If specified, then QuirksAddRemoveAlsoLocks
   123  	// also must be specified.
   124  	QuirksAddRemoveAlsoInstalls
   125  
   126  	// This constant indicates that lock also executes install
   127  	// subsequently, so it doesn't need to be run afterwards.
   128  	QuirksLockAlsoInstalls
   129  
   130  	// This constant indicates that remove cannot be performed
   131  	// without a lockfile.
   132  	QuirkRemoveNeedsLockfile
   133  )
   134  
   135  // LanguageBackend is the core abstraction of UPM. It represents an
   136  // implementation of all the core package management functionality of
   137  // UPM, for a specific programming language and package manager. For
   138  // example, python-python3-poetry and python-python2-poetry would be
   139  // different backends, as would python-python3-poetry and
   140  // python-python3-pipenv.
   141  //
   142  // Most of the fields of this struct are mandatory, and the Check
   143  // method will panic at UPM startup if they are not provided. Not all
   144  // language backends necessarily need to implement all operations; in
   145  // this case, the relevant functions should call util.NotImplemented,
   146  // which will cause UPM to exit with an appropriate error message.
   147  // (The limitation should be noted in the backend feature matrix in
   148  // the README.)
   149  //
   150  // Make sure to update the Check method when adding/removing fields
   151  // from this struct.
   152  type LanguageBackend struct {
   153  	// The name of the language backend. This is matched against
   154  	// the value of the --lang argument on the command line. The
   155  	// format is a sequence of one or more tokens separated by
   156  	// hyphens, as in ruby-bundler or python-python3-poetry. Each
   157  	// token gets matched separately against the --lang argument,
   158  	// so that python-python3-poetry will match against python or
   159  	// python3 or poetry or python-poetry. The name must be unique
   160  	// among all language backends.
   161  	//
   162  	// This field is mandatory.
   163  	Name string
   164  
   165  	// An alias for the backend, useful for backwards compatibility
   166  	// when renaming backends.
   167  	Alias string
   168  
   169  	// The filename of the specfile, e.g. "pyproject.toml" for
   170  	// Poetry.
   171  	//
   172  	// This field is mandatory.
   173  	Specfile string
   174  
   175  	// An optional function to analyze the specfile to determine compatibility
   176  	IsSpecfileCompatible func(fullPath string) (bool, error)
   177  
   178  	// The filename of the lockfile, e.g. "poetry.lock" for
   179  	// Poetry.
   180  	Lockfile string
   181  
   182  	// Check to see if we think we can run at all
   183  	IsAvailable func() bool
   184  
   185  	// List of filename globs that match against files written in
   186  	// this programming language, e.g. "*.py" for Python. These
   187  	// should not include any slashes, because they may be matched
   188  	// in any subdirectory.
   189  	//
   190  	// FilenamePatterns is used for two things: language backend
   191  	// autodetection (if matching files exist in the project
   192  	// directory, the project is autodetected for this backend,
   193  	// subject to being overridden by another heuristic), and
   194  	// regexp searches for dependency guessing.
   195  	//
   196  	// This field is mandatory.
   197  	FilenamePatterns []string
   198  
   199  	// QuirksNone if the language backend conforms to the core
   200  	// abstractions of UPM, and some bitwise disjunction of the
   201  	// Quirks constant values otherwise.
   202  	//
   203  	// This field is optional, and defaults to QuirksNone.
   204  	Quirks Quirks
   205  
   206  	// Function that normalizes packages as they come in as CLI args
   207  	//
   208  	// This function is optional, defaulting to split(" ", 2)
   209  	NormalizePackageArgs func(args []string) map[PkgName]PkgCoordinates
   210  
   211  	// Function that normalizes a package name. This is used to
   212  	// prevent duplicate packages getting added to the specfile.
   213  	// For example, in Python the package names "flask" and
   214  	// "Flask" are the same, so all package names are lowercased
   215  	// before comparison.
   216  	//
   217  	// This field is optional.
   218  	NormalizePackageName func(name PkgName) PkgName
   219  
   220  	// Return the path (relative to the project directory) in
   221  	// which packages are installed. The path need not exist.
   222  	GetPackageDir func() string
   223  
   224  	// Apply a sensible heuristic for sorting search results
   225  	// if we know we want to surface some packages over others.
   226  	SortPackages func(query string, packages []PkgInfo) []PkgInfo
   227  
   228  	// Search for packages using an online index. The query may
   229  	// contain any characters, including whitespace. Return a list
   230  	// of search results, which can be of any length. (It will be
   231  	// truncated by the command-line interface.) If the search
   232  	// fails, terminate the process. If it successfully returns no
   233  	// results, return an empty slice.
   234  	//
   235  	// This field is mandatory.
   236  	Search func(query string) []PkgInfo
   237  
   238  	// Retrieve information about a package from an online index.
   239  	// If the package doesn't exist, return a zero struct.
   240  	//
   241  	// This field is mandatory.
   242  	Info func(PkgName) PkgInfo
   243  
   244  	// Add packages to the specfile. The map is guaranteed to have
   245  	// at least one package, and all of the packages are
   246  	// guaranteed to not already be in the specfile (according to
   247  	// ListSpecfile). The specfile is *not* guaranteed to exist
   248  	// already. The specs may be empty, in which case default
   249  	// specs should be generated (for example, specifying the
   250  	// latest version or newer). This method must create the
   251  	// specfile if it does not exist already. Additional
   252  	// information needed to create the specfile can be passed
   253  	// as well. If a significant amount of additional info is
   254  	// required for initalizing specfiles, we can break that out
   255  	// to a seperate step.
   256  	//
   257  	// If QuirksAddRemoveAlsoInstalls, then also lock and install.
   258  	// In this case this method must also create the lockfile if
   259  	// it does not exist already.
   260  	//
   261  	// This field is mandatory.
   262  	Add func(context.Context, map[PkgName]PkgSpec, string)
   263  
   264  	// Remove packages from the specfile. The map is guaranteed to
   265  	// have at least one package, and all of the packages are
   266  	// guaranteed to already be in the specfile (according to
   267  	// ListSpecfile). The specfile is guaranteed to exist already.
   268  	// This method may not delete the specfile or lockfile.
   269  	//
   270  	// If QuirksAddRemoveAlsoInstalls, then also lock and install.
   271  	// In this case this method must also create the lockfile if
   272  	// it does not exist already.
   273  	//
   274  	// This field is mandatory.
   275  	Remove func(context.Context, map[PkgName]bool)
   276  
   277  	// Generate the lockfile from the specfile. The specfile is
   278  	// guaranteed to already exist. This method must create the
   279  	// lockfile if it does not exist already.
   280  	//
   281  	// If QuirksLockAlsoInstalls, then also install.
   282  	//
   283  	// This field is mandatory, unless QuirksNotReproducible in
   284  	// which case this field *may* not be specified.
   285  	Lock func(context.Context)
   286  
   287  	// Install packages from the lockfile. The specfile and
   288  	// lockfile are guaranteed to already exist, unless
   289  	// QuirksNotReproducible in which case only the specfile is
   290  	// guaranteed to exist.
   291  	//
   292  	// This field is mandatory.
   293  	Install func(context.Context)
   294  
   295  	// List the packages in the specfile. Names and specs should
   296  	// be returned in a format suitable for the Add method. The
   297  	// specfile is guaranteed to exist already.
   298  	//
   299  	// This field is mandatory.
   300  	ListSpecfile func(mergeAllGroups bool) map[PkgName]PkgSpec
   301  
   302  	// List the packages in the lockfile. Names should be returned
   303  	// in a format suitable for the Add method. The lockfile is
   304  	// guaranteed to exist already.
   305  	ListLockfile func() map[PkgName]PkgVersion
   306  
   307  	// Regexps used to determine if the Guess method really needs
   308  	// to be invoked, or if its previous return value can be
   309  	// re-used.
   310  	//
   311  	// These regexps should match imports, requires, or whatever
   312  	// is analogous for the language. They are run against every
   313  	// file in the project directory and its subdirectories,
   314  	// subject to FilenamePatterns and util.IgnoredPaths. If a
   315  	// regexp has no capture groups, the entire match is used;
   316  	// otherwise, the match of each of its capture groups is used.
   317  	// The list of all matches from all regexps against all files
   318  	// is aggregated and hashed, and this is used to determine if
   319  	// Guess needs to be re-run. So, these regexps should be
   320  	// written so that what they match will change whenever the
   321  	// return value of Guess might change.
   322  	//
   323  	// This field is optional; if it is omitted, then Guess will
   324  	// always be run without recourse to caching.
   325  	GuessRegexps []*regexp.Regexp
   326  
   327  	// Return a list of packages that are probably needed as
   328  	// dependencies of the project. It is better to be safe than
   329  	// sorry: only packages which are *definitely* project
   330  	// dependencies should be returned. Names should be returned
   331  	// in a format suitable for the Add method. There is no need
   332  	// to eliminate packages already installed.
   333  	//
   334  	// The second value indicates whether the bare imports search
   335  	// was fully successful. One reason why it might not be
   336  	// successful is if there is a syntax error in the code. It is
   337  	// important to get this right because a syntax error can
   338  	// cause an entire file to be skipped. Then if the error is
   339  	// fixed later, the GuessRegexps may return the same results,
   340  	// causing UPM to re-use the existing Guess return value
   341  	// (which is now wrong).
   342  	//
   343  	// This field is mandatory.
   344  	Guess func(ctx context.Context) (map[string][]PkgName, bool)
   345  
   346  	// Installs system dependencies into replit.nix for supported
   347  	// languages.
   348  	InstallReplitNixSystemDependencies func(context.Context, []PkgName)
   349  }
   350  
   351  // Setup panics if the given language backend does not specify all of
   352  // the mandatory fields. It also assigns some defaults.
   353  //
   354  // Honestly, this is a bit of a hack. We should really not expose the
   355  // struct fields through the API directly, or at least we should have
   356  // a builder function which can perform this normalization and
   357  // validation.
   358  func (b *LanguageBackend) Setup() {
   359  	condition2flag := map[string]bool{
   360  		"missing name":                     b.Name == "",
   361  		"missing specfile":                 b.Specfile == "",
   362  		"missing lockfile":                 b.QuirksIsReproducible() && b.Lockfile == "",
   363  		"need at least 1 filename pattern": len(b.FilenamePatterns) == 0,
   364  		"missing package dir":              b.GetPackageDir == nil,
   365  		"missing Search":                   b.Search == nil,
   366  		"missing Info":                     b.Info == nil,
   367  		"missing Add":                      b.Add == nil,
   368  		"missing Remove":                   b.Remove == nil,
   369  		"missing IsAvailable":              b.IsAvailable == nil,
   370  		// The lock method should be unimplemented if
   371  		// and only if builds are not reproducible.
   372  		"either implement Lock or mark QuirksIsNotReproducible": ((b.Lock == nil) != b.QuirksIsNotReproducible()),
   373  		"missing install":      b.Install == nil,
   374  		"missing ListSpecfile": b.ListSpecfile == nil,
   375  		"missing ListLockfile": b.QuirksIsReproducible() && b.ListLockfile == nil,
   376  		// If the backend isn't reproducible, then lock is
   377  		// unimplemented. So how could it also do
   378  		// installation?
   379  		"Lock installs, but is not implemented": b.QuirksDoesLockAlsoInstall() && b.QuirksIsNotReproducible(),
   380  		// If you install, then you have to lock.
   381  		"Add and Remove install, so they must also Lock": b.QuirksIsReproducible() && b.QuirksDoesAddRemoveAlsoInstall() && b.QuirksDoesAddRemoveNotAlsoLock(),
   382  	}
   383  
   384  	reasons := []string{}
   385  	for reason, flag := range condition2flag {
   386  		if flag {
   387  			reasons = append(reasons, reason)
   388  		}
   389  	}
   390  
   391  	if len(reasons) > 0 {
   392  		util.Panicf("language backend %s is incomplete or invalid: %s", b.Name, reasons)
   393  	}
   394  
   395  	if b.NormalizePackageArgs == nil {
   396  		b.NormalizePackageArgs = func(args []string) map[PkgName]PkgCoordinates {
   397  			normPkgs := map[PkgName]PkgCoordinates{}
   398  			for _, arg := range args {
   399  				fields := strings.SplitN(arg, " ", 2)
   400  				name := fields[0]
   401  				var spec PkgSpec
   402  				if len(fields) >= 2 {
   403  					spec = PkgSpec(fields[1])
   404  				}
   405  
   406  				normPkgs[b.NormalizePackageName(PkgName(name))] = PkgCoordinates{
   407  					Name: name,
   408  					Spec: spec,
   409  				}
   410  			}
   411  			return normPkgs
   412  		}
   413  	}
   414  
   415  	if b.NormalizePackageName == nil {
   416  		b.NormalizePackageName = func(name PkgName) PkgName {
   417  			return name
   418  		}
   419  	}
   420  }