Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d8cd18317 | ||
|
|
d679049aca | ||
|
|
1a3f4f2e44 | ||
|
|
476607e108 | ||
|
|
d19d7efefc | ||
|
|
79615dd529 | ||
|
|
5f64e2cae4 | ||
|
|
24a50e73dd | ||
|
|
07c212d962 | ||
|
|
befcadd260 |
@@ -2,6 +2,7 @@ name: Integration Tests
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
||||||
|
BOOTSTRAP_CI: 'true'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -19,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [debian, arch, fedora]
|
os: [debian, arch, fedora, ubuntu]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: macOS Manual Integration
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
||||||
|
BOOTSTRAP_CI: 'true'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ It installs:
|
|||||||
| Workflow | Trigger |
|
| Workflow | Trigger |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Unit Tests** — `go test -v ./...` + `go vet ./...` | Pull requests to `main` / `dev` |
|
| **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
|
## Install
|
||||||
|
|
||||||
|
|||||||
+55
-19
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -47,10 +47,12 @@ func main() {
|
|||||||
targets = []string{"archlinux:latest"}
|
targets = []string{"archlinux:latest"}
|
||||||
case "fedora":
|
case "fedora":
|
||||||
targets = []string{"fedora:latest"}
|
targets = []string{"fedora:latest"}
|
||||||
|
case "ubuntu":
|
||||||
|
targets = []string{"ubuntu:latest"}
|
||||||
case "all":
|
case "all":
|
||||||
targets = []string{"debian:latest", "archlinux:latest", "fedora:latest"}
|
targets = []string{"debian:latest", "archlinux:latest", "fedora:latest", "ubuntu:latest"}
|
||||||
default:
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,23 +65,28 @@ func main() {
|
|||||||
|
|
||||||
// 1. Prepare target container base and setup script based on OS distro
|
// 1. Prepare target container base and setup script based on OS distro
|
||||||
var testContainer *dagger.Container
|
var testContainer *dagger.Container
|
||||||
if strings.Contains(target, "debian") {
|
if strings.Contains(target, "debian") || strings.Contains(target, "ubuntu") {
|
||||||
testContainer = client.Container().
|
testContainer = client.Container().
|
||||||
From(target).
|
From(target).
|
||||||
WithExec([]string{"apt-get", "update"}).
|
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") {
|
} else if strings.Contains(target, "fedora") {
|
||||||
testContainer = client.Container().
|
testContainer = client.Container().
|
||||||
From(target).
|
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") {
|
} else if strings.Contains(target, "archlinux") {
|
||||||
testContainer = client.Container().
|
testContainer = client.Container().
|
||||||
From(target).
|
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 {
|
} else {
|
||||||
return fmt.Errorf("unsupported target OS: %s", target)
|
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)
|
// 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"})
|
testContainer = testContainer.WithExec([]string{"mkdir", "-p", "/root/.pyenv"})
|
||||||
|
|
||||||
@@ -88,6 +95,10 @@ func main() {
|
|||||||
WithFile("/usr/local/bin/bootstrap_environment", binaryFile).
|
WithFile("/usr/local/bin/bootstrap_environment", binaryFile).
|
||||||
WithWorkdir("/tmp")
|
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)
|
// Forward GitHub token so API calls are authenticated (avoids 403 rate-limits)
|
||||||
if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
|
if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
|
||||||
secret := client.SetSecret("github-token", tok)
|
secret := client.SetSecret("github-token", tok)
|
||||||
@@ -96,21 +107,47 @@ func main() {
|
|||||||
WithSecretVariable("GH_TOKEN", secret)
|
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.
|
// We pipe 'y' to satisfy the "Proceed? [y/N]" prompt.
|
||||||
fmt.Printf("[%s] Executing bootstrap_environment...\n", target)
|
var bootstrapCmd []string
|
||||||
testContainer = testContainer.WithExec([]string{"sh", "-c", "echo y | bootstrap_environment --gui"})
|
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
|
// 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)
|
fmt.Printf("[%s] Verifying package installations on PATH and running version checks...\n", target)
|
||||||
verifyCmd := []string{
|
var verifyCmd []string
|
||||||
"sh", "-c",
|
if strings.Contains(target, "ubuntu") {
|
||||||
"set -e -x; " +
|
verifyCmd = []string{
|
||||||
"export PATH=$PATH:/usr/local/go/bin:/usr/local/bin; " +
|
"sh", "-c",
|
||||||
"which go && go version && " +
|
"set -e -x; " +
|
||||||
"which nvim && nvim --version && " +
|
"export PATH=$PATH:/usr/local/bin:/root/.local/bin; " +
|
||||||
"which zig && zig version && " +
|
"which nvim && nvim --version && " +
|
||||||
"which firecracker && firecracker --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)
|
verifyOutput, err := testContainer.WithExec(verifyCmd).Stdout(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -118,7 +155,6 @@ func main() {
|
|||||||
return fmt.Errorf("verification failed on %s: %v", target, err)
|
return fmt.Errorf("verification failed on %s: %v", target, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fmt.Printf("[%s] Verification Output:\n%s\n", target, verifyOutput)
|
fmt.Printf("[%s] Verification Output:\n%s\n", target, verifyOutput)
|
||||||
fmt.Printf("--- PASS: Integration test on %s completed successfully ---\n", target)
|
fmt.Printf("--- PASS: Integration test on %s completed successfully ---\n", target)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -31,4 +31,7 @@ var (
|
|||||||
// Filesystem paths
|
// Filesystem paths
|
||||||
osReleasePath = "/etc/os-release"
|
osReleasePath = "/etc/os-release"
|
||||||
passwdPath = "/etc/passwd"
|
passwdPath = "/etc/passwd"
|
||||||
|
|
||||||
|
// Testing override
|
||||||
|
disableProgressTracking = false
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -241,9 +241,9 @@ func TestNpmInstalledNotFound(t *testing.T) {
|
|||||||
func TestIsCustomPkgInstalledNpmNames(t *testing.T) {
|
func TestIsCustomPkgInstalledNpmNames(t *testing.T) {
|
||||||
defer resetMocks()
|
defer resetMocks()
|
||||||
hasCmd = func(name string) bool {
|
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})
|
ok, _ := isCustomPkgInstalled(&CustomPackage{Name: name})
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("expected %s detected as installed", name)
|
t.Errorf("expected %s detected as installed", name)
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ func isCustomPkgInstalled(pkg *CustomPackage) (bool, string) {
|
|||||||
if name == "playwright" {
|
if name == "playwright" {
|
||||||
return npmInstalled("playwright")
|
return npmInstalled("playwright")
|
||||||
}
|
}
|
||||||
|
if name == "mdts" {
|
||||||
|
return npmInstalled("mdts")
|
||||||
|
}
|
||||||
raw := pkg.InstallPath
|
raw := pkg.InstallPath
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
raw = defaultInstallPath(pkg)
|
raw = defaultInstallPath(pkg)
|
||||||
@@ -574,6 +577,7 @@ func nodeDependentPkgs() map[string]bool {
|
|||||||
"codex": true,
|
"codex": true,
|
||||||
"copilot": true,
|
"copilot": true,
|
||||||
"playwright": true,
|
"playwright": true,
|
||||||
|
"mdts": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,6 +701,7 @@ func installNpmToolsBatch(pkgs []*CustomPackage) {
|
|||||||
"codex": "@openai/codex",
|
"codex": "@openai/codex",
|
||||||
"copilot": "@github/copilot",
|
"copilot": "@github/copilot",
|
||||||
"playwright": "playwright",
|
"playwright": "playwright",
|
||||||
|
"mdts": "mdts",
|
||||||
}
|
}
|
||||||
|
|
||||||
var npmPkgs []string
|
var npmPkgs []string
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ var (
|
|||||||
isMacOS bool
|
isMacOS bool
|
||||||
isRHELFamily bool
|
isRHELFamily bool
|
||||||
isArchFamily bool
|
isArchFamily bool
|
||||||
|
isTesting bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
+3
-1
@@ -23,13 +23,13 @@ func resetMocks() {
|
|||||||
osRemove = os.Remove
|
osRemove = os.Remove
|
||||||
osRemoveAll = os.RemoveAll
|
osRemoveAll = os.RemoveAll
|
||||||
osExit = os.Exit
|
osExit = os.Exit
|
||||||
|
progressConfigPath = progressConfigPathReal
|
||||||
// Default to an empty reader so un-mocked tests don't hang waiting for user input.
|
// Default to an empty reader so un-mocked tests don't hang waiting for user input.
|
||||||
stdin = strings.NewReader("")
|
stdin = strings.NewReader("")
|
||||||
readPassword = func() ([]byte, error) {
|
readPassword = func() ([]byte, error) {
|
||||||
return nil, errors.New("terminal blocked in test")
|
return nil, errors.New("terminal blocked in test")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset global state variables to safe defaults
|
|
||||||
isMacOS = false
|
isMacOS = false
|
||||||
pkgMgr = "dnf"
|
pkgMgr = "dnf"
|
||||||
isRHELFamily = true
|
isRHELFamily = true
|
||||||
@@ -37,6 +37,8 @@ func resetMocks() {
|
|||||||
osName = "linux"
|
osName = "linux"
|
||||||
archName = "x86_64"
|
archName = "x86_64"
|
||||||
|
|
||||||
|
disableProgressTracking = true
|
||||||
|
|
||||||
osReleasePath = "/etc/os-release"
|
osReleasePath = "/etc/os-release"
|
||||||
passwdPath = "/etc/passwd"
|
passwdPath = "/etc/passwd"
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ func runMain(args []string) {
|
|||||||
gui := fs.Bool("gui", false, "Include GUI applications (headed environments).")
|
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.")
|
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).")
|
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:])
|
_ = fs.Parse(args[1:])
|
||||||
|
|
||||||
switch *only {
|
switch *only {
|
||||||
@@ -53,22 +55,61 @@ func runMain(args []string) {
|
|||||||
|
|
||||||
initPkgMgr()
|
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...)
|
systemPkgs := append([]string(nil), SystemPackages...)
|
||||||
flatpakPkgs := append([]string(nil), FlatpakPackages...)
|
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()
|
custom := customPackages()
|
||||||
customPtrs := make([]*CustomPackage, 0, len(custom))
|
customPtrs := make([]*CustomPackage, 0, len(custom))
|
||||||
for i := range custom {
|
for i := range custom {
|
||||||
|
name := strings.ToLower(custom[i].Name)
|
||||||
// Drop firecracker on macOS — it's provisioned inside the Fedora VM
|
// Drop firecracker on macOS — it's provisioned inside the Fedora VM
|
||||||
// (see setupFirecrackerVM), not on the host.
|
// (see setupFirecrackerVM), not on the host.
|
||||||
if isMacOS && strings.ToLower(custom[i].Name) == "firecracker" {
|
if isMacOS && name == "firecracker" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if *noAI {
|
if *noAI {
|
||||||
name := strings.ToLower(custom[i].Name)
|
|
||||||
if name == "agy" || name == "claude" || name == "codex" || name == "copilot" {
|
if name == "agy" || name == "claude" || name == "codex" || name == "copilot" {
|
||||||
continue
|
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])
|
customPtrs = append(customPtrs, &custom[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +147,55 @@ func runMain(args []string) {
|
|||||||
|
|
||||||
doFlatpak := (*only == "" || *only == "flatpak") && *gui && !isMacOS
|
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(
|
sysCheck, flatCheck, custCheck := checkAllInParallel(
|
||||||
*only == "" || *only == "system", systemPkgs,
|
*only == "" || *only == "system", systemPkgs,
|
||||||
doFlatpak, flatpakPkgs,
|
doFlatpak, flatpakPkgs,
|
||||||
@@ -114,7 +204,52 @@ func runMain(args []string) {
|
|||||||
|
|
||||||
total := printCheckSummary(sysCheck, flatCheck, custCheck, *only)
|
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 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.")
|
fmt.Println("\nAll packages already installed.")
|
||||||
writeRunLog()
|
writeRunLog()
|
||||||
return
|
return
|
||||||
@@ -129,9 +264,30 @@ func runMain(args []string) {
|
|||||||
checkSudo()
|
checkSudo()
|
||||||
promptGitHubToken()
|
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" {
|
if *only == "" || *only == "system" {
|
||||||
installSystemPackages(sysCheck.toInstallRegular, sysCheck.toInstallSpecial)
|
installSystemPackages(sysCheck.toInstallRegular, sysCheck.toInstallSpecial)
|
||||||
ensureZshDefault()
|
if disableProgressTracking {
|
||||||
|
ensureZshDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if doFlatpak {
|
if doFlatpak {
|
||||||
@@ -161,6 +317,12 @@ func runMain(args []string) {
|
|||||||
pyenvWG.Wait()
|
pyenvWG.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !disableProgressTracking {
|
||||||
|
if err := writeProgressStep(2); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write progress: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeRunLog()
|
writeRunLog()
|
||||||
printNotices()
|
printNotices()
|
||||||
fmt.Println("\nDone.")
|
fmt.Println("\nDone.")
|
||||||
@@ -240,3 +402,39 @@ func checkSudo() {
|
|||||||
return
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,3 +127,43 @@ func TestRunMainMacos(t *testing.T) {
|
|||||||
t.Error("expected program to complete successfully on macOS mock run")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,31 @@ var httpClient = &http.Client{Timeout: httpClientTimeout}
|
|||||||
|
|
||||||
// downloadReal streams url -> dest. Returns true on success.
|
// downloadReal streams url -> dest. Returns true on success.
|
||||||
func downloadReal(url, dest string) bool {
|
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)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errLog(fmt.Sprintf("Download failed for %s: %v", url, err))
|
errLog(fmt.Sprintf("Download failed for %s: %v", url, err))
|
||||||
|
|||||||
+121
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -10,10 +10,10 @@ package main
|
|||||||
// URL templates use the substitutions described in formatURL.
|
// URL templates use the substitutions described in formatURL.
|
||||||
|
|
||||||
var SystemPackages = []string{
|
var SystemPackages = []string{
|
||||||
|
"aria2",
|
||||||
"age",
|
"age",
|
||||||
"ansible",
|
"ansible",
|
||||||
"ansible-core",
|
"ansible-core",
|
||||||
"aria2",
|
|
||||||
"btm",
|
"btm",
|
||||||
"build-essential",
|
"build-essential",
|
||||||
"buildah",
|
"buildah",
|
||||||
@@ -151,6 +151,7 @@ func customPackages() []CustomPackage {
|
|||||||
{Name: "codex"},
|
{Name: "codex"},
|
||||||
{Name: "copilot"},
|
{Name: "copilot"},
|
||||||
{Name: "playwright"},
|
{Name: "playwright"},
|
||||||
|
{Name: "mdts"},
|
||||||
{Name: "gh-repo-bootstrap"},
|
{Name: "gh-repo-bootstrap"},
|
||||||
{
|
{
|
||||||
Name: "rustup",
|
Name: "rustup",
|
||||||
|
|||||||
@@ -387,6 +387,28 @@ func userLoginShell(uid string) string {
|
|||||||
return ""
|
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() {
|
func cloneNvimConfig() {
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
configDir := filepath.Join(home, ".config", "nvim")
|
configDir := filepath.Join(home, ".config", "nvim")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,3 +244,42 @@ func repoGroups() []repoGroup {
|
|||||||
{mk("lazygit"), setupLazygitCoprRepo},
|
{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{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Special packages: installed outside the regular package manager because
|
// Special packages: installed outside the regular package manager because
|
||||||
@@ -18,6 +20,9 @@ func specialPkgs() map[string]bool {
|
|||||||
"github-desktop": true, "zoom": true, "obsidian": true,
|
"github-desktop": true, "zoom": true, "obsidian": true,
|
||||||
"minikube": true, "pipx": true,
|
"minikube": true, "pipx": true,
|
||||||
"poetry": true, "pulumi": true, "semgrep": true,
|
"poetry": true, "pulumi": true, "semgrep": true,
|
||||||
|
"nvidia-drivers": true, "cuda-toolkit": true,
|
||||||
|
"nvidia-container-toolkit": true, "ollama": true,
|
||||||
|
"huggingface-cli": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +56,16 @@ func isSpecialPkgInstalled(pkg string) bool {
|
|||||||
return hasCmd("poetry")
|
return hasCmd("poetry")
|
||||||
case "semgrep":
|
case "semgrep":
|
||||||
return hasCmd("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)
|
return isSystemPkgInstalled(pkg)
|
||||||
}
|
}
|
||||||
@@ -357,6 +372,157 @@ func installSpecialPkg(pkg, tmp string) {
|
|||||||
installPoetry(tmp)
|
installPoetry(tmp)
|
||||||
case "semgrep":
|
case "semgrep":
|
||||||
installSemgrep(tmp)
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,6 +624,38 @@ func brewInstallMany(pkgs []string) (failed []string) {
|
|||||||
func installSystemPackages(regular, special []string) {
|
func installSystemPackages(regular, special []string) {
|
||||||
fmt.Println("\n=== System Packages ===")
|
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" {
|
if pkgMgr == "brew" {
|
||||||
failed := pkgInstallMany(regular)
|
failed := pkgInstallMany(regular)
|
||||||
for _, p := range failed {
|
for _, p := range failed {
|
||||||
|
|||||||
+165
@@ -495,3 +495,168 @@ func TestSystemGoEdgeCases(t *testing.T) {
|
|||||||
installSystemPackages([]string{"docker-ce"}, []string{})
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
|
||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
// don't want the human-facing "[ERROR] ..." prints polluting CI logs
|
// don't want the human-facing "[ERROR] ..." prints polluting CI logs
|
||||||
// (GitHub Actions auto-annotates "[ERROR]" lines as workflow errors).
|
// (GitHub Actions auto-annotates "[ERROR]" lines as workflow errors).
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
isTesting = true
|
||||||
issueLogWriter = io.Discard
|
issueLogWriter = io.Discard
|
||||||
|
disableProgressTracking = true
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user