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 }