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  }