github.com/whyrusleeping/gx@v0.14.3/gxutil/pm.go (about)

     1  package gxutil
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"os"
     9  	"os/exec"
    10  	"os/user"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	sh "github.com/ipfs/go-ipfs-api"
    18  	mh "github.com/multiformats/go-multihash"
    19  	prog "github.com/whyrusleeping/progmeter"
    20  	. "github.com/whyrusleeping/stump"
    21  )
    22  
    23  const GxVersion = "0.14.2"
    24  
    25  const PkgFileName = "package.json"
    26  const LckFileName = "gx-lock.json"
    27  
    28  var installPathsCache map[string]string
    29  var binarySuffix string
    30  
    31  func init() {
    32  	installPathsCache = make(map[string]string)
    33  
    34  	if runtime.GOOS == "windows" {
    35  		binarySuffix = ".exe"
    36  	}
    37  }
    38  
    39  type PM struct {
    40  	ipfssh *sh.Shell
    41  
    42  	cfg *Config
    43  
    44  	ProgMeter *prog.ProgMeter
    45  
    46  	global bool
    47  
    48  	// hash of the 'empty' ipfs dir to avoid extra calls to object new
    49  	blankDir string
    50  }
    51  
    52  func NewPM(cfg *Config) (*PM, error) {
    53  	sh := NewShell()
    54  	sh.SetTimeout(time.Minute * 8)
    55  	return &PM{
    56  		ipfssh: sh,
    57  		cfg:    cfg,
    58  	}, nil
    59  }
    60  
    61  func GetPackageRoot() (string, error) {
    62  	cwd, err := os.Getwd()
    63  	if err != nil {
    64  		return "", err
    65  	}
    66  
    67  	for cwd != "/" {
    68  		_, err := os.Stat(filepath.Join(cwd, PkgFileName))
    69  		switch {
    70  		case err == nil:
    71  			return cwd, nil
    72  		case os.IsNotExist(err):
    73  			cwd = filepath.Join(cwd, "..")
    74  		default:
    75  			return "", err
    76  		}
    77  	}
    78  
    79  	return "", fmt.Errorf("no package found in this directory or any above")
    80  }
    81  
    82  func (pm *PM) Shell() *sh.Shell {
    83  	if pm.ipfssh == nil {
    84  		pm.ipfssh = NewShell()
    85  		pm.ipfssh.SetTimeout(time.Minute * 8)
    86  	}
    87  
    88  	return pm.ipfssh
    89  }
    90  
    91  func (pm *PM) ShellOnline() bool {
    92  	_, err := pm.Shell().ID()
    93  	return err == nil
    94  }
    95  
    96  func (pm *PM) SetGlobal(g bool) {
    97  	pm.global = g
    98  }
    99  
   100  func maybeRunPostInstall(pkg *Package, pkgdir string, global bool) error {
   101  	dir := filepath.Join(pkgdir, pkg.Name)
   102  	if !pkgRanHook(dir, "post-install") {
   103  		before := time.Now()
   104  		VLog("  - running post install for %s:", pkg.Name, pkgdir)
   105  		args := []string{pkgdir}
   106  		if global {
   107  			args = append(args, "--global")
   108  		}
   109  		err := TryRunHook("post-install", pkg.Language, pkg.SubtoolRequired, args...)
   110  		if err != nil {
   111  			return err
   112  		}
   113  		VLog("  - post install finished in ", time.Since(before))
   114  		err = writePkgHook(dir, "post-install")
   115  		if err != nil {
   116  			return fmt.Errorf("error writing hook log: %s", err)
   117  		}
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  func (pm *PM) InstallPackage(hash, ipath string) (*Package, error) {
   124  	// if its already local, skip it
   125  	pkgdir := filepath.Join(ipath, "gx", "ipfs", hash)
   126  	cpkg := new(Package)
   127  	err := FindPackageInDir(cpkg, pkgdir)
   128  	if err != nil {
   129  		VLog("  - %s not found locally, fetching into %s", hash, pkgdir)
   130  		deppkg, err := pm.GetPackageTo(hash, pkgdir)
   131  		if err != nil {
   132  			return nil, fmt.Errorf("failed to fetch package: %s: %s", hash, err)
   133  		}
   134  		VLog("  - fetch complete!")
   135  		cpkg = deppkg
   136  	}
   137  
   138  	VLog("  - now processing dep %s-%s", cpkg.Name, hash)
   139  	err = pm.InstallDeps(cpkg, ipath)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	if err := maybeRunPostInstall(cpkg, pkgdir, pm.global); err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	return cpkg, nil
   149  }
   150  
   151  func isTempError(err error) bool {
   152  	return strings.Contains(err.Error(), "too many open files")
   153  }
   154  
   155  type DepWork struct {
   156  	CacheDir string
   157  	LinkDir  string
   158  	Dep      string
   159  	Ref      string
   160  }
   161  
   162  // InstallLock recursively installs all dependencies for the given lockfile
   163  func (pm *PM) InstallLock(lck Lock, cwd string) error {
   164  	lockList := []Lock{lck}
   165  
   166  	maxWorkers := 20
   167  	workers := make(chan DepWork, maxWorkers)
   168  
   169  	var wg sync.WaitGroup
   170  	var lk sync.Mutex
   171  	var firstError error
   172  
   173  	for i := 0; i < maxWorkers; i++ {
   174  		wg.Add(1)
   175  		go func() {
   176  			for work := range workers {
   177  				pm.ProgMeter.AddEntry(work.Ref, work.Dep, "[fetch]   <ELAPSED>"+work.Ref)
   178  
   179  				cacheloc := filepath.Join(work.CacheDir, work.Ref)
   180  				linkloc := filepath.Join(work.LinkDir, work.Dep)
   181  
   182  				if err := pm.CacheAndLinkPackage(work.Ref, cacheloc, linkloc); err != nil {
   183  					pm.ProgMeter.Error(work.Ref, err.Error())
   184  
   185  					lk.Lock()
   186  					if firstError == nil {
   187  						firstError = err
   188  					}
   189  					lk.Unlock()
   190  
   191  					continue
   192  				}
   193  
   194  				pm.ProgMeter.Finish(work.Ref)
   195  			}
   196  
   197  			wg.Done()
   198  		}()
   199  	}
   200  
   201  	for {
   202  		if len(lockList) == 0 {
   203  			break
   204  		}
   205  
   206  		curr := lockList[0]
   207  		lockList = lockList[1:]
   208  
   209  		newLocks, err := pm.installLock(curr, cwd, workers)
   210  		if err != nil {
   211  			return err
   212  		}
   213  
   214  		if firstError == nil {
   215  			lockList = append(lockList, newLocks...)
   216  		}
   217  	}
   218  
   219  	close(workers)
   220  	wg.Wait()
   221  
   222  	return firstError
   223  }
   224  
   225  func (pm *PM) installLock(lck Lock, cwd string, workers chan<- DepWork) ([]Lock, error) {
   226  	// Install all the direct dependencies for this lock
   227  
   228  	// Each lock contains a mapping of languages to their own dependencies
   229  	returnList := []Lock{}
   230  
   231  	for lang, langdeps := range lck.Deps {
   232  		ipath, err := InstallPath(lang, cwd, false)
   233  		if err != nil {
   234  			return []Lock{}, err
   235  		}
   236  
   237  		pm.ProgMeter.AddTodos(len(langdeps))
   238  
   239  		for dep, deplock := range langdeps {
   240  			if deplock.Deps != nil {
   241  				returnList = append(returnList, deplock)
   242  			}
   243  
   244  			workers <- DepWork{
   245  				CacheDir: filepath.Join(cwd, ".gx", "cache"),
   246  				LinkDir:  ipath,
   247  				Dep:      dep,
   248  				Ref:      deplock.Ref,
   249  			}
   250  
   251  		}
   252  	}
   253  
   254  	return returnList, nil
   255  }
   256  
   257  func (pm *PM) SetProgMeter(meter *prog.ProgMeter) {
   258  	pm.ProgMeter = meter
   259  }
   260  
   261  func padRight(s string, w int) string {
   262  	if len(s) < w {
   263  		return s + strings.Repeat(" ", len(s)-w)
   264  	}
   265  	return s
   266  }
   267  
   268  func pkgRanHook(dir, hook string) bool {
   269  	p := filepath.Join(dir, ".gx", hook)
   270  	_, err := os.Stat(p)
   271  	return err == nil
   272  }
   273  
   274  func writePkgHook(dir, hook string) error {
   275  	gxdir := filepath.Join(dir, ".gx")
   276  	err := os.MkdirAll(gxdir, 0755)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	fipath := filepath.Join(gxdir, hook)
   282  	fi, err := os.Create(fipath)
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	return fi.Close()
   288  }
   289  
   290  func (pm *PM) InitPkg(dir, name, lang string, setup func(*Package)) error {
   291  	// check for existing packagefile
   292  	p := filepath.Join(dir, PkgFileName)
   293  	_, err := os.Stat(p)
   294  	if err == nil {
   295  		return errors.New("package file already exists in working dir")
   296  	}
   297  
   298  	username := pm.cfg.User.Name
   299  	if username == "" {
   300  		u, err := user.Current()
   301  		if err != nil {
   302  			return fmt.Errorf("error looking up current user: %s", err)
   303  		}
   304  		username = u.Username
   305  	}
   306  
   307  	pkg := &Package{
   308  		PackageBase: PackageBase{
   309  			Name:       name,
   310  			Author:     username,
   311  			Language:   lang,
   312  			Version:    "0.0.0",
   313  			GxVersion:  GxVersion,
   314  			ReleaseCmd: "git commit -a -m \"gx publish $VERSION\"",
   315  		},
   316  	}
   317  
   318  	if setup != nil {
   319  		setup(pkg)
   320  	}
   321  
   322  	// check if the user has a tool installed for the selected language
   323  	CheckForHelperTools(lang)
   324  
   325  	err = SavePackageFile(pkg, p)
   326  	if err != nil {
   327  		return err
   328  	}
   329  
   330  	err = TryRunHook("post-init", lang, pkg.SubtoolRequired, dir)
   331  	return err
   332  }
   333  
   334  func CheckForHelperTools(lang string) {
   335  	p, err := getSubtoolPath(lang)
   336  	if err == nil && p != "" {
   337  		return
   338  	}
   339  
   340  	if p == "" || strings.Contains(err.Error(), "file not found") {
   341  		Log("notice: no helper tool found for", lang)
   342  		return
   343  	}
   344  
   345  	Error("checking for helper tool:", err)
   346  }
   347  
   348  // ImportPackage downloads the package specified by dephash into the package
   349  // in the directory 'dir'
   350  func (pm *PM) ImportPackage(dir, dephash string) (*Dependency, error) {
   351  	pkgpath := filepath.Join(dir, "gx", "ipfs", dephash)
   352  	// check if its already imported
   353  	_, err := os.Stat(pkgpath)
   354  	if err == nil {
   355  		var pkg Package
   356  		err := FindPackageInDir(&pkg, pkgpath)
   357  		if err != nil {
   358  			return nil, fmt.Errorf("dir for package already exists, but no package found:%v", err)
   359  		}
   360  
   361  		return &Dependency{
   362  			Name:    pkg.Name,
   363  			Hash:    dephash,
   364  			Version: pkg.Version,
   365  		}, nil
   366  	}
   367  
   368  	ndep, err := pm.GetPackageTo(dephash, pkgpath)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  
   373  	err = maybeRunPostInstall(ndep, pkgpath, pm.global)
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  
   378  	for _, child := range ndep.Dependencies {
   379  		_, err := pm.ImportPackage(dir, child.Hash)
   380  		if err != nil {
   381  			return nil, err
   382  		}
   383  	}
   384  
   385  	err = TryRunHook("post-import", ndep.Language, ndep.SubtoolRequired, dephash)
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  
   390  	return &Dependency{
   391  		Name:    ndep.Name,
   392  		Hash:    dephash,
   393  		Version: ndep.Version,
   394  	}, nil
   395  }
   396  
   397  // ResolveDepName resolves a given package name to a hash
   398  // using configured repos as a mapping.
   399  func (pm *PM) ResolveDepName(name string) (string, error) {
   400  	_, err := mh.FromB58String(name)
   401  	if err == nil {
   402  		return name, nil
   403  	}
   404  
   405  	if strings.HasPrefix(name, "github.com/") {
   406  		return pm.resolveGithubDep(name)
   407  	}
   408  
   409  	return pm.resolveNameInRepos(name)
   410  }
   411  
   412  func githubRawPath(repo string) string {
   413  	base := strings.Replace(repo, "github.com", "raw.githubusercontent.com", 1)
   414  	return base + "/master"
   415  }
   416  
   417  func (pm *PM) resolveGithubDep(name string) (string, error) {
   418  	resp, err := http.Get("https://" + githubRawPath(name) + "/.gx/lastpubver")
   419  	if err != nil {
   420  		return "", err
   421  	}
   422  	defer resp.Body.Close()
   423  
   424  	switch resp.StatusCode {
   425  	case 200:
   426  		out, err := ioutil.ReadAll(resp.Body)
   427  		if err != nil {
   428  			return "", err
   429  		}
   430  
   431  		parts := strings.Split(string(out), ": ")
   432  		if len(parts) < 2 {
   433  			return "", fmt.Errorf("unrecognized format on .gx/lastpubver")
   434  		}
   435  		VLog("  - resolved %q to %s, version %s", name, parts[1], parts[0])
   436  		return strings.TrimSpace(parts[1]), nil
   437  	case 404:
   438  		return "", fmt.Errorf("no gx package found at %s", name)
   439  	default:
   440  		return "", fmt.Errorf("unrecognized http response from github: %d: %s", resp.StatusCode, resp.Status)
   441  	}
   442  }
   443  
   444  func (pm *PM) resolveNameInRepos(name string) (string, error) {
   445  	if strings.Contains(name, "/") {
   446  		parts := strings.Split(name, "/")
   447  		rpath, ok := pm.cfg.GetRepos()[parts[0]]
   448  		if !ok {
   449  			return "", fmt.Errorf("unknown repo: '%s'", parts[0])
   450  		}
   451  
   452  		pkgs, err := pm.FetchRepo(rpath, true)
   453  		if err != nil {
   454  			return "", err
   455  		}
   456  
   457  		val, ok := pkgs[parts[1]]
   458  		if !ok {
   459  			return "", fmt.Errorf("package %s not found in repo %s", parts[1], parts[0])
   460  		}
   461  
   462  		return val, nil
   463  	}
   464  
   465  	out, err := pm.QueryRepos(name)
   466  	if err != nil {
   467  		return "", err
   468  	}
   469  
   470  	if len(out) == 0 {
   471  		return "", fmt.Errorf("could not find package by name: %s", name)
   472  	}
   473  
   474  	if len(out) == 1 {
   475  		for _, v := range out {
   476  			return v, nil
   477  		}
   478  	}
   479  
   480  	return "", fmt.Errorf("ambiguous ref, appears in multiple repos")
   481  }
   482  
   483  func (pm *PM) EnumerateDependencies(pkg *Package) (map[string]string, error) {
   484  	deps := make(map[string]string)
   485  	err := pm.enumerateDepsRec(pkg, deps)
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  
   490  	return deps, nil
   491  }
   492  
   493  func (pm *PM) enumerateDepsRec(pkg *Package, set map[string]string) error {
   494  	for _, d := range pkg.Dependencies {
   495  		if _, ok := set[d.Hash]; ok {
   496  			continue
   497  		}
   498  
   499  		set[d.Hash] = d.Name
   500  
   501  		var depkg Package
   502  		err := LoadPackage(&depkg, pkg.Language, d.Hash)
   503  		if err != nil {
   504  			if os.IsNotExist(err) {
   505  				return fmt.Errorf("package %s (%s) not found", d.Name, d.Hash)
   506  			}
   507  			return err
   508  		}
   509  
   510  		err = pm.enumerateDepsRec(&depkg, set)
   511  		if err != nil {
   512  			return err
   513  		}
   514  	}
   515  	return nil
   516  }
   517  
   518  type PkgStats struct {
   519  	totalDepth   int
   520  	AverageDepth float64
   521  
   522  	TotalImports int
   523  }
   524  
   525  type DepStats struct {
   526  	TotalCount  int
   527  	TotalUnique int
   528  
   529  	AverageDepth float64
   530  	totalDepth   int
   531  
   532  	Packages map[string]*PkgStats
   533  }
   534  
   535  func (ds *DepStats) Finalize() {
   536  	ds.AverageDepth = float64(ds.totalDepth) / float64(ds.TotalCount)
   537  
   538  	for _, pkg := range ds.Packages {
   539  		pkg.AverageDepth = float64(pkg.totalDepth) / float64(pkg.TotalImports)
   540  	}
   541  }
   542  
   543  func newDepStats() *DepStats {
   544  	return &DepStats{
   545  		Packages: make(map[string]*PkgStats),
   546  	}
   547  }
   548  
   549  func GetDepStats(pkg *Package) (*DepStats, error) {
   550  	ds := newDepStats()
   551  	err := getDepStatsRec(pkg, ds, 1)
   552  	if err != nil {
   553  		return nil, err
   554  	}
   555  
   556  	ds.Finalize()
   557  
   558  	return ds, nil
   559  }
   560  
   561  func getDepStatsRec(pkg *Package, stats *DepStats, depth int) error {
   562  	return pkg.ForEachDep(func(dep *Dependency, dpkg *Package) error {
   563  		stats.TotalCount++
   564  		stats.totalDepth += depth
   565  
   566  		ps, ok := stats.Packages[dep.Hash]
   567  		if !ok {
   568  			stats.TotalUnique++
   569  			ps = new(PkgStats)
   570  			stats.Packages[dep.Hash] = ps
   571  		}
   572  
   573  		ps.totalDepth += depth
   574  		ps.TotalImports++
   575  
   576  		return getDepStatsRec(dpkg, stats, depth+1)
   577  	})
   578  }
   579  
   580  func LocalPackageByName(dir, name string, out interface{}) error {
   581  	if IsHash(name) {
   582  		return FindPackageInDir(out, filepath.Join(dir, name))
   583  	}
   584  
   585  	var local Package
   586  	err := LoadPackageFile(&local, PkgFileName)
   587  	if err != nil {
   588  		return err
   589  	}
   590  
   591  	return resolveDepName(&local, out, dir, name, make(map[string]struct{}))
   592  }
   593  
   594  func LoadPackage(out interface{}, env, hash string) error {
   595  	VLog("  - load package:", hash)
   596  	ipath, err := InstallPath(env, "", true)
   597  	if err != nil {
   598  		return err
   599  	}
   600  
   601  	p := filepath.Join(ipath, "gx", "ipfs", hash)
   602  	err = FindPackageInDir(out, p)
   603  	if err == nil {
   604  		return nil
   605  	}
   606  
   607  	ipath, err = InstallPath(env, "", false)
   608  	if err != nil {
   609  		return err
   610  	}
   611  
   612  	p = filepath.Join(ipath, "gx", "ipfs", hash)
   613  	return FindPackageInDir(out, p)
   614  }
   615  
   616  var ErrUnrecognizedName = fmt.Errorf("unrecognized package name")
   617  
   618  func resolveDepName(pkg *Package, out interface{}, dir, name string, checked map[string]struct{}) error {
   619  	// first check if its a direct dependency of this package
   620  	for _, d := range pkg.Dependencies {
   621  		if d.Name == name {
   622  			return LoadPackageFile(out, filepath.Join(dir, d.Hash, d.Name, PkgFileName))
   623  		}
   624  	}
   625  
   626  	// recurse!
   627  	var dpkg Package
   628  	for _, d := range pkg.Dependencies {
   629  		if _, ok := checked[d.Hash]; ok {
   630  			continue
   631  		}
   632  
   633  		err := LoadPackageFile(&dpkg, filepath.Join(dir, d.Hash, d.Name, PkgFileName))
   634  		if err != nil {
   635  			return err
   636  		}
   637  
   638  		err = resolveDepName(&dpkg, out, dir, name, checked)
   639  		switch err {
   640  		case nil:
   641  			return nil // success!
   642  		case ErrUnrecognizedName:
   643  			checked[d.Hash] = struct{}{}
   644  		default:
   645  			return err
   646  		}
   647  	}
   648  
   649  	return ErrUnrecognizedName
   650  }
   651  func IsSubtoolInstalled(env string) (bool, error) {
   652  	p, err := getSubtoolPath(env)
   653  	if err != nil {
   654  		return false, err
   655  	}
   656  
   657  	return p != "", nil
   658  }
   659  
   660  func getSubtoolPath(env string) (string, error) {
   661  	if env == "" {
   662  		return "", nil
   663  	}
   664  
   665  	binname := "gx-" + env + binarySuffix
   666  	_, err := exec.LookPath(binname)
   667  	if err != nil {
   668  		if eErr, ok := err.(*exec.Error); ok {
   669  			if eErr.Err != exec.ErrNotFound {
   670  				return "", err
   671  			}
   672  		} else {
   673  			return "", err
   674  		}
   675  
   676  		if dir, file := filepath.Split(os.Args[0]); dir != "" {
   677  			fileNoExe := strings.TrimSuffix(file, binarySuffix)
   678  			nearBin := filepath.Join(dir, fileNoExe+"-"+env+binarySuffix)
   679  
   680  			if _, err := os.Stat(nearBin); err != nil {
   681  				VLog("subtool_exec: No gx helper tool found for", env)
   682  				return "", nil
   683  			}
   684  			binname = nearBin
   685  		} else {
   686  			return "", nil
   687  		}
   688  	}
   689  
   690  	return binname, nil
   691  }
   692  
   693  func TryRunHook(hook, env string, req bool, args ...string) error {
   694  	binname, err := getSubtoolPath(env)
   695  	if err != nil {
   696  		return err
   697  	}
   698  
   699  	if binname == "" {
   700  		if req {
   701  			return fmt.Errorf("no binary named gx-%s was found.", env)
   702  		}
   703  		return nil
   704  	}
   705  
   706  	args = append([]string{"hook", hook}, args...)
   707  	cmd := exec.Command(binname, args...)
   708  	cmd.Stderr = os.Stderr
   709  	cmd.Stdout = os.Stdout
   710  	cmd.Stdin = os.Stdin
   711  
   712  	err = cmd.Run()
   713  	if err != nil {
   714  		return fmt.Errorf("%s hook failed: %s", hook, err)
   715  	}
   716  
   717  	return nil
   718  }
   719  
   720  const defaultLocalPath = "vendor"
   721  
   722  func InstallPath(env, relpath string, global bool) (string, error) {
   723  	if env == "" {
   724  		VLog("no env, returning empty install path")
   725  		return defaultLocalPath, nil
   726  	}
   727  
   728  	if cached, ok := checkInstallPathCache(env, global); ok {
   729  		return cached, nil
   730  	}
   731  
   732  	binname, err := getSubtoolPath(env)
   733  	if err != nil {
   734  		return "", err
   735  	}
   736  	if binname == "" {
   737  		return defaultLocalPath, nil
   738  	}
   739  
   740  	args := []string{"hook", "install-path"}
   741  	if global {
   742  		args = append(args, "--global")
   743  	}
   744  	cmd := exec.Command(binname, args...)
   745  
   746  	cmd.Stderr = os.Stderr
   747  	cmd.Dir = relpath
   748  	out, err := cmd.Output()
   749  	if err != nil {
   750  		return "", fmt.Errorf("install-path hook failed: %s", err)
   751  	}
   752  
   753  	val := strings.Trim(string(out), " \t\n")
   754  	setInstallPathCache(env, global, val)
   755  	return val, nil
   756  }
   757  
   758  func checkInstallPathCache(env string, global bool) (string, bool) {
   759  	if global {
   760  		env += " --global"
   761  	}
   762  	v, ok := installPathsCache[env]
   763  	return v, ok
   764  }
   765  
   766  func setInstallPathCache(env string, global bool, val string) {
   767  	if global {
   768  		env += " --global"
   769  	}
   770  
   771  	installPathsCache[env] = val
   772  }
   773  
   774  func IsHash(s string) bool {
   775  	return strings.HasPrefix(s, "Qm") && len(s) == 46
   776  }
   777  
   778  // InstallDeps fetches all dependencies for the given package (in parallel)
   779  // and then calls the `post-install` hook on each one. Those two processes
   780  // are not combined because the rewrite process in the `post-install` hook
   781  // needs all of the dependencies (directs and transitives) of a package to
   782  // compute the rewrite map which enforces a particular order in the traversal
   783  // of the dependency graph and that constraint invalidates the parallel fetch
   784  // in `fetchDependencies` (where the dependencies are processes in the random
   785  // order they are fetched, without consideration for their order in the
   786  // dependency graph).
   787  func (pm *PM) InstallDeps(pkg *Package, location string) error {
   788  	err := pm.fetchDependencies(pkg, location)
   789  	if err != nil {
   790  		return err
   791  	}
   792  
   793  	return pm.dependenciesPostInstall(pkg, location)
   794  }
   795  
   796  // Queue of dependency packages to install. Supported by a slice,
   797  // it's not very performant but the main bottleneck here is the
   798  // fetch operation (`GetPackageTo`).
   799  type DependencyQueue struct {
   800  	// Slice that supports the queue.
   801  	queue []*Dependency
   802  	// Map that keeps track of the dependencies already added to
   803  	// the queue (at some point, may not be in the queue at the
   804  	// moment), accessed by the `Dependency.Hash` (set to `true`
   805  	// if the dependency has already been added).
   806  	added map[string]bool
   807  }
   808  
   809  // NewDependencyQueue creates a new `DependencyQueue` with
   810  // the specified `initialCapacity` for the slice.
   811  func NewDependencyQueue(initialCapacity int) *DependencyQueue {
   812  	return &DependencyQueue{
   813  		queue: make([]*Dependency, 0, initialCapacity),
   814  		added: make(map[string]bool),
   815  	}
   816  }
   817  
   818  // AddPackageDependencies adds all of the dependencies of `pkg`
   819  // to the queue that had not been already added. Return the
   820  // actual number of dependencies added to the queue.
   821  func (dq *DependencyQueue) AddPackageDependencies(pkg *Package) int {
   822  	addedDepCount := 0
   823  	for _, dep := range pkg.Dependencies {
   824  		if dq.added[dep.Hash] == false {
   825  			dq.queue = append(dq.queue, dep)
   826  			addedDepCount++
   827  			dq.added[dep.Hash] = true
   828  		}
   829  	}
   830  	return addedDepCount
   831  }
   832  
   833  // Len returns the number of dependencies currently stored in the queue.
   834  func (dq *DependencyQueue) Len() int {
   835  	return len(dq.queue)
   836  }
   837  
   838  // Pop the first dependency in the queue and return it
   839  // (or `nil` if the queue is empty).
   840  func (dq *DependencyQueue) Pop() *Dependency {
   841  	if dq.Len() == 0 {
   842  		return nil
   843  	}
   844  
   845  	dep := dq.queue[0]
   846  	dq.queue = dq.queue[1:]
   847  	return dep
   848  }
   849  
   850  // Fetch all of the dependencies of this package (direct and transitive
   851  // ones). Use (if possible) `maxGoroutines` goroutines working in parallel
   852  // (coordinated by this function). Each new dependency fetched is another
   853  // package with more (potentially new) dependencies that may also be fetched.
   854  //
   855  // TODO: Depending on the perspective sometimes we use the *package*
   856  // term and others *dependency* (of another package), that should
   857  // be unified and clarified as much as possible (not just in this function).
   858  func (pm *PM) fetchDependencies(pkg *Package, location string) error {
   859  
   860  	// Maximum number of goroutines allowed to run in parallel fetching
   861  	// packages.
   862  	const maxGoroutines = 20
   863  	// TODO: Consider making this value a parameter of the function
   864  	// (or an attribute of the `PM` structure).
   865  
   866  	// Central queue of dependencies that need to be fetched. Handled only
   867  	// by this function. Created with an initial a capacity on the rough
   868  	// estimate of twice the maximum goroutines running.
   869  	depQueue := NewDependencyQueue(maxGoroutines * 2)
   870  
   871  	// List of channels for each spawned goroutine to store either the
   872  	// fetched package or an error. To ensure they are non blocking the
   873  	// maximum number of goroutines it's assigned for their capacity
   874  	// (worst case scenario).
   875  	fetchedPackages := make(chan *Package, maxGoroutines)
   876  	fetchErrs := make(chan error, maxGoroutines)
   877  
   878  	// Save the first fetch error as the function return value,
   879  	// if more errors come after that they will be logged but not
   880  	// returned.
   881  	var firstFetchErr error
   882  
   883  	// To start the process add the dependencies of the root package.
   884  	addedDepCount := depQueue.AddPackageDependencies(pkg)
   885  	pm.ProgMeter.AddTodos(addedDepCount)
   886  
   887  	// Counter to keep track of spawned goroutines, it's not locked as it's
   888  	// only handled by this function. Decremented any time a message is received
   889  	// on the above channels, which indicates that the goroutine has finished.
   890  	activeGoroutines := 0
   891  
   892  	// Main loop of the function.
   893  	for {
   894  		// If there are no more dependencies to install and there aren't any
   895  		// goroutines running (which could potentially add new dependencies
   896  		// to the queue) we're finished.
   897  		if depQueue.Len() == 0 && activeGoroutines == 0 {
   898  			return nil
   899  		}
   900  
   901  		// Keep spawning goroutines until the allowed maximum or until
   902  		// there are no new dependencies to fetch at the moment.
   903  		for activeGoroutines < maxGoroutines && depQueue.Len() > 0 {
   904  			// TODO: Use the worker pattern here (https://gobyexample.com/worker-pools),
   905  			// instead of counting active goroutines we should be counting active jobs.
   906  
   907  			// Goroutine that only calls `GetPackageTo` to fetch the dependency,
   908  			// it either returns it or returns an error.
   909  			go func(dep *Dependency) {
   910  
   911  				pkgDir := filepath.Join(location, "gx", "ipfs", dep.Hash)
   912  				// TODO: Encapsulate in a function. Used in too many places
   913  				// and is part of the standard.
   914  
   915  				pm.ProgMeter.AddEntry(dep.Hash, dep.Name, "[fetch]   <ELAPSED>"+dep.Hash)
   916  				pkg, err := pm.GetPackageTo(dep.Hash, pkgDir)
   917  
   918  				// Either with error or with the package the goroutine ends here.
   919  				if err != nil {
   920  					fetchErrs <- fmt.Errorf("failed to fetch package: %s: %s", dep.Hash, err)
   921  					pm.ProgMeter.Error(dep.Hash, err.Error())
   922  				} else {
   923  					fetchedPackages <- pkg
   924  					pm.ProgMeter.Finish(dep.Hash)
   925  				}
   926  			}(depQueue.Pop())
   927  
   928  			activeGoroutines++
   929  		}
   930  
   931  		// Once all the possible goroutines have been spawned wait
   932  		// for anyone to finish and analyze (restart loop) if more
   933  		// goroutines can be called.
   934  		select {
   935  		case fetchedPkg := <-fetchedPackages:
   936  			VLog("fetched dep: %s", fetchedPkg.Name)
   937  			addedDepCount := depQueue.AddPackageDependencies(fetchedPkg)
   938  			pm.ProgMeter.AddTodos(addedDepCount)
   939  		case firstFetchErr = <-fetchErrs:
   940  			Error("parallel fetch: %s", firstFetchErr)
   941  		}
   942  		activeGoroutines--
   943  
   944  		if firstFetchErr != nil {
   945  			break
   946  			// An error happened inside a fetch goroutine, stop the main `for`,
   947  			// do not order more fetches.
   948  			// TODO: If `GetPackageTo` or the `shell.Get()` function had a `Context`
   949  			// it would be useful to issue a `cancel()` here before returning.
   950  		}
   951  	}
   952  
   953  	// We broke out of the `for`, at least one error was detected in the
   954  	// fetch operations, wait for the rest of the goroutines to finish.
   955  	for activeGoroutines > 0 {
   956  		select {
   957  		case err := <-fetchErrs:
   958  			Error("parallel fetch: %s", err)
   959  		case _ = <-fetchedPackages:
   960  		}
   961  		activeGoroutines--
   962  	}
   963  
   964  	return firstFetchErr
   965  }
   966  
   967  // Call the `post-install` hook on each of the dependencies of this package
   968  // (direct or transitive).
   969  //
   970  // TODO: This function could also use the same parallel goroutine processing
   971  // structure of `fetchDependencies` but right now the `post-install` hook
   972  // of the only sub-tool (`gx-go rewrite`) already does a parallel processing
   973  // of its own, so there's little to gain here.
   974  func (pm *PM) dependenciesPostInstall(pkg *Package, location string) error {
   975  	depQueue := NewDependencyQueue(len(pkg.Dependencies) * 2)
   976  
   977  	addedDepCount := depQueue.AddPackageDependencies(pkg)
   978  	pm.ProgMeter.AddTodos(addedDepCount)
   979  
   980  	for {
   981  		dep := depQueue.Pop()
   982  		if dep == nil {
   983  			return nil
   984  			// No more dependencies to process
   985  		}
   986  
   987  		hash := dep.Hash
   988  		pkgdir := filepath.Join(location, "gx", "ipfs", hash)
   989  		// TODO: Encapsulate in a function.
   990  
   991  		pkg := new(Package)
   992  		err := FindPackageInDir(pkg, pkgdir)
   993  		if err != nil {
   994  			return err
   995  		}
   996  
   997  		pm.ProgMeter.AddEntry(dep.Hash, dep.Name, "[install] <ELAPSED>"+dep.Hash)
   998  		pm.ProgMeter.Working(dep.Hash, "work")
   999  		if err := maybeRunPostInstall(pkg, pkgdir, pm.global); err != nil {
  1000  			pm.ProgMeter.Error(dep.Hash, err.Error())
  1001  			return err
  1002  		}
  1003  		pm.ProgMeter.Finish(dep.Hash)
  1004  
  1005  		// Add the dependencies of this package to the queue.
  1006  		addedDepCount := depQueue.AddPackageDependencies(pkg)
  1007  		pm.ProgMeter.AddTodos(addedDepCount)
  1008  	}
  1009  }