diff --git a/batch_test.go b/batch_test.go new file mode 100644 index 0000000..cd57c6f --- /dev/null +++ b/batch_test.go @@ -0,0 +1,473 @@ +package main + +import ( + "os" + "strings" + "sync" + "testing" + "time" +) + +// TestPkgInstallManyBatchesDnf verifies a single batched dnf call rather +// than one per package. +func TestPkgInstallManyBatchesDnf(t *testing.T) { + defer resetMocks() + pkgMgr = "dnf" + + var calls [][]string + runCmd = func(argv []string, _ CmdOpts) CmdResult { + calls = append(calls, append([]string(nil), argv...)) + return CmdResult{ExitCode: 0} + } + + failed := pkgInstallMany([]string{"git", "curl", "vim"}) + if len(failed) != 0 { + t.Errorf("expected no failures, got %v", failed) + } + if len(calls) != 1 { + t.Fatalf("expected exactly 1 batched call, got %d: %v", len(calls), calls) + } + got := strings.Join(calls[0], " ") + if !strings.HasPrefix(got, "dnf install -y") { + t.Errorf("expected 'dnf install -y …' prefix, got: %q", got) + } + for _, pkg := range []string{"git", "curl", "vim"} { + if !strings.Contains(got, pkg) { + t.Errorf("expected %s in batched call, got: %q", pkg, got) + } + } +} + +// TestPkgInstallManyBatchesApt verifies the same for apt-get. +func TestPkgInstallManyBatchesApt(t *testing.T) { + defer resetMocks() + pkgMgr = "apt-get" + + var calls [][]string + runCmd = func(argv []string, _ CmdOpts) CmdResult { + calls = append(calls, append([]string(nil), argv...)) + return CmdResult{ExitCode: 0} + } + + pkgInstallMany([]string{"a", "b", "c"}) + if len(calls) != 1 { + t.Fatalf("expected 1 batched call, got %d", len(calls)) + } + if calls[0][0] != "apt-get" || calls[0][1] != "install" || calls[0][2] != "-y" { + t.Errorf("expected 'apt-get install -y' prefix, got: %v", calls[0]) + } +} + +// TestPkgInstallManyBatchesPacman verifies pacman flags. +func TestPkgInstallManyBatchesPacman(t *testing.T) { + defer resetMocks() + pkgMgr = "pacman" + + var calls [][]string + runCmd = func(argv []string, _ CmdOpts) CmdResult { + calls = append(calls, append([]string(nil), argv...)) + return CmdResult{ExitCode: 0} + } + + pkgInstallMany([]string{"a", "b"}) + if len(calls) != 1 || calls[0][0] != "pacman" { + t.Fatalf("expected single pacman call, got %v", calls) + } + joined := strings.Join(calls[0], " ") + if !strings.Contains(joined, "--noconfirm") || !strings.Contains(joined, "--needed") { + t.Errorf("expected --noconfirm --needed in pacman call, got: %q", joined) + } +} + +// TestPkgInstallManyFallback verifies that a failed batch retries per-package +// and returns the failures it identifies on the per-package retry. +func TestPkgInstallManyFallback(t *testing.T) { + defer resetMocks() + pkgMgr = "dnf" + + calls := 0 + runCmd = func(argv []string, _ CmdOpts) CmdResult { + calls++ + // Fail the first (batched) call, succeed individual retries except for "bad". + if calls == 1 { + return CmdResult{ExitCode: 1} + } + for _, a := range argv { + if a == "bad" { + return CmdResult{ExitCode: 1} + } + } + return CmdResult{ExitCode: 0} + } + + failed := pkgInstallMany([]string{"good1", "good2", "bad"}) + if len(failed) != 1 || failed[0] != "bad" { + t.Errorf("expected only 'bad' to fail, got %v", failed) + } + // 1 batch + 3 per-package retries = 4 calls. + if calls != 4 { + t.Errorf("expected 4 total calls (1 batch + 3 retries), got %d", calls) + } +} + +// TestPkgInstallManyEmpty: no-op on empty input, no calls. +func TestPkgInstallManyEmpty(t *testing.T) { + defer resetMocks() + pkgMgr = "dnf" + called := false + runCmd = func(argv []string, _ CmdOpts) CmdResult { + called = true + return CmdResult{ExitCode: 0} + } + failed := pkgInstallMany(nil) + if len(failed) != 0 { + t.Errorf("expected no failures, got %v", failed) + } + if called { + t.Error("expected no runCmd call for empty input") + } +} + +// TestPkgInstallManyBrew uses parallel per-package install (no batching at +// the CLI level because brew formula installs may have their own preferences). +func TestPkgInstallManyBrew(t *testing.T) { + defer resetMocks() + pkgMgr = "brew" + + var mu sync.Mutex + var seen []string + runCmd = func(argv []string, _ CmdOpts) CmdResult { + mu.Lock() + // last arg is the package name for `brew install ` + seen = append(seen, argv[len(argv)-1]) + mu.Unlock() + if argv[len(argv)-1] == "broken" { + return CmdResult{ExitCode: 1} + } + return CmdResult{ExitCode: 0} + } + + failed := pkgInstallMany([]string{"git", "broken", "curl"}) + if len(failed) != 1 || failed[0] != "broken" { + t.Errorf("expected only 'broken' to fail, got %v", failed) + } + if len(seen) != 3 { + t.Errorf("expected 3 brew calls (one per pkg), got %d: %v", len(seen), seen) + } +} + +// TestInstallFlatpakBatched: a single batched flatpak install for the +// happy path. +func TestInstallFlatpakBatched(t *testing.T) { + defer resetMocks() + + hasCmd = func(name string) bool { return name == "flatpak" } + var calls [][]string + runCmd = func(argv []string, _ CmdOpts) CmdResult { + calls = append(calls, append([]string(nil), argv...)) + return CmdResult{ExitCode: 0} + } + + installFlatpakPackages([]string{"a.app", "b.app", "c.app"}) + + // Expect: remote-add (1) + single batched install (1) = 2 calls. + if len(calls) != 2 { + t.Fatalf("expected 2 calls (remote-add + batched install), got %d: %v", len(calls), calls) + } + if calls[1][1] != "install" { + t.Errorf("expected install as second call, got %v", calls[1]) + } + for _, app := range []string{"a.app", "b.app", "c.app"} { + found := false + for _, a := range calls[1] { + if a == app { + found = true + break + } + } + if !found { + t.Errorf("expected %s in batched call, got %v", app, calls[1]) + } + } +} + +// TestInstallFlatpakBatchFallback: failed batch retries per-package. +func TestInstallFlatpakBatchFallback(t *testing.T) { + defer resetMocks() + + hasCmd = func(name string) bool { return name == "flatpak" } + calls := 0 + runCmd = func(argv []string, _ CmdOpts) CmdResult { + calls++ + // First call: remote-add (always OK) + // Second call: batched install (fail) + // Following calls: per-package retries (OK) + if calls == 2 { + return CmdResult{ExitCode: 1} + } + return CmdResult{ExitCode: 0} + } + + installFlatpakPackages([]string{"a.app", "b.app"}) + + // 1 (remote-add) + 1 (failed batch) + 2 (per-package retries) = 4 calls. + if calls != 4 { + t.Errorf("expected 4 calls (remote-add + batch + 2 retries), got %d", calls) + } +} + +// TestCheckSystemPackagesParallelOrdering: ordering preserved despite +// concurrent probes. +func TestCheckSystemPackagesParallelOrdering(t *testing.T) { + defer resetMocks() + pkgMgr = "dnf" + + // odd-indexed packages "installed", even-indexed "not installed" + probe = func(argv []string, _ time.Duration) (CmdResult, bool) { + pkg := argv[len(argv)-1] + // pkg-0..pkg-7 + idx := pkg[len(pkg)-1] - '0' + if idx%2 == 1 { + return CmdResult{ExitCode: 0}, true // installed + } + return CmdResult{ExitCode: 1}, true // not installed + } + + names := []string{"pkg-0", "pkg-1", "pkg-2", "pkg-3", "pkg-4", "pkg-5", "pkg-6", "pkg-7"} + res := checkSystemPackages(names) + + wantToInstall := []string{"pkg-0", "pkg-2", "pkg-4", "pkg-6"} + wantAlready := []string{"pkg-1", "pkg-3", "pkg-5", "pkg-7"} + if !equalStringSlices(res.toInstallRegular, wantToInstall) { + t.Errorf("toInstall: want %v, got %v", wantToInstall, res.toInstallRegular) + } + if !equalStringSlices(res.alreadyInstalled, wantAlready) { + t.Errorf("alreadyInstalled: want %v, got %v", wantAlready, res.alreadyInstalled) + } +} + +func TestCheckCustomPackagesParallel(t *testing.T) { + defer resetMocks() + + // pkg with InstallPath /tmp/foo-N; "installed" iff N is odd. + osStat = func(name string) (os.FileInfo, error) { + // Map: name like /tmp/foo-1 → installed; /tmp/foo-0 → not. + idx := name[len(name)-1] - '0' + if idx%2 == 1 { + return nil, nil + } + return nil, os.ErrNotExist + } + + pkgs := []*CustomPackage{ + {Name: "p0", InstallPath: "/tmp/foo-0"}, + {Name: "p1", InstallPath: "/tmp/foo-1"}, + {Name: "p2", InstallPath: "/tmp/foo-2"}, + {Name: "p3", InstallPath: "/tmp/foo-3"}, + } + res := checkCustomPackages(pkgs) + + if len(res.toInstall) != 2 || res.toInstall[0].Name != "p0" || res.toInstall[1].Name != "p2" { + t.Errorf("toInstall: want p0,p2 in order, got %v", names(res.toInstall)) + } + if len(res.alreadyInstalled) != 2 || res.alreadyInstalled[0].pkg.Name != "p1" || res.alreadyInstalled[1].pkg.Name != "p3" { + t.Errorf("already: want p1,p3 in order, got %v", customNames(res.alreadyInstalled)) + } +} + +func TestInstallNpmToolsBatchSinglePnpmCall(t *testing.T) { + defer resetMocks() + + // Pretend ~/.nvm exists so NVM check passes. + osStat = func(name string) (os.FileInfo, error) { + if strings.HasSuffix(name, ".nvm") { + return nil, nil + } + return nil, os.ErrNotExist + } + + var shellCalls []string + runShell = func(cmd string, _ CmdOpts) CmdResult { + shellCalls = append(shellCalls, cmd) + return CmdResult{ExitCode: 0} + } + + pkgs := []*CustomPackage{ + {Name: "claude"}, + {Name: "codex"}, + {Name: "copilot"}, + } + installNpmToolsBatch(pkgs) + + // Expect exactly one pnpm add -g call containing all three packages. + addCalls := 0 + for _, c := range shellCalls { + if strings.Contains(c, "pnpm add -g") { + addCalls++ + if !strings.Contains(c, "@anthropic-ai/claude-code") || + !strings.Contains(c, "@openai/codex") || + !strings.Contains(c, "@github/copilot") { + t.Errorf("expected all three npm names in batched call, got: %q", c) + } + } + } + if addCalls != 1 { + t.Errorf("expected exactly 1 batched pnpm add call, got %d (all calls: %v)", addCalls, shellCalls) + } +} + +func TestInstallNpmToolsBatchFallback(t *testing.T) { + defer resetMocks() + osStat = func(name string) (os.FileInfo, error) { + if strings.HasSuffix(name, ".nvm") { + return nil, nil + } + return nil, os.ErrNotExist + } + + calls := 0 + runShell = func(cmd string, _ CmdOpts) CmdResult { + calls++ + // Fail the first (batched) pnpm add call; succeed thereafter. + if calls == 1 && strings.Contains(cmd, "pnpm add -g") { + return CmdResult{ExitCode: 1} + } + return CmdResult{ExitCode: 0} + } + + pkgs := []*CustomPackage{ + {Name: "claude"}, + {Name: "codex"}, + } + installNpmToolsBatch(pkgs) + + // 1 ensureNodeLTS + 1 batch + 2 per-package retries (each may emit + // 2 shell calls: ensureNodeLTS again + add). Just sanity-check that + // retries happened. + if calls < 3 { + t.Errorf("expected at least 3 shell calls after batch failure, got %d", calls) + } +} + +func TestInstallCustomPackagesWavesIndependentFirst(t *testing.T) { + defer resetMocks() + + // Make hasCmd / osStat permissive. ~/.nvm must "exist" so the + // npm-batch path doesn't bail out at its precondition check. + hasCmd = func(name string) bool { return true } + osStat = func(name string) (os.FileInfo, error) { + if strings.HasSuffix(name, ".nvm") { + return nil, nil + } + return nil, os.ErrNotExist + } + osReadFile = func(name string) ([]byte, error) { + return []byte{}, os.ErrNotExist + } + + var orderMu sync.Mutex + var order []string + runCmd = func(argv []string, _ CmdOpts) CmdResult { + orderMu.Lock() + order = append(order, strings.Join(argv, " ")) + orderMu.Unlock() + return CmdResult{ExitCode: 0} + } + runShell = func(cmd string, _ CmdOpts) CmdResult { + orderMu.Lock() + order = append(order, cmd) + orderMu.Unlock() + return CmdResult{ExitCode: 0} + } + download = func(_, _ string) bool { return true } + fetchJSON = func(_ string, _ any) bool { return false } + fetchText = func(_ string) string { return "" } + + // Mix of independent + node-dependent. We just verify dispatch order: + // the npm-batched call must appear after some Wave A activity. + pkgs := []*CustomPackage{ + {Name: "agy"}, + {Name: "oh-my-zsh"}, + {Name: "claude"}, + {Name: "codex"}, + } + installCustomPackages(pkgs) + + // The pnpm add -g call must exist and appear after agy/oh-my-zsh + // install attempts. + var firstBatchIdx, firstWaveAIdx int = -1, -1 + for i, c := range order { + if strings.Contains(c, "pnpm add -g @anthropic-ai/claude-code") { + firstBatchIdx = i + } + if (strings.Contains(c, "antigravity.google") || strings.Contains(c, "ohmyzsh")) && firstWaveAIdx == -1 { + firstWaveAIdx = i + } + } + if firstWaveAIdx == -1 { + t.Errorf("expected to see Wave A activity (agy/oh-my-zsh), got order: %v", order) + } + if firstBatchIdx == -1 { + t.Errorf("expected to see batched pnpm add call, got order: %v", order) + } + if firstWaveAIdx > firstBatchIdx { + t.Errorf("expected Wave A activity to begin before Wave B batch, got waveA@%d batch@%d", firstWaveAIdx, firstBatchIdx) + } +} + +func TestResolveLatestAllRunsInParallel(t *testing.T) { + defer resetMocks() + + // Register a custom resolver that records start order. + var mu sync.Mutex + var starts []string + + latestResolvers["test-fast"] = func(p *CustomPackage) (string, string, bool) { + mu.Lock() + starts = append(starts, p.Name) + mu.Unlock() + return p.Version, p.SHA256, true + } + defer delete(latestResolvers, "test-fast") + + pkgs := []*CustomPackage{ + {Name: "x", Version: "1", SHA256: "a", FetchLatest: "test-fast"}, + {Name: "y", Version: "2", SHA256: "b", FetchLatest: "test-fast"}, + {Name: "z", Version: "3", SHA256: "c", FetchLatest: "test-fast"}, + } + resolveLatestAll(pkgs) + + if len(starts) != 3 { + t.Errorf("expected all 3 resolvers invoked, got %d: %v", len(starts), starts) + } +} + +// ── small helpers/fakes ──────────────────────────────────────────────── + +func equalStringSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func names(pkgs []*CustomPackage) []string { + out := make([]string, len(pkgs)) + for i, p := range pkgs { + out[i] = p.Name + } + return out +} + +func customNames(s []customStatus) []string { + out := make([]string, len(s)) + for i, st := range s { + out[i] = st.pkg.Name + } + return out +} diff --git a/check.go b/check.go index 904c702..4eed6e0 100644 --- a/check.go +++ b/check.go @@ -3,6 +3,7 @@ package main import ( "fmt" "strings" + "sync" ) type systemCheckResult struct { @@ -33,6 +34,28 @@ type customStatus struct { path string } +// parallelPartition runs check(item) over items concurrently (using the +// configured cpuWorkers pool) and returns the items where check returned +// true first, then those where it returned false — both in input order. +// We preserve input order so the displayed package lists stay stable. +func parallelPartition[T any](items []T, check func(T) bool) (truthy, falsy []T) { + if len(items) == 0 { + return nil, nil + } + results := make([]bool, len(items)) + parallelDo(items, cpuWorkers(), func(i int, item T) { + results[i] = check(item) + }) + for i, item := range items { + if results[i] { + truthy = append(truthy, item) + } else { + falsy = append(falsy, item) + } + } + return +} + func checkSystemPackages(names []string) systemCheckResult { overrides := packageOverrides[pkgMgr] resolved, skipped := resolveSystemPkgs(names) @@ -54,22 +77,8 @@ func checkSystemPackages(names []string) systemCheckResult { } } - var toR, alreadyR []string - for _, p := range regular { - if isSystemPkgInstalled(p) { - alreadyR = append(alreadyR, p) - } else { - toR = append(toR, p) - } - } - var toS, alreadyS []string - for _, p := range special { - if isSpecialPkgInstalled(p) { - alreadyS = append(alreadyS, p) - } else { - toS = append(toS, p) - } - } + alreadyR, toR := parallelPartition(regular, isSystemPkgInstalled) + alreadyS, toS := parallelPartition(special, isSpecialPkgInstalled) return systemCheckResult{ toInstallRegular: toR, toInstallSpecial: toS, @@ -80,24 +89,25 @@ func checkSystemPackages(names []string) systemCheckResult { } func checkFlatpakPackages(ids []string) flatpakCheckResult { - var to, already []string - for _, p := range ids { - if isFlatpakInstalled(p) { - already = append(already, p) - } else { - to = append(to, p) - } - } + already, to := parallelPartition(ids, isFlatpakInstalled) return flatpakCheckResult{toInstall: to, alreadyInstalled: already} } func checkCustomPackages(pkgs []*CustomPackage) customCheckResult { + type result struct { + installed bool + path string + } + results := make([]result, len(pkgs)) + parallelDo(pkgs, cpuWorkers(), func(i int, p *CustomPackage) { + installed, path := isCustomPkgInstalled(p) + results[i] = result{installed: installed, path: path} + }) var to []*CustomPackage var already []customStatus - for _, p := range pkgs { - installed, path := isCustomPkgInstalled(p) - if installed { - already = append(already, customStatus{pkg: p, path: path}) + for i, p := range pkgs { + if results[i].installed { + already = append(already, customStatus{pkg: p, path: results[i].path}) } else { to = append(to, p) } @@ -105,6 +115,43 @@ func checkCustomPackages(pkgs []*CustomPackage) customCheckResult { return customCheckResult{toInstall: to, alreadyInstalled: already} } +// checkAllInParallel runs the three check passes concurrently. The caller +// must still gate which checks to run via *only; we accept already-prepared +// inputs and skip when the corresponding slice/conditional indicates no work. +func checkAllInParallel( + runSys bool, sysPkgs []string, + runFlat bool, flatPkgs []string, + runCust bool, customPkgs []*CustomPackage, +) (systemCheckResult, flatpakCheckResult, customCheckResult) { + var sys systemCheckResult + var flat flatpakCheckResult + var cust customCheckResult + var wg sync.WaitGroup + if runSys { + wg.Add(1) + go func() { + defer wg.Done() + sys = checkSystemPackages(sysPkgs) + }() + } + if runFlat { + wg.Add(1) + go func() { + defer wg.Done() + flat = checkFlatpakPackages(flatPkgs) + }() + } + if runCust { + wg.Add(1) + go func() { + defer wg.Done() + cust = checkCustomPackages(customPkgs) + }() + } + wg.Wait() + return sys, flat, cust +} + func fmtList(items []string, limit int) string { if len(items) <= limit { return strings.Join(items, " ") diff --git a/coverage.out b/coverage.out deleted file mode 100644 index 94c0675..0000000 --- a/coverage.out +++ /dev/null @@ -1,1049 +0,0 @@ -mode: set -github.com/JMR-dev/bootstrap_dev_env/check.go:36.60,41.26 4 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:41.26,42.45 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:42.45,44.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:47.2,49.29 3 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:49.29,50.18 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:50.18,52.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:52.9,54.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:57.2,58.28 2 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:58.28,59.30 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:59.30,61.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:61.9,63.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:65.2,66.28 2 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:66.28,67.31 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:67.31,69.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:69.9,71.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:73.2,79.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:82.60,84.24 2 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:84.24,85.28 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:85.28,87.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:87.9,89.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:91.2,91.69 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:94.67,97.25 3 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:97.25,99.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:99.16,101.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:101.9,103.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:105.2,105.68 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:108.48,109.25 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:109.25,111.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:112.2,112.91 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:115.113,118.36 2 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:118.36,123.18 5 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:123.18,125.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:126.3,127.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:127.12,131.4 3 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:132.3,132.27 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:132.27,134.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:135.3,135.28 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:135.28,137.35 2 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:137.35,139.5 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:140.4,140.75 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:142.3,142.13 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:145.2,145.102 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:145.102,147.37 2 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:147.37,149.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:150.3,150.30 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:150.30,152.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:153.3,153.31 1 0 -github.com/JMR-dev/bootstrap_dev_env/check.go:156.2,156.36 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:156.36,158.43 2 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:158.43,160.20 2 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:160.20,162.5 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:163.4,163.65 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:165.3,165.36 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:165.36,168.18 3 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:168.18,170.5 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:171.4,171.61 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:173.3,173.31 1 1 -github.com/JMR-dev/bootstrap_dev_env/check.go:176.2,176.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:16.45,17.25 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:17.25,19.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:20.2,20.44 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:23.51,24.31 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:24.31,26.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:27.2,27.50 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:30.49,31.20 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:31.20,33.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:34.2,34.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:34.24,36.36 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:36.36,38.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:40.2,40.11 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:43.46,44.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:44.21,46.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:47.2,47.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:61.34,62.31 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:62.31,64.17 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:64.17,66.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:68.2,68.10 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:71.52,72.65 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:72.65,74.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:75.2,75.11 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:78.26,79.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:79.24,81.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:82.2,83.30 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:89.50,90.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:90.21,91.51 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:91.51,93.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:94.3,94.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:96.2,97.16 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:97.16,99.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:100.2,101.22 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:101.22,103.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:104.2,105.22 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:105.22,107.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:108.2,108.18 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:111.62,113.19 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:113.19,115.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:116.2,116.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:116.22,118.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:119.2,119.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:119.21,121.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:122.2,122.23 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:122.23,124.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:125.2,126.15 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:126.15,128.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:129.2,129.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:129.15,131.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:132.2,133.41 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:133.41,135.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:136.2,136.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:142.61,143.54 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:143.54,145.17 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:145.17,148.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:149.3,149.25 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:149.25,152.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:153.3,154.14 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:156.2,156.52 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:156.52,158.33 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:158.33,160.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:161.3,161.26 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:161.26,164.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:165.3,166.28 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:166.28,168.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:169.3,169.35 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:169.35,172.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:173.3,173.31 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:175.2,175.13 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:178.41,180.15 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:180.15,182.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:183.2,183.32 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:183.32,185.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:186.2,186.28 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:186.28,190.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:191.2,191.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:196.32,198.42 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:198.42,201.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:202.2,204.46 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:207.46,208.74 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:208.74,211.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:212.2,213.74 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:213.74,214.33 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:214.33,216.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:217.3,218.46 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:218.46,220.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:221.3,221.75 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:221.75,223.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:224.3,224.19 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:224.19,226.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:227.3,227.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:229.2,229.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:229.18,232.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:233.2,236.53 4 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:239.53,242.42 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:242.42,244.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:245.2,249.28 4 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:249.28,250.18 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:250.18,252.9 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:255.2,257.73 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:260.50,262.84 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:262.84,264.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:265.2,270.28 5 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:270.28,271.38 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:271.38,273.9 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:276.2,276.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:276.18,279.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:280.2,280.49 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:280.49,283.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:284.2,286.47 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:286.47,288.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:289.2,290.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:290.16,293.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:294.2,294.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:294.24,297.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:298.2,309.80 10 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:314.63,316.54 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:316.54,318.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:320.2,330.77 4 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:330.77,332.54 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:332.54,334.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:335.3,335.33 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:337.2,339.19 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:339.19,341.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:342.2,343.33 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:343.33,344.73 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:344.73,346.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:348.2,348.22 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:351.72,352.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:352.13,354.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:355.2,356.102 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:356.102,358.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:359.2,360.19 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:360.19,362.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:363.2,365.31 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:365.31,366.29 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:366.29,368.17 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:368.17,370.5 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:371.4,371.48 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:374.2,374.22 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:379.64,381.66 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:381.66,383.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:384.2,385.22 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:385.22,386.51 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:386.51,388.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:390.2,390.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:390.22,392.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:393.2,393.41 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:393.41,395.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:396.2,399.9 4 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:399.9,401.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:402.2,403.15 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:403.15,405.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:406.2,406.27 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:409.33,412.46 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:412.46,415.15 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:415.15,416.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:416.15,418.5 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:419.4,419.12 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:422.2,422.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:433.40,435.9 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:435.9,437.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:438.2,439.15 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:439.15,440.31 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:440.31,443.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:445.2,446.12 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:446.12,450.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:451.2,451.28 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:451.28,454.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:455.2,458.28 4 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:463.56,466.32 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:466.32,470.22 4 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:470.22,472.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:473.3,474.39 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:474.39,476.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:478.3,478.39 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:478.39,480.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:483.3,483.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:484.14,487.12 3 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:488.16,490.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:491.14,493.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:494.20,496.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:497.17,499.18 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:499.18,501.13 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:503.4,505.12 3 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:506.14,508.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:509.17,511.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:512.16,514.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:515.18,517.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:520.3,523.16 3 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:523.16,525.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:527.3,527.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:527.22,528.12 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:531.3,532.17 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:532.17,534.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:536.3,537.30 2 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:537.30,539.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:541.3,541.35 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:541.35,543.12 2 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:545.3,545.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:546.13,547.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:548.22,549.36 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:550.14,551.28 1 1 -github.com/JMR-dev/bootstrap_dev_env/custom.go:552.11,553.75 1 0 -github.com/JMR-dev/bootstrap_dev_env/custom.go:555.3,555.19 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:24.13,28.2 3 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:30.24,31.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:32.15,33.17 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:34.16,35.17 1 0 -github.com/JMR-dev/bootstrap_dev_env/detect.go:36.10,39.12 3 0 -github.com/JMR-dev/bootstrap_dev_env/detect.go:43.26,44.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:45.15,46.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:47.15,48.19 1 0 -github.com/JMR-dev/bootstrap_dev_env/detect.go:49.10,52.12 3 0 -github.com/JMR-dev/bootstrap_dev_env/detect.go:75.49,86.2 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:88.25,89.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:89.26,91.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:92.2,92.17 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:95.42,97.39 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:97.39,98.31 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:98.31,100.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:102.2,102.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:105.42,107.46 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:107.46,108.31 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:108.31,110.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:112.2,112.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:118.42,120.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:120.16,122.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:123.2,124.57 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:124.57,125.38 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:125.38,127.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:129.2,129.11 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:132.30,134.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:134.16,136.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:137.2,138.57 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:138.57,139.76 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:139.76,143.4 3 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:145.2,146.27 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:146.27,147.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:147.14,149.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:151.2,151.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:154.30,156.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:156.16,158.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:159.2,160.57 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:160.57,161.76 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:161.76,165.4 3 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:167.2,168.27 2 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:168.27,169.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:169.14,171.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/detect.go:173.2,173.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:38.30,38.72 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:41.56,42.23 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:42.23,44.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:45.2,45.38 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:45.38,47.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:48.2,54.20 5 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:54.20,56.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:57.2,57.23 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:57.23,59.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:61.2,62.18 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:62.18,65.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:65.8,68.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:70.2,73.43 3 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:73.43,78.3 4 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:79.2,79.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:79.16,81.31 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:81.31,85.4 3 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:86.3,88.16 3 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:90.2,90.12 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:95.55,96.23 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:96.23,98.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:99.2,105.20 5 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:105.20,107.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:108.2,108.23 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:108.23,110.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:112.2,113.18 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:113.18,116.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:116.8,119.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:121.2,124.43 3 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:124.43,129.3 4 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:130.2,130.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:130.16,132.31 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:132.31,136.4 3 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:137.3,139.16 3 0 -github.com/JMR-dev/bootstrap_dev_env/exec.go:141.2,141.12 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:145.35,148.2 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:153.72,154.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:154.18,156.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:157.2,167.43 9 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:167.43,170.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:171.2,171.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:171.16,173.31 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:173.31,176.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:177.3,178.20 2 1 -github.com/JMR-dev/bootstrap_dev_env/exec.go:180.2,180.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/flatpak.go:5.49,8.24 2 1 -github.com/JMR-dev/bootstrap_dev_env/flatpak.go:8.24,10.46 2 1 -github.com/JMR-dev/bootstrap_dev_env/flatpak.go:10.46,13.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/flatpak.go:14.3,15.38 2 1 -github.com/JMR-dev/bootstrap_dev_env/flatpak.go:15.38,18.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/flatpak.go:21.2,26.34 2 1 -github.com/JMR-dev/bootstrap_dev_env/flatpak.go:26.34,29.16 3 1 -github.com/JMR-dev/bootstrap_dev_env/flatpak.go:29.16,31.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:21.34,26.2 4 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:28.25,28.50 1 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:29.25,29.51 1 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:31.25,35.2 3 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:37.26,39.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:39.16,41.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/issues.go:42.2,42.62 1 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:45.20,48.22 3 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:48.22,51.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:52.2,57.66 6 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:57.66,60.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/issues.go:61.2,61.64 1 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:64.21,67.23 3 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:67.23,69.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:70.2,71.28 2 1 -github.com/JMR-dev/bootstrap_dev_env/issues.go:71.28,73.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:17.26,18.27 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:18.27,20.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:21.2,21.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:24.23,25.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:25.14,27.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:28.2,29.29 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:29.29,32.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:33.2,37.6 5 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:37.6,39.28 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:39.28,40.9 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:42.3,42.30 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:44.2,44.51 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:47.23,48.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:48.14,50.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:51.2,51.20 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:51.20,55.3 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:56.2,58.42 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:58.42,61.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:63.2,65.44 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:65.44,68.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:70.2,78.91 4 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:98.21,101.2 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:103.23,104.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:104.14,106.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:107.2,108.28 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:108.28,110.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:111.2,112.13 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:112.13,114.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:115.2,117.16 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:117.16,119.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:120.2,120.10 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:123.35,124.39 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:124.39,126.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:127.2,128.28 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:128.28,130.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:131.2,133.13 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:133.13,135.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:136.2,138.69 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:138.69,140.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:141.2,141.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:141.22,143.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:144.2,145.10 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:148.31,149.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:149.14,151.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:152.2,152.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:152.26,155.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:156.2,158.29 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:158.29,161.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:162.2,163.14 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:163.14,165.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:166.2,167.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:167.16,169.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:170.2,178.11 9 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:181.80,184.19 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:184.19,186.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:187.2,190.61 4 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:190.61,192.17 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:192.17,193.12 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:195.3,195.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:195.15,198.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:200.2,201.31 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:201.31,204.16 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:204.16,205.12 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:207.3,211.29 5 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:211.29,212.12 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:214.3,214.59 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:216.2,216.8 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:219.56,221.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:221.16,223.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:224.2,227.14 4 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:227.14,230.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:231.2,234.16 4 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:234.16,237.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:238.2,238.41 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:238.41,241.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:242.2,243.13 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:246.53,247.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:247.21,249.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:250.2,251.92 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:288.55,289.51 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:289.51,291.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:292.2,293.98 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:293.98,295.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:296.2,297.82 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:300.49,307.2 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:309.36,351.2 9 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:353.80,354.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:354.18,356.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:357.2,369.9 4 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:369.9,371.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:372.2,372.10 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:375.63,378.34 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:378.34,379.49 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:379.49,382.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:383.3,383.30 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:385.2,385.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:388.73,391.34 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:391.34,392.85 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:392.85,395.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:396.3,396.31 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:398.2,398.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:401.52,437.2 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:439.52,443.45 4 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:443.45,445.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:446.2,448.35 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:448.35,450.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:450.8,451.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:451.21,453.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:453.9,455.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:457.2,458.67 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:461.30,462.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:462.26,464.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:465.2,466.82 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:466.82,470.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:471.2,471.27 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:471.27,474.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:475.2,475.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:478.58,479.25 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:479.25,481.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:482.2,490.13 7 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:490.13,491.48 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:491.48,493.109 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:493.109,496.5 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:497.4,497.95 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:499.3,507.22 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:507.22,510.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:511.3,532.16 5 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:533.8,535.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:537.2,547.19 4 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:550.27,551.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:551.14,553.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:554.2,555.19 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:555.19,557.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:559.2,565.51 6 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:565.51,567.102 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:567.102,570.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:573.2,574.41 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:574.41,576.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:576.8,579.10 3 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:579.10,582.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:583.3,585.50 3 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:585.50,588.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:589.3,589.52 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:589.52,592.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:593.3,594.25 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:594.25,597.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:600.2,603.16 4 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:603.16,606.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:607.2,607.73 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:607.73,610.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:611.2,612.37 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:612.37,615.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:617.2,618.23 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:618.23,619.37 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:619.37,622.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:623.3,624.39 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:625.8,627.24 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:627.24,629.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:632.2,633.52 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:633.52,636.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:638.2,638.43 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:638.43,641.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:643.2,643.54 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:643.54,647.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/macos.go:649.2,654.23 5 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:654.23,656.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/macos.go:656.8,658.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:32.13,34.2 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:36.29,44.15 7 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:45.41,45.41 0 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:46.10,49.9 3 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:52.2,58.24 6 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:58.24,61.66 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:61.66,62.12 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:64.3,64.12 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:64.12,66.81 2 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:66.81,67.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:70.3,70.46 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:73.2,76.11 4 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:76.11,78.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:80.2,80.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:80.13,85.3 3 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:87.2,89.11 2 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:89.11,91.32 2 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:91.32,92.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:92.24,94.5 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:94.10,96.5 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:98.3,99.26 2 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:99.26,101.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:102.3,102.20 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:105.2,111.38 5 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:111.38,113.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:114.2,114.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:114.15,116.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:117.2,117.38 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:117.38,119.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:121.2,123.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:123.16,127.3 3 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:129.2,129.76 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:129.76,133.3 3 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:135.2,137.38 2 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:137.38,140.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:142.2,142.15 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:142.15,144.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:146.2,147.38 2 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:147.38,150.44 3 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:150.44,152.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:155.2,155.17 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:155.17,158.24 3 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:158.24,160.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:163.2,163.20 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:163.20,166.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:168.2,174.19 6 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:174.19,175.42 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:175.42,178.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:182.18,183.23 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:183.23,184.14 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:184.14,189.4 3 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:190.3,190.9 1 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:192.2,192.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:192.21,196.3 3 0 -github.com/JMR-dev/bootstrap_dev_env/main.go:197.2,199.21 3 1 -github.com/JMR-dev/bootstrap_dev_env/main.go:199.21,203.3 3 0 -github.com/JMR-dev/bootstrap_dev_env/net.go:21.42,24.16 3 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:24.16,27.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:28.2,29.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:29.16,32.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:33.2,34.30 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:34.30,37.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:38.2,39.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:39.16,42.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:43.2,44.49 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:44.49,47.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/net.go:48.2,48.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:53.44,55.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:55.16,58.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:59.2,60.49 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:60.49,62.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/net.go:63.2,64.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:64.16,67.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:68.2,69.30 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:69.30,72.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:73.2,73.61 1 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:73.61,76.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:77.2,77.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:81.39,83.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:83.16,86.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:87.2,88.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:88.16,91.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:92.2,93.30 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:93.30,96.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:97.2,98.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:98.16,101.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:102.2,102.37 1 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:105.44,107.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:107.16,109.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:110.2,112.41 3 1 -github.com/JMR-dev/bootstrap_dev_env/net.go:112.41,114.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/net.go:115.2,115.44 1 1 -github.com/JMR-dev/bootstrap_dev_env/packages.go:101.39,144.2 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:12.19,16.2 3 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:18.28,19.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:19.13,23.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:24.2,24.59 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:24.59,25.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:25.18,27.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:29.2,31.11 3 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:44.35,44.71 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:45.45,47.2 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:182.61,185.28 3 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:185.28,187.10 2 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:187.10,189.12 2 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:191.3,191.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:191.14,193.12 2 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:195.3,195.49 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:197.2,197.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:201.44,202.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:203.13,205.31 2 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:206.17,208.74 2 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:209.16,211.31 2 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:212.14,213.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:213.22,215.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:216.3,216.56 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:216.56,218.29 2 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:218.29,220.5 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:222.3,222.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:224.2,224.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:227.44,228.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:228.24,230.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/pkgmgr.go:231.2,232.30 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:19.21,21.64 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:21.64,24.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:25.2,25.46 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:28.30,29.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:29.24,31.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:32.2,33.30 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:36.31,37.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:38.17,39.86 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:40.13,41.82 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:42.16,43.95 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:45.2,45.27 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:48.19,49.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:49.24,52.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:53.2,53.25 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:53.25,55.26 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:55.26,57.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:57.9,63.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:66.2,68.21 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:68.21,69.17 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:70.18,72.96 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:72.96,75.5 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:76.17,78.110 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:78.110,81.5 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:82.11,84.10 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:87.2,89.19 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:89.19,91.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:94.49,96.9 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:96.9,99.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:100.2,100.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:100.21,103.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:104.2,107.61 4 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:107.61,109.15 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:109.15,110.12 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:112.3,115.13 4 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:115.13,117.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:119.2,119.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:119.24,121.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:122.2,122.43 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:122.43,123.37 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:123.37,125.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:126.3,126.37 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:126.37,128.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:129.3,129.39 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:131.2,132.47 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:138.43,141.44 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:141.44,143.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:144.2,145.44 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:145.44,148.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:150.2,151.18 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:151.18,154.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:156.2,157.9 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:157.9,159.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:160.2,161.30 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:161.30,162.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:162.18,165.69 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:165.69,167.5 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:168.4,168.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:172.2,176.12 5 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:176.12,181.14 4 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:181.14,183.25 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:183.25,185.24 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:185.24,187.6 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:188.5,188.73 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:190.4,190.10 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:192.3,193.40 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:193.40,196.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:197.3,197.98 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:199.2,199.12 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:204.19,206.81 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:206.81,208.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:209.2,210.19 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:210.19,213.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:214.2,216.78 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:216.78,219.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:220.2,220.55 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:223.22,225.63 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:225.63,227.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:228.2,231.43 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:231.43,233.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:233.8,235.87 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:235.87,238.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:239.3,239.42 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:242.2,243.111 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:243.111,245.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:247.2,248.191 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:248.191,250.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:255.23,256.20 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:256.20,259.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:260.2,260.20 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:260.20,263.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:264.2,266.42 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:266.42,268.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:268.8,271.43 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:271.43,274.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:277.2,279.16 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:279.16,282.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:283.2,286.26 4 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:286.26,288.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:288.8,290.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:291.2,291.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:291.21,292.68 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:292.68,295.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:296.3,296.52 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:297.8,299.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:304.28,305.42 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:305.42,307.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:308.2,308.42 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:308.42,310.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:311.2,311.11 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:314.25,315.20 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:315.20,318.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:319.2,320.84 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:320.84,321.56 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:321.56,323.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:325.2,326.20 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:326.20,329.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:330.2,331.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:331.16,334.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:335.2,336.24 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:336.24,339.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:341.2,342.18 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:342.18,344.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:344.8,344.25 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:344.25,346.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:346.8,346.20 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:346.20,348.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:349.2,352.18 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:352.18,354.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:354.8,356.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:357.2,357.46 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:357.46,359.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:359.8,361.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:367.40,369.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:369.16,371.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:372.2,372.57 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:372.57,374.21 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:374.21,375.12 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:377.3,377.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:377.22,379.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:381.2,381.11 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:384.24,391.45 5 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:391.45,394.7 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:394.7,396.52 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:396.52,397.10 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:399.4,399.7 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:401.3,402.53 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:402.53,405.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:406.3,406.72 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:409.2,416.75 6 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:416.75,419.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:420.2,420.28 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:420.28,423.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:424.2,424.63 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:427.24,430.2 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:432.25,433.19 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:433.19,436.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:437.2,437.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:437.18,440.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:441.2,441.84 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:441.84,443.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:444.2,445.15 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:445.15,448.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:452.32,456.33 4 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:456.33,459.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:460.2,461.22 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:464.19,466.94 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:466.94,468.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/post.go:471.40,473.63 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:473.63,476.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:477.2,479.105 3 1 -github.com/JMR-dev/bootstrap_dev_env/post.go:479.105,481.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/repos.go:9.43,10.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:10.26,11.38 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:11.38,13.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:15.2,15.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:18.62,25.2 3 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:27.24,28.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:29.13,30.56 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:30.56,32.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/repos.go:33.3,34.86 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:35.17,36.60 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:36.60,38.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/repos.go:39.3,43.51 5 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:43.51,45.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/repos.go:46.3,65.63 6 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:70.20,71.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:72.13,73.53 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:73.53,75.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:76.3,77.78 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:78.17,79.64 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:79.64,81.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:82.3,91.63 3 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:96.24,97.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:97.26,100.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:101.2,101.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:102.13,103.60 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:103.60,105.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:106.3,110.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/repos.go:111.17,112.67 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:112.67,114.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:115.3,123.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:127.25,128.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:128.26,131.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:132.2,132.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:133.13,134.54 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:134.54,136.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:137.3,141.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/repos.go:142.17,143.61 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:143.61,145.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:146.3,154.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:158.25,159.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:160.13,161.55 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:161.55,163.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:164.3,168.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:169.17,170.62 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:170.62,172.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:173.3,182.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:186.24,188.25 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:188.25,190.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:191.2,194.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:194.4,196.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:197.2,208.3 4 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:216.31,217.46 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:217.46,219.27 2 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:219.27,221.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:222.3,222.11 1 1 -github.com/JMR-dev/bootstrap_dev_env/repos.go:224.2,232.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:13.36,14.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:14.13,16.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:17.2,21.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:39.45,41.32 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:41.32,41.74 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:42.2,42.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:43.18,44.43 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:45.18,46.65 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:47.17,48.84 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:49.16,50.58 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:51.14,52.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:53.16,54.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:56.2,56.34 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:72.39,74.87 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:74.87,76.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:77.2,78.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:79.13,80.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:81.17,82.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:83.10,85.9 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:87.2,90.36 3 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:90.36,92.36 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:92.36,94.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:95.3,96.32 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:96.32,97.30 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:97.30,99.10 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:102.3,102.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:102.15,104.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:105.3,105.35 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:105.35,107.33 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:107.33,108.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:108.15,110.11 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:113.4,113.41 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:113.41,115.5 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:117.3,117.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:120.2,121.28 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:121.28,122.34 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:122.34,124.9 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:127.2,127.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:127.18,130.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:131.2,132.47 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:132.47,134.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:135.2,136.25 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:136.25,138.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:139.2,139.75 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:142.30,143.26 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:143.26,146.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:147.2,147.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:148.13,150.71 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:150.71,152.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:153.3,153.72 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:154.17,156.70 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:156.70,158.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:159.3,159.76 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:160.10,161.58 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:165.34,167.99 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:167.99,169.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:170.2,173.36 3 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:173.36,175.41 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:175.41,177.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:178.3,179.32 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:179.32,180.30 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:180.30,182.10 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:185.3,185.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:185.15,187.4 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:188.3,188.33 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:188.33,190.33 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:190.33,191.15 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:191.15,193.11 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:196.4,196.41 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:196.41,198.5 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:200.3,200.14 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:203.2,204.28 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:204.28,205.34 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:205.34,207.9 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:210.2,210.18 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:210.18,213.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:214.2,215.47 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:215.47,217.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:218.2,221.66 4 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:224.34,228.30 4 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:228.30,230.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:231.2,233.19 3 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:233.19,235.3 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:236.2,238.16 3 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:238.16,241.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:242.2,242.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:242.24,245.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:246.2,250.57 5 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:253.31,256.44 3 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:256.44,258.71 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:258.71,261.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:262.8,264.113 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:264.113,267.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:269.2,269.85 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:269.85,272.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:273.2,274.94 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:277.32,279.19 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:279.19,282.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:283.2,288.39 6 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:288.39,290.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:292.2,293.21 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:293.21,296.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:297.2,298.54 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:298.54,299.58 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:299.58,301.9 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:304.2,304.20 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:304.20,307.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:308.2,309.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:309.16,312.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:313.2,313.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:313.24,316.3 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:317.2,326.66 8 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:329.28,330.24 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:330.24,333.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:334.2,335.20 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:335.20,337.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:337.8,339.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:342.30,343.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:343.21,346.3 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:347.2,347.58 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:350.41,351.13 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:352.24,353.28 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:354.14,355.19 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:356.18,357.23 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:358.18,359.23 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:360.17,361.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:362.16,363.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:364.14,365.19 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:366.16,367.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:373.39,374.16 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:375.16,376.97 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:377.14,378.21 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:378.21,380.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:381.3,381.61 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:382.10,383.79 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:388.55,391.22 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:391.22,392.31 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:392.31,394.17 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:394.17,396.5 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:399.3,399.9 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:402.2,404.30 3 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:404.30,405.28 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:405.28,406.39 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:406.39,410.5 3 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:414.2,414.30 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:414.30,416.16 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:416.16,418.4 1 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:421.2,421.22 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:421.22,423.17 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:423.17,426.4 2 0 -github.com/JMR-dev/bootstrap_dev_env/system.go:427.3,428.31 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:428.31,431.4 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:437.49,439.13 2 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:439.13,441.3 1 1 -github.com/JMR-dev/bootstrap_dev_env/system.go:442.2,443.60 2 1 diff --git a/custom.go b/custom.go index 6e087c3..f92d8f9 100644 --- a/custom.go +++ b/custom.go @@ -154,7 +154,7 @@ func verifyArchive(archive string, pkg *CustomPackage) bool { errLog(fmt.Sprintf("SHA256 mismatch for %s: expected %s, got %s", pkg.Name, expected, actual)) return false } - fmt.Println(" SHA256 OK") + taskPrintln(" SHA256 OK") return true } if sigURL := pkg.resolveSHA256URL(); sigURL != "" { @@ -170,11 +170,11 @@ func verifyArchive(archive string, pkg *CustomPackage) bool { if pkg.MinisignKey != "" { cmd = append(cmd, "-P", pkg.MinisignKey) } - if !runCmd(cmd, CmdOpts{}).OK() { + if !runCmd(cmd, CmdOpts{Out: taskOut()}).OK() { errLog(fmt.Sprintf("minisign verification failed for %s", pkg.Name)) return false } - fmt.Println(" minisign OK") + taskPrintln(" minisign OK") } return true } @@ -198,18 +198,20 @@ func urlArchOK(pkg *CustomPackage) bool { // ── per-package install handlers ──────────────────────────────────────── func installGo(archive string) { + out := taskOut() goRoot := "/usr/local/go" if _, err := osStat(goRoot); err == nil { - fmt.Printf(" Removing existing Go at %s ...\n", goRoot) - runCmd([]string{"rm", "-rf", goRoot}, CmdOpts{AsSudo: true}) + taskPrintf(" Removing existing Go at %s ...\n", goRoot) + runCmd([]string{"rm", "-rf", goRoot}, CmdOpts{AsSudo: true, Out: out}) } - runCmd([]string{"tar", "-C", "/usr/local", "-xzf", archive}, CmdOpts{AsSudo: true}) + runCmd([]string{"tar", "-C", "/usr/local", "-xzf", archive}, CmdOpts{AsSudo: true, Out: out}) appendProfileLine("local_go", "export PATH=$PATH:/usr/local/go/bin") - fmt.Printf(" Go installed to %s\n", goRoot) + taskPrintf(" Go installed to %s\n", goRoot) } func installFirecracker(archive, tmp string) { - if !runCmd([]string{"tar", "-C", tmp, "-xzf", archive}, CmdOpts{}).OK() { + out := taskOut() + if !runCmd([]string{"tar", "-C", tmp, "-xzf", archive}, CmdOpts{Out: out}).OK() { errLog("firecracker tar extraction failed") return } @@ -239,33 +241,35 @@ func installFirecracker(archive, tmp string) { return } dest := "/usr/local/bin/firecracker" - runCmd([]string{"cp", binary, dest}, CmdOpts{AsSudo: true}) - runCmd([]string{"chmod", "755", dest}, CmdOpts{AsSudo: true}) - fmt.Printf(" firecracker installed to %s\n", dest) + runCmd([]string{"cp", binary, dest}, CmdOpts{AsSudo: true, Out: out}) + runCmd([]string{"chmod", "755", dest}, CmdOpts{AsSudo: true, Out: out}) + taskPrintf(" firecracker installed to %s\n", dest) } func installZig(pkg *CustomPackage, archive string) { + out := taskOut() parent := "/usr/local" zigDir := filepath.Join(parent, "zig-"+pkg.Version) if _, err := osStat(zigDir); err == nil { - runCmd([]string{"rm", "-rf", zigDir}, CmdOpts{AsSudo: true}) + runCmd([]string{"rm", "-rf", zigDir}, CmdOpts{AsSudo: true, Out: out}) } - runCmd([]string{"tar", "-C", parent, "-xJf", archive}, CmdOpts{AsSudo: true}) + runCmd([]string{"tar", "-C", parent, "-xJf", archive}, CmdOpts{AsSudo: true, Out: out}) pattern := filepath.Join(parent, fmt.Sprintf("zig-%s-%s*", archName, osZig[osName])) matches, _ := filepath.Glob(pattern) for _, m := range matches { if m != zigDir { - runCmd([]string{"mv", m, zigDir}, CmdOpts{AsSudo: true}) + runCmd([]string{"mv", m, zigDir}, CmdOpts{AsSudo: true, Out: out}) break } } symlink := "/usr/local/bin/zig" - runCmd([]string{"ln", "-sf", filepath.Join(zigDir, "zig"), symlink}, CmdOpts{AsSudo: true}) - fmt.Printf(" Zig installed to %s, symlinked at %s\n", zigDir, symlink) + runCmd([]string{"ln", "-sf", filepath.Join(zigDir, "zig"), symlink}, CmdOpts{AsSudo: true, Out: out}) + taskPrintf(" Zig installed to %s, symlinked at %s\n", zigDir, symlink) } func installNeovim(_ *CustomPackage, tmp string) { + out := taskOut() var rel ghRelease if !fetchJSON("https://api.github.com/repos/neovim/neovim/releases/latest", &rel) { return @@ -303,18 +307,18 @@ func installNeovim(_ *CustomPackage, tmp string) { errLog(fmt.Sprintf("Neovim SHA256 mismatch: expected %s, got %s", expected, actual)) return } - fmt.Println(" SHA256 OK") + taskPrintln(" SHA256 OK") installDir := fmt.Sprintf("/opt/nvim-%s-%s", osTok, archTok) - fmt.Println(" Extracting Neovim to /opt ...") - runCmd([]string{"mkdir", "-p", "/opt"}, CmdOpts{AsSudo: true}) - runCmd([]string{"rm", "-rf", installDir}, CmdOpts{AsSudo: true}) - runCmd([]string{"tar", "-C", "/opt", "-xzf", dest}, CmdOpts{AsSudo: true}) + taskPrintln(" Extracting Neovim to /opt ...") + runCmd([]string{"mkdir", "-p", "/opt"}, CmdOpts{AsSudo: true, Out: out}) + runCmd([]string{"rm", "-rf", installDir}, CmdOpts{AsSudo: true, Out: out}) + runCmd([]string{"tar", "-C", "/opt", "-xzf", dest}, CmdOpts{AsSudo: true, Out: out}) - runCmd([]string{"mkdir", "-p", "/usr/local/bin"}, CmdOpts{AsSudo: true}) + runCmd([]string{"mkdir", "-p", "/usr/local/bin"}, CmdOpts{AsSudo: true, Out: out}) symlink := "/usr/local/bin/nvim" - runCmd([]string{"ln", "-sf", filepath.Join(installDir, "bin", "nvim"), symlink}, CmdOpts{AsSudo: true}) - fmt.Printf(" Neovim installed to %s, symlinked at %s\n", installDir, symlink) + runCmd([]string{"ln", "-sf", filepath.Join(installDir, "bin", "nvim"), symlink}, CmdOpts{AsSudo: true, Out: out}) + taskPrintf(" Neovim installed to %s, symlinked at %s\n", installDir, symlink) } // ── latest-version resolvers ──────────────────────────────────────────── @@ -443,7 +447,7 @@ func resolveLatest(pkg *CustomPackage) { if !ok { return } - fmt.Printf(" Checking latest version for %s ...\n", pkg.Name) + taskPrintf(" Checking latest version for %s ...\n", pkg.Name) defer func() { if r := recover(); r != nil { warn(fmt.Sprintf("%s: latest-version lookup panicked %v; falling back to pinned version %s", @@ -457,115 +461,241 @@ func resolveLatest(pkg *CustomPackage) { return } if version == pkg.Version { - fmt.Printf(" Pinned version %s is already the latest.\n", pkg.Version) + taskPrintf(" Pinned version %s is already the latest.\n", pkg.Version) return } - fmt.Printf(" Latest is %s (pinned was %s); using latest.\n", version, pkg.Version) + taskPrintf(" Latest is %s (pinned was %s); using latest.\n", version, pkg.Version) pkg.Version = version pkg.SHA256 = strings.ToLower(sha) pkg.SHA256URLTemplate = "" // prefer the freshly resolved sha256 } +// resolveLatestAll fetches latest versions for all packages with a +// FetchLatest hint in parallel — three small HTTP calls today, but enough to +// matter on slower connections. Each lookup is independent and idempotent. +func resolveLatestAll(pkgs []*CustomPackage) { + var withLatest []*CustomPackage + for _, p := range pkgs { + if _, ok := latestResolvers[p.FetchLatest]; ok { + withLatest = append(withLatest, p) + } + } + if len(withLatest) == 0 { + return + } + parallelDo(withLatest, httpWorkers(), func(_ int, p *CustomPackage) { + resolveLatest(p) + }) +} + // ── orchestration ─────────────────────────────────────────────────────── +// Dependency map for custom packages: +// +// nvm → claude, codex, copilot, playwright (need node from nvm) +// (none) → go, firecracker, zig, neovim, pyenv, pip, oh-my-zsh, agy, +// gh-repo-bootstrap (independent) +// +// Within "independent", we further split: +// +// Wave A (parallel, idempotent on disk targets that don't overlap): +// go, firecracker, zig, neovim, pyenv, pip, oh-my-zsh, agy, nvm, +// gh-repo-bootstrap +// +// Wave B (after Wave A; needs nvm/node to exist): +// claude, codex, copilot, playwright — batched into one pnpm call +// +// We parallelize Wave A up to cpuWorkers(). Each install runs under its own +// taskOutput so output stays grouped per-package. Wave B runs after Wave A +// has produced ~/.nvm; it batches the npm tools into a single `pnpm add -g` +// call (single Node startup, single pnpm dep solve). + +func nodeDependentPkgs() map[string]bool { + return map[string]bool{ + "claude": true, + "codex": true, + "copilot": true, + "playwright": true, + } +} + +// runOneCustomInstall executes a single custom package's install handler. +// The caller is responsible for setting up the goroutine-local task output +// when running in parallel. extracted from the old switch statement. +func runOneCustomInstall(pkg *CustomPackage) { + name := strings.ToLower(pkg.Name) + _, checkPath := isCustomPkgInstalled(pkg) + extra := "" + if checkPath != "" { + extra = fmt.Sprintf(" (install path: %s)", checkPath) + } + taskPrintf("\n Installing %s ...%s\n", pkg.displayName(), extra) + if checkPath == "" && name != "pip" { + warn(fmt.Sprintf("%s: no known install path — script will not detect future installs", pkg.Name)) + } + + if name == "firecracker" && isMacOS { + warn(fmt.Sprintf("%s: Linux-only — skipping on macOS", pkg.Name)) + return + } + + switch name { + case "nvm": + installNVM() + return + case "pyenv": + installPyenv() + return + case "pip": + installPip() + return + case "oh-my-zsh": + installOhMyZsh() + return + case "neovim": + tmp, err := os.MkdirTemp("", "bootstrap-nvim-") + if err != nil { + errLog(fmt.Sprintf("neovim tmp dir failed: %v", err)) + return + } + installNeovim(pkg, tmp) + osRemoveAll(tmp) + return + case "agy": + installAgy() + return + case "gh-repo-bootstrap": + installGHExtension("JMR-dev/gh-repo-bootstrap") + return + } + + url := pkg.resolveURL() + if url == "" { + warn(fmt.Sprintf("No URL or install handler for '%s' — skipping", pkg.Name)) + return + } + if !urlArchOK(pkg) { + return + } + + tmp, err := os.MkdirTemp("", "bootstrap-custom-") + if err != nil { + errLog(fmt.Sprintf("tmp dir failed for %s: %v", pkg.Name, err)) + return + } + defer osRemoveAll(tmp) + archive := filepath.Join(tmp, filepath.Base(url)) + if !download(url, archive) { + return + } + if !verifyArchive(archive, pkg) { + return + } + switch name { + case "go": + installGo(archive) + case "firecracker": + installFirecracker(archive, tmp) + case "zig": + installZig(pkg, archive) + default: + warn(fmt.Sprintf("No install handler for '%s' — skipping", pkg.Name)) + } +} + +// installNpmToolsBatch installs all npm-based CLI tools (claude, codex, +// copilot, playwright) in a single `pnpm add -g` invocation. This is +// significantly faster than per-tool installs because pnpm only resolves +// the dep graph and starts Node once. On batch failure we fall back to +// per-package installs so we can report exactly which tool broke. +// +// playwright is special: after the npm install we still need to provision +// browsers via `pnpx playwright install`. We do that after the batch. +func installNpmToolsBatch(pkgs []*CustomPackage) { + if len(pkgs) == 0 { + return + } + home, _ := os.UserHomeDir() + if _, err := osStat(filepath.Join(home, ".nvm")); err != nil { + errLog("NVM is not installed — cannot install npm-based tools") + return + } + ensureNodeLTS() + + npmNames := map[string]string{ + "claude": "@anthropic-ai/claude-code", + "codex": "@openai/codex", + "copilot": "@github/copilot", + "playwright": "playwright", + } + + var npmPkgs []string + var hasPlaywright bool + for _, p := range pkgs { + n := strings.ToLower(p.Name) + if pkg, ok := npmNames[n]; ok { + npmPkgs = append(npmPkgs, pkg) + if n == "playwright" { + hasPlaywright = true + } + } + } + if len(npmPkgs) == 0 { + return + } + + fmt.Printf("\n Installing %d npm tool(s) via pnpm in one batch ...\n", len(npmPkgs)) + addCmd := fmt.Sprintf(`bash -c '%ssource ~/.nvm/nvm.sh && pnpm add -g %s'`, + pnpmEnvPrefix(), strings.Join(npmPkgs, " ")) + if !runShell(addCmd, CmdOpts{}).OK() { + warn("Batched pnpm add -g failed; retrying per-package to isolate failures ...") + for _, p := range pkgs { + n := strings.ToLower(p.Name) + if pkg, ok := npmNames[n]; ok { + installNpmPackage(pkg) + } + } + } + + if hasPlaywright { + installPlaywrightBrowsers() + } +} + func installCustomPackages(toInstall []*CustomPackage) { fmt.Println("\n=== Custom Packages ===") - ensureNodeLTS() - for _, pkg := range toInstall { - name := strings.ToLower(pkg.Name) - _, checkPath := isCustomPkgInstalled(pkg) - extra := "" - if checkPath != "" { - extra = fmt.Sprintf(" (install path: %s)", checkPath) - } - fmt.Printf("\n Installing %s ...%s\n", pkg.displayName(), extra) - if checkPath == "" && name != "pip" { - warn(fmt.Sprintf("%s: no known install path — script will not detect future installs", pkg.Name)) - } - - if name == "firecracker" && isMacOS { - warn(fmt.Sprintf("%s: Linux-only — skipping on macOS", pkg.Name)) - continue - } - - switch name { - case "nvm": - installNVM() - ensureNodeLTS() - continue - case "pyenv": - installPyenv() - continue - case "pip": - installPip() - continue - case "oh-my-zsh": - installOhMyZsh() - continue - case "neovim": - tmp, err := os.MkdirTemp("", "bootstrap-nvim-") - if err != nil { - errLog(fmt.Sprintf("neovim tmp dir failed: %v", err)) - continue - } - installNeovim(pkg, tmp) - osRemoveAll(tmp) - continue - case "agy": - installAgy() - continue - case "claude": - installNpmPackage("@anthropic-ai/claude-code") - continue - case "codex": - installNpmPackage("@openai/codex") - continue - case "copilot": - installNpmPackage("@github/copilot") - continue - case "playwright": - installPlaywright() - continue - case "gh-repo-bootstrap": - installGHExtension("JMR-dev/gh-repo-bootstrap") - continue - } - - resolveLatest(pkg) - - url := pkg.resolveURL() - if url == "" { - warn(fmt.Sprintf("No URL or install handler for '%s' — skipping", pkg.Name)) - continue - } - if !urlArchOK(pkg) { - continue - } - - tmp, err := os.MkdirTemp("", "bootstrap-custom-") - if err != nil { - errLog(fmt.Sprintf("tmp dir failed for %s: %v", pkg.Name, err)) - continue - } - archive := filepath.Join(tmp, filepath.Base(url)) - if !download(url, archive) { - osRemoveAll(tmp) - continue - } - if !verifyArchive(archive, pkg) { - osRemoveAll(tmp) - continue - } - switch name { - case "go": - installGo(archive) - case "firecracker": - installFirecracker(archive, tmp) - case "zig": - installZig(pkg, archive) - default: - warn(fmt.Sprintf("No install handler for '%s' — skipping", pkg.Name)) - } - osRemoveAll(tmp) + if len(toInstall) == 0 { + return } + + // Fetch latest versions for all to-install packages in parallel up + // front — small HTTP calls but they add up serially on slow links. + resolveLatestAll(toInstall) + + // Split into independent (Wave A) vs node-dependent (Wave B). + nodeDeps := nodeDependentPkgs() + var waveA, waveB []*CustomPackage + for _, p := range toInstall { + if nodeDeps[strings.ToLower(p.Name)] { + waveB = append(waveB, p) + } else { + waveA = append(waveA, p) + } + } + + // Wave A: parallel up to cpuWorkers(). Each package's output is buffered + // to a per-task taskOutput and flushed on completion so that concurrent + // installs don't interleave on stdout. + parallelDo(waveA, cpuWorkers(), func(_ int, pkg *CustomPackage) { + tOut := newCapturedOutput(pkg.Name) + withTaskOutput(tOut, func() { + runOneCustomInstall(pkg) + }) + tOut.Flush(os.Stdout) + }) + + // Wave B (npm tools): batched into a single pnpm call. Requires Wave A + // to have completed (specifically: nvm install + ensureNodeLTS), so we + // run it after the parallel block returns. + installNpmToolsBatch(waveB) } diff --git a/exec.go b/exec.go index 5f6465e..4dbe06d 100644 --- a/exec.go +++ b/exec.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "io" "os" "os/exec" "strings" @@ -18,6 +19,14 @@ import ( const defaultSubprocessTimeout = 30 * time.Minute // CmdOpts captures the optional knobs on runCmd / runShell. +// +// Out, when non-nil, switches the call into "captured-routed" mode: stdout +// and stderr are buffered, then the "$ cmd" echo, captured stdout, and +// captured stderr are written to Out in order. Capture is forced true. +// This is how parallel workers route output into per-task buffers without +// interleaving on os.Stdout. When Out is nil (default) the call streams to +// os.Stdout exactly as before, preserving the live-tail behavior used by +// the sequential code paths. type CmdOpts struct { AsSudo bool Check bool // exit on failure (kept for parity but treated as advisory — we return the error instead) @@ -25,6 +34,7 @@ type CmdOpts struct { Capture bool Cwd string Timeout time.Duration // zero = defaultSubprocessTimeout + Out io.Writer // optional sink for echo + captured streams } // CmdResult holds the outcome of a subprocess invocation. @@ -45,7 +55,12 @@ func runCmdReal(argv []string, opts CmdOpts) CmdResult { if opts.AsSudo && os.Geteuid() != 0 { argv = append([]string{"sudo"}, argv...) } - fmt.Printf(" $ %s\n", strings.Join(argv, " ")) + if opts.Out != nil { + fmt.Fprintf(opts.Out, "$ %s\n", strings.Join(argv, " ")) + opts.Capture = true + } else { + fmt.Printf(" $ %s\n", strings.Join(argv, " ")) + } ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout) defer cancel() @@ -69,6 +84,10 @@ func runCmdReal(argv []string, opts CmdOpts) CmdResult { err := cmd.Run() res := CmdResult{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()} + if opts.Out != nil { + writeToOut(opts.Out, res.Stdout) + writeToOut(opts.Out, res.Stderr) + } if ctx.Err() == context.DeadlineExceeded { warn(fmt.Sprintf("%q timed out after %s", argv[0], opts.Timeout)) @@ -96,7 +115,12 @@ func runShellReal(cmd string, opts CmdOpts) CmdResult { if opts.Timeout == 0 { opts.Timeout = defaultSubprocessTimeout } - fmt.Printf(" $ %s\n", cmd) + if opts.Out != nil { + fmt.Fprintf(opts.Out, "$ %s\n", cmd) + opts.Capture = true + } else { + fmt.Printf(" $ %s\n", cmd) + } ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout) defer cancel() @@ -120,6 +144,10 @@ func runShellReal(cmd string, opts CmdOpts) CmdResult { err := c.Run() res := CmdResult{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()} + if opts.Out != nil { + writeToOut(opts.Out, res.Stdout) + writeToOut(opts.Out, res.Stderr) + } if ctx.Err() == context.DeadlineExceeded { warn(fmt.Sprintf("shell command timed out after %s", opts.Timeout)) @@ -141,6 +169,19 @@ func runShellReal(cmd string, opts CmdOpts) CmdResult { return res } +// writeToOut writes data to w, appending a trailing newline if data is +// non-empty and doesn't already end with one. Used by runCmd / runShell to +// keep captured stdout/stderr neatly separated when routed to a task buffer. +func writeToOut(w io.Writer, data []byte) { + if len(data) == 0 { + return + } + _, _ = w.Write(data) + if data[len(data)-1] != '\n' { + _, _ = w.Write([]byte{'\n'}) + } +} + // hasCmdReal is shutil.which() — returns true if name resolves on PATH. func hasCmdReal(name string) bool { _, err := exec.LookPath(name) diff --git a/flatpak.go b/flatpak.go index 6c2f444..9fd204b 100644 --- a/flatpak.go +++ b/flatpak.go @@ -23,6 +23,19 @@ func installFlatpakPackages(toInstall []string) { "https://dl.flathub.org/repo/flathub.flatpakrepo", }, CmdOpts{AsSudo: true}) + if len(toInstall) == 0 { + return + } + + // Single batched install — flatpak supports multiple refs per invocation + // and resolves them concurrently internally. Fall back to per-package + // installs on failure so callers see exactly which IDs broke. + argv := append([]string{"flatpak", "install", "--noninteractive", "flathub"}, toInstall...) + fmt.Printf("\n Installing %d Flatpak(s) in one batch ...\n", len(toInstall)) + if runCmd(argv, CmdOpts{}).OK() { + return + } + warn("Batched flatpak install failed; retrying per-package to isolate failures ...") for _, pkgID := range toInstall { fmt.Printf("\n Installing %s ...\n", pkgID) res := runCmd([]string{"flatpak", "install", "--noninteractive", "flathub", pkgID}, CmdOpts{}) diff --git a/go.mod b/go.mod index cda42a5..63d3f7e 100644 --- a/go.mod +++ b/go.mod @@ -20,5 +20,6 @@ require ( go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/term v0.43.0 // indirect ) diff --git a/go.sum b/go.sum index 76984f4..ebc8aa6 100644 --- a/go.sum +++ b/go.sum @@ -47,5 +47,9 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/issues.go b/issues.go index 7c7b301..f17db4d 100644 --- a/issues.go +++ b/issues.go @@ -26,7 +26,15 @@ var ( func logIssue(level, msg string) { issuesMu.Lock() defer issuesMu.Unlock() - fmt.Fprintf(issueLogWriter, " [%s] %s\n", level, msg) + // Route the human-facing line through the active task's buffer when + // running inside a parallel worker, so concurrent warns/errLogs don't + // interleave on stdout. The structured issue (added to the slice below) + // still flows into the global issues log used by writeRunLog. + if t := currentTask(); t != nil { + t.Printf("[%s] %s\n", level, msg) + } else { + fmt.Fprintf(issueLogWriter, " [%s] %s\n", level, msg) + } issues = append(issues, fmt.Sprintf("[%s] %s", level, msg)) if level == "ERROR" { errorCount++ diff --git a/main.go b/main.go index 4b27ba0..dbe3f48 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,8 @@ import ( "path/filepath" "strings" "time" + + "golang.org/x/term" ) func main() { @@ -104,19 +106,11 @@ func runMain(args []string) { doFlatpak := (*only == "" || *only == "flatpak") && *gui && !isMacOS - var sysCheck systemCheckResult - var flatCheck flatpakCheckResult - var custCheck customCheckResult - - if *only == "" || *only == "system" { - sysCheck = checkSystemPackages(systemPkgs) - } - if doFlatpak { - flatCheck = checkFlatpakPackages(flatpakPkgs) - } - if *only == "" || *only == "custom" { - custCheck = checkCustomPackages(customPtrs) - } + sysCheck, flatCheck, custCheck := checkAllInParallel( + *only == "" || *only == "system", systemPkgs, + doFlatpak, flatpakPkgs, + *only == "" || *only == "custom", customPtrs, + ) total := printCheckSummary(sysCheck, flatCheck, custCheck, *only) @@ -133,6 +127,7 @@ func runMain(args []string) { } checkSudo() + promptGitHubToken() if *only == "" || *only == "system" { installSystemPackages(sysCheck.toInstallRegular, sysCheck.toInstallSpecial) @@ -184,6 +179,39 @@ func runMain(args []string) { } } +// promptGitHubToken asks the user if they want to supply a GitHub token +// after they've authenticated sudo. With a token, our HTTP-bound worker +// pool uncaps from the conservative 8-worker default up to runtime.NumCPU(), +// because authenticated GitHub requests get 5000/hour instead of the +// unauthenticated 60/hour. A token in the environment is honored without +// prompting. Token input is read with echo off via golang.org/x/term so it +// doesn't leak into terminal scrollback or recorded sessions. +func promptGitHubToken() { + if existing := strings.TrimSpace(os.Getenv("GITHUB_TOKEN")); existing != "" { + githubTokenSet = true + fmt.Printf("[GitHub] GITHUB_TOKEN found in environment — HTTP workers uncapped to %d.\n", cpuWorkers()) + return + } + if !askYN("\n[GitHub] Provide a GitHub token to uncap HTTP workers from 8 to your CPU count? [y/N] ") { + return + } + fmt.Print(" Paste token (input hidden): ") + tokenBytes, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + warn(fmt.Sprintf("could not read token: %v — continuing without uncap", err)) + return + } + token := strings.TrimSpace(string(tokenBytes)) + if token == "" { + fmt.Println(" No token provided — keeping the conservative HTTP worker cap.") + return + } + os.Setenv("GITHUB_TOKEN", token) + githubTokenSet = true + fmt.Printf(" Token accepted — HTTP workers uncapped to %d.\n", cpuWorkers()) +} + func checkSudo() { if os.Geteuid() == 0 { if isMacOS { diff --git a/parallel.go b/parallel.go new file mode 100644 index 0000000..ba9ec19 --- /dev/null +++ b/parallel.go @@ -0,0 +1,170 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "os" + "runtime" + "sync" +) + +// cpuWorkers returns the parallelism level for install/check work. +// Defaults to runtime.NumCPU(); overridable via BOOTSTRAP_PARALLELISM +// (e.g. for tests / constrained hosts) and clamped to >=1. +func cpuWorkers() int { + if v := os.Getenv("BOOTSTRAP_PARALLELISM"); v != "" { + var n int + _, _ = fmt.Sscanf(v, "%d", &n) + if n >= 1 { + return n + } + } + n := runtime.NumCPU() + if n < 1 { + return 1 + } + return n +} + +// httpWorkersCap is the polite ceiling for HTTP-bound concurrency when no +// GitHub token has been provided (GitHub anon rate-limits at 60/hour). +// Authenticated requests get 5000/hour so we lift the cap when a token is +// available — see githubTokenSet. +const httpWorkersCap = 8 + +var githubTokenSet bool + +// httpWorkers caps cpuWorkers() to httpWorkersCap unless a GitHub token has +// been supplied (in which case we use the full processor count). +func httpWorkers() int { + n := cpuWorkers() + if githubTokenSet { + return n + } + if n > httpWorkersCap { + return httpWorkersCap + } + return n +} + +// parallelDo runs fn(i, items[i]) over items with at most maxWorkers +// goroutines in flight. Returns once every task has finished. Order of +// completion is not guaranteed; fn is responsible for its own synchronization +// when writing shared state. +func parallelDo[T any](items []T, maxWorkers int, fn func(i int, item T)) { + if len(items) == 0 { + return + } + if maxWorkers < 1 { + maxWorkers = 1 + } + if maxWorkers > len(items) { + maxWorkers = len(items) + } + sem := make(chan struct{}, maxWorkers) + var wg sync.WaitGroup + for i, item := range items { + wg.Add(1) + sem <- struct{}{} + go func(i int, item T) { + defer wg.Done() + defer func() { <-sem }() + fn(i, item) + }(i, item) + } + wg.Wait() +} + +// taskOutput is the per-call sink for status text and subprocess output. +// +// Two modes: +// +// Sequential (label==""): Printf goes straight to os.Stdout, and Writer() +// returns nil so runCmd falls back to its default streamed-to-stdout mode. +// Behavior matches the pre-parallelism code exactly. +// +// Captured (label!=""): Printf and runCmd output both land in an internal +// buffer; Flush() prints the whole block at once with a " [label] " prefix +// on every line. Used by parallel install workers so concurrent output +// doesn't interleave. +type taskOutput struct { + label string + buf bytes.Buffer + mu sync.Mutex +} + +func newSerialOutput() *taskOutput { return &taskOutput{} } +func newCapturedOutput(label string) *taskOutput { + return &taskOutput{label: label} +} + +// Printf writes to the task's destination. +func (t *taskOutput) Printf(format string, args ...any) { + if t.label == "" { + fmt.Printf(format, args...) + return + } + t.mu.Lock() + defer t.mu.Unlock() + fmt.Fprintf(&t.buf, format, args...) +} + +// Println writes a line to the task's destination. +func (t *taskOutput) Println(args ...any) { + if t.label == "" { + fmt.Println(args...) + return + } + t.mu.Lock() + defer t.mu.Unlock() + fmt.Fprintln(&t.buf, args...) +} + +// Writer returns the io.Writer that runCmd/runShell should target via +// CmdOpts.Out. Returns nil in sequential mode (preserves streamed stdout). +func (t *taskOutput) Writer() io.Writer { + if t.label == "" { + return nil + } + return &lockingWriter{mu: &t.mu, w: &t.buf} +} + +// Flush emits the captured buffer to w with the task label prefixed onto +// every line. Idempotent and a no-op in sequential mode. +func (t *taskOutput) Flush(w io.Writer) { + if t.label == "" { + return + } + t.mu.Lock() + defer t.mu.Unlock() + if t.buf.Len() == 0 { + return + } + if w == nil { + w = os.Stdout + } + prefix := fmt.Sprintf(" [%s] ", t.label) + lines := bytes.Split(t.buf.Bytes(), []byte{'\n'}) + for i, line := range lines { + if i == len(lines)-1 && len(line) == 0 { + break + } + fmt.Fprintf(w, "%s%s\n", prefix, line) + } + t.buf.Reset() +} + +// lockingWriter is a thin io.Writer that holds the taskOutput mutex while +// writing, so runCmd / runShell can stream into the buffer concurrently with +// status Printf calls on the same task without corrupting the buffer. +type lockingWriter struct { + mu *sync.Mutex + w io.Writer +} + +func (l *lockingWriter) Write(p []byte) (int, error) { + l.mu.Lock() + defer l.mu.Unlock() + return l.w.Write(p) +} diff --git a/parallel_test.go b/parallel_test.go new file mode 100644 index 0000000..f09bcb7 --- /dev/null +++ b/parallel_test.go @@ -0,0 +1,356 @@ +package main + +import ( + "bytes" + "os" + "strings" + "sync" + "sync/atomic" + "testing" +) + +func TestCpuWorkers(t *testing.T) { + t.Setenv("BOOTSTRAP_PARALLELISM", "4") + if n := cpuWorkers(); n != 4 { + t.Errorf("expected 4 workers via env, got %d", n) + } + t.Setenv("BOOTSTRAP_PARALLELISM", "") + if n := cpuWorkers(); n < 1 { + t.Errorf("expected at least 1 worker, got %d", n) + } +} + +func TestHttpWorkersRespectsCap(t *testing.T) { + t.Setenv("BOOTSTRAP_PARALLELISM", "32") + defer func() { githubTokenSet = false }() + + githubTokenSet = false + if n := httpWorkers(); n != httpWorkersCap { + t.Errorf("expected http workers capped at %d without token, got %d", httpWorkersCap, n) + } + + githubTokenSet = true + if n := httpWorkers(); n != 32 { + t.Errorf("expected http workers uncapped to 32 with token, got %d", n) + } +} + +func TestParallelDoConcurrency(t *testing.T) { + const items = 16 + var inFlight, peak int32 + work := make([]int, items) + for i := range work { + work[i] = i + } + + parallelDo(work, 4, func(_ int, _ int) { + now := atomic.AddInt32(&inFlight, 1) + for { + cur := atomic.LoadInt32(&peak) + if now <= cur || atomic.CompareAndSwapInt32(&peak, cur, now) { + break + } + } + // brief busy spin to keep multiple workers overlapping + for i := 0; i < 50000; i++ { + _ = i * i + } + atomic.AddInt32(&inFlight, -1) + }) + if peak < 2 { + t.Errorf("expected at least 2 concurrent workers, observed peak %d", peak) + } + if peak > 4 { + t.Errorf("worker cap violated: peak %d > 4", peak) + } +} + +func TestParallelDoEmpty(t *testing.T) { + called := false + parallelDo([]int{}, 4, func(_ int, _ int) { called = true }) + if called { + t.Error("expected fn to not be invoked on empty input") + } +} + +func TestParallelDoAllItemsProcessed(t *testing.T) { + items := []int{1, 2, 3, 4, 5, 6, 7, 8} + var sum int64 + parallelDo(items, 3, func(_ int, v int) { + atomic.AddInt64(&sum, int64(v)) + }) + if sum != 36 { + t.Errorf("expected sum 36, got %d", sum) + } +} + +func TestTaskOutputSerial(t *testing.T) { + t.Cleanup(resetMocks) + tOut := newSerialOutput() + // Serial: writes go straight to stdout; Writer() returns nil. + if tOut.Writer() != nil { + t.Error("expected Writer() to be nil in serial mode") + } + // flushing serial mode is a no-op + var buf bytes.Buffer + tOut.Flush(&buf) + if buf.Len() != 0 { + t.Error("expected Flush to be no-op in serial mode") + } +} + +func TestTaskOutputCaptured(t *testing.T) { + tOut := newCapturedOutput("mypkg") + tOut.Printf("first %s\n", "line") + tOut.Println("second line") + tOut.Printf("third line") + + var buf bytes.Buffer + tOut.Flush(&buf) + got := buf.String() + + expected := " [mypkg] first line\n [mypkg] second line\n [mypkg] third line\n" + if got != expected { + t.Errorf("captured output mismatch:\nexpected:\n%q\ngot:\n%q", expected, got) + } + + // Flush is idempotent (second call writes nothing). + buf.Reset() + tOut.Flush(&buf) + if buf.Len() != 0 { + t.Errorf("expected second Flush to be empty, got %q", buf.String()) + } +} + +func TestWithTaskOutputRoutesPrints(t *testing.T) { + tOut := newCapturedOutput("worker") + withTaskOutput(tOut, func() { + taskPrintf("hello %d\n", 7) + taskPrintln("world") + }) + + var buf bytes.Buffer + tOut.Flush(&buf) + got := buf.String() + if !strings.Contains(got, "[worker] hello 7") || !strings.Contains(got, "[worker] world") { + t.Errorf("expected routed output with prefix, got: %q", got) + } +} + +func TestWithTaskOutputUnsetsAfter(t *testing.T) { + tOut := newCapturedOutput("worker") + withTaskOutput(tOut, func() { + if currentTask() == nil { + t.Error("expected active task inside withTaskOutput") + } + }) + if currentTask() != nil { + t.Error("expected no active task after withTaskOutput returns") + } +} + +func TestParallelTaskOutputIsolation(t *testing.T) { + // Each goroutine should see only its own task output, even though + // they all share package-global state. + const n = 8 + var wg sync.WaitGroup + results := make([]string, n) + + for i := 0; i < n; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + label := "g" + string(rune('a'+i)) + tOut := newCapturedOutput(label) + withTaskOutput(tOut, func() { + taskPrintf("from %s\n", label) + }) + var buf bytes.Buffer + tOut.Flush(&buf) + results[i] = buf.String() + }(i) + } + wg.Wait() + + for i, got := range results { + label := "g" + string(rune('a'+i)) + want := " [" + label + "] from " + label + "\n" + if got != want { + t.Errorf("goroutine %d: expected %q, got %q", i, want, got) + } + } +} + +func TestCmdOptsOutRoutesRunCmd(t *testing.T) { + defer resetMocks() + + var buf bytes.Buffer + // Pick a command guaranteed to exist and produce output. + result := runCmdReal([]string{"echo", "hello-out"}, CmdOpts{Out: &buf}) + if !result.OK() { + t.Fatalf("echo failed: %v", result.Err) + } + got := buf.String() + if !strings.Contains(got, "$ echo hello-out") { + t.Errorf("expected command echo in Out, got: %q", got) + } + if !strings.Contains(got, "hello-out") { + t.Errorf("expected stdout in Out, got: %q", got) + } +} + +func TestCmdOptsOutRoutesRunShell(t *testing.T) { + defer resetMocks() + + var buf bytes.Buffer + result := runShellReal("echo shell-out", CmdOpts{Out: &buf}) + if !result.OK() { + t.Fatalf("shell failed: %v", result.Err) + } + got := buf.String() + if !strings.Contains(got, "$ echo shell-out") { + t.Errorf("expected command echo in Out, got: %q", got) + } + if !strings.Contains(got, "shell-out") { + t.Errorf("expected stdout in Out, got: %q", got) + } +} + +func TestIssueLogRoutesViaTaskOutput(t *testing.T) { + defer resetMocks() + // Suppress fallback stdout for the non-task branch. + issueLogWriter = &bytes.Buffer{} + + tOut := newCapturedOutput("isolated") + withTaskOutput(tOut, func() { + warn("a warning") + errLog("an error") + }) + + var buf bytes.Buffer + tOut.Flush(&buf) + out := buf.String() + if !strings.Contains(out, "[isolated] [WARN] a warning") { + t.Errorf("expected routed WARN line, got: %q", out) + } + if !strings.Contains(out, "[isolated] [ERROR] an error") { + t.Errorf("expected routed ERROR line, got: %q", out) + } + // Issues should still flow into the global issues slice. + if !hasErrors() { + t.Error("expected errLog to register a global error even when routed via task") + } +} + +func TestParallelPartitionOrdering(t *testing.T) { + items := []int{1, 2, 3, 4, 5, 6, 7, 8} + even, odd := parallelPartition(items, func(v int) bool { return v%2 == 0 }) + + wantEven := []int{2, 4, 6, 8} + wantOdd := []int{1, 3, 5, 7} + if !equalIntSlices(even, wantEven) { + t.Errorf("evens: want %v, got %v", wantEven, even) + } + if !equalIntSlices(odd, wantOdd) { + t.Errorf("odds: want %v, got %v", wantOdd, odd) + } +} + +func equalIntSlices(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestGoidUnique(t *testing.T) { + mainID := goid() + if mainID == 0 { + t.Error("goid returned 0 for main goroutine") + } + var wg sync.WaitGroup + wg.Add(1) + var childID uint64 + go func() { + defer wg.Done() + childID = goid() + }() + wg.Wait() + if childID == 0 { + t.Error("goid returned 0 for child goroutine") + } + if childID == mainID { + t.Errorf("expected child goroutine id %d to differ from main id %d", childID, mainID) + } +} + +// Verify BOOTSTRAP_PARALLELISM=0 falls back to NumCPU rather than 0 workers. +func TestCpuWorkersInvalidEnv(t *testing.T) { + t.Setenv("BOOTSTRAP_PARALLELISM", "0") + if n := cpuWorkers(); n < 1 { + t.Errorf("expected fallback to runtime.NumCPU for invalid env, got %d", n) + } +} + +func TestCpuWorkersBadEnv(t *testing.T) { + t.Setenv("BOOTSTRAP_PARALLELISM", "notanumber") + if n := cpuWorkers(); n < 1 { + t.Errorf("expected fallback for non-numeric env, got %d", n) + } +} + +// Ensure the parallelDo guard returns when len(items) == 0 even with +// maxWorkers larger than 1. Smoke test for the early return. +func TestParallelDoBigWorkersSmallInput(t *testing.T) { + items := []string{"only"} + called := 0 + var mu sync.Mutex + parallelDo(items, 32, func(_ int, _ string) { + mu.Lock() + called++ + mu.Unlock() + }) + if called != 1 { + t.Errorf("expected exactly 1 invocation, got %d", called) + } +} + +// Confirm runCmd inside withTaskOutput auto-routes via Out without callers +// having to set it explicitly — that's the contract that lets install +// handlers rely on taskOut(). +func TestRunCmdInsideWithTaskOutput(t *testing.T) { + defer resetMocks() + tOut := newCapturedOutput("autoroute") + + withTaskOutput(tOut, func() { + runCmdReal([]string{"echo", "auto"}, CmdOpts{Out: taskOut()}) + }) + + var buf bytes.Buffer + tOut.Flush(&buf) + got := buf.String() + if !strings.Contains(got, "[autoroute] $ echo auto") { + t.Errorf("expected routed echo command, got: %q", got) + } + if !strings.Contains(got, "[autoroute] auto") { + t.Errorf("expected routed echo stdout, got: %q", got) + } +} + +// Sanity: when no token is in env and the user prompt is suppressed, +// httpWorkers stays capped. Just exercise the path; ensures no panic. +func TestPromptGitHubTokenNoOp(t *testing.T) { + defer func() { githubTokenSet = false }() + githubTokenSet = false + os.Unsetenv("GITHUB_TOKEN") + // We can't easily prompt in a test; just confirm cap is in effect. + n := httpWorkers() + if n > httpWorkersCap { + t.Errorf("expected http workers <= %d without token, got %d", httpWorkersCap, n) + } +} diff --git a/post.go b/post.go index 6f6857d..41996c1 100644 --- a/post.go +++ b/post.go @@ -17,12 +17,12 @@ import ( // ── pyenv / Python ────────────────────────────────────────────────────── func installPyenv() { - fmt.Println(" Installing pyenv via curl ...") - if !runShell("curl https://pyenv.run | bash", CmdOpts{}).OK() { + taskPrintln(" Installing pyenv via curl ...") + if !runShell("curl https://pyenv.run | bash", CmdOpts{Out: taskOut()}).OK() { errLog("pyenv installation failed") return } - fmt.Println(" pyenv installed to ~/.pyenv") + taskPrintln(" pyenv installed to ~/.pyenv") } func python3DecimalOK() bool { @@ -46,6 +46,7 @@ func fixPython3Decimal() bool { } func installPip() { + out := taskOut() if !hasCmd("python3") { errLog("python3 is not installed — cannot install pip") return @@ -53,7 +54,7 @@ func installPip() { if !python3DecimalOK() { warn("Python 3 _decimal C extension failed to import — attempting fix ...") if fixPython3Decimal() { - fmt.Println(" Python 3 _decimal extension restored.") + taskPrintln(" Python 3 _decimal extension restored.") } else { errLog("Python 3 _decimal C extension could not be fixed. " + "Run: sudo apt-get install python3-full (Debian/Ubuntu), " + @@ -63,19 +64,19 @@ func installPip() { } } - fmt.Println(" Bootstrapping pip via 'python3 -m ensurepip --upgrade' ...") - bootstrap := runCmd([]string{"python3", "-m", "ensurepip", "--upgrade"}, CmdOpts{AsSudo: true}) + taskPrintln(" Bootstrapping pip via 'python3 -m ensurepip --upgrade' ...") + bootstrap := runCmd([]string{"python3", "-m", "ensurepip", "--upgrade"}, CmdOpts{AsSudo: true, Out: out}) if !bootstrap.OK() { switch pkgMgr { case "apt-get": warn("ensurepip unavailable in system Python — installing python3-pip via apt-get") - if !runCmd([]string{"apt-get", "install", "-y", "python3-pip"}, CmdOpts{AsSudo: true}).OK() { + if !runCmd([]string{"apt-get", "install", "-y", "python3-pip"}, CmdOpts{AsSudo: true, Out: out}).OK() { errLog("python3-pip failed to install via apt-get — skipping pip bootstrap") return } case "pacman": warn("ensurepip unavailable in system Python — installing python-pip via pacman") - if !runCmd([]string{"pacman", "-S", "--noconfirm", "--needed", "python-pip"}, CmdOpts{AsSudo: true}).OK() { + if !runCmd([]string{"pacman", "-S", "--noconfirm", "--needed", "python-pip"}, CmdOpts{AsSudo: true, Out: out}).OK() { errLog("python-pip failed to install via pacman — skipping pip bootstrap") return } @@ -84,8 +85,8 @@ func installPip() { return } } - fmt.Println(" Upgrading pip to the latest version ...") - upgrade := runCmd([]string{"python3", "-m", "pip", "install", "--upgrade", "pip"}, CmdOpts{AsSudo: true}) + taskPrintln(" Upgrading pip to the latest version ...") + upgrade := runCmd([]string{"python3", "-m", "pip", "install", "--upgrade", "pip"}, CmdOpts{AsSudo: true, Out: out}) if !upgrade.OK() { warn("pip self-upgrade failed (likely PEP 668 externally-managed); ensurepip-provided pip remains") } @@ -212,12 +213,12 @@ func installNVM() { return } installURL := fmt.Sprintf("https://raw.githubusercontent.com/nvm-sh/nvm/%s/install.sh", version) - fmt.Printf(" Installing NVM %s via curl ...\n", version) - if !runShell(fmt.Sprintf("curl -o- %s | bash", installURL), CmdOpts{}).OK() { + taskPrintf(" Installing NVM %s via curl ...\n", version) + if !runShell(fmt.Sprintf("curl -o- %s | bash", installURL), CmdOpts{Out: taskOut()}).OK() { errLog("NVM installation failed") return } - fmt.Printf(" NVM %s installed to ~/.nvm\n", version) + taskPrintf(" NVM %s installed to ~/.nvm\n", version) } func ensureNodeLTS() { @@ -269,11 +270,11 @@ func installOhMyZsh() { home, _ := os.UserHomeDir() target := filepath.Join(home, ".oh-my-zsh") if _, err := osStat(target); err == nil { - fmt.Printf(" oh-my-zsh already present at %s; updating theme only\n", target) + taskPrintf(" oh-my-zsh already present at %s; updating theme only\n", target) } else { - fmt.Println(" Installing oh-my-zsh via the official installer ...") + taskPrintln(" Installing oh-my-zsh via the official installer ...") installer := `sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended` - if !runShell(installer, CmdOpts{}).OK() { + if !runShell(installer, CmdOpts{Out: taskOut()}).OK() { errLog("oh-my-zsh installer failed") return } @@ -298,9 +299,9 @@ func installOhMyZsh() { errLog(fmt.Sprintf("could not write ~/.zshrc: %v", err)) return } - fmt.Println(` Set ZSH_THEME="gnzh" in ~/.zshrc`) + taskPrintln(` Set ZSH_THEME="gnzh" in ~/.zshrc`) } else { - fmt.Println(` ~/.zshrc already has ZSH_THEME="gnzh"`) + taskPrintln(` ~/.zshrc already has ZSH_THEME="gnzh"`) } } @@ -472,8 +473,8 @@ func askYN(prompt string) bool { } func installAgy() { - fmt.Println(" Installing agy via curl ...") - if !runShell("curl -fsSL https://antigravity.google/cli/install.sh | bash", CmdOpts{}).OK() { + taskPrintln(" Installing agy via curl ...") + if !runShell("curl -fsSL https://antigravity.google/cli/install.sh | bash", CmdOpts{Out: taskOut()}).OK() { errLog("agy installation failed") } } @@ -492,26 +493,18 @@ func installNpmPackage(pkgName string) { return } ensureNodeLTS() - fmt.Printf(" Installing %s via pnpm ...\n", pkgName) + taskPrintf(" Installing %s via pnpm ...\n", pkgName) cmd := fmt.Sprintf(`bash -c '%ssource ~/.nvm/nvm.sh && pnpm add -g %s'`, pnpmEnvPrefix(), pkgName) - if !runShell(cmd, CmdOpts{}).OK() { + if !runShell(cmd, CmdOpts{Out: taskOut()}).OK() { errLog(fmt.Sprintf("%s installation failed", pkgName)) } } -func installPlaywright() { - home, _ := os.UserHomeDir() - if _, err := osStat(filepath.Join(home, ".nvm")); err != nil { - errLog("NVM is not installed — cannot install playwright") - return - } - ensureNodeLTS() - fmt.Println(" Installing playwright via pnpm ...") - addCmd := fmt.Sprintf(`bash -c '%ssource ~/.nvm/nvm.sh && pnpm add -g playwright'`, pnpmEnvPrefix()) - if !runShell(addCmd, CmdOpts{}).OK() { - errLog("playwright installation failed") - return - } +// installPlaywrightBrowsers runs `pnpx playwright install` (with --with-deps +// on apt-get). Separated from the npm-side install so that installNpmToolsBatch +// can do all `pnpm add -g` work in one call and then just provision browsers +// once if playwright was in the batch. +func installPlaywrightBrowsers() { installCmd := "pnpx playwright install" if pkgMgr == "apt-get" { fmt.Println(" Installing Playwright browsers with dependencies ...") @@ -524,13 +517,29 @@ func installPlaywright() { } } +func installPlaywright() { + home, _ := os.UserHomeDir() + if _, err := osStat(filepath.Join(home, ".nvm")); err != nil { + errLog("NVM is not installed — cannot install playwright") + return + } + ensureNodeLTS() + taskPrintln(" Installing playwright via pnpm ...") + addCmd := fmt.Sprintf(`bash -c '%ssource ~/.nvm/nvm.sh && pnpm add -g playwright'`, pnpmEnvPrefix()) + if !runShell(addCmd, CmdOpts{Out: taskOut()}).OK() { + errLog("playwright installation failed") + return + } + installPlaywrightBrowsers() +} + func installGHExtension(repo string) { if !hasCmd("gh") { errLog("gh CLI is not installed — cannot install extension " + repo) return } - fmt.Printf(" Installing gh extension %s ...\n", repo) - if !runCmd([]string{"gh", "extension", "install", repo}, CmdOpts{}).OK() { + taskPrintf(" Installing gh extension %s ...\n", repo) + if !runCmd([]string{"gh", "extension", "install", repo}, CmdOpts{Out: taskOut()}).OK() { errLog(fmt.Sprintf("gh extension install %s failed", repo)) } } diff --git a/system.go b/system.go index 57443ee..5fe690b 100644 --- a/system.go +++ b/system.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "sync" ) // Special packages: installed outside the regular package manager because @@ -393,16 +394,60 @@ func pkgInstall(pkg string) CmdResult { } } +// pkgInstallMany installs all named packages in a single invocation of the +// host package manager. This is dramatically faster than per-package install +// loops because apt/dnf/pacman amortize metadata refresh, dependency +// resolution, and (most importantly) only acquire the install lock once. +// +// On batch failure we fall back to per-package installs so callers can +// continue to report which specific packages failed via errLog. brew gets +// each formula in parallel goroutines (it tolerates concurrent invocations +// when packages don't share build dependencies; cask installs go through +// the same path). +func pkgInstallMany(pkgs []string) (failed []string) { + if len(pkgs) == 0 { + return nil + } + if pkgMgr == "brew" { + var mu sync.Mutex + parallelDo(pkgs, cpuWorkers(), func(_ int, p string) { + if !pkgInstall(p).OK() { + mu.Lock() + failed = append(failed, p) + mu.Unlock() + } + }) + return failed + } + var argv []string + switch pkgMgr { + case "pacman": + argv = append([]string{"pacman", "-S", "--noconfirm", "--needed"}, pkgs...) + default: + argv = append([]string{pkgMgr, "install", "-y"}, pkgs...) + } + if runCmd(argv, CmdOpts{AsSudo: true}).OK() { + return nil + } + // Batch failed — retry per-package so we can report exactly which + // packages broke. Slower, but only happens on the error path. + warn(fmt.Sprintf("Batched install failed; retrying %d packages individually to isolate failures ...", len(pkgs))) + for _, p := range pkgs { + if !pkgInstall(p).OK() { + failed = append(failed, p) + } + } + return failed +} + // installSystemPackages installs the regular + special package lists. func installSystemPackages(regular, special []string) { fmt.Println("\n=== System Packages ===") if pkgMgr == "brew" { - for _, pkg := range regular { - res := pkgInstall(pkg) - if !res.OK() { - errLog(fmt.Sprintf("System package failed to install: %s", pkg)) - } + failed := pkgInstallMany(regular) + for _, p := range failed { + errLog(fmt.Sprintf("System package failed to install: %s", p)) } // No special packages on macOS — brew covers all of them. return @@ -420,11 +465,9 @@ func installSystemPackages(regular, special []string) { } } - for _, pkg := range regular { - res := pkgInstall(pkg) - if !res.OK() { - errLog(fmt.Sprintf("System package failed to install: %s", pkg)) - } + failed := pkgInstallMany(regular) + for _, p := range failed { + errLog(fmt.Sprintf("System package failed to install: %s", p)) } if len(special) > 0 { diff --git a/taskctx.go b/taskctx.go new file mode 100644 index 0000000..e5e3eb3 --- /dev/null +++ b/taskctx.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "io" + "runtime" + "strconv" + "strings" + "sync" +) + +// Goroutine-local task output routing. +// +// Why: install handlers in custom.go and post.go are deeply nested calls +// that use fmt.Printf/Println directly and pass CmdOpts to runCmd. To route +// their output into a per-task buffer (so parallel workers don't interleave +// on os.Stdout), we'd otherwise need to thread an io.Writer through every +// signature — ~30 call sites of churn including tests. +// +// Instead we keep a sync.Map keyed by goroutine id. The parallel orchestrator +// associates a taskOutput with its worker goroutine before invoking the +// handler; helpers below check the map and route output to the active task +// when present, falling back to direct stdout otherwise. Sequential callers +// observe no behavior change. +// +// goid() uses runtime.Stack — a small hack, but stable and idiomatic for +// goroutine-local state where context.Context threading would dwarf the +// surrounding work. + +var activeTaskByGoroutine sync.Map // map[uint64]*taskOutput + +func goid() uint64 { + var buf [64]byte + n := runtime.Stack(buf[:], false) + s := string(buf[:n]) + s = strings.TrimPrefix(s, "goroutine ") + end := strings.IndexByte(s, ' ') + if end < 0 { + return 0 + } + id, _ := strconv.ParseUint(s[:end], 10, 64) + return id +} + +// withTaskOutput pins tOut to the current goroutine for the duration of fn, +// then unpins. Re-entrant calls overwrite the previous binding and restore +// it on return. A nil tOut is treated as "no binding" (sequential mode). +func withTaskOutput(tOut *taskOutput, fn func()) { + if tOut == nil { + fn() + return + } + id := goid() + prev, hadPrev := activeTaskByGoroutine.Load(id) + activeTaskByGoroutine.Store(id, tOut) + defer func() { + if hadPrev { + activeTaskByGoroutine.Store(id, prev) + } else { + activeTaskByGoroutine.Delete(id) + } + }() + fn() +} + +// currentTask returns the taskOutput pinned to the current goroutine, or +// nil if none. Cheap enough to call per print (~microseconds). +func currentTask() *taskOutput { + v, ok := activeTaskByGoroutine.Load(goid()) + if !ok { + return nil + } + return v.(*taskOutput) +} + +// taskPrintf routes via the active task (if any) or directly to stdout. +func taskPrintf(format string, args ...any) { + if t := currentTask(); t != nil { + t.Printf(format, args...) + return + } + fmt.Printf(format, args...) +} + +// taskPrintln routes via the active task (if any) or directly to stdout. +func taskPrintln(args ...any) { + if t := currentTask(); t != nil { + t.Println(args...) + return + } + fmt.Println(args...) +} + +// taskOut returns the io.Writer that runCmd / runShell should target via +// CmdOpts.Out for the active task. Returns nil when there is no active task, +// which preserves runCmd's default streamed-to-stdout behavior. +func taskOut() io.Writer { + if t := currentTask(); t != nil { + return t.Writer() + } + return nil +}