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 }