github.com/replit/upm@v0.0.0-20240423230255-9ce4fc3ea24c/internal/cli/cmds.go (about) 1 package cli 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "os" 8 "reflect" 9 "sort" 10 "strings" 11 12 "github.com/replit/upm/internal/api" 13 "github.com/replit/upm/internal/backends" 14 "github.com/replit/upm/internal/config" 15 "github.com/replit/upm/internal/store" 16 "github.com/replit/upm/internal/table" 17 "github.com/replit/upm/internal/trace" 18 "github.com/replit/upm/internal/util" 19 "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 20 ) 21 22 // subroutineSilencer is used to easily enable and restore 23 // config.Quiet for part of a function. 24 type subroutineSilencer struct { 25 origQuiet bool 26 } 27 28 // silenceSubroutines turns on config.Quiet and returns a struct that 29 // can be used to restore its value. This only happens if 30 // UPM_SILENCE_SUBROUTINES is non-empty. 31 func silenceSubroutines() subroutineSilencer { 32 s := subroutineSilencer{origQuiet: config.Quiet} 33 if os.Getenv("UPM_SILENCE_SUBROUTINES") != "" { 34 config.Quiet = true 35 } 36 return s 37 } 38 39 // restore restores the previous value of config.Quiet. 40 func (s *subroutineSilencer) restore() { 41 config.Quiet = s.origQuiet 42 } 43 44 // runWhichLanguage implements 'upm which-language'. 45 func runWhichLanguage(language string) { 46 b := backends.GetBackend(context.Background(), language) 47 fmt.Println(b.Name) 48 } 49 50 // runListLanguages implements 'upm list-languages'. 51 func runListLanguages() { 52 for _, info := range backends.GetBackendNames() { 53 if info.Available { 54 fmt.Println(info.Name) 55 } else { 56 fmt.Println(info.Name + " (unavailable)") 57 } 58 } 59 } 60 61 func makeLoweredHM(normalizePackageName func(api.PkgName) api.PkgName, names []string) map[api.PkgName]bool { 62 // Build a hashset. struct{}{} purportedly is of size 0, so this is as good as we get 63 set := make(map[api.PkgName]bool) 64 for _, pkg := range names { 65 normal := normalizePackageName(api.PkgName(pkg)) 66 set[normal] = true 67 } 68 return set 69 } 70 71 // runSearch implements 'upm search'. 72 func runSearch(language string, args []string, outputFormat outputFormat, ignoredPackages []string) { 73 query := strings.Join(args, " ") 74 b := backends.GetBackend(context.Background(), language) 75 76 var results []api.PkgInfo 77 if strings.TrimSpace(query) == "" { 78 results = []api.PkgInfo{} 79 } else { 80 results = b.Search(query) 81 } 82 83 { // Filter out ignored packages 84 ignoredPackageSet := makeLoweredHM(b.NormalizePackageName, ignoredPackages) 85 filtered := []api.PkgInfo{} 86 for _, pkg := range results { 87 lower := b.NormalizePackageName(api.PkgName(pkg.Name)) 88 if ignoredPackageSet[lower] { 89 continue 90 } 91 filtered = append(filtered, pkg) 92 } 93 94 results = filtered 95 } 96 97 // Apply some heuristics to give results that more closely resemble the user's query 98 if b.SortPackages != nil { 99 results = b.SortPackages(query, results) 100 } 101 102 // Output a reasonable number of results. 103 if len(results) > 20 { 104 results = results[:20] 105 } 106 107 switch outputFormat { 108 case outputFormatTable: 109 if len(results) == 0 { 110 util.Log("no search results") 111 return 112 } 113 t := table.FromStructs(results) 114 t.Print() 115 116 case outputFormatJSON: 117 outputB, err := json.Marshal(results) 118 if err != nil { 119 panic(err) 120 } 121 fmt.Println(string(outputB)) 122 } 123 } 124 125 // infoLine represents one line in the table emitted by 'upm info'. 126 type infoLine struct { 127 Field string 128 Value string 129 } 130 131 // runInfo implements 'upm info'. 132 func runInfo(language string, pkg string, outputFormat outputFormat) { 133 b := backends.GetBackend(context.Background(), language) 134 info := b.Info(api.PkgName(pkg)) 135 if info.Name == "" { 136 util.DieConsistency("no such package: %s", pkg) 137 } 138 139 switch outputFormat { 140 case outputFormatTable: 141 infoT := reflect.TypeOf(info) 142 infoV := reflect.ValueOf(info) 143 rows := []infoLine{} 144 for i := 0; i < infoT.NumField(); i++ { 145 field := infoT.Field(i).Tag.Get("pretty") 146 var value string 147 switch infoV.Field(i).Kind() { 148 case reflect.String: 149 value = infoV.Field(i).String() 150 case reflect.Slice: 151 parts := []string{} 152 length := infoV.Field(i).Len() 153 for j := 0; j < length; j++ { 154 str := infoV.Field(i).Index(j).String() 155 parts = append(parts, str) 156 } 157 value = strings.Join(parts, ", ") 158 } 159 if value == "" { 160 continue 161 } 162 163 rows = append(rows, infoLine{Field: field, Value: value}) 164 } 165 166 if len(rows) == 0 { 167 util.Panicf( 168 "no fields returned from backend %s", 169 b.Name, 170 ) 171 } 172 173 width := len(rows[0].Field) 174 for i := 1; i < len(rows); i++ { 175 if len(rows[i].Field) > width { 176 width = len(rows[i].Field) 177 } 178 } 179 180 for _, row := range rows { 181 padLength := width - len(row.Field) 182 padding := strings.Repeat(" ", padLength) 183 fmt.Println(row.Field + ":" + padding + " " + row.Value) 184 } 185 186 case outputFormatJSON: 187 outputB, err := json.Marshal(info) 188 if err != nil { 189 panic(err) 190 } 191 fmt.Println(string(outputB)) 192 } 193 } 194 195 // deleteLockfile deletes the project's lockfile, if one exists. 196 func deleteLockfile(ctx context.Context, b api.LanguageBackend) { 197 //nolint:ineffassign,wastedassign,staticcheck 198 span, ctx := tracer.StartSpanFromContext(ctx, "deleteLockfile") 199 defer span.Finish() 200 if util.Exists(b.Lockfile) { 201 util.ProgressMsg("delete " + b.Lockfile) 202 os.Remove(b.Lockfile) 203 } 204 } 205 206 // maybeLock either runs lock or not, depending on the backend, store, 207 // and command-line options. It returns true if it actually ran lock. 208 func maybeLock(ctx context.Context, b api.LanguageBackend, forceLock bool) bool { 209 span, ctx := tracer.StartSpanFromContext(ctx, "maybeLock") 210 defer span.Finish() 211 if b.QuirksIsNotReproducible() { 212 return false 213 } 214 215 if !util.Exists(b.Specfile) { 216 return false 217 } 218 219 shouldLock := forceLock || !util.Exists(b.Lockfile) || store.HasSpecfileChanged(b) 220 if !shouldLock { 221 if packageDir := b.GetPackageDir(); packageDir != "" && !util.Exists(packageDir) { 222 // Only run lock if a specfile exists and it lists at least one package. 223 shouldLock = util.Exists(b.Specfile) && len(b.ListSpecfile(true)) > 0 224 } 225 } 226 227 if shouldLock { 228 b.Lock(ctx) 229 return true 230 } 231 232 return false 233 } 234 235 // maybeInstall either runs install or not, depending on the backend, 236 // store, and command-line options. 237 func maybeInstall(ctx context.Context, b api.LanguageBackend, forceInstall bool) { 238 span, ctx := tracer.StartSpanFromContext(ctx, "maybeInstall") 239 defer span.Finish() 240 if b.QuirksIsReproducible() { 241 if !util.Exists(b.Lockfile) { 242 return 243 } 244 if forceInstall || store.HasLockfileChanged(b) { 245 b.Install(ctx) 246 } 247 } else { 248 if !util.Exists(b.Specfile) { 249 return 250 } 251 var needsPackageDir bool 252 if packageDir := b.GetPackageDir(); packageDir != "" { 253 needsPackageDir = !util.Exists(packageDir) 254 } 255 if forceInstall || store.HasSpecfileChanged(b) || needsPackageDir { 256 b.Install(ctx) 257 } 258 } 259 } 260 261 // runAdd implements 'upm add'. 262 func runAdd( 263 language string, args []string, upgrade bool, 264 guess bool, forceGuess bool, ignoredPackages []string, 265 forceLock bool, forceInstall bool, name string) { 266 span, ctx := trace.StartSpanFromExistingContext("runAdd") 267 defer span.Finish() 268 b := backends.GetBackend(ctx, language) 269 270 normPkgs := b.NormalizePackageArgs(args) 271 272 if guess { 273 guessed := store.GuessWithCache(ctx, b, forceGuess) 274 275 // Map from normalized package names to original 276 // names. 277 guessedNorm := map[string][]api.PkgName{} 278 for key, guesses := range guessed { 279 normalized := []api.PkgName{} 280 for _, guess := range guesses { 281 normalized = append(normalized, b.NormalizePackageName(guess)) 282 } 283 guessedNorm[key] = normalized 284 } 285 286 for _, pkg := range ignoredPackages { 287 pkg := b.NormalizePackageName(api.PkgName(pkg)) 288 for key, guesses := range guessedNorm { 289 for _, guess := range guesses { 290 if pkg == guess { 291 delete(guessedNorm, key) 292 } 293 } 294 } 295 } 296 297 for _, guesses := range guessedNorm { 298 found := false 299 for _, guess := range guesses { 300 if _, ok := normPkgs[guess]; !ok { 301 found = true 302 break 303 } 304 } 305 if !found { 306 normPkgs[b.NormalizePackageName(guesses[0])] = api.PkgCoordinates{ 307 Name: string(guesses[0]), 308 Spec: "", 309 } 310 } 311 } 312 } 313 314 if util.Exists(b.Specfile) { 315 s := silenceSubroutines() 316 for name := range b.ListSpecfile(true) { 317 delete(normPkgs, b.NormalizePackageName(name)) 318 } 319 s.restore() 320 } 321 322 if upgrade { 323 deleteLockfile(ctx, b) 324 } 325 326 if len(normPkgs) >= 1 { 327 pkgs := map[api.PkgName]api.PkgSpec{} 328 for _, nameAndSpec := range normPkgs { 329 pkgs[api.PkgName(nameAndSpec.Name)] = nameAndSpec.Spec 330 } 331 332 b.Add(ctx, pkgs, name) 333 } 334 335 if len(normPkgs) == 0 || b.QuirksDoesAddRemoveNotAlsoLock() { 336 didLock := maybeLock(ctx, b, forceLock) 337 338 if !(didLock && b.QuirksDoesLockAlsoInstall()) { 339 maybeInstall(ctx, b, forceInstall) 340 } 341 } else if len(normPkgs) == 0 || b.QuirksDoesAddRemoveNotAlsoInstall() { 342 maybeInstall(ctx, b, forceInstall) 343 } 344 345 store.Read(ctx, b) 346 store.ClearGuesses(ctx, b) 347 store.UpdateFileHashes(ctx, b) 348 store.Write(ctx) 349 } 350 351 // runRemove implements 'upm remove'. 352 func runRemove(language string, args []string, upgrade bool, 353 forceLock bool, forceInstall bool) { 354 span, ctx := trace.StartSpanFromExistingContext("runRemove") 355 defer span.Finish() 356 b := backends.GetBackend(ctx, language) 357 358 if !util.Exists(b.Specfile) { 359 return 360 } 361 362 s := silenceSubroutines() 363 specfilePkgs := b.ListSpecfile(true) 364 s.restore() 365 366 // Map whose keys are normalized package names. 367 normSpecfilePkgs := map[api.PkgName]bool{} 368 for name := range specfilePkgs { 369 normSpecfilePkgs[b.NormalizePackageName(name)] = true 370 } 371 372 // Map from normalized package names to original package 373 // names. 374 normPkgs := map[api.PkgName]string{} 375 for _, arg := range args { 376 name := arg 377 norm := b.NormalizePackageName(api.PkgName(arg)) 378 if normSpecfilePkgs[norm] { 379 normPkgs[norm] = name 380 } 381 } 382 383 if upgrade { 384 deleteLockfile(ctx, b) 385 } 386 387 if len(normPkgs) >= 1 { 388 pkgs := map[api.PkgName]bool{} 389 for name := range normPkgs { 390 pkgs[name] = true 391 } 392 b.Remove(ctx, pkgs) 393 } 394 395 if len(normPkgs) == 0 || b.QuirksDoesAddRemoveNotAlsoLock() { 396 didLock := maybeLock(ctx, b, forceLock) 397 398 if !(didLock && b.QuirksDoesLockAlsoInstall()) { 399 maybeInstall(ctx, b, forceInstall) 400 } 401 } else if len(normPkgs) == 0 || b.QuirksDoesAddRemoveNotAlsoInstall() { 402 maybeInstall(ctx, b, forceInstall) 403 } 404 405 store.Read(ctx, b) 406 store.ClearGuesses(ctx, b) 407 store.UpdateFileHashes(ctx, b) 408 store.Write(ctx) 409 } 410 411 // runLock implements 'upm lock'. 412 func runLock(language string, upgrade bool, forceLock bool, forceInstall bool) { 413 span, ctx := trace.StartSpanFromExistingContext("runLock") 414 defer span.Finish() 415 b := backends.GetBackend(ctx, language) 416 417 if upgrade { 418 deleteLockfile(ctx, b) 419 } 420 421 didLock := maybeLock(ctx, b, forceLock) 422 423 if !(didLock && b.QuirksDoesLockAlsoInstall()) { 424 maybeInstall(ctx, b, forceInstall) 425 } 426 427 store.Read(ctx, b) 428 store.UpdateFileHashes(ctx, b) 429 store.Write(ctx) 430 } 431 432 // runInstall implements 'upm install'. 433 func runInstall(language string, force bool) { 434 span, ctx := trace.StartSpanFromExistingContext("runInstall") 435 defer span.Finish() 436 b := backends.GetBackend(ctx, language) 437 438 maybeInstall(ctx, b, force) 439 440 store.Read(ctx, b) 441 store.UpdateFileHashes(ctx, b) 442 store.Write(ctx) 443 } 444 445 // listSpecfileJSONEntry represents one entry in the JSON list emitted 446 // by 'upm list'. 447 type listSpecfileJSONEntry struct { 448 Name string `json:"name"` 449 Spec string `json:"spec"` 450 } 451 452 // listLockfileJSONEntry represents one entry in the JSON list emitted 453 // by 'upm list -a'. 454 type listLockfileJSONEntry struct { 455 Name string `json:"name"` 456 Version string `json:"version"` 457 } 458 459 // runList implements 'upm list'. 460 func runList(language string, all bool, outputFormat outputFormat) { 461 span, ctx := trace.StartSpanFromExistingContext("runList") 462 defer span.Finish() 463 b := backends.GetBackend(ctx, language) 464 if !all { 465 var results map[api.PkgName]api.PkgSpec = nil 466 fileExists := util.Exists(b.Specfile) 467 if fileExists { 468 results = b.ListSpecfile(true) 469 } 470 switch outputFormat { 471 case outputFormatTable: 472 switch { 473 case !fileExists: 474 util.Log("no specfile") 475 return 476 case len(results) == 0: 477 util.Log("no packages in specfile") 478 return 479 } 480 t := table.New("name", "spec") 481 for name, spec := range results { 482 t.AddRow(string(name), string(spec)) 483 } 484 t.SortBy("name") 485 t.Print() 486 487 case outputFormatJSON: 488 j := []listSpecfileJSONEntry{} 489 for name, spec := range results { 490 j = append(j, listSpecfileJSONEntry{ 491 Name: string(name), 492 Spec: string(spec), 493 }) 494 } 495 outputB, err := json.Marshal(j) 496 if err != nil { 497 panic("couldn't marshal json") 498 } 499 fmt.Println(string(outputB)) 500 501 default: 502 util.Panicf("unknown output format %d", outputFormat) 503 } 504 } else { 505 var results map[api.PkgName]api.PkgVersion = nil 506 fileExists := util.Exists(b.Lockfile) 507 if fileExists { 508 results = b.ListLockfile() 509 } 510 switch outputFormat { 511 case outputFormatTable: 512 switch { 513 case !fileExists: 514 util.Log("no lockfile") 515 return 516 case len(results) == 0: 517 util.Log("no packages in lockfile") 518 return 519 } 520 t := table.New("name", "version") 521 for name, version := range results { 522 t.AddRow(string(name), string(version)) 523 } 524 t.SortBy("name") 525 t.Print() 526 527 case outputFormatJSON: 528 j := []listLockfileJSONEntry{} 529 for name, version := range results { 530 j = append(j, listLockfileJSONEntry{ 531 Name: string(name), 532 Version: string(version), 533 }) 534 } 535 outputB, err := json.Marshal(j) 536 if err != nil { 537 panic("couldn't marshal json") 538 } 539 fmt.Println(string(outputB)) 540 541 default: 542 util.Panicf("unknown output format %d", outputFormat) 543 } 544 } 545 } 546 547 // runGuess implements 'upm guess'. 548 func runGuess( 549 language string, all bool, 550 forceGuess bool, ignoredPackages []string) { 551 span, ctx := trace.StartSpanFromExistingContext("runGuess") 552 defer span.Finish() 553 b := backends.GetBackend(ctx, language) 554 guessed := store.GuessWithCache(ctx, b, forceGuess) 555 556 // Map from normalized to original names. 557 normPkgs := map[string][]api.PkgName{} 558 for key, guesses := range guessed { 559 normalized := []api.PkgName{} 560 for _, guess := range guesses { 561 normalized = append(normalized, b.NormalizePackageName(guess)) 562 } 563 normPkgs[key] = normalized 564 } 565 566 if !all { 567 if util.Exists(b.Specfile) { 568 for name := range b.ListSpecfile(true) { 569 name := b.NormalizePackageName(name) 570 for key, pkgs := range normPkgs { 571 for _, pkg := range pkgs { 572 if pkg == name { 573 delete(normPkgs, key) 574 } 575 } 576 } 577 } 578 } 579 } 580 581 for _, ignored := range ignoredPackages { 582 ignored := b.NormalizePackageName(api.PkgName(ignored)) 583 for key, pkgs := range normPkgs { 584 for _, pkg := range pkgs { 585 if pkg == ignored { 586 delete(normPkgs, key) 587 } 588 } 589 } 590 } 591 592 lines := []string{} 593 for _, pkgs := range normPkgs { 594 lines = append(lines, string(pkgs[0])) 595 } 596 sort.Strings(lines) 597 598 for _, line := range lines { 599 fmt.Println(line) 600 } 601 602 store.Write(ctx) 603 } 604 605 // runShowSpecfile implements 'upm show-specfile'. 606 func runShowSpecfile(language string) { 607 fmt.Println(backends.GetBackend(context.Background(), language).Specfile) 608 } 609 610 // runShowLockfile implements 'upm show-lockfile'. 611 func runShowLockfile(language string) { 612 b := backends.GetBackend(context.Background(), language) 613 if b.Lockfile != "" { 614 fmt.Println(b.Lockfile) 615 } 616 } 617 618 // runShowPackageDir implements 'upm show-package-dir'. 619 func runShowPackageDir(language string) { 620 b := backends.GetBackend(context.Background(), language) 621 dir := b.GetPackageDir() 622 if dir != "" { 623 fmt.Println(dir) 624 } 625 } 626 627 // runInstallReplitNixSystemDependencies implements 'upm install-replit-nix-system-dependencies'. 628 func runInstallReplitNixSystemDependencies(language string, args []string) { 629 span, ctx := trace.StartSpanFromExistingContext("runInstallReplitNixSystemDependencies") 630 defer span.Finish() 631 b := backends.GetBackend(ctx, language) 632 normPkgs := b.NormalizePackageArgs(args) 633 pkgs := []api.PkgName{} 634 for p := range normPkgs { 635 pkgs = append(pkgs, p) 636 } 637 b.InstallReplitNixSystemDependencies(ctx, pkgs) 638 }