Compare commits

...
Author SHA1 Message Date
Jason Ross 5d8cd18317 Merge pull request #27 from JMR-dev/fix-install-behavior
Fix install behavior
2026-06-11 11:51:42 -05:00
JMR-dev d679049aca UID fix for tests 2026-06-11 11:40:11 -05:00
JMR-dev 1a3f4f2e44 unit test fix to mock successful package install return codes 2026-06-11 11:36:26 -05:00
JMR-dev 476607e108 add ubuntu to test matrix for local ai and test fixes 2026-06-11 11:27:14 -05:00
JMR-dev d19d7efefc fixed install behavior and added local ai configuration pathway 2026-06-11 11:17:56 -05:00
JMR-dev 79615dd529 ensure zsh is installed first 2026-06-11 08:19:31 -05:00
Jason Ross 5f64e2cae4 Merge pull request #26 from JMR-dev/feat-add-mdts
adding mdts
2026-06-04 18:04:11 -05:00
JMR-dev 24a50e73dd adding mdts 2026-06-04 18:02:43 -05:00
Jason Ross 07c212d962 Merge pull request #25 from JMR-dev/feat-add-aria2
adding aria2 for faster downloads
2026-06-04 17:42:05 -05:00
JMR-dev befcadd260 adding aria2 for faster downloads 2026-06-04 17:10:57 -05:00
Jason Ross 60cf195f45 Merge pull request #24 from JMR-dev/feat-add-libreoffice-extension
add auto save extension to LibreOffice
2026-06-01 11:40:00 -05:00
JMR-dev a5726a7a60 add auto save extension to LibreOffice 2026-06-01 11:09:38 -05:00
Jason Ross ac933f7c7d Merge pull request #23 from JMR-dev/feat-package-additions
package changes
2026-05-30 17:21:54 -05:00
JMR-dev 206b4cb999 fixed unit tests 2026-05-30 17:19:10 -05:00
JMR-dev 83c929c24b test and logic fixes 2026-05-30 17:08:14 -05:00
JMR-dev 430b018aa9 fixes for bashtop removal 2026-05-29 18:09:30 -05:00
JMR-dev c4f7f6d240 package changes 2026-05-29 17:59:11 -05:00
Jason Ross bc021018cd Merge pull request #22 from JMR-dev/fix-update-gha-versions
update GHA workflows
2026-05-27 18:09:25 -05:00
27 changed files with 1484 additions and 171 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ name: Integration Tests
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
BOOTSTRAP_CI: 'true'
on:
push:
@@ -19,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
os: [debian, arch, fedora]
os: [debian, arch, fedora, ubuntu]
fail-fast: false
steps:
+1
View File
@@ -2,6 +2,7 @@ name: macOS Manual Integration
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
BOOTSTRAP_CI: 'true'
on:
workflow_dispatch:
+1 -1
View File
@@ -27,7 +27,7 @@ It installs:
| Workflow | Trigger |
|---|---|
| **Unit Tests** — `go test -v ./...` + `go vet ./...` | Pull requests to `main` / `dev` |
| **Integration Tests** — full run on Debian, Arch, Fedora (Dagger) and macOS (native) | Push + pull requests to `main` / `dev` |
| **Integration Tests** — full run on Debian, Arch, Fedora, Ubuntu (Dagger) and macOS (native) | Push + pull requests to `main` / `dev` |
## Install
+58 -20
View File
@@ -12,7 +12,7 @@ import (
)
func main() {
osFlag := flag.String("os", "all", "OS to test (debian, arch, fedora, or all)")
osFlag := flag.String("os", "all", "OS to test (debian, arch, fedora, ubuntu, or all)")
flag.Parse()
ctx := context.Background()
@@ -34,7 +34,7 @@ func main() {
From("golang:1.25").
WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "bootstrap_environment", "."})
WithExec([]string{"go", "build", "-buildvcs=false", "-o", "bootstrap_environment", "."})
binaryFile := builder.File("bootstrap_environment")
@@ -47,10 +47,12 @@ func main() {
targets = []string{"archlinux:latest"}
case "fedora":
targets = []string{"fedora:latest"}
case "ubuntu":
targets = []string{"ubuntu:latest"}
case "all":
targets = []string{"debian:latest", "archlinux:latest", "fedora:latest"}
targets = []string{"debian:latest", "archlinux:latest", "fedora:latest", "ubuntu:latest"}
default:
fmt.Fprintf(os.Stderr, "Unsupported OS: %s. Supported: debian, arch, fedora, all\n", *osFlag)
fmt.Fprintf(os.Stderr, "Unsupported OS: %s. Supported: debian, arch, fedora, ubuntu, all\n", *osFlag)
os.Exit(1)
}
@@ -63,23 +65,28 @@ func main() {
// 1. Prepare target container base and setup script based on OS distro
var testContainer *dagger.Container
if strings.Contains(target, "debian") {
if strings.Contains(target, "debian") || strings.Contains(target, "ubuntu") {
testContainer = client.Container().
From(target).
WithExec([]string{"apt-get", "update"}).
WithExec([]string{"apt-get", "install", "-y", "sudo", "curl", "git", "wget", "tar", "unzip", "xz-utils", "make", "python3", "which"})
WithExec([]string{"apt-get", "install", "-y", "sudo", "curl", "git", "wget", "tar", "unzip", "xz-utils", "make", "python3", "which", "zstd"})
} else if strings.Contains(target, "fedora") {
testContainer = client.Container().
From(target).
WithExec([]string{"dnf", "install", "-y", "sudo", "curl", "git", "wget", "tar", "unzip", "xz", "make", "python3", "which"})
WithExec([]string{"dnf", "install", "-y", "sudo", "curl", "git", "wget", "tar", "unzip", "xz", "make", "python3", "which", "zstd"})
} else if strings.Contains(target, "archlinux") {
testContainer = client.Container().
From(target).
WithExec([]string{"pacman", "-Sy", "--noconfirm", "sudo", "curl", "git", "wget", "tar", "unzip", "xz", "make", "python", "which"})
WithExec([]string{"pacman", "-Sy", "--noconfirm", "sudo", "curl", "git", "wget", "tar", "unzip", "xz", "make", "python", "which", "zstd"})
} else {
return fmt.Errorf("unsupported target OS: %s", target)
}
// Mock sync to prevent hangs during package updates/update-shells on overlayfs/FUSE
testContainer = testContainer.
WithExec([]string{"ln", "-sf", "/bin/true", "/bin/sync"}).
WithExec([]string{"ln", "-sf", "/bin/true", "/usr/bin/sync"})
// 2. Pre-create the ~/.pyenv directory to skip python source compilation in integration test (saves ~10 minutes)
testContainer = testContainer.WithExec([]string{"mkdir", "-p", "/root/.pyenv"})
@@ -88,6 +95,10 @@ func main() {
WithFile("/usr/local/bin/bootstrap_environment", binaryFile).
WithWorkdir("/tmp")
if val := os.Getenv("BOOTSTRAP_CI"); val != "" {
testContainer = testContainer.WithEnvVariable("BOOTSTRAP_CI", val)
}
// Forward GitHub token so API calls are authenticated (avoids 403 rate-limits)
if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
secret := client.SetSecret("github-token", tok)
@@ -96,27 +107,54 @@ func main() {
WithSecretVariable("GH_TOKEN", secret)
}
// 4. Run bootstrap binary against the full package set (no scope flags).
// 4. Run bootstrap binary.
// We run it twice:
// Run 1: Step 1 (zsh, git, curl, oh-my-zsh) which configures the default shell and exits.
// Run 2: Step 2 (the rest of the packages) once the shell configuration is complete.
// We pipe 'y' to satisfy the "Proceed? [y/N]" prompt.
fmt.Printf("[%s] Executing bootstrap_environment...\n", target)
testContainer = testContainer.WithExec([]string{"sh", "-c", "echo y | bootstrap_environment --gui"})
var bootstrapCmd []string
if strings.Contains(target, "ubuntu") {
bootstrapCmd = []string{"sh", "-c", "echo y | bootstrap_environment --local-ai"}
} else {
bootstrapCmd = []string{"sh", "-c", "echo y | bootstrap_environment --gui"}
}
fmt.Printf("[%s] Executing bootstrap_environment (Run 1: ZSH/Step 1)...\n", target)
testContainer = testContainer.WithExec(bootstrapCmd)
fmt.Printf("[%s] Executing bootstrap_environment (Run 2: Step 2)...\n", target)
testContainer = testContainer.WithExec(bootstrapCmd)
// 5. Verify all installed custom packages return a path and zero exit code from version command
fmt.Printf("[%s] Verifying package installations on PATH and running version checks...\n", target)
verifyCmd := []string{
"sh", "-c",
"export PATH=$PATH:/usr/local/go/bin; " +
"which go && go version && " +
"which nvim && nvim --version && " +
"which zig && zig version && " +
"which firecracker && firecracker --version",
var verifyCmd []string
if strings.Contains(target, "ubuntu") {
verifyCmd = []string{
"sh", "-c",
"set -e -x; " +
"export PATH=$PATH:/usr/local/bin:/root/.local/bin; " +
"which nvim && nvim --version && " +
"which firecracker && firecracker --version && " +
"which ollama && ollama --version && " +
"which hf && hf --help",
}
} else {
verifyCmd = []string{
"sh", "-c",
"set -e -x; " +
"export PATH=$PATH:/usr/local/go/bin:/usr/local/bin; " +
"which go && go version && " +
"which nvim && nvim --version && " +
"which zig && zig version && " +
"which firecracker && firecracker --version",
}
}
verifyOutput, err := testContainer.WithExec(verifyCmd).Stdout(ctx)
if err != nil {
return fmt.Errorf("verification failed on %s: package not found on PATH or exited with error (%w)", target, err)
// To see the stdout/stderr of the failing command, we can try to extract it from dagger's ExecError
return fmt.Errorf("verification failed on %s: %v", target, err)
}
fmt.Printf("[%s] Verification Output:\n%s\n", target, verifyOutput)
fmt.Printf("--- PASS: Integration test on %s completed successfully ---\n", target)
return nil
+3
View File
@@ -31,4 +31,7 @@ var (
// Filesystem paths
osReleasePath = "/etc/os-release"
passwdPath = "/etc/passwd"
// Testing override
disableProgressTracking = false
)
+4 -53
View File
@@ -260,6 +260,7 @@ func TestPromptGitHubTokenAcceptThenEmpty(t *testing.T) {
// password read can't be cleanly mocked.
stdin = strings.NewReader("y\n")
defer func() { stdin = os.Stdin }()
readPassword = func() ([]byte, error) { return nil, errors.New("mocked error") }
captureStdout(t, func() {
// term.ReadPassword on a non-terminal returns an error,
// landing in the "could not read token" warn branch.
@@ -313,11 +314,11 @@ func TestInstallSystemPackagesBrewFailures(t *testing.T) {
defer resetMocks()
pkgMgr = "brew"
runCmd = func(_ []string, _ CmdOpts) CmdResult { return CmdResult{ExitCode: 1} }
captureStdout(t, func() {
out := captureStdout(t, func() {
installSystemPackages([]string{"git"}, nil)
})
if !hasIssueContaining("System package failed to install") {
t.Error("expected error logged for brew failure")
if !strings.Contains(out, "System package failed to install") {
t.Error("expected warning logged for brew failure")
}
}
@@ -610,57 +611,7 @@ func TestSetupVivaldiRepoExisting(t *testing.T) {
// ── special installer branches ──────────────────────────────────────────
func TestInstallBashtopExistingClone(t *testing.T) {
defer resetMocks()
osStat = func(_ string) (os.FileInfo, error) { return nil, nil }
runCmd = func(_ []string, _ CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
installBashtop("/tmp")
}
func TestInstallBashtopPullFails(t *testing.T) {
defer resetMocks()
osStat = func(_ string) (os.FileInfo, error) { return nil, nil }
runCmd = func(argv []string, _ CmdOpts) CmdResult {
if len(argv) > 1 && argv[1] == "-C" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installBashtop("/tmp")
if !hasIssueContaining("bashtop git pull failed") {
t.Error("expected pull failure error")
}
}
func TestInstallBashtopCloneFails(t *testing.T) {
defer resetMocks()
osStat = func(_ string) (os.FileInfo, error) { return nil, os.ErrNotExist }
runCmd = func(argv []string, _ CmdOpts) CmdResult {
if argv[0] == "git" && argv[1] == "clone" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installBashtop("/tmp")
if !hasIssueContaining("bashtop git clone failed") {
t.Error("expected clone failure error")
}
}
func TestInstallBashtopMakeFails(t *testing.T) {
defer resetMocks()
osStat = func(_ string) (os.FileInfo, error) { return nil, os.ErrNotExist }
runCmd = func(argv []string, _ CmdOpts) CmdResult {
if argv[0] == "make" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installBashtop("/tmp")
if !hasIssueContaining("make install") {
t.Error("expected make install failure error")
}
}
func TestInstallPulumiNoVersion(t *testing.T) {
defer resetMocks()
+4
View File
@@ -229,6 +229,7 @@ func TestEnsureHomebrewInstallerFails(t *testing.T) {
defer resetMocks()
isMacOS = true
hasCmd = func(_ string) bool { return false }
runCmd = func(_ []string, _ CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
runShell = func(_ string, _ CmdOpts) CmdResult { return CmdResult{ExitCode: 1} }
called := false
osExit = func(_ int) { called = true }
@@ -245,6 +246,7 @@ func TestEnsureHomebrewBrewNotAtExpectedPath(t *testing.T) {
isMacOS = true
archName = "x86_64"
hasCmd = func(_ string) bool { return false }
runCmd = func(_ []string, _ CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
runShell = func(_ string, _ CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
osStat = func(_ string) (os.FileInfo, error) { return nil, os.ErrNotExist }
called := false
@@ -614,6 +616,8 @@ func TestRunMainHasErrorsExitsOne(t *testing.T) {
hasCmd = func(_ string) bool { return true }
runCmd = func(_ []string, _ CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
runShell = func(_ string, _ CmdOpts) CmdResult { return CmdResult{ExitCode: 1} } // install fails
fetchJSON = func(_ string, _ any) bool { return false }
download = func(_, dest string) bool { return true }
stdin = strings.NewReader("y\n")
defer func() { stdin = os.Stdin }()
exitCode := -1
+2 -2
View File
@@ -241,9 +241,9 @@ func TestNpmInstalledNotFound(t *testing.T) {
func TestIsCustomPkgInstalledNpmNames(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool {
return name == "claude" || name == "codex" || name == "copilot" || name == "playwright"
return name == "claude" || name == "codex" || name == "copilot" || name == "playwright" || name == "mdts"
}
for _, name := range []string{"claude", "codex", "copilot", "playwright"} {
for _, name := range []string{"claude", "codex", "copilot", "playwright", "mdts"} {
ok, _ := isCustomPkgInstalled(&CustomPackage{Name: name})
if !ok {
t.Errorf("expected %s detected as installed", name)
+81 -6
View File
@@ -53,10 +53,16 @@ var defaultInstallPaths = map[string]string{
"zig": "/usr/local/bin/zig",
"nvm": "~/.nvm",
"pyenv": "~/.pyenv",
"neovim": "/usr/local/bin/nvim",
"oh-my-zsh": "~/.oh-my-zsh",
"neovim": "/usr/local/bin/nvim",
"oh-my-zsh": "~/.oh-my-zsh",
"agy": "~/.local/bin/agy",
"gh-repo-bootstrap": "~/.local/share/gh/extensions/gh-repo-bootstrap",
"yq": "/usr/local/bin/yq",
"rustup": "~/.cargo/bin/rustup",
"dagger": "/usr/local/bin/dagger",
"trivy": "/usr/local/bin/trivy",
"cosign": "/usr/local/bin/cosign",
"gitleaks": "/usr/local/bin/gitleaks",
}
func expandHome(p string) string {
@@ -126,6 +132,9 @@ func isCustomPkgInstalled(pkg *CustomPackage) (bool, string) {
if name == "playwright" {
return npmInstalled("playwright")
}
if name == "mdts" {
return npmInstalled("mdts")
}
raw := pkg.InstallPath
if raw == "" {
raw = defaultInstallPath(pkg)
@@ -224,10 +233,7 @@ func installFirecracker(archive, tmp string) {
if strings.HasSuffix(name, ".tgz") || strings.HasSuffix(name, ".tar.gz") {
return nil
}
if !strings.HasPrefix(name, "firecracker") {
return nil
}
if strings.HasSuffix(name, ".debug") || strings.Contains(name, "debug") {
if !strings.HasPrefix(name, "firecracker-v") || strings.Contains(name, "debug") {
return nil
}
if binary == "" {
@@ -321,6 +327,48 @@ func installNeovim(_ *CustomPackage, tmp string) {
taskPrintf(" Neovim installed to %s, symlinked at %s\n", installDir, symlink)
}
func installRustup(archive string) {
out := taskOut()
runCmd([]string{"chmod", "+x", archive}, CmdOpts{Out: out})
runCmd([]string{archive, "-y", "--no-modify-path"}, CmdOpts{Out: out})
taskPrintln(" rustup installed.")
}
func installYq(archive string) {
out := taskOut()
runCmd([]string{"cp", archive, "/usr/local/bin/yq"}, CmdOpts{AsSudo: true, Out: out})
runCmd([]string{"chmod", "+x", "/usr/local/bin/yq"}, CmdOpts{AsSudo: true, Out: out})
taskPrintln(" yq installed.")
}
func installDagger(archive, tmp string) {
out := taskOut()
runCmd([]string{"tar", "-C", tmp, "-xzf", archive}, CmdOpts{Out: out})
runCmd([]string{"mv", filepath.Join(tmp, "dagger"), "/usr/local/bin/dagger"}, CmdOpts{AsSudo: true, Out: out})
taskPrintln(" dagger installed.")
}
func installTrivy(archive, tmp string) {
out := taskOut()
runCmd([]string{"tar", "-C", tmp, "-xzf", archive}, CmdOpts{Out: out})
runCmd([]string{"mv", filepath.Join(tmp, "trivy"), "/usr/local/bin/trivy"}, CmdOpts{AsSudo: true, Out: out})
taskPrintln(" trivy installed.")
}
func installCosign(archive string) {
out := taskOut()
runCmd([]string{"cp", archive, "/usr/local/bin/cosign"}, CmdOpts{AsSudo: true, Out: out})
runCmd([]string{"chmod", "+x", "/usr/local/bin/cosign"}, CmdOpts{AsSudo: true, Out: out})
taskPrintln(" cosign installed.")
}
func installGitleaks(archive, tmp string) {
out := taskOut()
runCmd([]string{"tar", "-C", tmp, "-xzf", archive}, CmdOpts{Out: out})
runCmd([]string{"mv", filepath.Join(tmp, "gitleaks"), "/usr/local/bin/gitleaks"}, CmdOpts{AsSudo: true, Out: out})
taskPrintln(" gitleaks installed.")
}
// ── latest-version resolvers ────────────────────────────────────────────
func resolveLatestGo(_ *CustomPackage) (string, string, bool) {
@@ -434,10 +482,23 @@ func cmpSemver(a, b string) int {
return len(pa) - len(pb)
}
func resolveLatestYq(_ *CustomPackage) (string, string, bool) {
var rel ghRelease
if !fetchJSON("https://api.github.com/repos/mikefarah/yq/releases/latest", &rel) {
return "", "", false
}
version := strings.TrimPrefix(rel.TagName, "v")
if version == "" {
return "", "", false
}
return version, "", true
}
var latestResolvers = map[string]func(*CustomPackage) (string, string, bool){
"go": resolveLatestGo,
"firecracker": resolveLatestFirecracker,
"zig": resolveLatestZig,
"yq": resolveLatestYq,
}
// resolveLatest best-effort upgrades pkg.Version/SHA256 to the latest release.
@@ -516,6 +577,7 @@ func nodeDependentPkgs() map[string]bool {
"codex": true,
"copilot": true,
"playwright": true,
"mdts": true,
}
}
@@ -598,6 +660,18 @@ func runOneCustomInstall(pkg *CustomPackage) {
installFirecracker(archive, tmp)
case "zig":
installZig(pkg, archive)
case "rustup":
installRustup(archive)
case "yq":
installYq(archive)
case "dagger":
installDagger(archive, tmp)
case "trivy":
installTrivy(archive, tmp)
case "cosign":
installCosign(archive)
case "gitleaks":
installGitleaks(archive, tmp)
default:
warn(fmt.Sprintf("No install handler for '%s' — skipping", pkg.Name))
}
@@ -627,6 +701,7 @@ func installNpmToolsBatch(pkgs []*CustomPackage) {
"codex": "@openai/codex",
"copilot": "@github/copilot",
"playwright": "playwright",
"mdts": "mdts",
}
var npmPkgs []string
+12
View File
@@ -19,6 +19,7 @@ var (
isMacOS bool
isRHELFamily bool
isArchFamily bool
isTesting bool
)
func init() {
@@ -64,6 +65,12 @@ var (
osZig = map[string]string{"linux": "linux", "macos": "macos"}
osNvim = map[string]string{"linux": "linux", "macos": "macos"}
osRustup = map[string]string{"linux": "unknown-linux-gnu", "macos": "apple-darwin"}
archRustup = map[string]string{"x86_64": "x86_64", "aarch64": "aarch64"}
osTrivy = map[string]string{"linux": "Linux", "macos": "macOS"}
archTrivy = map[string]string{"x86_64": "64bit", "aarch64": "ARM64"}
archGitleaks = map[string]string{"x86_64": "x64", "aarch64": "arm64"}
archTokens = map[string][]string{
"x86_64": {"x86_64", "amd64", "x64"},
"aarch64": {"aarch64", "arm64"},
@@ -81,6 +88,11 @@ func formatURL(template, version string) string {
"{os_go}", osGo[osName],
"{os_zig}", osZig[osName],
"{os_nvim}", osNvim[osName],
"{os_rustup}", osRustup[osName],
"{arch_rustup}", archRustup[archName],
"{os_trivy}", osTrivy[osName],
"{arch_trivy}", archTrivy[archName],
"{arch_gitleaks}", archGitleaks[archName],
)
return r.Replace(template)
}
+10 -3
View File
@@ -1,7 +1,9 @@
package main
import (
"errors"
"os"
"strings"
)
func resetMocks() {
@@ -20,11 +22,14 @@ func resetMocks() {
osMkdirAll = os.MkdirAll
osRemove = os.Remove
osRemoveAll = os.RemoveAll
osRename = os.Rename
osExit = os.Exit
stdin = os.Stdin
progressConfigPath = progressConfigPathReal
// Default to an empty reader so un-mocked tests don't hang waiting for user input.
stdin = strings.NewReader("")
readPassword = func() ([]byte, error) {
return nil, errors.New("terminal blocked in test")
}
// Reset global state variables to safe defaults
isMacOS = false
pkgMgr = "dnf"
isRHELFamily = true
@@ -32,6 +37,8 @@ func resetMocks() {
osName = "linux"
archName = "x86_64"
disableProgressTracking = true
osReleasePath = "/etc/os-release"
passwdPath = "/etc/passwd"
+16 -2
View File
@@ -380,7 +380,14 @@ func waitForVMSSH(privKey string, timeout time.Duration) bool {
fmt.Println(" VM SSH ready.")
return true
}
time.Sleep(5 * time.Second)
rem := time.Until(deadline)
if rem <= 0 {
break
}
if rem > 5*time.Second {
rem = 5 * time.Second
}
time.Sleep(rem)
}
return false
}
@@ -393,7 +400,14 @@ func waitForFirecrackerInVM(privKey string, timeout time.Duration) bool {
fmt.Println(" firecracker is installed inside the VM.")
return true
}
time.Sleep(10 * time.Second)
rem := time.Until(deadline)
if rem <= 0 {
break
}
if rem > 10*time.Second {
rem = 10 * time.Second
}
time.Sleep(rem)
}
return false
}
+207 -4
View File
@@ -25,6 +25,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -41,6 +42,7 @@ func runMain(args []string) {
gui := fs.Bool("gui", false, "Include GUI applications (headed environments).")
noVM := fs.Bool("no-vm", false, "macOS only: skip provisioning the Fedora-on-QEMU VM that backs the firecracker() zsh wrapper.")
noAI := fs.Bool("no-ai", false, "Skip installation of LLM/AI CLI tools (agy, claude, codex, copilot).")
localAI := fs.Bool("local-ai", false, "Ubuntu only: Install local ML inference environment (Nvidia drivers, CUDA, Nvidia Container Toolkit, Ollama, Hugging Face CLI).")
_ = fs.Parse(args[1:])
switch *only {
@@ -53,22 +55,61 @@ func runMain(args []string) {
initPkgMgr()
if *localAI {
if isMacOS || pkgMgr != "apt-get" {
fmt.Fprintln(os.Stderr, "Error: --local-ai flag is only supported on Ubuntu/Debian (apt-get package manager)")
osExit(1)
return
}
}
systemPkgs := append([]string(nil), SystemPackages...)
flatpakPkgs := append([]string(nil), FlatpakPackages...)
if *localAI {
var filteredSys []string
excludeSys := map[string]bool{
"ansible": true, "ansible-core": true, "buildah": true,
"dotnet-sdk-10.0": true, "helm": true, "kubectl": true,
"minikube": true, "pulumi": true, "sops": true,
"vagrant": true, "virt-manager": true, "wireshark": true,
"zoom": true, "obs-studio": true, "webcamoid": true,
"obsidian": true,
}
for _, p := range systemPkgs {
if !excludeSys[p] {
filteredSys = append(filteredSys, p)
}
}
filteredSys = append(filteredSys, "nvidia-drivers", "cuda-toolkit", "nvidia-container-toolkit", "ollama", "huggingface-cli")
systemPkgs = filteredSys
flatpakPkgs = nil
}
custom := customPackages()
customPtrs := make([]*CustomPackage, 0, len(custom))
for i := range custom {
name := strings.ToLower(custom[i].Name)
// Drop firecracker on macOS — it's provisioned inside the Fedora VM
// (see setupFirecrackerVM), not on the host.
if isMacOS && strings.ToLower(custom[i].Name) == "firecracker" {
if isMacOS && name == "firecracker" {
continue
}
if *noAI {
name := strings.ToLower(custom[i].Name)
if name == "agy" || name == "claude" || name == "codex" || name == "copilot" {
continue
}
}
if *localAI {
excludeCust := map[string]bool{
"go": true, "zig": true, "rustup": true, "dagger": true,
"cosign": true, "agy": true, "claude": true, "copilot": true,
"codex": true, "playwright": true,
}
if excludeCust[name] {
continue
}
}
customPtrs = append(customPtrs, &custom[i])
}
@@ -106,6 +147,55 @@ func runMain(args []string) {
doFlatpak := (*only == "" || *only == "flatpak") && *gui && !isMacOS
step := 0
if !disableProgressTracking {
step = readProgressStep()
if step >= 2 {
fmt.Println("\nBootstrap is already completed according to progress.config.")
fmt.Println("If you want to re-run, delete or reset progress.config.")
return
}
}
originalSystemPkgs := systemPkgs
originalCustomPtrs := customPtrs
originalDoFlatpak := doFlatpak
originalFlatpakPkgs := flatpakPkgs
if !disableProgressTracking && step == 0 {
// Minimum required system packages: zsh, git, curl
step1Sys := map[string]bool{"zsh": true, "git": true, "curl": true}
var keptSys []string
for _, p := range systemPkgs {
if step1Sys[p] {
keptSys = append(keptSys, p)
}
}
systemPkgs = keptSys
// Minimum required custom packages: oh-my-zsh
var keptCust []*CustomPackage
for _, p := range customPtrs {
if strings.ToLower(p.Name) == "oh-my-zsh" {
keptCust = append(keptCust, p)
}
}
customPtrs = keptCust
// No flatpaks in Step 1
flatpakPkgs = nil
doFlatpak = false
} else if !disableProgressTracking && step == 1 {
// Exclude oh-my-zsh in Step 2
var keptCust []*CustomPackage
for _, p := range customPtrs {
if strings.ToLower(p.Name) != "oh-my-zsh" {
keptCust = append(keptCust, p)
}
}
customPtrs = keptCust
}
sysCheck, flatCheck, custCheck := checkAllInParallel(
*only == "" || *only == "system", systemPkgs,
doFlatpak, flatpakPkgs,
@@ -114,7 +204,52 @@ func runMain(args []string) {
total := printCheckSummary(sysCheck, flatCheck, custCheck, *only)
if !disableProgressTracking && step == 0 {
// If we didn't need to install anything for step 1, and default shell is already zsh,
// we can skip step 1 and proceed to step 2 in the same run.
if total == 0 && isZshDefault() {
fmt.Println("\n[zsh/oh-my-zsh] Zsh and oh-my-zsh already installed and default shell is zsh. Proceeding to Step 2...")
if err := writeProgressStep(1); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write progress: %v\n", err)
}
step = 1
// Restore original packages for Step 2
systemPkgs = originalSystemPkgs
flatpakPkgs = originalFlatpakPkgs
doFlatpak = originalDoFlatpak
// Exclude oh-my-zsh
var keptCust []*CustomPackage
for _, p := range originalCustomPtrs {
if strings.ToLower(p.Name) != "oh-my-zsh" {
keptCust = append(keptCust, p)
}
}
customPtrs = keptCust
// Re-run checks for Step 2
sysCheck, flatCheck, custCheck = checkAllInParallel(
*only == "" || *only == "system", systemPkgs,
doFlatpak, flatpakPkgs,
*only == "" || *only == "custom", customPtrs,
)
total = printCheckSummary(sysCheck, flatCheck, custCheck, *only)
}
}
if total == 0 {
if !disableProgressTracking && step == 0 {
// Zsh, git, curl and oh-my-zsh are installed, but default shell is not zsh.
ensureZshDefault()
if err := writeProgressStep(1); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write progress: %v\n", err)
}
fmt.Println("\n[zsh] Default shell has been updated to zsh.")
fmt.Println("IMPORTANT: Please log out of your current session and log back in (or restart your terminal) for the shell change to take effect.")
fmt.Println("Once logged back in, please re-run the bootstrapper to complete the rest of the installation.")
osExit(0)
return
}
fmt.Println("\nAll packages already installed.")
writeRunLog()
return
@@ -129,9 +264,30 @@ func runMain(args []string) {
checkSudo()
promptGitHubToken()
if !disableProgressTracking && step == 0 {
// Run only Step 1: minimum required system packages, default shell, minimum custom packages (oh-my-zsh)
if *only == "" || *only == "system" {
installSystemPackages(sysCheck.toInstallRegular, sysCheck.toInstallSpecial)
ensureZshDefault()
}
if *only == "" || *only == "custom" {
installCustomPackages(custCheck.toInstall)
}
if err := writeProgressStep(1); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write progress: %v\n", err)
}
fmt.Println("\n[zsh/oh-my-zsh] Step 1 of bootstrap completed successfully.")
fmt.Println("IMPORTANT: Please log out of your current session and log back in (or restart your terminal) so that zsh becomes your active shell.")
fmt.Println("Once logged back in, please re-run the bootstrapper to complete the rest of the installation.")
osExit(0)
return
}
if *only == "" || *only == "system" {
installSystemPackages(sysCheck.toInstallRegular, sysCheck.toInstallSpecial)
ensureZshDefault()
if disableProgressTracking {
ensureZshDefault()
}
}
if doFlatpak {
@@ -153,6 +309,7 @@ func runMain(args []string) {
if isMacOS && !*noVM {
setupFirecrackerVM()
}
ensureLibreOfficeAutoSave()
}
if pyenvWG != nil {
@@ -160,6 +317,12 @@ func runMain(args []string) {
pyenvWG.Wait()
}
if !disableProgressTracking {
if err := writeProgressStep(2); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write progress: %v\n", err)
}
}
writeRunLog()
printNotices()
fmt.Println("\nDone.")
@@ -179,6 +342,10 @@ func runMain(args []string) {
}
}
var readPassword = func() ([]byte, error) {
return term.ReadPassword(int(os.Stdin.Fd()))
}
// 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(),
@@ -196,7 +363,7 @@ func promptGitHubToken() {
return
}
fmt.Print(" Paste token (input hidden): ")
tokenBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
tokenBytes, err := readPassword()
fmt.Println()
if err != nil {
warn(fmt.Sprintf("could not read token: %v — continuing without uncap", err))
@@ -235,3 +402,39 @@ func checkSudo() {
return
}
}
var progressConfigPathReal = func() string {
exe, err := os.Executable()
if err != nil {
return "progress.config"
}
return filepath.Join(filepath.Dir(exe), "progress.config")
}
var progressConfigPath = progressConfigPathReal
func readProgressStep() int {
path := progressConfigPath()
data, err := osReadFile(path)
if err != nil {
return 0
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "step=") {
stepStr := strings.TrimPrefix(line, "step=")
if step, err := strconv.Atoi(stepStr); err == nil {
return step
}
}
}
return 0
}
func writeProgressStep(step int) error {
path := progressConfigPath()
content := fmt.Sprintf("step=%d\n", step)
return osWriteFile(path, []byte(content), 0o644)
}
+40
View File
@@ -127,3 +127,43 @@ func TestRunMainMacos(t *testing.T) {
t.Error("expected program to complete successfully on macOS mock run")
}
}
func TestRunMainLocalAIValidation(t *testing.T) {
defer resetMocks()
// 1. macOS validation
isMacOS = true
pkgMgr = "brew"
var exited bool
var exitCode int
osExit = func(code int) {
exited = true
exitCode = code
}
runMain([]string{"bootstrap_environment", "--local-ai"})
if !exited {
t.Error("expected runMain with --local-ai on macOS to exit")
}
if exitCode != 1 {
t.Errorf("expected exit code 1, got %d", exitCode)
}
// 2. Linux non-apt validation
resetMocks()
isMacOS = false
pkgMgr = "dnf"
exited = false
osExit = func(code int) {
exited = true
exitCode = code
}
runMain([]string{"bootstrap_environment", "--local-ai"})
if !exited {
t.Error("expected runMain with --local-ai on Fedora/dnf to exit")
}
if exitCode != 1 {
t.Errorf("expected exit code 1, got %d", exitCode)
}
}
+25 -1
View File
@@ -19,7 +19,31 @@ var httpClient = &http.Client{Timeout: httpClientTimeout}
// downloadReal streams url -> dest. Returns true on success.
func downloadReal(url, dest string) bool {
fmt.Printf(" Downloading %s ...\n", filepath.Base(url))
taskPrintf(" Downloading %s ...\n", filepath.Base(url))
dir, file := filepath.Split(dest)
dir = filepath.Clean(dir)
// Try aria2c first if available
if hasCmd("aria2c") {
res := runCmd([]string{"aria2c", "-x", "16", "-s", "16", "-k", "1M", "-d", dir, "-o", file, url}, CmdOpts{Out: taskOut()})
if res.OK() {
return true
}
taskPrintf(" [WARN] aria2c download failed for %s, falling back to curl ...\n", url)
}
// Fallback to curl
if hasCmd("curl") {
res := runCmd([]string{"curl", "-L", "--fail", "-o", dest, url}, CmdOpts{Out: taskOut()})
if res.OK() {
return true
}
taskPrintf(" [WARN] curl download failed for %s ...\n", url)
}
// Final fallback: Go built-in HTTP client
taskPrintf(" Falling back to built-in HTTP client for %s ...\n", url)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
errLog(fmt.Sprintf("Download failed for %s: %v", url, err))
+121
View File
@@ -236,3 +236,124 @@ func TestNetRealErrors(t *testing.T) {
}
}
func TestDownloadRealFallbackChain(t *testing.T) {
defer resetMocks()
tmpDir := t.TempDir()
destFile := filepath.Join(tmpDir, "out.txt")
// Case 1: aria2c works
var aria2cCalled bool
var curlCalled bool
hasCmd = func(name string) bool {
if name == "aria2c" || name == "curl" {
return true
}
return false
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "aria2c" {
aria2cCalled = true
_ = os.WriteFile(destFile, []byte("aria2c content"), 0644)
return CmdResult{ExitCode: 0}
}
if argv[0] == "curl" {
curlCalled = true
return CmdResult{ExitCode: 0}
}
return CmdResult{ExitCode: 1}
}
success := downloadReal("https://example.com/file", destFile)
if !success {
t.Fatal("expected download via aria2c to succeed")
}
if !aria2cCalled {
t.Error("expected aria2c to be called")
}
if curlCalled {
t.Error("expected curl NOT to be called when aria2c succeeds")
}
// Case 2: aria2c fails, falls back to curl, curl succeeds
resetMocks()
aria2cCalled = false
curlCalled = false
hasCmd = func(name string) bool {
if name == "aria2c" || name == "curl" {
return true
}
return false
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "aria2c" {
aria2cCalled = true
return CmdResult{ExitCode: 1, Err: fmt.Errorf("aria2c simulated error")}
}
if argv[0] == "curl" {
curlCalled = true
_ = os.WriteFile(destFile, []byte("curl content"), 0644)
return CmdResult{ExitCode: 0}
}
return CmdResult{ExitCode: 1}
}
success = downloadReal("https://example.com/file", destFile)
if !success {
t.Fatal("expected download to succeed via curl fallback")
}
if !aria2cCalled {
t.Error("expected aria2c to be attempted")
}
if !curlCalled {
t.Error("expected curl to be attempted after aria2c failed")
}
// Case 3: aria2c fails, curl fails, falls back to Go HTTP client
resetMocks()
aria2cCalled = false
curlCalled = false
hasCmd = func(name string) bool {
if name == "aria2c" || name == "curl" {
return true
}
return false
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "aria2c" {
aria2cCalled = true
return CmdResult{ExitCode: 1, Err: fmt.Errorf("aria2c simulated error")}
}
if argv[0] == "curl" {
curlCalled = true
return CmdResult{ExitCode: 1, Err: fmt.Errorf("curl simulated error")}
}
return CmdResult{ExitCode: 1}
}
oldTransport := httpClient.Transport
defer func() { httpClient.Transport = oldTransport }()
httpClient.Transport = &mockTripper{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString("go http content")),
}, nil
},
}
success = downloadReal("https://example.com/file", destFile)
if !success {
t.Fatal("expected download to succeed via Go http fallback")
}
if !aria2cCalled {
t.Error("expected aria2c to be attempted")
}
if !curlCalled {
t.Error("expected curl to be attempted")
}
data, _ := os.ReadFile(destFile)
if string(data) != "go http content" {
t.Errorf("expected file content to be 'go http content', got %q", string(data))
}
}
+44 -2
View File
@@ -10,10 +10,11 @@ package main
// URL templates use the substitutions described in formatURL.
var SystemPackages = []string{
"aria2",
"age",
"ansible",
"ansible-core",
"aria2",
"bashtop",
"btm",
"build-essential",
"buildah",
"containerd.io",
@@ -29,10 +30,14 @@ var SystemPackages = []string{
"git",
"github-desktop",
"google-chrome-stable",
"helm",
"jq",
"kubectl",
"lazygit",
"lua",
"minisign",
"minikube",
"nmap",
"obs-studio",
"obsidian",
"pipx",
@@ -42,8 +47,11 @@ var SystemPackages = []string{
"qemu",
"restic",
"rg",
"semgrep",
"shutter",
"sops",
"temurin-25-jdk",
"tmux",
"vagrant",
"virt-manager",
"vivaldi-stable",
@@ -52,6 +60,8 @@ var SystemPackages = []string{
"yt-dlp",
"zoom",
"zsh",
"fzf",
"fd",
"bzip2",
"bzip2-devel",
"curl",
@@ -141,6 +151,38 @@ func customPackages() []CustomPackage {
{Name: "codex"},
{Name: "copilot"},
{Name: "playwright"},
{Name: "mdts"},
{Name: "gh-repo-bootstrap"},
{
Name: "rustup",
Version: "latest",
URLTemplate: "https://static.rust-lang.org/rustup/dist/{arch_rustup}-{os_rustup}/rustup-init",
},
{
Name: "yq",
Version: "4.44.1",
URLTemplate: "https://github.com/mikefarah/yq/releases/download/v{version}/yq_{os_go}_{arch_go}",
FetchLatest: "yq",
},
{
Name: "dagger",
Version: "0.11.4",
URLTemplate: "https://github.com/dagger/dagger/releases/download/v{version}/dagger_v{version}_{os_go}_{arch_go}.tar.gz",
},
{
Name: "trivy",
Version: "0.70.0",
URLTemplate: "https://github.com/aquasecurity/trivy/releases/download/v{version}/trivy_{version}_{os_trivy}-{arch_trivy}.tar.gz",
},
{
Name: "cosign",
Version: "2.2.4",
URLTemplate: "https://github.com/sigstore/cosign/releases/download/v{version}/cosign-{os_go}-{arch_go}",
},
{
Name: "gitleaks",
Version: "8.18.2",
URLTemplate: "https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_{os_go}_{arch_gitleaks}.tar.gz",
},
}
}
+1 -1
View File
@@ -77,6 +77,7 @@ var packageOverrides = map[string]map[string]overrideEntry{
"webcamoid": skipOverride(),
},
"apt-get": {
"fd": replace("fd-find"),
"ffmpeg-free": replace("ffmpeg"),
"lua": replace("lua5.4"),
"qemu": replace("qemu-system"),
@@ -165,7 +166,6 @@ var packageOverrides = map[string]map[string]overrideEntry{
"rg": replace("ripgrep"),
"temurin-25-jdk": replace("temurin"),
"vivaldi-stable": replace("vivaldi"),
"bashtop": replace("btop"),
"buildah": skipOverride(),
"shutter": skipOverride(),
"virt-manager": skipOverride(),
+4 -4
View File
@@ -85,11 +85,11 @@ func TestResolveSystemPkgs(t *testing.T) {
t.Errorf("expected skipped to be [docker-compose], got %v", skipped)
}
// brew: bashtop -> btop (replace), buildah -> skipped
// brew: buildah -> skipped
pkgMgr = "brew"
resolved, skipped = resolveSystemPkgs([]string{"bashtop", "buildah", "rg"})
if len(resolved) != 2 || resolved[0] != "btop" || resolved[1] != "ripgrep" {
t.Errorf("expected [btop ripgrep], got %v", resolved)
resolved, skipped = resolveSystemPkgs([]string{"buildah", "rg"})
if len(resolved) != 1 || resolved[0] != "ripgrep" {
t.Errorf("expected [ripgrep], got %v", resolved)
}
// pacman: docker-ce-rootless-extras and vagrant are AUR-only -> skipped;
// pipx is replaced with python-pipx.
+64
View File
@@ -387,6 +387,28 @@ func userLoginShell(uid string) string {
return ""
}
func isZshDefault() bool {
if !hasCmd("zsh") {
return false
}
zshPath := "/bin/zsh"
if r, ok := probe([]string{"which", "zsh"}, 5*time.Second); ok && r.ExitCode == 0 {
if p := strings.TrimSpace(string(r.Stdout)); p != "" {
zshPath = p
}
}
username := invokingUser()
if username == "" {
return false
}
u, err := user.Lookup(username)
if err != nil {
return false
}
current := userLoginShell(u.Uid)
return current == zshPath
}
func cloneNvimConfig() {
home, _ := os.UserHomeDir()
configDir := filepath.Join(home, ".config", "nvim")
@@ -544,3 +566,45 @@ func installGHExtension(repo string) {
}
}
// ── LibreOffice AutoSave Extension ──────────────────────────────────────
func ensureLibreOfficeAutoSave() {
var unopkgPath string
if isMacOS {
unopkgPath = "/Applications/LibreOffice.app/Contents/MacOS/unopkg"
if _, err := osStat(unopkgPath); err != nil {
return
}
} else {
if !hasCmd("unopkg") {
return
}
unopkgPath = "unopkg"
}
fmt.Println("\n[LibreOffice] LibreOffice detected. Installing AutoSave extension ...")
tmp, err := os.MkdirTemp("", "libreoffice-autosave-")
if err != nil {
errLog(fmt.Sprintf("LibreOffice AutoSave temp dir failed: %v", err))
return
}
defer osRemoveAll(tmp)
url := "https://github.com/JMR-dev/LibreOfficeAutoSave/releases/latest/download/AutoSave.oxt"
dest := filepath.Join(tmp, "AutoSave.oxt")
if !download(url, dest) {
errLog("Failed to download LibreOffice AutoSave extension")
return
}
if !runCmd([]string{unopkgPath, "add", "-f", dest}, CmdOpts{}).OK() {
errLog("Failed to install LibreOffice AutoSave extension")
return
}
fmt.Println(" LibreOffice AutoSave extension installed successfully.")
}
+94
View File
@@ -905,4 +905,98 @@ func TestEnsurePythonLatestBackgroundInstall(t *testing.T) {
}
}
func TestEnsureLibreOfficeAutoSave(t *testing.T) {
defer resetMocks()
// 1. isMacOS = false, LibreOffice not installed
isMacOS = false
hasCmd = func(name string) bool {
if name == "unopkg" {
return false
}
return true
}
var calledDownload bool
download = func(url, dest string) bool {
calledDownload = true
return true
}
ensureLibreOfficeAutoSave()
if calledDownload {
t.Error("expected download not to be called when unopkg is missing")
}
// 2. isMacOS = true, LibreOffice not installed
isMacOS = true
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
calledDownload = false
ensureLibreOfficeAutoSave()
if calledDownload {
t.Error("expected download not to be called when unopkg is missing on macOS")
}
// 3. isMacOS = false, LibreOffice installed, download fails
isMacOS = false
hasCmd = func(name string) bool {
return name == "unopkg"
}
download = func(url, dest string) bool {
return false
}
var runCmdCalled bool
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalled = true
return CmdResult{ExitCode: 0}
}
ensureLibreOfficeAutoSave()
if runCmdCalled {
t.Error("expected runCmd not to be called when download fails")
}
// 4. isMacOS = false, LibreOffice installed, download succeeds, runCmd fails
download = func(url, dest string) bool {
return true
}
var unopkgArgv []string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
unopkgArgv = argv
return CmdResult{ExitCode: 1}
}
ensureLibreOfficeAutoSave()
if len(unopkgArgv) == 0 || unopkgArgv[0] != "unopkg" {
t.Errorf("expected runCmd with unopkg, got: %v", unopkgArgv)
}
// 5. isMacOS = false, LibreOffice installed, download succeeds, runCmd succeeds
runCmd = func(argv []string, opts CmdOpts) CmdResult {
unopkgArgv = argv
return CmdResult{ExitCode: 0}
}
ensureLibreOfficeAutoSave()
if len(unopkgArgv) == 0 || unopkgArgv[0] != "unopkg" || unopkgArgv[1] != "add" || unopkgArgv[2] != "-f" {
t.Errorf("expected runCmd with unopkg add -f, got: %v", unopkgArgv)
}
// 6. isMacOS = true, LibreOffice installed, download succeeds, runCmd succeeds
isMacOS = true
osStat = func(name string) (os.FileInfo, error) {
if name == "/Applications/LibreOffice.app/Contents/MacOS/unopkg" {
return nil, nil
}
return nil, os.ErrNotExist
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
unopkgArgv = argv
return CmdResult{ExitCode: 0}
}
ensureLibreOfficeAutoSave()
expectedMacPath := "/Applications/LibreOffice.app/Contents/MacOS/unopkg"
if len(unopkgArgv) == 0 || unopkgArgv[0] != expectedMacPath {
t.Errorf("expected runCmd with %s, got: %v", expectedMacPath, unopkgArgv)
}
}
+264
View File
@@ -0,0 +1,264 @@
package main
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"testing"
"time"
)
func TestReadWriteProgressStep(t *testing.T) {
defer resetMocks()
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "progress.config")
// Set up mock file operations
var fileContents []byte
osReadFile = func(name string) ([]byte, error) {
if name == configPath {
if fileContents == nil {
return nil, os.ErrNotExist
}
return fileContents, nil
}
return nil, os.ErrNotExist
}
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
if name == configPath {
fileContents = data
return nil
}
return fmt.Errorf("unexpected file write to %s", name)
}
// Override progressConfigPath
originalPathFunc := progressConfigPath
progressConfigPath = func() string {
return configPath
}
defer func() {
progressConfigPath = originalPathFunc
}()
// 1. Initial state (no file)
disableProgressTracking = false
step := readProgressStep()
if step != 0 {
t.Errorf("expected step 0 initially, got %d", step)
}
// 2. Write step 1
err := writeProgressStep(1)
if err != nil {
t.Fatalf("failed to write step 1: %v", err)
}
step = readProgressStep()
if step != 1 {
t.Errorf("expected step 1 after writing, got %d", step)
}
// 3. Write step 2
err = writeProgressStep(2)
if err != nil {
t.Fatalf("failed to write step 2: %v", err)
}
step = readProgressStep()
if step != 2 {
t.Errorf("expected step 2 after writing, got %d", step)
}
}
func TestRunMainStep2Exit(t *testing.T) {
defer resetMocks()
// Mock file read to return step=2
osReadFile = func(name string) ([]byte, error) {
if strings.HasSuffix(name, "progress.config") {
return []byte("step=2\n"), nil
}
return nil, os.ErrNotExist
}
disableProgressTracking = false
var exited bool
osExit = func(code int) {
exited = true
}
runMain([]string{"bootstrap_environment"})
if exited {
t.Error("expected runMain to return normally rather than exit when step=2")
}
}
func TestRunMainStep0OnlyZshGitCurl(t *testing.T) {
defer resetMocks()
u, _ := user.Current()
uid := "1000"
username := os.Getenv("USER")
if u != nil {
uid = u.Uid
username = u.Username
}
if username == "" {
username = "user"
}
// Mock file read to return nothing (step=0)
var writtenData []byte
osReadFile = func(name string) ([]byte, error) {
if strings.HasSuffix(name, "progress.config") {
return nil, os.ErrNotExist
}
if strings.HasSuffix(name, "/etc/passwd") {
return []byte(fmt.Sprintf("%s:x:%s:%s::/home/user:/bin/bash\n", username, uid, uid)), nil
}
return nil, os.ErrNotExist
}
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
if strings.HasSuffix(name, "progress.config") {
writtenData = data
return nil
}
return nil
}
hasCmd = func(name string) bool {
return name == "zsh" || name == "git" || name == "curl"
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "which" && argv[1] == "zsh" {
return CmdResult{ExitCode: 0, Stdout: []byte("/bin/zsh\n")}, true
}
return CmdResult{ExitCode: 1}, true
}
disableProgressTracking = false
// Mock user selection: Accept
stdin = strings.NewReader("y\n")
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
var runShellCalls []string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCalls = append(runShellCalls, cmd)
return CmdResult{ExitCode: 0}
}
var exited bool
osExit = func(code int) {
exited = true
}
runMain([]string{"bootstrap_environment"})
if !exited {
t.Error("expected runMain to exit at step 1")
}
// Verify step 1 was written to progress.config
if !strings.Contains(string(writtenData), "step=1") {
t.Errorf("expected step=1 written to progress.config, got: %q", string(writtenData))
}
}
func TestRunMainStep0TransitionToStep2(t *testing.T) {
defer resetMocks()
u, _ := user.Current()
uid := "1000"
username := os.Getenv("USER")
if u != nil {
uid = u.Uid
username = u.Username
}
if username == "" {
username = "user"
}
// Zsh default is true, oh-my-zsh and zsh/git/curl already installed
// Under step=0, we should transition directly to step=1 and then execute step 2 in the same run.
var writtenData []byte
osReadFile = func(name string) ([]byte, error) {
if strings.HasSuffix(name, "progress.config") {
return nil, os.ErrNotExist
}
if strings.HasSuffix(name, "/etc/passwd") {
// passwd already says zsh is default shell
return []byte(fmt.Sprintf("%s:x:%s:%s::/home/user:/bin/zsh\n", username, uid, uid)), nil
}
return nil, os.ErrNotExist
}
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
if strings.HasSuffix(name, "progress.config") {
writtenData = data
return nil
}
return nil
}
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, "/go") ||
strings.HasSuffix(name, "/firecracker") ||
strings.HasSuffix(name, "/nvim") ||
strings.HasSuffix(name, ".oh-my-zsh") {
return nil, nil
}
return nil, os.ErrNotExist
}
hasCmd = func(name string) bool {
return true // all installed
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "which" && argv[1] == "zsh" {
return CmdResult{ExitCode: 0, Stdout: []byte("/bin/zsh\n")}, true
}
if len(argv) >= 3 && argv[1] == "install" && argv[2] == "--list" {
return CmdResult{ExitCode: 0, Stdout: []byte(" 3.10.0\n 3.11.0\n")}, true
}
return CmdResult{ExitCode: 0}, true
}
disableProgressTracking = false
// Mock user selection: Accept
stdin = strings.NewReader("y\n")
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
runShell = func(cmd string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
var exited bool
osExit = func(code int) {
exited = true
}
runMain([]string{"bootstrap_environment", "--only", "custom"})
// Since they are already installed, it will continue.
// Since we mock all installed, total items to install for Step 2 is 0.
// It should exit normally or print all packages installed.
if exited {
t.Errorf("did not expect exit during transition path. Issues: %v", issues)
}
// Should have written step 1, then eventually step 2
if !strings.Contains(string(writtenData), "step=2") {
t.Errorf("expected step=2 written to progress.config, got: %q", string(writtenData))
}
}
+39
View File
@@ -244,3 +244,42 @@ func repoGroups() []repoGroup {
{mk("lazygit"), setupLazygitCoprRepo},
}
}
func setupCUDARepo() {
if pkgMgr != "apt-get" {
return
}
if repoFileExists("/etc/apt/sources.list.d/cuda.list") || repoFileExists("/etc/apt/sources.list.d/cuda-keyring.list") {
return
}
versionID := strings.Trim(osReleaseField("VERSION_ID"), `"`)
ubuntuVer := strings.ReplaceAll(versionID, ".", "")
if ubuntuVer == "" {
ubuntuVer = "2204"
}
debURL := fmt.Sprintf("https://developer.download.nvidia.com/compute/cuda/repos/ubuntu%s/x86_64/cuda-keyring_1.1-1_all.deb", ubuntuVer)
runShell(
fmt.Sprintf("curl -fsSL %s -o /tmp/cuda-keyring.deb && "+
"sudo dpkg -i /tmp/cuda-keyring.deb && "+
"sudo apt-get update", debURL),
CmdOpts{},
)
}
func setupNvidiaContainerToolkitRepo() {
if pkgMgr != "apt-get" {
return
}
if repoFileExists("/etc/apt/sources.list.d/nvidia-container-toolkit.list") {
return
}
runShell(
"curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | "+
"sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg && "+
"curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | "+
"sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | "+
"sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list >/dev/null && "+
"sudo apt-get update",
CmdOpts{},
)
}
+218 -33
View File
@@ -3,8 +3,10 @@ package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// Special packages: installed outside the regular package manager because
@@ -16,8 +18,11 @@ func specialPkgs() map[string]bool {
}
return map[string]bool{
"github-desktop": true, "zoom": true, "obsidian": true,
"minikube": true, "bashtop": true, "pipx": true,
"poetry": true, "pulumi": true,
"minikube": true, "pipx": true,
"poetry": true, "pulumi": true, "semgrep": true,
"nvidia-drivers": true, "cuda-toolkit": true,
"nvidia-container-toolkit": true, "ollama": true,
"huggingface-cli": true,
}
}
@@ -37,21 +42,30 @@ var guiSystemPkgs = map[string]bool{
}
func isSpecialPkgInstalled(pkg string) bool {
home, _ := os.UserHomeDir()
exists := func(p string) bool { _, err := osStat(p); return err == nil }
switch pkg {
case "obsidian":
return exists("/usr/local/bin/obsidian")
case "minikube":
return exists("/usr/local/bin/minikube") || hasCmd("minikube")
case "bashtop":
return exists("/usr/local/bin/bashtop") || exists(filepath.Join(home, "bashtop"))
case "pulumi":
return exists("/opt/pulumi/pulumi") || hasCmd("pulumi")
case "pipx":
return hasCmd("pipx")
case "poetry":
return hasCmd("poetry")
case "semgrep":
return hasCmd("semgrep")
case "nvidia-drivers":
return hasCmd("nvidia-smi")
case "cuda-toolkit":
return hasCmd("nvcc") || exists("/usr/local/cuda/bin/nvcc")
case "nvidia-container-toolkit":
return hasCmd("nvidia-ctk")
case "ollama":
return hasCmd("ollama")
case "huggingface-cli":
return hasCmd("huggingface-cli")
}
return isSystemPkgInstalled(pkg)
}
@@ -259,30 +273,6 @@ func installMinikube(tmp string) {
fmt.Printf(" minikube installed to %s\n", installPath)
}
func installBashtop(_ string) {
home, _ := os.UserHomeDir()
cloneDir := filepath.Join(home, "bashtop")
if _, err := osStat(cloneDir); err == nil {
fmt.Printf(" Updating existing clone at %s ...\n", cloneDir)
if !runCmd([]string{"git", "-C", cloneDir, "pull"}, CmdOpts{}).OK() {
errLog("bashtop git pull failed")
return
}
} else {
fmt.Printf(" Cloning bashtop to %s ...\n", cloneDir)
if !runCmd([]string{"git", "clone", "https://github.com/aristocratos/bashtop.git", cloneDir}, CmdOpts{}).OK() {
errLog("bashtop git clone failed")
return
}
}
if !runCmd([]string{"make", "install"}, CmdOpts{AsSudo: true, Cwd: cloneDir}).OK() {
errLog("bashtop 'make install' failed")
return
}
appendProfileLine("bashtop", "export PATH=$PATH:"+cloneDir)
fmt.Printf(" bashtop installed. Clone at %s, binary at /usr/local/bin/bashtop\n", cloneDir)
}
func installPulumi(tmp string) {
version := fetchText("https://www.pulumi.com/latest-version")
if version == "" {
@@ -356,6 +346,14 @@ func installPoetry(_ string) {
runCmd([]string{"pipx", "install", "poetry"}, CmdOpts{})
}
func installSemgrep(_ string) {
if !hasCmd("pipx") {
errLog("pipx is not installed — cannot install semgrep")
return
}
runCmd([]string{"pipx", "install", "semgrep"}, CmdOpts{})
}
func installSpecialPkg(pkg, tmp string) {
switch pkg {
case "github-desktop":
@@ -366,14 +364,165 @@ func installSpecialPkg(pkg, tmp string) {
installObsidian(tmp)
case "minikube":
installMinikube(tmp)
case "bashtop":
installBashtop(tmp)
case "pulumi":
installPulumi(tmp)
case "pipx":
installPipx(tmp)
case "poetry":
installPoetry(tmp)
case "semgrep":
installSemgrep(tmp)
case "nvidia-drivers":
installNvidiaDrivers()
case "cuda-toolkit":
installCUDAToolkit()
case "nvidia-container-toolkit":
installNvidiaContainerToolkit()
case "ollama":
installOllama()
case "huggingface-cli":
installHuggingFaceCLI()
}
}
func installNvidiaDrivers() {
if pkgMgr != "apt-get" {
errLog("nvidia-drivers can only be automatically installed via apt-get on Ubuntu/Debian")
return
}
fmt.Println(" Installing NVIDIA driver (nvidia-driver-550) ...")
res := pkgInstall("nvidia-driver-550")
if !res.OK() {
errLog(fmt.Sprintf("failed to install nvidia-driver-550: %v", res.Err))
}
}
func installCUDAToolkit() {
if pkgMgr != "apt-get" {
errLog("cuda-toolkit can only be automatically installed via apt-get on Ubuntu/Debian")
return
}
fmt.Println(" Setting up CUDA repository keyring ...")
setupCUDARepo()
fmt.Println(" Installing cuda-toolkit ...")
res := pkgInstall("cuda-toolkit")
if !res.OK() {
errLog(fmt.Sprintf("failed to install cuda-toolkit: %v", res.Err))
return
}
fmt.Println(" Configuring system path for CUDA ...")
appendProfileLine("cuda", `export PATH="/usr/local/cuda/bin:$PATH"`)
appendProfileLine("cuda", `export LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"`)
}
func installNvidiaContainerToolkit() {
if pkgMgr != "apt-get" {
errLog("nvidia-container-toolkit can only be automatically installed via apt-get on Ubuntu/Debian")
return
}
fmt.Println(" Setting up NVIDIA Container Toolkit repository ...")
setupNvidiaContainerToolkitRepo()
fmt.Println(" Installing nvidia-container-toolkit ...")
res := pkgInstall("nvidia-container-toolkit")
if !res.OK() {
errLog(fmt.Sprintf("failed to install nvidia-container-toolkit: %v", res.Err))
return
}
fmt.Println(" Configuring Docker runtime for NVIDIA Container Toolkit ...")
configureRes := runCmd([]string{"nvidia-ctk", "runtime", "configure", "--runtime=docker"}, CmdOpts{AsSudo: true})
if !configureRes.OK() {
warn(fmt.Sprintf("failed to configure docker runtime: %v", configureRes.Err))
}
fmt.Println(" Restarting Docker service ...")
restartRes := runCmd([]string{"systemctl", "restart", "docker"}, CmdOpts{AsSudo: true})
if !restartRes.OK() {
warn(fmt.Sprintf("failed to restart docker service: %v", restartRes.Err))
}
}
func installOllama() {
fmt.Println(" Installing Ollama via official install script ...")
res := runShell("curl -fsSL https://ollama.com/install.sh | sh", CmdOpts{})
if !res.OK() {
errLog(fmt.Sprintf("Ollama installation failed: %v", res.Err))
return
}
isCI := os.Getenv("BOOTSTRAP_CI") == "true"
if isCI {
fmt.Println(" [CI] Skipping pulling large Ollama models in integration test container.")
return
}
// Pull Gemma 4 E4B and Qwen2.5-Coder 7B
fmt.Println(" Pulling Gemma 4 E4B and Qwen2.5-Coder 7B models ...")
serverRunning := false
// Check if Ollama server is already responding
for i := 0; i < 5; i++ {
r := runCmd([]string{"ollama", "list"}, CmdOpts{Capture: true})
if r.OK() {
serverRunning = true
break
}
if isTesting {
break
}
time.Sleep(1 * time.Second)
}
var cmd *exec.Cmd
if !serverRunning && !isTesting {
fmt.Println(" Ollama server not running. Starting in background for model pulling ...")
cmd = exec.Command("ollama", "serve")
cmd.Stdout = nil
cmd.Stderr = nil
if err := cmd.Start(); err != nil {
warn(fmt.Sprintf("Failed to start Ollama server in background: %v", err))
} else {
// Wait up to 30 seconds for server to start
for i := 0; i < 30; i++ {
r := runCmd([]string{"ollama", "list"}, CmdOpts{Capture: true})
if r.OK() {
serverRunning = true
break
}
time.Sleep(1 * time.Second)
}
}
}
if serverRunning || isTesting {
fmt.Println(" Pulling Gemma 4 E4B (gemma4:e4b) ...")
pull1 := runCmd([]string{"ollama", "pull", "gemma4:e4b"}, CmdOpts{})
if !pull1.OK() {
errLog(fmt.Sprintf("Failed to pull gemma4:e4b: %v", pull1.Err))
}
fmt.Println(" Pulling Qwen2.5-Coder 7B (qwen2.5-coder:7b) ...")
pull2 := runCmd([]string{"ollama", "pull", "qwen2.5-coder:7b"}, CmdOpts{})
if !pull2.OK() {
errLog(fmt.Sprintf("Failed to pull qwen2.5-coder:7b: %v", pull2.Err))
}
} else {
errLog("Ollama server failed to start — cannot pull models")
}
if cmd != nil && cmd.Process != nil {
fmt.Println(" Stopping background Ollama server ...")
cmd.Process.Kill()
}
}
func installHuggingFaceCLI() {
if !hasCmd("pipx") {
errLog("pipx is not installed — cannot install huggingface-cli")
return
}
fmt.Println(" Installing huggingface-cli via pipx ...")
res := runCmd([]string{"pipx", "install", "huggingface_hub[cli]"}, CmdOpts{})
if !res.OK() {
errLog(fmt.Sprintf("failed to install huggingface-cli: %v", res.Err))
}
}
@@ -475,10 +624,42 @@ func brewInstallMany(pkgs []string) (failed []string) {
func installSystemPackages(regular, special []string) {
fmt.Println("\n=== System Packages ===")
// Ensure aria2 is installed first and on the system path
var installAria2 bool
var remainingRegular []string
for _, p := range regular {
if p == "aria2" {
installAria2 = true
} else {
remainingRegular = append(remainingRegular, p)
}
}
if installAria2 || !hasCmd("aria2c") {
fmt.Println(" Ensuring aria2 is installed first and on the system path ...")
var res CmdResult
switch pkgMgr {
case "brew":
res = runCmd([]string{"brew", "install", "aria2"}, CmdOpts{})
case "pacman":
res = runCmd([]string{"pacman", "-S", "--noconfirm", "--needed", "aria2"}, CmdOpts{AsSudo: true})
default: // dnf, apt-get
res = runCmd([]string{pkgMgr, "install", "-y", "aria2"}, CmdOpts{AsSudo: true})
}
if !res.OK() {
warn(fmt.Sprintf("Failed to install aria2: %v", res.Err))
} else if !hasCmd("aria2c") {
warn("aria2 was installed but 'aria2c' is not found on the system path")
} else {
fmt.Println(" aria2 is installed and on the system path.")
}
regular = remainingRegular
}
if pkgMgr == "brew" {
failed := pkgInstallMany(regular)
for _, p := range failed {
errLog(fmt.Sprintf("System package failed to install: %s", p))
taskPrintf(" [WARN] System package failed to install: %s\n", p)
}
// No special packages on macOS — brew covers all of them.
return
@@ -498,7 +679,7 @@ func installSystemPackages(regular, special []string) {
failed := pkgInstallMany(regular)
for _, p := range failed {
errLog(fmt.Sprintf("System package failed to install: %s", p))
taskPrintf(" [WARN] System package failed to install: %s\n", p)
}
if len(special) > 0 {
@@ -513,6 +694,10 @@ func installSystemPackages(regular, special []string) {
installSpecialPkg(pkg, tmp)
}
}
if pkgMgr == "apt-get" && hasCmd("fdfind") {
runCmd([]string{"ln", "-sf", "/usr/bin/fdfind", "/usr/local/bin/fd"}, CmdOpts{AsSudo: true})
}
}
// appendProfileLine adds a PATH/env line to a system-wide login-shell profile,
+167 -36
View File
@@ -247,32 +247,7 @@ func TestInstallMinikube(t *testing.T) {
}
}
func TestInstallBashtop(t *testing.T) {
defer resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist // Not cloned yet
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
installBashtop("/tmp")
// Git clone and make install should be run
if len(runCmdCalls) < 2 {
t.Fatalf("expected git clone and make install, got calls: %v", runCmdCalls)
}
if runCmdCalls[0][1] != "clone" {
t.Errorf("expected git clone, got %v", runCmdCalls[0])
}
if runCmdCalls[1][0] != "make" || runCmdCalls[1][1] != "install" {
t.Errorf("expected make install, got %v", runCmdCalls[1])
}
}
func TestInstallPulumi(t *testing.T) {
defer resetMocks()
@@ -402,9 +377,7 @@ func TestSystemGoEdgeCases(t *testing.T) {
}
hasCmd = func(name string) bool { return true }
if !isSpecialPkgInstalled("bashtop") {
t.Error("expected bashtop to be installed")
}
if !isSpecialPkgInstalled("pulumi") {
t.Error("expected pulumi to be installed")
}
@@ -470,14 +443,7 @@ func TestSystemGoEdgeCases(t *testing.T) {
fetchText = func(url string) string { return "mismatch-checksum minikube" }
installSpecialPkg("minikube", "/tmp")
resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, nil
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 1}
}
installSpecialPkg("bashtop", "/tmp")
resetMocks()
fetchText = func(url string) string { return "some-sha pulumi-v3.90.0-linux-x64.tar.gz" }
@@ -529,3 +495,168 @@ func TestSystemGoEdgeCases(t *testing.T) {
installSystemPackages([]string{"docker-ce"}, []string{})
}
func TestInstallSystemPackagesAria2First(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
hasCmd = func(name string) bool {
if name == "aria2c" {
return false
}
return true
}
installSystemPackages([]string{"git", "aria2", "tmux"}, []string{})
if len(runCmdCalls) < 2 {
t.Fatalf("expected at least 2 command calls, got %d: %v", len(runCmdCalls), runCmdCalls)
}
firstCall := runCmdCalls[0]
if len(firstCall) < 4 || firstCall[0] != "dnf" || firstCall[1] != "install" || firstCall[3] != "aria2" {
t.Errorf("expected first call to be installing aria2, got: %v", firstCall)
}
secondCall := runCmdCalls[1]
if len(secondCall) < 5 || secondCall[0] != "dnf" || secondCall[1] != "install" || secondCall[3] != "git" || secondCall[4] != "tmux" {
t.Errorf("expected second call to install remaining packages, got: %v", secondCall)
}
}
func TestInstallLocalAISpecialPkgs(t *testing.T) {
defer resetMocks()
pkgMgr = "apt-get"
isMacOS = false
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
var runShellCalls []string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCalls = append(runShellCalls, cmd)
return CmdResult{ExitCode: 0}
}
// 1. nvidia-drivers
installSpecialPkg("nvidia-drivers", "/tmp")
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "apt-get" || runCmdCalls[0][3] != "nvidia-driver-550" {
t.Errorf("expected apt-get install nvidia-driver-550, got calls: %v", runCmdCalls)
}
// 2. cuda-toolkit
runCmdCalls = nil
runShellCalls = nil
installSpecialPkg("cuda-toolkit", "/tmp")
if len(runCmdCalls) < 1 || runCmdCalls[0][3] != "cuda-toolkit" {
t.Errorf("expected apt-get install cuda-toolkit, got calls: %v", runCmdCalls)
}
if len(runShellCalls) != 1 || !strings.Contains(runShellCalls[0], "cuda-keyring") {
t.Errorf("expected setupCUDARepo runShell call, got calls: %v", runShellCalls)
}
// 3. nvidia-container-toolkit
runCmdCalls = nil
runShellCalls = nil
installSpecialPkg("nvidia-container-toolkit", "/tmp")
if len(runCmdCalls) < 3 || runCmdCalls[0][3] != "nvidia-container-toolkit" {
t.Errorf("expected apt-get install nvidia-container-toolkit, got calls: %v", runCmdCalls)
}
if len(runShellCalls) != 1 || !strings.Contains(runShellCalls[0], "nvidia-container-toolkit.list") {
t.Errorf("expected setupNvidiaContainerToolkitRepo runShell call, got calls: %v", runShellCalls)
}
// 4. ollama
runCmdCalls = nil
runShellCalls = nil
installSpecialPkg("ollama", "/tmp")
if len(runShellCalls) != 1 || !strings.Contains(runShellCalls[0], "ollama.com/install.sh") {
t.Errorf("expected ollama installer runShell call, got calls: %v", runShellCalls)
}
// 5. huggingface-cli
runCmdCalls = nil
runShellCalls = nil
hasCmd = func(name string) bool {
return name == "pipx"
}
installSpecialPkg("huggingface-cli", "/tmp")
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "pipx" || runCmdCalls[0][2] != "huggingface_hub[cli]" {
t.Errorf("expected pipx install huggingface_hub[cli], got calls: %v", runCmdCalls)
}
}
func TestIsSpecialPkgInstalledLocalAI(t *testing.T) {
defer resetMocks()
isMacOS = false
// Test case 1: None of the commands/files exist
hasCmd = func(name string) bool { return false }
osStat = func(name string) (os.FileInfo, error) { return nil, os.ErrNotExist }
pkgs := []string{"nvidia-drivers", "cuda-toolkit", "nvidia-container-toolkit", "ollama", "huggingface-cli"}
for _, p := range pkgs {
if isSpecialPkgInstalled(p) {
t.Errorf("expected %s to be not installed", p)
}
}
// Test case 2: Check nvidia-drivers
hasCmd = func(name string) bool { return name == "nvidia-smi" }
if !isSpecialPkgInstalled("nvidia-drivers") {
t.Error("expected nvidia-drivers to be installed when nvidia-smi exists")
}
// Test case 3: Check cuda-toolkit via hasCmd
hasCmd = func(name string) bool { return name == "nvcc" }
if !isSpecialPkgInstalled("cuda-toolkit") {
t.Error("expected cuda-toolkit to be installed when nvcc command exists")
}
// Test case 4: Check cuda-toolkit via path existence
hasCmd = func(name string) bool { return false }
osStat = func(name string) (os.FileInfo, error) {
if name == "/usr/local/cuda/bin/nvcc" {
return nil, nil // exists
}
return nil, os.ErrNotExist
}
if !isSpecialPkgInstalled("cuda-toolkit") {
t.Error("expected cuda-toolkit to be installed when /usr/local/cuda/bin/nvcc exists")
}
// Test case 5: Check nvidia-container-toolkit
resetMocks()
isMacOS = false
hasCmd = func(name string) bool { return name == "nvidia-ctk" }
if !isSpecialPkgInstalled("nvidia-container-toolkit") {
t.Error("expected nvidia-container-toolkit to be installed when nvidia-ctk command exists")
}
// Test case 6: Check ollama
resetMocks()
isMacOS = false
hasCmd = func(name string) bool { return name == "ollama" }
if !isSpecialPkgInstalled("ollama") {
t.Error("expected ollama to be installed when ollama command exists")
}
// Test case 7: Check huggingface-cli
resetMocks()
isMacOS = false
hasCmd = func(name string) bool { return name == "huggingface-cli" }
if !isSpecialPkgInstalled("huggingface-cli") {
t.Error("expected huggingface-cli to be installed when huggingface-cli command exists")
}
}
-2
View File
@@ -1,2 +0,0 @@
ok github.com/JMR-dev/bootstrap_dev_env 20.206s
? github.com/JMR-dev/bootstrap_dev_env/ci [no test files]
+2
View File
@@ -11,6 +11,8 @@ import (
// don't want the human-facing "[ERROR] ..." prints polluting CI logs
// (GitHub Actions auto-annotates "[ERROR]" lines as workflow errors).
func TestMain(m *testing.M) {
isTesting = true
issueLogWriter = io.Discard
disableProgressTracking = true
os.Exit(m.Run())
}