Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f0176cbf5 | ||
|
|
1392a503f1 | ||
|
|
14e61733e9 | ||
|
|
4568326d9d | ||
|
|
42cbd09288 |
@@ -0,0 +1 @@
|
||||
C:/Users/jasonross/.gemini/config/projects/a670deb9-cd84-4166-a5d9-ed65679c7591.json
|
||||
@@ -1,20 +0,0 @@
|
||||
name: integration-test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build-and-run:
|
||||
name: Build and run
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force dist | Out-Null
|
||||
go build -o dist/bootstrap_windows_env.exe ./cmd/bootstrap_windows_env
|
||||
- name: Run (dry-run)
|
||||
run: ./dist/bootstrap_windows_env.exe --dry-run --yes --headless --no-wsl
|
||||
@@ -15,8 +15,8 @@ jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build
|
||||
|
||||
@@ -8,8 +8,8 @@ jobs:
|
||||
name: Unit tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Run unit tests
|
||||
|
||||
+9
-1
@@ -1,6 +1,14 @@
|
||||
bootstrap_windows_env.exe
|
||||
*.exe
|
||||
dist/
|
||||
bin/
|
||||
*.test
|
||||
coverage
|
||||
coverage.out
|
||||
test_out.txt
|
||||
*.log
|
||||
|
||||
|
||||
# Local environment overrides (e.g. test/integration/packer/.env).
|
||||
.env
|
||||
*.box
|
||||
.vagrant/
|
||||
|
||||
@@ -46,6 +46,24 @@ Visual Studio Community 2026 is installed as `Microsoft.VisualStudio.Community`
|
||||
|
||||
`wsl` enables WSL prerequisites, installs the current official `FedoraLinux-*` distro reported by `wsl --list --online`, stops for reboot or first-launch username creation when needed, then invokes the latest Linux bootstrap release with `--wsl --headless --yes`.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests run on every pull request via `.github/workflows/unit-test.yml`:
|
||||
|
||||
```powershell
|
||||
go test ./...
|
||||
```
|
||||
|
||||
End-to-end integration runs against a real Windows client VM through Vagrant +
|
||||
Hyper-V. It is not run in CI (GitHub-hosted runners are Windows Server and have
|
||||
no Hyper-V), so execute it locally:
|
||||
|
||||
```powershell
|
||||
go run ./test/integration
|
||||
```
|
||||
|
||||
See `test/integration/README.md` for prerequisites, flags, and tuning options.
|
||||
|
||||
## Current Deferrals
|
||||
|
||||
`Webcamoid` is deferred because no exact `winget` manifest was available during implementation. Native Docker Desktop, Podman Desktop, Buildah, Minikube, Firecracker, QEMU, virt-manager, Windows Terminal, and VS Code are also intentionally excluded from Windows v1.
|
||||
|
||||
@@ -19,7 +19,7 @@ type Action struct {
|
||||
AI bool
|
||||
}
|
||||
|
||||
const refreshPath = `$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User'); `
|
||||
const refreshPath = `$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User')+';C:\ProgramData\chocolatey\bin'; if (Get-Command fnm -ErrorAction SilentlyContinue) { fnm env --shell powershell | Invoke-Expression }; `
|
||||
|
||||
func powerShell(script string) CommandSpec {
|
||||
return CommandSpec{
|
||||
@@ -32,21 +32,63 @@ func checkedNativePowerShell(command string) CommandSpec {
|
||||
return powerShell(`$ErrorActionPreference='Stop'; ` + command + `; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }`)
|
||||
}
|
||||
|
||||
func HostActions() []Action {
|
||||
return []Action{
|
||||
func HostActions(opts Options) []Action {
|
||||
actions := []Action{
|
||||
{
|
||||
Name: "Node.js LTS through fnm",
|
||||
Name: "Visual Studio Community 2026",
|
||||
Phase: PhaseHost,
|
||||
Probe: checkedNativePowerShell(`fnm exec --using=lts-latest node --version`),
|
||||
Probe: powerShell(`if (Test-Path "${env:ProgramFiles}\Microsoft Visual Studio\2026\Community\Common7\IDE\devenv.exe") { exit 0 } else { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
checkedNativePowerShell(`fnm install --lts`),
|
||||
checkedNativePowerShell(`fnm default lts-latest`),
|
||||
powerShell(fmt.Sprintf(
|
||||
`$tempExe = Join-Path $env:TEMP 'vs_community.exe'; `+
|
||||
`if (Test-Path $tempExe) { Remove-Item -Force $tempExe }; `+
|
||||
`[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `+
|
||||
`Write-Host 'Downloading Visual Studio 2026 bootstrapper...'; `+
|
||||
`Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_community.exe' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop; `+
|
||||
`Unblock-File -Path $tempExe; `+
|
||||
`Write-Host 'Installing Visual Studio 2026 with workloads...'; `+
|
||||
`$p = Start-Process -FilePath $tempExe -ArgumentList '--passive --wait --config "%s"' -Wait -PassThru; `+
|
||||
`if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 3010) { throw 'Visual Studio installation failed with exit code ' + $p.ExitCode }`,
|
||||
mustInstallAssetPath("assets/visual-studio-community.vsconfig"),
|
||||
)),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "fnm and Node.js LTS",
|
||||
Phase: PhaseHost,
|
||||
Probe: powerShell(`fnm exec --using=lts-latest node --version | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
powerShell(`$ErrorActionPreference='Stop'
|
||||
function Invoke-Native {
|
||||
param([Parameter(Mandatory=$true)][string]$File, [Parameter(Mandatory=$true)][string[]]$Arguments)
|
||||
& $File @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
$scoop = Get-Command scoop -ErrorAction SilentlyContinue
|
||||
$scoopShim = Join-Path $HOME 'scoop\shims\scoop.ps1'
|
||||
if ($scoop) {
|
||||
Invoke-Native $scoop.Source @('install', 'fnm')
|
||||
} elseif (Test-Path $scoopShim) {
|
||||
Invoke-Native $scoopShim @('install', 'fnm')
|
||||
} elseif (Get-Command choco -ErrorAction SilentlyContinue) {
|
||||
Invoke-Native 'choco' @('install', 'fnm', '-y', '--no-progress')
|
||||
} else {
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Invoke-RestMethod -Uri 'https://community.chocolatey.org/install.ps1' | Invoke-Expression
|
||||
$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User')
|
||||
Invoke-Native 'choco' @('install', 'fnm', '-y', '--no-progress')
|
||||
}
|
||||
$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User')
|
||||
Invoke-Native 'fnm' @('install', '--lts')
|
||||
Invoke-Native 'fnm' @('default', 'lts-latest')`),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pyenv-win through Scoop or Chocolatey",
|
||||
Phase: PhaseHost,
|
||||
Probe: checkedNativePowerShell(`pyenv --version`),
|
||||
Probe: powerShell(`pyenv --version | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
powerShell(`$ErrorActionPreference='Stop'
|
||||
function Invoke-Native {
|
||||
@@ -65,7 +107,8 @@ if ($scoop) {
|
||||
} elseif (Get-Command choco -ErrorAction SilentlyContinue) {
|
||||
Invoke-Native 'choco' @('install', 'pyenv-win', '-y', '--no-progress')
|
||||
} else {
|
||||
Invoke-Native 'winget' @('install', '--id', 'Chocolatey.Chocolatey', '--exact', '--source', 'winget', '--accept-source-agreements', '--accept-package-agreements', '--disable-interactivity')
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Invoke-RestMethod -Uri 'https://community.chocolatey.org/install.ps1' | Invoke-Expression
|
||||
$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User')
|
||||
Invoke-Native 'choco' @('install', 'pyenv-win', '-y', '--no-progress')
|
||||
}`),
|
||||
@@ -74,7 +117,7 @@ if ($scoop) {
|
||||
{
|
||||
Name: "latest stable Python through pyenv-win",
|
||||
Phase: PhaseHost,
|
||||
Probe: powerShell(`$v = pyenv version-name 2>$null; if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($v) -or $v.Trim() -eq 'system') { exit 1 }`),
|
||||
Probe: powerShell(`$v = pyenv version-name; if ([string]::IsNullOrWhiteSpace($v) -or $v -eq 'system') { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
powerShell(`$ErrorActionPreference='Stop'
|
||||
function Invoke-Native {
|
||||
@@ -84,20 +127,34 @@ function Invoke-Native {
|
||||
throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
$v=(pyenv install --list | Select-String '^\s*3\.\d+\.\d+\s*$' | ForEach-Object {$_.Line.Trim()} | Sort-Object {[version]$_} -Descending | Select-Object -First 1)
|
||||
if (-not $v) { throw 'no stable Python version found' }
|
||||
$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User')
|
||||
$out = Invoke-Native 'pyenv' @('install', '-l')
|
||||
$v = $out -split '\r?\n' | Where-Object { $_ -match '^\s*3\.\d+\.\d+\s*$' } | Sort-Object -Descending | Select-Object -First 1
|
||||
if (-not $v) { throw 'No stable Python 3.x version found' }
|
||||
$v = $v.Trim()
|
||||
Invoke-Native 'pyenv' @('install', '-s', $v)
|
||||
Invoke-Native 'pyenv' @('global', $v)`),
|
||||
Invoke-Native 'pyenv' @('global', $v)
|
||||
Invoke-Native 'pyenv' @('exec', 'python', '-m', 'pip', 'install', '--upgrade', 'pip')
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "VLC default media player associations",
|
||||
Name: "semgrep",
|
||||
Phase: PhaseHost,
|
||||
Probe: powerShell(`$marker = Join-Path $env:ProgramData 'bootstrap_windows_env\vlc-default-media.done'; if (-not (Test-Path $marker)) { exit 1 }`),
|
||||
Probe: powerShell(`$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User'); pyenv exec semgrep --version | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
powerShell(`$ErrorActionPreference='Stop'
|
||||
$stateDir = Join-Path $env:ProgramData 'bootstrap_windows_env'
|
||||
New-Item -ItemType Directory -Force -Path $stateDir | Out-Null
|
||||
checkedNativePowerShell(`$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User'); pyenv exec pip install semgrep`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actions = append(actions, Action{
|
||||
Name: "VLC default media player associations",
|
||||
Phase: PhaseHost,
|
||||
Probe: powerShell(`$stateDir = Join-Path $env:ProgramData 'bootstrap_windows_env'; if (-not (Test-Path (Join-Path $stateDir 'vlc-default-media.done'))) { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
powerShell(`$stateDir = Join-Path $env:ProgramData 'bootstrap_windows_env'
|
||||
if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Force -Path $stateDir | Out-Null }
|
||||
$vlcPaths = @(
|
||||
(Join-Path $env:ProgramFiles 'VideoLAN\VLC\vlc.exe'),
|
||||
(Join-Path ${env:ProgramFiles(x86)} 'VideoLAN\VLC\vlc.exe')
|
||||
@@ -106,8 +163,7 @@ if ($vlcPaths.Count -eq 0 -and -not (Get-Command vlc.exe -ErrorAction SilentlyCo
|
||||
throw 'VLC is not installed or is not discoverable.'
|
||||
}
|
||||
$extensions = @(
|
||||
'.3g2', '.3gp', '.aac', '.aiff', '.alac', '.amr', '.ape', '.asf', '.au',
|
||||
'.avi', '.divx', '.flac', '.flv', '.m2ts', '.m4a', '.m4v', '.mka', '.mkv',
|
||||
'.3g2', '.3gp', '.3gp2', '.3gpp', '.amv', '.asf', '.avi', '.divx', '.flac', '.flv', '.m2ts', '.m4a', '.m4v', '.mka', '.mkv',
|
||||
'.mov', '.mp2', '.mp3', '.mp4', '.mp4v', '.mpeg', '.mpg', '.mts', '.oga',
|
||||
'.ogg', '.ogm', '.ogv', '.opus', '.ts', '.vob', '.wav', '.webm', '.wma', '.wmv'
|
||||
)
|
||||
@@ -149,9 +205,10 @@ if ($LASTEXITCODE -ne 0) {
|
||||
}
|
||||
Set-Content -Path (Join-Path $stateDir 'vlc-default-media.done') -Value (Get-Date -Format o) -Encoding UTF8
|
||||
Write-Output "Imported VLC default media associations from $xmlPath. Windows may still require user confirmation for an existing signed-in profile."`),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
func CustomActions(opts Options) []Action {
|
||||
@@ -170,8 +227,8 @@ func CustomActions(opts Options) []Action {
|
||||
Phase: PhaseCustom,
|
||||
Probe: powerShell(`npm list --global playwright --depth=0 | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
checkedNativePowerShell(`fnm exec --using=lts-latest npm install --global playwright`),
|
||||
checkedNativePowerShell(`fnm exec --using=lts-latest playwright install`),
|
||||
checkedNativePowerShell(`npm install --global playwright`),
|
||||
checkedNativePowerShell(`playwright install`),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -188,7 +245,7 @@ func CustomActions(opts Options) []Action {
|
||||
AI: true,
|
||||
Probe: powerShell(`npm list --global '@anthropic-ai/claude-code' --depth=0 | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
checkedNativePowerShell(`fnm exec --using=lts-latest npm install --global '@anthropic-ai/claude-code'`),
|
||||
checkedNativePowerShell(`npm install --global '@anthropic-ai/claude-code'`),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -197,7 +254,7 @@ func CustomActions(opts Options) []Action {
|
||||
AI: true,
|
||||
Probe: powerShell(`npm list --global '@openai/codex' --depth=0 | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
checkedNativePowerShell(`fnm exec --using=lts-latest npm install --global '@openai/codex'`),
|
||||
checkedNativePowerShell(`npm install --global '@openai/codex'`),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -206,7 +263,7 @@ func CustomActions(opts Options) []Action {
|
||||
AI: true,
|
||||
Probe: powerShell(`npm list --global '@github/copilot' --depth=0 | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`),
|
||||
Commands: []CommandSpec{
|
||||
checkedNativePowerShell(`fnm exec --using=lts-latest npm install --global '@github/copilot'`),
|
||||
checkedNativePowerShell(`npm install --global '@github/copilot'`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@ func TestCustomActionsNoAIStillKeepsPlaywrightAndExtension(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRuntimeAndCustomDependencyOrdering(t *testing.T) {
|
||||
host := HostActions()
|
||||
if len(host) != 4 || host[0].Name != "Node.js LTS through fnm" || host[1].Name != "pyenv-win through Scoop or Chocolatey" || host[2].Name != "latest stable Python through pyenv-win" || host[3].Name != "VLC default media player associations" {
|
||||
host := HostActions(Options{})
|
||||
if len(host) != 6 || host[0].Name != "Visual Studio Community 2026" || host[1].Name != "fnm and Node.js LTS" || host[2].Name != "pyenv-win through Scoop or Chocolatey" || host[3].Name != "latest stable Python through pyenv-win" || host[4].Name != "semgrep" || host[5].Name != "VLC default media player associations" {
|
||||
t.Fatalf("unexpected host action order: %#v", host)
|
||||
}
|
||||
pyenvInstall := strings.Join(host[1].Commands[0].Args, " ")
|
||||
for _, want := range []string{"scoop", "Invoke-Native 'choco' @('install', 'pyenv-win'", "Chocolatey.Chocolatey"} {
|
||||
pyenvInstall := strings.Join(host[2].Commands[0].Args, " ")
|
||||
for _, want := range []string{"scoop", "Invoke-Native 'choco' @('install', 'pyenv-win'", "community.chocolatey.org"} {
|
||||
if !strings.Contains(pyenvInstall, want) {
|
||||
t.Fatalf("pyenv install command missing %q: %s", want, pyenvInstall)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func TestRuntimeAndCustomDependencyOrdering(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVLCDefaultMediaActionCoversCommonAudioVideoExtensions(t *testing.T) {
|
||||
host := HostActions()
|
||||
host := HostActions(Options{})
|
||||
vlc := host[len(host)-1]
|
||||
script := strings.Join(vlc.Commands[0].Args, " ")
|
||||
for _, want := range []string{
|
||||
|
||||
+19
-11
@@ -62,7 +62,7 @@ func BuildPlanForPhases(ctx context.Context, runner Runner, opts Options, phases
|
||||
}
|
||||
switch phase {
|
||||
case PhaseHost:
|
||||
actions := HostActions()
|
||||
actions := HostActions(opts)
|
||||
if checkState {
|
||||
plan.ActionStates[phase] = CheckActions(ctx, runner, actions)
|
||||
} else {
|
||||
@@ -93,7 +93,7 @@ func PrintPlan(out io.Writer, plan Plan, opts Options) {
|
||||
} else if state.CheckErr != nil {
|
||||
status = "check failed; will retry install"
|
||||
}
|
||||
fmt.Fprintf(out, " winget: %s (%s) - %s\n", state.Package.Name, state.Package.WingetID, status)
|
||||
fmt.Fprintf(out, " choco: %s (%s) - %s\n", state.Package.Name, state.Package.ChocoID, status)
|
||||
}
|
||||
}
|
||||
actions := plan.ActionStates[phase]
|
||||
@@ -122,9 +122,6 @@ func PrintPlan(out io.Writer, plan Plan, opts Options) {
|
||||
}
|
||||
|
||||
func (b *Bootstrapper) Run(ctx context.Context, opts Options) (runErr error) {
|
||||
if b.Runner == nil {
|
||||
b.Runner = ExecRunner{Timeout: 45 * time.Minute}
|
||||
}
|
||||
if b.In == nil {
|
||||
b.In = os.Stdin
|
||||
}
|
||||
@@ -134,6 +131,9 @@ func (b *Bootstrapper) Run(ctx context.Context, opts Options) (runErr error) {
|
||||
if b.Err == nil {
|
||||
b.Err = os.Stderr
|
||||
}
|
||||
if b.Runner == nil {
|
||||
b.Runner = ExecRunner{Timeout: 45 * time.Minute, Stream: b.Out}
|
||||
}
|
||||
|
||||
phases := SelectedPhases(opts)
|
||||
report := NewRunReport(opts, phases)
|
||||
@@ -180,10 +180,15 @@ func (b *Bootstrapper) Run(ctx context.Context, opts Options) (runErr error) {
|
||||
PrintPlan(b.Out, plan, opts)
|
||||
issues = append(issues, b.executeCheckedPlan(ctx, plan, opts, report)...)
|
||||
}
|
||||
return issues.Err()
|
||||
} else {
|
||||
issues = append(issues, b.executeCheckedPlan(ctx, plan, opts, report)...)
|
||||
}
|
||||
|
||||
if opts.Includes(PhaseConfig) {
|
||||
authIssues := b.offerInteractiveAuth(ctx, opts)
|
||||
b.recordIssues(report, &issues, authIssues)
|
||||
}
|
||||
|
||||
issues = append(issues, b.executeCheckedPlan(ctx, plan, opts, report)...)
|
||||
return issues.Err()
|
||||
}
|
||||
|
||||
@@ -300,14 +305,14 @@ func (b *Bootstrapper) runConfigPhase(ctx context.Context, opts Options) Issues
|
||||
if !IsEmptyIssue(issue) {
|
||||
issues = append(issues, issue)
|
||||
}
|
||||
if !opts.Headless {
|
||||
issues = append(issues, b.offerInteractiveAuth(ctx)...)
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
func (b *Bootstrapper) offerInteractiveAuth(ctx context.Context) Issues {
|
||||
func (b *Bootstrapper) offerInteractiveAuth(ctx context.Context, opts Options) Issues {
|
||||
var issues Issues
|
||||
if opts.Yes || IsSessionZero() {
|
||||
return issues
|
||||
}
|
||||
if result := b.Runner.Run(ctx, "gh", "auth", "status"); result.Err != nil {
|
||||
if confirm(b.In, b.Out, "Run `gh auth login` now? [y/N] ") {
|
||||
login := b.Runner.Run(ctx, "gh", "auth", "login")
|
||||
@@ -341,6 +346,9 @@ func (b *Bootstrapper) offerInteractiveAuth(ctx context.Context) Issues {
|
||||
}
|
||||
|
||||
func confirm(in io.Reader, out io.Writer, prompt string) bool {
|
||||
if in == nil {
|
||||
return false
|
||||
}
|
||||
fmt.Fprint(out, prompt)
|
||||
scanner := bufio.NewScanner(in)
|
||||
if !scanner.Scan() {
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestRunWithOSDoesNotProbePackagesBeforeWindowsUpdate(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(context.Background(), Options{Yes: true, NoWSL: true, Headless: true}); err != nil {
|
||||
if err := app.Run(context.Background(), Options{Yes: true, NoWSL: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func TestRunStopsAfterOSRebootNotice(t *testing.T) {
|
||||
Err: &bytes.Buffer{},
|
||||
Paths: testUserPaths(temp),
|
||||
}
|
||||
if err := app.Run(context.Background(), Options{Yes: true, NoWSL: true, Headless: true}); err != nil {
|
||||
if err := app.Run(context.Background(), Options{Yes: true, NoWSL: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "require a reboot") {
|
||||
@@ -90,7 +90,7 @@ func TestNewBootstrapperInitializesDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildPlanWrapperAndPrintPlanShowStatuses(t *testing.T) {
|
||||
pkg := Package{Name: "Git", WingetID: "Git.Git", WingetSource: "winget", Phase: PhaseHost}
|
||||
pkg := Package{Name: "Git", ChocoID: "git", Phase: PhaseHost}
|
||||
action := Action{Name: "done action"}
|
||||
plan := BuildPlan(context.Background(), &fakeRunner{}, Options{Only: PhaseOS})
|
||||
if len(plan.Phases) != 1 || plan.Phases[0] != PhaseOS {
|
||||
@@ -103,7 +103,7 @@ func TestBuildPlanWrapperAndPrintPlanShowStatuses(t *testing.T) {
|
||||
PackageStates: map[Phase][]PackageState{
|
||||
PhaseHost: {
|
||||
{Package: pkg, Installed: true},
|
||||
{Package: Package{Name: "Broken", WingetID: "Broken.ID"}, CheckErr: errors.New("check failed")},
|
||||
{Package: Package{Name: "Broken", ChocoID: "Broken.ID"}, CheckErr: errors.New("check failed")},
|
||||
},
|
||||
},
|
||||
ActionStates: map[Phase][]ActionState{
|
||||
@@ -174,7 +174,7 @@ func TestRunOnlyHostSkipsOSAndExecutesHostPlan(t *testing.T) {
|
||||
Err: &bytes.Buffer{},
|
||||
Paths: testUserPaths(temp),
|
||||
}
|
||||
if err := app.Run(context.Background(), Options{Only: PhaseHost, Yes: true, Headless: true}); err != nil {
|
||||
if err := app.Run(context.Background(), Options{Only: PhaseHost, Yes: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
update := WindowsUpdateCommand()
|
||||
@@ -263,7 +263,7 @@ func TestRunConfigPhaseRecordsDeploymentIssue(t *testing.T) {
|
||||
Documents: filepath.Join(temp, "docs"),
|
||||
},
|
||||
}
|
||||
issues := app.runConfigPhase(context.Background(), Options{Headless: true})
|
||||
issues := app.runConfigPhase(context.Background(), Options{})
|
||||
if len(issues) == 0 || !strings.Contains(issues[0].Step, "deploy managed config") {
|
||||
t.Fatalf("issues = %#v, want deployment issue", issues)
|
||||
}
|
||||
@@ -280,13 +280,13 @@ func TestRunConfigPhaseRecordsNeovimIssue(t *testing.T) {
|
||||
Err: &bytes.Buffer{},
|
||||
Paths: testUserPaths(temp),
|
||||
}
|
||||
issues := app.runConfigPhase(context.Background(), Options{Headless: true})
|
||||
issues := app.runConfigPhase(context.Background(), Options{})
|
||||
if len(issues) != 1 || issues[0].Step != "clone Neovim config" {
|
||||
t.Fatalf("issues = %#v, want Neovim clone issue", issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPhaseOffersAuthWhenInteractive(t *testing.T) {
|
||||
func TestRunOffersAuthWhenInteractive(t *testing.T) {
|
||||
temp := t.TempDir()
|
||||
pub := filepath.Join(temp, "home", ".ssh", "id_ed25519.pub")
|
||||
if err := os.MkdirAll(filepath.Dir(pub), 0o755); err != nil {
|
||||
@@ -297,13 +297,13 @@ func TestRunConfigPhaseOffersAuthWhenInteractive(t *testing.T) {
|
||||
}
|
||||
app := &Bootstrapper{
|
||||
Runner: &fakeRunner{},
|
||||
In: strings.NewReader(""),
|
||||
In: strings.NewReader("y\n"),
|
||||
Out: &bytes.Buffer{},
|
||||
Err: &bytes.Buffer{},
|
||||
Paths: testUserPaths(temp),
|
||||
}
|
||||
if issues := app.runConfigPhase(context.Background(), Options{}); issues.Err() != nil {
|
||||
t.Fatalf("issues = %#v", issues)
|
||||
if err := app.Run(context.Background(), Options{Only: PhaseConfig}); err != nil {
|
||||
t.Fatalf("Run err = %v", err)
|
||||
}
|
||||
if !app.Runner.(*fakeRunner).called("gh", "auth", "status") {
|
||||
t.Fatal("expected interactive auth status check")
|
||||
@@ -351,7 +351,7 @@ func TestOfferInteractiveAuthCoversLoginFailure(t *testing.T) {
|
||||
Err: &bytes.Buffer{},
|
||||
Paths: UserPaths{Home: temp},
|
||||
}
|
||||
issues := app.offerInteractiveAuth(context.Background())
|
||||
issues := app.offerInteractiveAuth(context.Background(), Options{})
|
||||
if len(issues) != 1 || issues[0].Step != "gh auth login" {
|
||||
t.Fatalf("issues = %#v, want login failure", issues)
|
||||
}
|
||||
@@ -374,7 +374,7 @@ func TestOfferInteractiveAuthCoversSSHUploadFailure(t *testing.T) {
|
||||
Err: &bytes.Buffer{},
|
||||
Paths: UserPaths{Home: temp},
|
||||
}
|
||||
issues := app.offerInteractiveAuth(context.Background())
|
||||
issues := app.offerInteractiveAuth(context.Background(), Options{})
|
||||
if len(issues) != 1 || issues[0].Step != "gh ssh-key add" {
|
||||
t.Fatalf("issues = %#v, want upload failure", issues)
|
||||
}
|
||||
@@ -398,7 +398,7 @@ func TestOfferInteractiveAuthCoversSSHDirectoryAndKeygenFailures(t *testing.T) {
|
||||
Err: &bytes.Buffer{},
|
||||
Paths: UserPaths{Home: homeFile},
|
||||
}
|
||||
issues := app.offerInteractiveAuth(context.Background())
|
||||
issues := app.offerInteractiveAuth(context.Background(), Options{})
|
||||
if len(issues) != 1 || issues[0].Step != "create SSH directory" {
|
||||
t.Fatalf("issues = %#v, want SSH directory failure", issues)
|
||||
}
|
||||
@@ -417,7 +417,7 @@ func TestOfferInteractiveAuthCoversSSHDirectoryAndKeygenFailures(t *testing.T) {
|
||||
Err: &bytes.Buffer{},
|
||||
Paths: UserPaths{Home: filepath.Join(temp, "home2")},
|
||||
}
|
||||
issues = app.offerInteractiveAuth(context.Background())
|
||||
issues = app.offerInteractiveAuth(context.Background(), Options{})
|
||||
if len(issues) != 1 || issues[0].Step != "ssh-keygen" {
|
||||
t.Fatalf("issues = %#v, want ssh-keygen failure", issues)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,884 @@
|
||||
{
|
||||
"desktop": {
|
||||
"GeForce 8800 Ultra": "8",
|
||||
"GeForce 8800 GTX": "7",
|
||||
"GeForce 8800 GTS 512": "270",
|
||||
"GeForce 8800 GTS": "6",
|
||||
"GeForce 8800 GT": "258",
|
||||
"GeForce 8800 GS": "281",
|
||||
"GeForce 8600 GTS": "5",
|
||||
"GeForce 8600 GT": "4",
|
||||
"GeForce 8600 GS": "233",
|
||||
"GeForce 8500 GT": "3",
|
||||
"GeForce 8400 GS": "2",
|
||||
"GeForce 8400 SE": "234",
|
||||
"GeForce 8400": "272",
|
||||
"GeForce 8300 GS": "1",
|
||||
"GeForce 8300": "288",
|
||||
"GeForce 8200": "293",
|
||||
"GeForce 8100": "290",
|
||||
"GeForce 7025": "9",
|
||||
"GeForce 7050 PV": "10",
|
||||
"GeForce 7050": "238",
|
||||
"GeForce 7100": "263",
|
||||
"GeForce 7100 GS": "11",
|
||||
"GeForce 7150": "240",
|
||||
"GeForce 7300 SE": "15",
|
||||
"GeForce 7300 LE": "14",
|
||||
"GeForce 7300 GS": "12",
|
||||
"GeForce 7300 GT": "13",
|
||||
"GeForce 7350 LE": "16",
|
||||
"GeForce 7500 LE": "17",
|
||||
"GeForce 7550 LE": "18",
|
||||
"GeForce 7600 LE": "21",
|
||||
"GeForce 7600 GS": "19",
|
||||
"GeForce 7600 GT": "20",
|
||||
"GeForce 7650 GS": "22",
|
||||
"GeForce 7800 GT": "648",
|
||||
"GeForce 7800 GS": "23",
|
||||
"GeForce 7800 GTX": "24",
|
||||
"GeForce 7800 SLI": "25",
|
||||
"GeForce 7900 GS": "26",
|
||||
"GeForce 7900 GT": "27",
|
||||
"GeForce 7900 GTX": "28",
|
||||
"GeForce 7950 GT": "29",
|
||||
"GeForce 7950 GX2": "30",
|
||||
"GeForce 6100": "31",
|
||||
"GeForce 6100 nForce 400": "32",
|
||||
"GeForce 6100 nForce 405": "33",
|
||||
"GeForce 6100 nForce 420": "34",
|
||||
"GeForce 6150": "35",
|
||||
"GeForce 6150 LE": "36",
|
||||
"GeForce 6150LE": "262",
|
||||
"GeForce 6150SE nForce 430": "37",
|
||||
"GeForce 6200": "38",
|
||||
"GeForce 6200 A-LE": "39",
|
||||
"GeForce 6200 LE": "40",
|
||||
"GeForce 6200 TurboCache(TM)": "41",
|
||||
"GeForce 6200SE TurboCache(TM)": "42",
|
||||
"GeForce 6250": "43",
|
||||
"GeForce 6500": "44",
|
||||
"GeForce 6600": "45",
|
||||
"GeForce 6600 GT": "46",
|
||||
"GeForce 6600 LE": "47",
|
||||
"GeForce 6600 VE": "48",
|
||||
"GeForce 6610 XL": "49",
|
||||
"GeForce 6700 XL": "50",
|
||||
"GeForce 6800": "51",
|
||||
"GeForce 6800 GS": "53",
|
||||
"GeForce 6800 GT": "54",
|
||||
"GeForce 6800 LE": "55",
|
||||
"GeForce 6800 Ultra": "56",
|
||||
"GeForce 6800 XE": "57",
|
||||
"GeForce 6800 XT": "58",
|
||||
"GeForce FX 5100": "59",
|
||||
"GeForce FX 5200": "60",
|
||||
"GeForce FX 5200 Ultra": "61",
|
||||
"GeForce FX 5200LE": "62",
|
||||
"GeForce FX 5500": "63",
|
||||
"GeForce FX 5600": "64",
|
||||
"GeForce FX 5600 Ultra": "65",
|
||||
"GeForce FX 5600XT": "66",
|
||||
"GeForce FX 5700": "67",
|
||||
"GeForce FX 5700 Ultra": "68",
|
||||
"GeForce FX 5700LE": "69",
|
||||
"GeForce FX 5700VE": "70",
|
||||
"GeForce FX 5800": "71",
|
||||
"GeForce FX 5800 Ultra": "72",
|
||||
"GeForce FX 5900": "73",
|
||||
"GeForce FX 5900 Ultra": "74",
|
||||
"GeForce FX 5900XT": "75",
|
||||
"GeForce FX 5900ZT": "76",
|
||||
"GeForce FX 5950 Ultra": "77",
|
||||
"GeForce PCX 5300": "78",
|
||||
"GeForce PCX 5750": "79",
|
||||
"GeForce PCX 5900": "80",
|
||||
"nForce 680i SLI": "177",
|
||||
"nForce 680i LT SLI": "178",
|
||||
"nForce 650i SLI": "179",
|
||||
"nForce 650i Ultra": "180",
|
||||
"nForce 630i": "256",
|
||||
"nForce 610i": "257",
|
||||
"nForce 680a SLI": "181",
|
||||
"nForce 630a": "243",
|
||||
"nForce 590 SLI Intel": "182",
|
||||
"nForce 570 SLI Intel": "183",
|
||||
"nForce 590 SLI AMD": "184",
|
||||
"nForce 570 SLI AMD": "185",
|
||||
"nForce 570 Ultra AMD": "186",
|
||||
"nForce 560": "224",
|
||||
"nForce 550": "187",
|
||||
"nForce 520": "190",
|
||||
"nForce4 SLI X16 Intel": "191",
|
||||
"nForce4 SLI Intel": "192",
|
||||
"nForce4 SLI XE Intel": "193",
|
||||
"nForce4 Ultra Intel": "194",
|
||||
"nForce4 SLI X16 AMD": "195",
|
||||
"nForce4 SLI AMD": "196",
|
||||
"nForce4 Ultra AMD": "197",
|
||||
"nForce4 AMD": "198",
|
||||
"nForce 430": "206",
|
||||
"nForce 410": "204",
|
||||
"nForce 405": "207",
|
||||
"nForce 400": "208",
|
||||
"nForce 420": "209",
|
||||
"nForce Professional 2200": "210",
|
||||
"nForce Professional 22200": "211",
|
||||
"nForce Professional 3600": "214",
|
||||
"nForce Professional 3400": "213",
|
||||
"Quadro Plex S Series": "302",
|
||||
"Quadro Plex Model II": "82",
|
||||
"Quadro Plex D Series": "301",
|
||||
"Quadro Plex Model IV": "84",
|
||||
"Quadro Plex 7000": "534",
|
||||
"Quadro CX": "318",
|
||||
"Quadro FX 330": "107",
|
||||
"Quadro FX 350": "85",
|
||||
"Quadro FX 370": "245",
|
||||
"Quadro FX 370 Low Profile": "333",
|
||||
"Quadro FX 380": "412",
|
||||
"Quadro FX 380 Low Profile": "469",
|
||||
"Quadro FX 470": "319",
|
||||
"Quadro FX 500": "86",
|
||||
"Quadro FX 540": "87",
|
||||
"Quadro FX 550": "88",
|
||||
"Quadro FX 560": "89",
|
||||
"Quadro FX 570": "246",
|
||||
"Quadro FX 580": "413",
|
||||
"Quadro FX 700": "90",
|
||||
"Quadro FX 1000": "91",
|
||||
"Quadro FX 1100": "92",
|
||||
"Quadro FX 1300": "93",
|
||||
"Quadro FX 1400": "94",
|
||||
"Quadro FX 1500": "95",
|
||||
"Quadro FX 1700": "247",
|
||||
"Quadro FX 1800": "411",
|
||||
"Quadro FX 2000": "96",
|
||||
"Quadro FX 3000": "97",
|
||||
"Quadro FX 3400": "98",
|
||||
"Quadro FX 3450": "306",
|
||||
"Quadro FX 3500": "100",
|
||||
"Quadro FX 3700": "277",
|
||||
"Quadro FX 3800": "410",
|
||||
"Quadro FX 4000": "101",
|
||||
"Quadro FX 4500": "102",
|
||||
"Quadro FX 4500 X2": "103",
|
||||
"Quadro FX 4600": "104",
|
||||
"Quadro FX 4700 X2": "294",
|
||||
"Quadro FX 4800": "335",
|
||||
"Quadro FX 5500": "105",
|
||||
"Quadro FX 5600": "106",
|
||||
"Quadro FX 5800": "336",
|
||||
"Quadro NVS 50": "580",
|
||||
"Quadro NVS 55": "581",
|
||||
"Quadro NVS 210": "582",
|
||||
"Quadro NVS 280": "583",
|
||||
"Quadro NVS 285": "584",
|
||||
"Quadro NVS 290": "585",
|
||||
"Quadro NVS 295": "586",
|
||||
"NVS 810": "786",
|
||||
"NVS 510": "655",
|
||||
"NVS 315": "697",
|
||||
"NVS 310": "641",
|
||||
"NVS 300": "531",
|
||||
"Quadro NVS 420": "587",
|
||||
"Quadro NVS 440": "588",
|
||||
"Quadro NVS 450": "589",
|
||||
"Quadro4 380 XGL": "115",
|
||||
"Quadro4 550 XGL": "130",
|
||||
"Quadro4 580 XGL": "117",
|
||||
"Quadro4 700 XGL": "118",
|
||||
"Quadro4 750 XGL": "119",
|
||||
"Quadro4 780 XGL": "120",
|
||||
"Quadro4 900 XGL": "121",
|
||||
"Quadro4 980 XGL": "122",
|
||||
"Quadro DCC": "123",
|
||||
"Quadro4 500 GoGL": "131",
|
||||
"Quadro4 700 GoGL": "132",
|
||||
"Quadro2 MXR": "168",
|
||||
"GeForce4 Ti 4200": "134",
|
||||
"GeForce4 Ti 4200 with AGP8X": "135",
|
||||
"GeForce4 Ti 4400": "136",
|
||||
"GeForce4 Ti 4600": "137",
|
||||
"GeForce4 Ti 4800": "138",
|
||||
"GeForce4 Ti 4800 SE": "139",
|
||||
"GeForce4 MX 4000": "140",
|
||||
"GeForce4 MX 420": "141",
|
||||
"GeForce4 MX 420 with AGP8X": "142",
|
||||
"GeForce4 MX 440": "143",
|
||||
"GeForce4 MX 440 with AGP8X": "144",
|
||||
"GeForce4 MX 440-SE": "145",
|
||||
"GeForce4 MX 440SE with AGP8X": "146",
|
||||
"GeForce4 MX 460": "147",
|
||||
"GeForce4 MX Integrated GPU": "148",
|
||||
"GeForce3": "259",
|
||||
"GeForce3 Ti 200": "260",
|
||||
"GeForce3 Ti 500": "261",
|
||||
"GeForce2 GTS": "152",
|
||||
"GeForce2 Ti": "153",
|
||||
"GeForce2 Ultra": "154",
|
||||
"GeForce2 Integrated GPU": "155",
|
||||
"GeForce2 MX 100": "156",
|
||||
"GeForce2 MX": "157",
|
||||
"GeForce 256": "158",
|
||||
"GeForce 256 DDR": "159",
|
||||
"Riva 128": "161",
|
||||
"Riva 128 AGP": "160",
|
||||
"RIVA TNT": "162",
|
||||
"Aladdin TNT2": "163",
|
||||
"RIVA TNT2 Model 64": "164",
|
||||
"RIVA TNT2 Ultra": "165",
|
||||
"RIVA TNT2": "166",
|
||||
"Vanta": "170",
|
||||
"Quadro2 Pro": "167",
|
||||
"GeForce Go 7950 GTX": "276",
|
||||
"GeForce Go 7900 GTX": "176",
|
||||
"GeForce Go 7800 GTX": "174",
|
||||
"GeForce Go 7900 GS": "175",
|
||||
"Quadro NVS 210S": "215",
|
||||
"NF-SPP-100": "216",
|
||||
"GeForce Go 6150": "217",
|
||||
"GeForce Go 6100": "218",
|
||||
"GeForce Go 6155": "219",
|
||||
"nForce 630M": "220",
|
||||
"GeForce 7190M": "221",
|
||||
"GeForce 7150M": "222",
|
||||
"GeForce 7000M": "223",
|
||||
"Quadro Sync II": "838",
|
||||
"Quadro Sync": "665",
|
||||
"Quadro G-Sync II": "226",
|
||||
"Quadro G-Sync I": "225",
|
||||
"nForce 1": "304",
|
||||
"nForce 3": "305",
|
||||
"MAXtreme": "227",
|
||||
"POWERdraft": "228",
|
||||
"GeForce 2 MX Series": "241",
|
||||
"TNT and TNT2 Series": "282",
|
||||
"nForce Professional 2000 Series": "296",
|
||||
"nForce Professional 3000 Series": "297",
|
||||
"nForce 790i Ultra SLI": "285",
|
||||
"nForce 790i SLI": "284",
|
||||
"nForce 780i SLI": "273",
|
||||
"nForce 760i SLI": "409",
|
||||
"nForce 750i SLI": "274",
|
||||
"nForce 740i SLI": "403",
|
||||
"nForce 730i": "320",
|
||||
"nForce 720i": "378",
|
||||
"nForce 780a SLI": "292",
|
||||
"nForce 750a SLI": "404",
|
||||
"nForce 740a SLI": "405",
|
||||
"nForce 730a": "406",
|
||||
"nForce 720a - GeForce 8100": "407",
|
||||
"nForce 710a": "408",
|
||||
"GeForce 8800M GTX": "315",
|
||||
"GeForce 8800M GTS": "314",
|
||||
"GeForce 8700M GT": "275",
|
||||
"Quadro SDI": "298",
|
||||
"Tesla C2075": "573",
|
||||
"Tesla C2070": "513",
|
||||
"Tesla C2050": "492",
|
||||
"Tesla C1060": "327",
|
||||
"Tesla C870": "278",
|
||||
"D870": "279",
|
||||
"S2050": "505",
|
||||
"S1070": "317",
|
||||
"S870": "280",
|
||||
"GeForce 9800 GX2": "286",
|
||||
"GeForce 9800 GTX": "287",
|
||||
"GeForce 9800 GT": "309",
|
||||
"GeForce 9600 GT": "283",
|
||||
"GeForce 9600 GSO": "295",
|
||||
"GeForce 9600 GSO 512": "321",
|
||||
"GeForce 9600 GS": "316",
|
||||
"GeForce 9500 GT": "307",
|
||||
"GeForce 9500 GS": "310",
|
||||
"GeForce 9400 GT": "311",
|
||||
"GeForce 9400": "325",
|
||||
"GeForce 9300 GS": "312",
|
||||
"GeForce 9300 GE": "313",
|
||||
"GeForce 9300 SE": "322",
|
||||
"GeForce 9300": "323",
|
||||
"GeForce 9200": "324",
|
||||
"GeForce 9100": "418",
|
||||
"GeForce GTX 295": "376",
|
||||
"GeForce GTX 285": "377",
|
||||
"GeForce GTX 280": "299",
|
||||
"GeForce GTX 275": "416",
|
||||
"GeForce GTX 260": "300",
|
||||
"GeForce GTS 250": "399",
|
||||
"GeForce GTS 240": "450",
|
||||
"GeForce GT 230": "449",
|
||||
"GeForce GT 240": "470",
|
||||
"GeForce GT 220": "448",
|
||||
"GeForce G210": "447",
|
||||
"GeForce 210": "453",
|
||||
"GeForce 205": "471",
|
||||
"Full driver CD for GeForce": "381",
|
||||
"Full driver CD for Notebook PCs": "488",
|
||||
"3D Vision driver for GeForce": "432",
|
||||
"3D Vision driver for Notebook": "555",
|
||||
"3D Vision driver for Quadro": "452",
|
||||
"3D Vision Video Player": "451",
|
||||
"nForce 980a SLI": "398",
|
||||
"GeForce GT 140": "400",
|
||||
"GeForce GT 130": "401",
|
||||
"GeForce GT 120": "402",
|
||||
"GeForce G100": "415",
|
||||
"ION": "431",
|
||||
"3D Vision Pro driver": "535",
|
||||
"3D Vision Pro Video Player": "536",
|
||||
"ION LE": "446",
|
||||
"GeForce GT 340": "473",
|
||||
"GeForce GT 330": "474",
|
||||
"GeForce GT 320": "475",
|
||||
"GeForce 315": "494",
|
||||
"GeForce 310": "493",
|
||||
"GeForce GTX 480": "490",
|
||||
"GeForce GTX 470": "491",
|
||||
"GeForce GTX 465": "498",
|
||||
"GeForce GTX 460 SE v2": "600",
|
||||
"GeForce GTX 460 SE": "530",
|
||||
"GeForce GTX 460": "499",
|
||||
"GeForce GTS 450": "514",
|
||||
"GeForce GT 440": "541",
|
||||
"GeForce GT 430": "516",
|
||||
"GeForce GT 420": "538",
|
||||
"GeForce 405": "572",
|
||||
"Quadro GV100": "866",
|
||||
"Quadro GP100": "839",
|
||||
"Quadro P6000": "824",
|
||||
"Quadro P5200": "869",
|
||||
"Quadro P5000": "823",
|
||||
"Quadro P4000": "840",
|
||||
"Quadro P2200": "900",
|
||||
"Quadro P2000": "841",
|
||||
"Quadro P1000": "842",
|
||||
"Quadro P620": "865",
|
||||
"Quadro P600": "843",
|
||||
"Quadro P400": "844",
|
||||
"Quadro M6000": "768",
|
||||
"Quadro M5000": "780",
|
||||
"Quadro M4000": "781",
|
||||
"Quadro M2000": "811",
|
||||
"Quadro K6000": "708",
|
||||
"Quadro K5000 for Mac": "696",
|
||||
"Quadro K5200": "752",
|
||||
"Quadro K5000": "654",
|
||||
"Quadro K4000": "681",
|
||||
"Quadro K4200": "751",
|
||||
"Quadro K2200": "750",
|
||||
"Quadro K2000": "678",
|
||||
"Quadro K2000D": "682",
|
||||
"Quadro K1200": "767",
|
||||
"Quadro K620": "749",
|
||||
"Quadro K600": "679",
|
||||
"Quadro K420": "748",
|
||||
"Quadro 7000": "625",
|
||||
"Quadro 6000": "508",
|
||||
"Quadro 5000": "509",
|
||||
"Quadro 4000": "510",
|
||||
"Quadro 4000 for Mac": "529",
|
||||
"Quadro 2000": "511",
|
||||
"Quadro 2000D": "543",
|
||||
"Quadro 600": "512",
|
||||
"Quadro 410": "645",
|
||||
"Quadro 400": "553",
|
||||
"M60": "783",
|
||||
"M40": "794",
|
||||
"M6": "784",
|
||||
"M4": "799",
|
||||
"M2090": "570",
|
||||
"M2075": "571",
|
||||
"M2070": "525",
|
||||
"M2070-Q": "542",
|
||||
"M2050": "526",
|
||||
"M1060": "527",
|
||||
"GeForce GTX 590": "545",
|
||||
"GeForce GTX 580": "528",
|
||||
"GeForce GTX 570": "532",
|
||||
"GeForce GTX 560 Ti": "540",
|
||||
"GeForce GTX 560 SE": "609",
|
||||
"GeForce GTX 560": "560",
|
||||
"GeForce GTX 555": "626",
|
||||
"GeForce GTX 550 Ti": "544",
|
||||
"GeForce GT 545": "559",
|
||||
"GeForce GT 530": "557",
|
||||
"GeForce GT 520": "554",
|
||||
"GeForce 510": "605",
|
||||
"Tesla X2070": "717",
|
||||
"Tesla X2090": "608",
|
||||
"GeForce GTX 690": "627",
|
||||
"GeForce GTX 680": "610",
|
||||
"GeForce GTX 670": "629",
|
||||
"GeForce GTX 660 Ti": "653",
|
||||
"GeForce GTX 660": "660",
|
||||
"GeForce GTX 650 Ti BOOST": "683",
|
||||
"GeForce GTX 650 Ti": "666",
|
||||
"GeForce GTX 650": "661",
|
||||
"GeForce GTX 645": "731",
|
||||
"GeForce GT 645": "633",
|
||||
"GeForce GT 640": "632",
|
||||
"GeForce GT 635": "685",
|
||||
"GeForce GT 630": "631",
|
||||
"GeForce GT 625": "741",
|
||||
"GeForce GT 620": "611",
|
||||
"GeForce GT 610": "630",
|
||||
"GeForce 605": "624",
|
||||
"Tesla K80": "762",
|
||||
"Tesla K520": "856",
|
||||
"Tesla K40c": "713",
|
||||
"Tesla K40m": "714",
|
||||
"Tesla K40s": "715",
|
||||
"Tesla K40st": "716",
|
||||
"Tesla K40t": "777",
|
||||
"Tesla K20Xm": "670",
|
||||
"Tesla K20m": "668",
|
||||
"Tesla K20s": "684",
|
||||
"Tesla K20c": "667",
|
||||
"Tesla K10": "652",
|
||||
"Tesla K8": "760",
|
||||
"CUDA Toolkit 5.5": "673",
|
||||
"GRID K2": "710",
|
||||
"GRID K520": "704",
|
||||
"GRID K1": "709",
|
||||
"GRID K340": "705",
|
||||
"GeForce GTX 780 Ti": "712",
|
||||
"GeForce GTX 780": "691",
|
||||
"GeForce GTX 770": "694",
|
||||
"GeForce GTX 760": "703",
|
||||
"GeForce GTX 760 Ti": "711",
|
||||
"GeForce GTX 750 Ti": "727",
|
||||
"GeForce GTX 750": "728",
|
||||
"GeForce GTX 745": "730",
|
||||
"GeForce GT 740": "745",
|
||||
"GeForce GT 730": "746",
|
||||
"GeForce GT 720": "754",
|
||||
"GeForce GT 710": "742",
|
||||
"GeForce GT 705": "761",
|
||||
"GeForce GTX 980 Ti": "778",
|
||||
"GeForce GTX 980": "755",
|
||||
"GeForce GTX 970": "756",
|
||||
"GeForce GTX 960": "764",
|
||||
"GeForce GTX 950": "782",
|
||||
"GeForce GTX 1080 Ti": "845",
|
||||
"GeForce GTX 1080": "815",
|
||||
"GeForce GTX 1070 Ti": "859",
|
||||
"GeForce GTX 1070": "816",
|
||||
"GeForce GTX 1060": "817",
|
||||
"GeForce GTX 1050 Ti": "825",
|
||||
"GeForce GTX 1050": "826",
|
||||
"GeForce GT 1030": "852",
|
||||
"GeForce GT 1010": "936",
|
||||
"Tesla P100": "822",
|
||||
"Tesla P40": "827",
|
||||
"Tesla P6": "858",
|
||||
"Tesla P4": "831",
|
||||
"Tesla V100": "857",
|
||||
"TITAN RTX": "885",
|
||||
"TITAN V": "861",
|
||||
"TITAN Xp": "850",
|
||||
"TITAN X (Pascal)": "818",
|
||||
"GeForce GTX TITAN X": "766",
|
||||
"GeForce GTX TITAN": "695",
|
||||
"GeForce GTX TITAN Black": "729",
|
||||
"GeForce GTX TITAN Z": "744",
|
||||
"GeForce RTX 2080 Ti": "877",
|
||||
"GeForce RTX 2080 SUPER": "904",
|
||||
"GeForce RTX 2080": "879",
|
||||
"GeForce RTX 2070 SUPER": "903",
|
||||
"GeForce RTX 2070": "880",
|
||||
"GeForce RTX 2060 SUPER": "902",
|
||||
"GeForce RTX 2060": "887",
|
||||
"Quadro RTX 8000": "886",
|
||||
"Quadro RTX 6000": "881",
|
||||
"Quadro RTX 5000": "882",
|
||||
"Quadro RTX 4000": "884",
|
||||
"Quadro RTX 3000": "905",
|
||||
"Tesla T4": "883",
|
||||
"GeForce GTX 1660 SUPER": "910",
|
||||
"GeForce GTX 1650 SUPER": "911",
|
||||
"GeForce GTX 1660 Ti": "892",
|
||||
"GeForce GTX 1660": "895",
|
||||
"GeForce GTX 1650": "897",
|
||||
"GeForce GTX 1630": "993",
|
||||
"HGX GB200": "1069",
|
||||
"HGX B200": "1064",
|
||||
"H20-3e": "1063",
|
||||
"H20": "1043",
|
||||
"GH200": "1034",
|
||||
"HGX H800": "1013",
|
||||
"HGX H200": "1050",
|
||||
"HGX H100": "1002",
|
||||
"HGX A800": "997",
|
||||
"HGX A100": "922",
|
||||
"HGX-2": "896",
|
||||
"AX800": "1032",
|
||||
"A800": "1031",
|
||||
"A100": "923",
|
||||
"A40": "937",
|
||||
"A30": "954",
|
||||
"A16": "966",
|
||||
"A10": "955",
|
||||
"A2": "970",
|
||||
"RTX 6000 Ada Generation": "1000",
|
||||
"RTX 5000 Ada Generation": "1025",
|
||||
"RTX 4500 Ada Generation": "1030",
|
||||
"RTX 4000 Ada Generation": "1026",
|
||||
"RTX 4000 SFF Ada Generation": "1014",
|
||||
"RTX 2000 Ada Generation": "1042",
|
||||
"RTX 2000E Ada Generation": "1049",
|
||||
"RTX A2000": "967",
|
||||
"RTX A1000": "1047",
|
||||
"RTX A400": "1048",
|
||||
"RTX 8000": "927",
|
||||
"RTX 6000": "928",
|
||||
"RTX A6000": "935",
|
||||
"RTX A5000": "943",
|
||||
"RTX A4000": "944",
|
||||
"T1000": "945",
|
||||
"T600": "946",
|
||||
"T400": "947",
|
||||
"GeForce RTX 3090 Ti": "985",
|
||||
"GeForce RTX 3090": "930",
|
||||
"GeForce RTX 3080 Ti": "964",
|
||||
"GeForce RTX 3080": "929",
|
||||
"GeForce RTX 3070 Ti": "965",
|
||||
"GeForce RTX 3070": "933",
|
||||
"GeForce RTX 3060 Ti": "934",
|
||||
"GeForce RTX 3060": "942",
|
||||
"GeForce RTX 3050": "975",
|
||||
"RTX 5880 Ada Generation": "1035",
|
||||
"RTX A5500": "984",
|
||||
"RTX A4500": "969",
|
||||
"RTX A4000H": "980",
|
||||
"T400E": "1044",
|
||||
"H100 PCIe": "994",
|
||||
"H100 NVL": "1028",
|
||||
"H800 PCIe": "1012",
|
||||
"H800 NVL": "1029",
|
||||
"H200 NVL": "1062",
|
||||
"GeForce RTX 4090 D": "1036",
|
||||
"GeForce RTX 4090": "995",
|
||||
"GeForce RTX 4080 SUPER": "1041",
|
||||
"GeForce RTX 4080": "999",
|
||||
"GeForce RTX 4070 Ti SUPER": "1040",
|
||||
"GeForce RTX 4070 Ti": "1001",
|
||||
"GeForce RTX 4070 SUPER": "1039",
|
||||
"GeForce RTX 4070": "1015",
|
||||
"GeForce RTX 4060 Ti": "1022",
|
||||
"GeForce RTX 4060": "1023",
|
||||
"L40": "1003",
|
||||
"L40S": "1027",
|
||||
"L20": "1038",
|
||||
"L4": "1009",
|
||||
"L2": "1037",
|
||||
"BlueField-3": "1011",
|
||||
"BlueField-2": "1010",
|
||||
"BlueField Drivers for Host": "1024",
|
||||
"GeForce RTX 5090 D": "1067",
|
||||
"GeForce RTX 5090": "1066",
|
||||
"GeForce RTX 5080": "1065",
|
||||
"GeForce RTX 5070 Ti": "1068",
|
||||
"GeForce RTX 5070": "1070",
|
||||
"GeForce RTX 5060 Ti": "1076",
|
||||
"RTX PRO 6000 Blackwell Workstation": "1071",
|
||||
"RTX PRO 6000 Blackwell Max-Q Workstation": "1072"
|
||||
},
|
||||
"notebook": {
|
||||
"NVS 5400M": "642",
|
||||
"NVS 5200M": "614",
|
||||
"NVS 5100M": "486",
|
||||
"NVS 4200M": "565",
|
||||
"NVS 3100M": "487",
|
||||
"NVS 2100M": "517",
|
||||
"Quadro NVS 510M": "590",
|
||||
"Quadro NVS 320M": "591",
|
||||
"Quadro NVS 300M": "592",
|
||||
"Quadro NVS 160M": "593",
|
||||
"Quadro NVS 150M": "594",
|
||||
"Quadro NVS 140M": "595",
|
||||
"Quadro NVS 135M": "596",
|
||||
"Quadro NVS 130M": "597",
|
||||
"Quadro NVS 120M": "598",
|
||||
"Quadro NVS 110M": "599",
|
||||
"Quadro FX 3800M": "483",
|
||||
"Quadro FX 3700M": "419",
|
||||
"Quadro FX 3600M": "444",
|
||||
"Quadro FX 2800M": "496",
|
||||
"Quadro FX 2700M": "126",
|
||||
"Quadro FX 1800M": "482",
|
||||
"Quadro FX 1700M": "252",
|
||||
"Quadro FX 1600M": "443",
|
||||
"Quadro FX 880M": "497",
|
||||
"Quadro FX 770M": "441",
|
||||
"Quadro FX 570M": "253",
|
||||
"Quadro FX 380M": "484",
|
||||
"Quadro FX 370M": "440",
|
||||
"Quadro FX 360M": "254",
|
||||
"GeForce 9800M GTX": "347",
|
||||
"GeForce 9800M GTS": "348",
|
||||
"GeForce 9800M GT": "349",
|
||||
"GeForce 9800M GS": "350",
|
||||
"GeForce 9700M GTS": "351",
|
||||
"GeForce 9700M GT": "352",
|
||||
"GeForce 9650M GT": "353",
|
||||
"GeForce 9650M GS": "354",
|
||||
"GeForce 9600M GT": "355",
|
||||
"GeForce 9600M GS": "356",
|
||||
"GeForce 9500M GS": "357",
|
||||
"GeForce 9500M G": "358",
|
||||
"GeForce 9400M G": "384",
|
||||
"GeForce 9400M": "383",
|
||||
"GeForce 9300M GS": "359",
|
||||
"GeForce 9300M G": "360",
|
||||
"GeForce 9200M GS": "361",
|
||||
"GeForce 9100M G": "382",
|
||||
"GeForce 8800M GTX": "363",
|
||||
"GeForce 8800M GTS": "364",
|
||||
"GeForce 8800M GS": "365",
|
||||
"GeForce 8700M GT": "366",
|
||||
"GeForce 8600M GT": "367",
|
||||
"GeForce 8600M GS": "368",
|
||||
"GeForce 8400M GT": "369",
|
||||
"GeForce 8400M GS": "370",
|
||||
"GeForce 8400M G": "371",
|
||||
"GeForce 8200M G": "385",
|
||||
"GeForce 8200M": "569",
|
||||
"GeForce Go 7950 GTX": "372",
|
||||
"GeForce Go 7900 GTX": "373",
|
||||
"GeForce Go 7900 GS": "375",
|
||||
"GeForce Go 7900 SE": "389",
|
||||
"GeForce Go 7800 GTX": "374",
|
||||
"GeForce Go 7800": "390",
|
||||
"GeForce Go 7700": "391",
|
||||
"GeForce Go 7600": "392",
|
||||
"GeForce Go 7400": "393",
|
||||
"GeForce Go 7300": "394",
|
||||
"GeForce Go 7200": "395",
|
||||
"GeForce 7150M": "397",
|
||||
"GeForce 7000M": "396",
|
||||
"GeForce GTS 160M": "424",
|
||||
"GeForce GTS 150M": "733",
|
||||
"GeForce GT 130M": "425",
|
||||
"GeForce GT 120M": "426",
|
||||
"GeForce G 110M": "427",
|
||||
"GeForce G 105M": "428",
|
||||
"GeForce G 103M": "429",
|
||||
"GeForce G 102M": "430",
|
||||
"GeForce GTX 285M": "454",
|
||||
"GeForce GTX 280M": "421",
|
||||
"GeForce GTX 260M": "422",
|
||||
"GeForce GTS 260M": "456",
|
||||
"GeForce GTS 250M": "457",
|
||||
"GeForce GT 240M": "460",
|
||||
"GeForce GT 230M": "461",
|
||||
"GeForce GT 220M": "462",
|
||||
"GeForce G210M": "465",
|
||||
"GeForce G205M": "647",
|
||||
"Quadro RTX 5000": "914",
|
||||
"Quadro RTX 3000": "912",
|
||||
"Quadro T2000": "908",
|
||||
"Quadro T1000": "909",
|
||||
"Quadro P5000": "847",
|
||||
"Quadro P3000": "849",
|
||||
"Quadro P2000": "873",
|
||||
"Quadro P1000": "874",
|
||||
"Quadro M5000 SE": "829",
|
||||
"Quadro M3000 SE": "828",
|
||||
"Quadro K3100M": "721",
|
||||
"Quadro 500M": "615",
|
||||
"Quadro 1000M": "547",
|
||||
"Quadro 3000M": "568",
|
||||
"Quadro 4000M": "567",
|
||||
"Quadro FX 560M": "442",
|
||||
"ION": "463",
|
||||
"ION LE": "464",
|
||||
"GeForce GTS 360M": "455",
|
||||
"GeForce GTS 350M": "478",
|
||||
"GeForce GT 335M": "458",
|
||||
"GeForce GT 330M": "459",
|
||||
"GeForce GT 325M": "476",
|
||||
"GeForce GT 320M": "495",
|
||||
"GeForce 320M": "563",
|
||||
"GeForce 315M": "564",
|
||||
"GeForce 310M": "466",
|
||||
"GeForce 305M": "467",
|
||||
"GeForce GTX 485M": "556",
|
||||
"GeForce GTX 480M": "500",
|
||||
"GeForce GTX 470M": "533",
|
||||
"GeForce GTX 460M": "518",
|
||||
"GeForce GT 445M": "523",
|
||||
"GeForce GT 435M": "519",
|
||||
"GeForce GT 425M": "520",
|
||||
"GeForce GT 420M": "521",
|
||||
"GeForce GT 415M": "522",
|
||||
"GeForce 410M": "562",
|
||||
"GeForce 405M": "732",
|
||||
"Quadro P5200": "870",
|
||||
"Quadro P4200": "871",
|
||||
"Quadro P3200": "872",
|
||||
"Quadro P4000": "848",
|
||||
"Quadro P620": "992",
|
||||
"Quadro P600": "875",
|
||||
"Quadro P520": "901",
|
||||
"Quadro P500": "876",
|
||||
"Quadro M2200": "834",
|
||||
"Quadro M1200": "835",
|
||||
"Quadro M620": "830",
|
||||
"Quadro M520": "851",
|
||||
"Quadro M5500": "810",
|
||||
"Quadro M5000M": "788",
|
||||
"Quadro M4000M": "789",
|
||||
"Quadro M3000M": "790",
|
||||
"Quadro M2000M": "791",
|
||||
"Quadro M1000M": "792",
|
||||
"Quadro M600M": "793",
|
||||
"Quadro M500M": "814",
|
||||
"Quadro K5100M": "723",
|
||||
"Quadro K5000M": "659",
|
||||
"Quadro K4100M": "722",
|
||||
"Quadro K4000M": "658",
|
||||
"Quadro K2200M": "774",
|
||||
"Quadro K2100M": "720",
|
||||
"Quadro K3000M": "657",
|
||||
"Quadro K2000M": "649",
|
||||
"Quadro K1100M": "719",
|
||||
"Quadro K1000M": "650",
|
||||
"Quadro K620M": "776",
|
||||
"Quadro K610M": "724",
|
||||
"Quadro K510M": "725",
|
||||
"Quadro K500M": "706",
|
||||
"Quadro 5010M": "566",
|
||||
"Quadro 5000M": "524",
|
||||
"Quadro 2000M": "546",
|
||||
"GeForce GTX 580M": "601",
|
||||
"GeForce GTX 570M": "603",
|
||||
"GeForce GTX 560M": "561",
|
||||
"GeForce GT 555M": "552",
|
||||
"GeForce GT 550M": "551",
|
||||
"GeForce GT 540M": "550",
|
||||
"GeForce GT 525M": "549",
|
||||
"GeForce GT 520M": "548",
|
||||
"GeForce GT 520MX": "602",
|
||||
"GeForce GTX 680MX": "677",
|
||||
"GeForce GTX 680M": "646",
|
||||
"GeForce GTX 675MX": "664",
|
||||
"GeForce GTX 675M": "638",
|
||||
"GeForce GTX 670MX": "663",
|
||||
"GeForce GTX 670M": "639",
|
||||
"GeForce GTX 660M": "637",
|
||||
"GeForce GT 650M": "636",
|
||||
"GeForce GT 645M": "662",
|
||||
"GeForce GT 640M": "635",
|
||||
"GeForce GT 640M LE": "644",
|
||||
"GeForce GT 635M": "619",
|
||||
"GeForce GT 630M": "612",
|
||||
"GeForce GT 625M": "671",
|
||||
"GeForce GT 620M": "634",
|
||||
"GeForce 610M": "620",
|
||||
"GeForce GTX 780M": "700",
|
||||
"GeForce GTX 775M": "740",
|
||||
"GeForce GTX 770M": "702",
|
||||
"GeForce GTX 765M": "701",
|
||||
"GeForce GTX 760M": "699",
|
||||
"GeForce GT 755M": "707",
|
||||
"GeForce GT 750M": "690",
|
||||
"GeForce GT 745M": "689",
|
||||
"GeForce GT 740M": "686",
|
||||
"GeForce GT 735M": "688",
|
||||
"GeForce GT 730M": "672",
|
||||
"GeForce GT 720M": "687",
|
||||
"GeForce GT 710M": "798",
|
||||
"GeForce 720M": "803",
|
||||
"GeForce 710M": "680",
|
||||
"GeForce 705M": "775",
|
||||
"GeForce GTX 880M": "734",
|
||||
"GeForce GTX 870M": "735",
|
||||
"GeForce GTX 860M": "736",
|
||||
"GeForce GTX 850M": "737",
|
||||
"GeForce 845M": "805",
|
||||
"GeForce 840M": "738",
|
||||
"GeForce 830M": "739",
|
||||
"GeForce 825M": "804",
|
||||
"GeForce 820M": "726",
|
||||
"GeForce 810M": "759",
|
||||
"GeForce 800M": "797",
|
||||
"GeForce GTX 980": "785",
|
||||
"GeForce GTX 980M": "757",
|
||||
"GeForce GTX 970M": "758",
|
||||
"GeForce GTX 965M": "765",
|
||||
"GeForce GTX 960M": "769",
|
||||
"GeForce GTX 950M": "770",
|
||||
"GeForce 945M": "795",
|
||||
"GeForce 940MX": "796",
|
||||
"GeForce 930MX": "807",
|
||||
"GeForce 920MX": "808",
|
||||
"GeForce 940M": "771",
|
||||
"GeForce 930M": "772",
|
||||
"GeForce 920M": "773",
|
||||
"GeForce 910M": "779",
|
||||
"GeForce GTX 1080": "819",
|
||||
"GeForce GTX 1070": "820",
|
||||
"GeForce GTX 1060": "821",
|
||||
"GeForce GTX 1050 Ti": "836",
|
||||
"GeForce GTX 1050": "837",
|
||||
"GeForce MX150": "853",
|
||||
"GeForce MX130": "862",
|
||||
"GeForce MX110": "863",
|
||||
"GeForce RTX 2080 SUPER": "919",
|
||||
"GeForce RTX 2080": "890",
|
||||
"GeForce RTX 2070 SUPER": "920",
|
||||
"GeForce RTX 2070": "889",
|
||||
"GeForce RTX 2060": "888",
|
||||
"GeForce RTX 2050": "978",
|
||||
"GeForce MX250": "894",
|
||||
"GeForce MX230": "893",
|
||||
"GeForce GTX 1660 Ti": "899",
|
||||
"GeForce GTX 1650 Ti": "921",
|
||||
"GeForce GTX 1650": "898",
|
||||
"Quadro RTX 6000": "932",
|
||||
"Quadro RTX 4000": "913",
|
||||
"GeForce MX350": "918",
|
||||
"GeForce MX330": "917",
|
||||
"GeForce MX450": "931",
|
||||
"GeForce RTX 3080 Ti Laptop GPU": "976",
|
||||
"GeForce RTX 3080 Laptop GPU": "938",
|
||||
"GeForce RTX 3070 Ti Laptop GPU": "979",
|
||||
"GeForce RTX 3070 Laptop GPU": "939",
|
||||
"GeForce RTX 3060 Laptop GPU": "940",
|
||||
"GeForce RTX 3050 Ti Laptop GPU": "962",
|
||||
"GeForce RTX 3050 Laptop GPU": "963",
|
||||
"RTX 5000 Ada Generation Laptop GPU": "1020",
|
||||
"RTX 4000 Ada Generation Laptop GPU": "1019",
|
||||
"RTX 3500 Ada Generation Laptop GPU": "1018",
|
||||
"RTX 3000 Ada Generation Laptop GPU": "1017",
|
||||
"RTX 2000 Ada Generation Laptop GPU": "1016",
|
||||
"RTX 1000 Ada Generation Laptop GPU": "1045",
|
||||
"RTX 500 Ada Generation Laptop GPU": "1046",
|
||||
"RTX A5500 Laptop GPU": "987",
|
||||
"RTX A5000 Laptop GPU": "958",
|
||||
"RTX A4500 Laptop GPU": "983",
|
||||
"RTX A4000 Laptop GPU": "959",
|
||||
"RTX A3000": "988",
|
||||
"RTX A3000 Laptop GPU": "956",
|
||||
"RTX A2000": "989",
|
||||
"RTX A2000 Laptop GPU": "957",
|
||||
"RTX A1000": "1021",
|
||||
"RTX A1000 Laptop GPU": "981",
|
||||
"RTX A500 Laptop GPU": "998",
|
||||
"T1200 Laptop GPU": "960",
|
||||
"T600 Laptop GPU": "961",
|
||||
"T550 Laptop GPU": "986",
|
||||
"T500": "941",
|
||||
"GeForce MX570": "977",
|
||||
"GeForce MX550": "974",
|
||||
"GeForce RTX 4090 Laptop GPU": "1004",
|
||||
"GeForce RTX 4080 Laptop GPU": "1005",
|
||||
"GeForce RTX 4070 Laptop GPU": "1006",
|
||||
"GeForce RTX 4060 Laptop GPU": "1007",
|
||||
"GeForce RTX 4050 Laptop GPU": "1008",
|
||||
"GeForce RTX 5090 Laptop GPU": "1073",
|
||||
"GeForce RTX 5080 Laptop GPU": "1074",
|
||||
"GeForce RTX 5070 Ti Laptop GPU": "1075"
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
"WPFTweaksLocation",
|
||||
"WPFTweaksServices",
|
||||
"WPFTweaksTelemetry",
|
||||
"WPFTweaksDiskCleanup",
|
||||
"WPFTweaksDeleteTempFiles",
|
||||
"WPFTweaksEndTaskOnTaskbar",
|
||||
"WPFTweaksPowershell7Tele"
|
||||
]
|
||||
|
||||
+113
-79
@@ -17,10 +17,10 @@ const (
|
||||
type Package struct {
|
||||
Name string
|
||||
Phase Phase
|
||||
WingetID string
|
||||
WingetSource string
|
||||
WingetOverride string
|
||||
WingetConfigAsset string
|
||||
ChocoID string
|
||||
ChocoParams string
|
||||
InstallerArgs string
|
||||
ChocoConfigAsset string
|
||||
Class Classification
|
||||
RequiresElevation bool
|
||||
Reboot RebootBehavior
|
||||
@@ -28,86 +28,101 @@ type Package struct {
|
||||
AI bool
|
||||
}
|
||||
|
||||
func wingetPackage(name, id string, phase Phase, class Classification) Package {
|
||||
func chocoPackage(name, id string, phase Phase, class Classification) Package {
|
||||
return Package{
|
||||
Name: name,
|
||||
Phase: phase,
|
||||
WingetID: id,
|
||||
WingetSource: "winget",
|
||||
Class: class,
|
||||
Reboot: RebootNone,
|
||||
Name: name,
|
||||
Phase: phase,
|
||||
ChocoID: id,
|
||||
Class: class,
|
||||
Reboot: RebootNone,
|
||||
}
|
||||
}
|
||||
|
||||
// Catalog is deliberately explicit: unlisted software is not installed by a fallback provider.
|
||||
func Catalog() []Package {
|
||||
packages := []Package{
|
||||
{
|
||||
Name: "Visual Studio Community 2026",
|
||||
Phase: PhaseHost,
|
||||
WingetID: "Microsoft.VisualStudio.Community",
|
||||
WingetSource: "winget",
|
||||
WingetOverride: "--passive --config {config}",
|
||||
WingetConfigAsset: "assets/visual-studio-community.vsconfig",
|
||||
Class: ClassGUI,
|
||||
RequiresElevation: true,
|
||||
Reboot: RebootPossible,
|
||||
PostInstall: "install workloads/components from managed .vsconfig; use Microsoft C/C++ compilers and CMake",
|
||||
chocoPackage("WezTerm", "wezterm", PhaseHost, ClassGUI),
|
||||
chocoPackage("PowerShell 7", "powershell-core", PhaseHost, ClassHeadless),
|
||||
chocoPackage("PowerToys", "powertoys", PhaseHost, ClassGUI),
|
||||
chocoPackage("JetBrainsMono Nerd Font", "nerd-fonts-jetbrainsmono", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Android Studio", "androidstudio", PhaseHost, ClassGUI),
|
||||
// FilePilot has no official Chocolatey package, but it's listed here.
|
||||
// We use empty ChocoID so our custom installer downloads and installs it directly.
|
||||
chocoPackage("FilePilot", "", PhaseHost, ClassGUI),
|
||||
chocoPackage("Vivaldi", "vivaldi", PhaseHost, ClassGUI),
|
||||
chocoPackage("OBS Studio", "obs-studio", PhaseHost, ClassGUI),
|
||||
chocoPackage("Obsidian", "obsidian", PhaseHost, ClassGUI),
|
||||
chocoPackage("Libre Hardware Monitor", "librehardwaremonitor", PhaseHost, ClassGUI),
|
||||
chocoPackage("WireGuard", "wireguard", PhaseHost, ClassGUI),
|
||||
chocoPackage("Steam", "steam", PhaseHost, ClassGUI),
|
||||
chocoPackage("Bruno", "bruno", PhaseHost, ClassGUI),
|
||||
chocoPackage("VLC", "vlc", PhaseHost, ClassGUI),
|
||||
chocoPackage("Wireshark", "wireshark", PhaseHost, ClassGUI),
|
||||
chocoPackage("HandBrake", "handbrake", PhaseHost, ClassGUI),
|
||||
chocoPackage("Gpg4win (includes gpgOL)", "gpg4win", PhaseHost, ClassGUI),
|
||||
chocoPackage("Tor Browser", "tor-browser", PhaseHost, ClassGUI),
|
||||
chocoPackage("LibreWolf", "librewolf", PhaseHost, ClassGUI),
|
||||
chocoPackage("Rufus", "rufus", PhaseHost, ClassGUI),
|
||||
chocoPackage("Slack", "slack", PhaseHost, ClassGUI),
|
||||
chocoPackage("Discord", "discord", PhaseHost, ClassGUI),
|
||||
chocoPackage("Revo Uninstaller", "revo-uninstaller", PhaseHost, ClassGUI),
|
||||
chocoPackage("darktable", "darktable", PhaseHost, ClassGUI),
|
||||
chocoPackage("Floorp", "floorp", PhaseHost, ClassGUI),
|
||||
chocoPackage("Krita", "krita", PhaseHost, ClassGUI),
|
||||
chocoPackage("Godot C++", "godot", PhaseHost, ClassGUI),
|
||||
chocoPackage("Blender", "blender", PhaseHost, ClassGUI),
|
||||
chocoPackage("Git", "git", PhaseHost, ClassHeadless),
|
||||
chocoPackage("GitHub CLI", "gh", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Neovim", "neovim", PhaseHost, ClassHeadless),
|
||||
chocoPackage("trivy", "trivy", PhaseHost, ClassHeadless),
|
||||
chocoPackage("cosign", "cosign", PhaseHost, ClassHeadless),
|
||||
chocoPackage("gitleaks", "gitleaks", PhaseHost, ClassHeadless),
|
||||
chocoPackage("jq", "jq", PhaseHost, ClassHeadless),
|
||||
chocoPackage("yq", "yq", PhaseHost, ClassHeadless),
|
||||
chocoPackage("fzf", "fzf", PhaseHost, ClassHeadless),
|
||||
chocoPackage("fd", "fd", PhaseHost, ClassHeadless),
|
||||
chocoPackage("bottom", "bottom", PhaseHost, ClassHeadless),
|
||||
chocoPackage("ripgrep", "ripgrep", PhaseHost, ClassHeadless),
|
||||
chocoPackage("FFmpeg", "ffmpeg", PhaseHost, ClassHeadless),
|
||||
chocoPackage("lazygit", "lazygit", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Lua", "lua", PhaseHost, ClassHeadless),
|
||||
chocoPackage("minisign", "minisign", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Google Cloud CLI", "gcloudsdk", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Rustup", "rustup.install", PhaseHost, ClassHeadless),
|
||||
chocoPackage("AWS CLI", "awscli", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Azure CLI", "azure-cli", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Pulumi", "pulumi", PhaseHost, ClassHeadless),
|
||||
chocoPackage("restic", "restic", PhaseHost, ClassHeadless),
|
||||
chocoPackage("yt-dlp", "yt-dlp", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Vagrant", "vagrant", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Go", "golang", PhaseHost, ClassHeadless),
|
||||
chocoPackage(".NET SDK", "dotnet-sdk", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Temurin JDK", "temurin", PhaseHost, ClassHeadless),
|
||||
chocoPackage("Zig", "zig", PhaseHost, ClassHeadless),
|
||||
chocoPackage("CMake", "cmake", PhaseHost, ClassHeadless),
|
||||
chocoPackage("oh-my-posh", "oh-my-posh", PhaseCustom, ClassHeadless),
|
||||
chocoPackage("Google Chrome", "googlechrome", PhaseCustom, ClassGUI),
|
||||
chocoPackage("Mullvad VPN", "mullvad-app", PhaseCustom, ClassGUI),
|
||||
chocoPackage("Figma", "figma", PhaseCustom, ClassGUI),
|
||||
chocoPackage("Zoom", "zoom", PhaseCustom, ClassGUI),
|
||||
chocoPackage("Adobe Acrobat Reader", "adobereader", PhaseCustom, ClassGUI),
|
||||
chocoPackage("WhatsApp", "", PhaseCustom, ClassGUI),
|
||||
chocoPackage("Affinity", "", PhaseCustom, ClassGUI),
|
||||
chocoPackage("Tidal", "", PhaseCustom, ClassGUI),
|
||||
chocoPackage("DaVinci Resolve", "", PhaseCustom, ClassGUI),
|
||||
chocoPackage("Ardour", "", PhaseCustom, ClassGUI),
|
||||
chocoPackage("Nvidia Driver", "", PhaseHost, ClassGUI),
|
||||
chocoPackage("Nvidia Broadcast", "", PhaseHost, ClassGUI),
|
||||
chocoPackage("AMD Auto-Detect", "", PhaseHost, ClassGUI),
|
||||
chocoPackage("Intel DSA", "", PhaseHost, ClassGUI),
|
||||
Package{
|
||||
Name: "Nmap",
|
||||
Phase: PhaseCustom,
|
||||
ChocoID: "nmap",
|
||||
InstallerArgs: `"/S /NPCAP=NO"`,
|
||||
Class: ClassHeadless,
|
||||
Reboot: RebootNone,
|
||||
},
|
||||
wingetPackage("WezTerm", "wez.wezterm", PhaseHost, ClassGUI),
|
||||
wingetPackage("PowerShell 7", "Microsoft.PowerShell", PhaseHost, ClassHeadless),
|
||||
wingetPackage("PowerToys", "Microsoft.PowerToys", PhaseHost, ClassGUI),
|
||||
wingetPackage("JetBrainsMono Nerd Font", "DEVCOM.JetBrainsMonoNerdFont", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Android Studio", "Google.AndroidStudio", PhaseHost, ClassGUI),
|
||||
wingetPackage("FilePilot", "Voidstar.FilePilot", PhaseHost, ClassGUI),
|
||||
wingetPackage("GitHub Desktop", "GitHub.GitHubDesktop", PhaseHost, ClassGUI),
|
||||
wingetPackage("Google Chrome", "Google.Chrome", PhaseHost, ClassGUI),
|
||||
wingetPackage("Vivaldi", "Vivaldi.Vivaldi", PhaseHost, ClassGUI),
|
||||
wingetPackage("OBS Studio", "OBSProject.OBSStudio", PhaseHost, ClassGUI),
|
||||
wingetPackage("Obsidian", "Obsidian.Obsidian", PhaseHost, ClassGUI),
|
||||
wingetPackage("Libre Hardware Monitor", "LibreHardwareMonitor.LibreHardwareMonitor", PhaseHost, ClassGUI),
|
||||
wingetPackage("Mullvad VPN", "MullvadVPN.MullvadVPN", PhaseHost, ClassGUI),
|
||||
wingetPackage("WireGuard", "WireGuard.WireGuard", PhaseHost, ClassGUI),
|
||||
wingetPackage("Steam", "Valve.Steam", PhaseHost, ClassGUI),
|
||||
wingetPackage("Bruno", "Bruno.Bruno", PhaseHost, ClassGUI),
|
||||
wingetPackage("Figma", "Figma.Figma", PhaseHost, ClassGUI),
|
||||
wingetPackage("VLC", "VideoLAN.VLC", PhaseHost, ClassGUI),
|
||||
wingetPackage("Wireshark", "WiresharkFoundation.Wireshark", PhaseHost, ClassGUI),
|
||||
wingetPackage("Zoom", "Zoom.Zoom", PhaseHost, ClassGUI),
|
||||
wingetPackage("HandBrake", "HandBrake.HandBrake", PhaseHost, ClassGUI),
|
||||
wingetPackage("Adobe Acrobat Reader", "Adobe.Acrobat.Reader.64-bit", PhaseHost, ClassGUI),
|
||||
wingetPackage("Gpg4win (includes gpgOL)", "GnuPG.Gpg4win", PhaseHost, ClassGUI),
|
||||
wingetPackage("Tor Browser", "TorProject.TorBrowser", PhaseHost, ClassGUI),
|
||||
wingetPackage("LibreWolf", "LibreWolf.LibreWolf", PhaseHost, ClassGUI),
|
||||
wingetPackage("Rufus", "Rufus.Rufus", PhaseHost, ClassGUI),
|
||||
wingetPackage("Slack", "SlackTechnologies.Slack", PhaseHost, ClassGUI),
|
||||
wingetPackage("Discord", "Discord.Discord", PhaseHost, ClassGUI),
|
||||
wingetPackage("Revo Uninstaller", "RevoUninstaller.RevoUninstaller", PhaseHost, ClassGUI),
|
||||
wingetPackage("darktable", "darktable.darktable", PhaseHost, ClassGUI),
|
||||
wingetPackage("Floorp", "Ablaze.Floorp", PhaseHost, ClassGUI),
|
||||
wingetPackage("Git", "Git.Git", PhaseHost, ClassHeadless),
|
||||
wingetPackage("GitHub CLI", "GitHub.cli", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Neovim", "Neovim.Neovim", PhaseHost, ClassHeadless),
|
||||
wingetPackage("ripgrep", "BurntSushi.ripgrep.MSVC", PhaseHost, ClassHeadless),
|
||||
wingetPackage("FFmpeg", "Gyan.FFmpeg", PhaseHost, ClassHeadless),
|
||||
wingetPackage("lazygit", "JesseDuffield.lazygit", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Lua", "DEVCOM.Lua", PhaseHost, ClassHeadless),
|
||||
wingetPackage("minisign", "jedisct1.minisign", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Google Cloud CLI", "Google.CloudSDK", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Rustup", "Rustlang.Rustup", PhaseHost, ClassHeadless),
|
||||
wingetPackage("AWS CLI", "Amazon.AWSCLI", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Azure CLI", "Microsoft.AzureCLI", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Pulumi", "Pulumi.Pulumi", PhaseHost, ClassHeadless),
|
||||
wingetPackage("restic", "restic.restic", PhaseHost, ClassHeadless),
|
||||
wingetPackage("yt-dlp", "yt-dlp.yt-dlp", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Vagrant", "Hashicorp.Vagrant", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Go", "GoLang.Go", PhaseHost, ClassHeadless),
|
||||
wingetPackage(".NET SDK", "Microsoft.DotNet.SDK.10", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Temurin JDK", "EclipseAdoptium.Temurin.21.JDK", PhaseHost, ClassHeadless),
|
||||
wingetPackage("Zig", "zig.zig", PhaseHost, ClassHeadless),
|
||||
wingetPackage("CMake", "Kitware.CMake", PhaseHost, ClassHeadless),
|
||||
wingetPackage("fnm", "Schniz.fnm", PhaseHost, ClassHeadless),
|
||||
wingetPackage("oh-my-posh", "JanDeDobbeleer.OhMyPosh", PhaseCustom, ClassHeadless),
|
||||
}
|
||||
for i := range packages {
|
||||
if packages[i].Name == "Vagrant" {
|
||||
@@ -115,9 +130,6 @@ func Catalog() []Package {
|
||||
packages[i].Reboot = RebootPossible
|
||||
packages[i].PostInstall = "configure Hyper-V provider prerequisites"
|
||||
}
|
||||
if packages[i].Name == "fnm" {
|
||||
packages[i].PostInstall = "install Node.js LTS"
|
||||
}
|
||||
}
|
||||
return packages
|
||||
}
|
||||
@@ -128,6 +140,28 @@ func PackagesForPhase(opts Options, phase Phase) []Package {
|
||||
if pkg.Phase != phase || (opts.NoAI && pkg.AI) {
|
||||
continue
|
||||
}
|
||||
if pkg.Name == "Nvidia Driver" {
|
||||
if !opts.Nvidia {
|
||||
continue
|
||||
}
|
||||
pkg.ChocoParams = opts.NvidiaModel
|
||||
pkg.InstallerArgs = opts.NvidiaType
|
||||
}
|
||||
if pkg.Name == "Nvidia Broadcast" {
|
||||
if !opts.Nvidia || !isRTX2050OrNewer(opts.NvidiaModel) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if pkg.Name == "AMD Auto-Detect" {
|
||||
if !opts.Amd {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if pkg.Name == "Intel DSA" {
|
||||
if !opts.Intel {
|
||||
continue
|
||||
}
|
||||
}
|
||||
selected = append(selected, pkg)
|
||||
}
|
||||
return selected
|
||||
|
||||
@@ -2,16 +2,15 @@ package bootstrap
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCatalogContainsExactWingetIDsAndDefersExcludedNativeTools(t *testing.T) {
|
||||
func TestCatalogContainsExactChocoIDsAndDefersExcludedNativeTools(t *testing.T) {
|
||||
seen := make(map[string]Package)
|
||||
for _, pkg := range Catalog() {
|
||||
if pkg.WingetID == "" || pkg.WingetSource != "winget" {
|
||||
t.Fatalf("invalid winget metadata for %#v", pkg)
|
||||
if pkg.ChocoID == "" && !isCustomPackage(pkg) {
|
||||
t.Fatalf("invalid choco metadata for %#v", pkg)
|
||||
}
|
||||
seen[pkg.Name] = pkg
|
||||
}
|
||||
for _, name := range []string{
|
||||
"Visual Studio Community 2026",
|
||||
"Android Studio",
|
||||
"FilePilot",
|
||||
"PowerShell 7",
|
||||
@@ -39,8 +38,16 @@ func TestCatalogContainsExactWingetIDsAndDefersExcludedNativeTools(t *testing.T)
|
||||
"CMake",
|
||||
"WezTerm",
|
||||
"Vagrant",
|
||||
"fnm",
|
||||
"oh-my-posh",
|
||||
"Nmap",
|
||||
"Krita",
|
||||
"Godot C++",
|
||||
"Blender",
|
||||
"WhatsApp",
|
||||
"Affinity",
|
||||
"Tidal",
|
||||
"DaVinci Resolve",
|
||||
"Ardour",
|
||||
} {
|
||||
if _, ok := seen[name]; !ok {
|
||||
t.Fatalf("missing catalog package %s", name)
|
||||
@@ -63,8 +70,4 @@ func TestCatalogContainsExactWingetIDsAndDefersExcludedNativeTools(t *testing.T)
|
||||
if _, ok := seen["g++"]; ok {
|
||||
t.Fatal("g++ should not be installed natively; use Visual Studio C/C++ tools")
|
||||
}
|
||||
vs := seen["Visual Studio Community 2026"]
|
||||
if vs.WingetID != "Microsoft.VisualStudio.Community" || vs.WingetConfigAsset == "" || vs.WingetOverride == "" {
|
||||
t.Fatalf("Visual Studio package missing managed config metadata: %#v", vs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var chocoCommand = "choco"
|
||||
|
||||
func init() {
|
||||
if _, err := exec.LookPath("choco"); err == nil {
|
||||
chocoCommand = "choco"
|
||||
return
|
||||
}
|
||||
if chocoInstall := os.Getenv("ChocolateyInstall"); chocoInstall != "" {
|
||||
path := filepath.Join(chocoInstall, "bin", "choco.exe")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
chocoCommand = path
|
||||
return
|
||||
}
|
||||
}
|
||||
defaultPath := `C:\ProgramData\chocolatey\bin\choco.exe`
|
||||
if _, err := os.Stat(defaultPath); err == nil {
|
||||
chocoCommand = defaultPath
|
||||
}
|
||||
}
|
||||
|
||||
func ChocoInstallArgs(pkg Package) []string {
|
||||
args := []string{
|
||||
"install", pkg.ChocoID, "-y", "--no-progress",
|
||||
}
|
||||
if pkg.ChocoParams != "" {
|
||||
args = append(args, "--package-parameters", pkg.ChocoParams)
|
||||
}
|
||||
if pkg.InstallerArgs != "" {
|
||||
args = append(args, "--install-arguments", pkg.InstallerArgs)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func ChocoListArgs(pkg Package) []string {
|
||||
return []string{
|
||||
"list", "--local-only", "--exact", pkg.ChocoID,
|
||||
}
|
||||
}
|
||||
|
||||
func ChocoOutputShowsInstalled(pkg Package, output string) bool {
|
||||
lower := strings.ToLower(output)
|
||||
lines := strings.Split(lower, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, strings.ToLower(pkg.ChocoID)+" ") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type PackageState struct {
|
||||
Package Package
|
||||
Installed bool
|
||||
CheckErr error
|
||||
}
|
||||
|
||||
func CheckPackages(ctx context.Context, runner Runner, packages []Package, workers int) []PackageState {
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
states := make([]PackageState, len(packages))
|
||||
type job struct {
|
||||
index int
|
||||
pkg Package
|
||||
}
|
||||
jobs := make(chan job)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for current := range jobs {
|
||||
state := PackageState{Package: current.pkg}
|
||||
if isCustomPackage(current.pkg) {
|
||||
cmd := ""
|
||||
switch current.pkg.Name {
|
||||
case "FilePilot":
|
||||
cmd = "if (Get-Command filepilot.exe -ErrorAction SilentlyContinue) { 'yes' } else { 'no' }"
|
||||
case "Affinity":
|
||||
cmd = `if (Get-AppxPackage *Affinity* -ErrorAction SilentlyContinue) { 'yes' } else { 'no' }`
|
||||
case "Tidal":
|
||||
cmd = `if (Test-Path "${env:LocalAppData}\TIDAL\TIDAL.exe" -or Test-Path "${env:ProgramFiles}\TIDAL\TIDAL.exe") { 'yes' } else { 'no' }`
|
||||
case "DaVinci Resolve":
|
||||
cmd = `if (Test-Path "${env:ProgramFiles}\Blackmagic Design\DaVinci Resolve\Resolve.exe") { 'yes' } else { 'no' }`
|
||||
case "WhatsApp":
|
||||
cmd = `if (Get-AppxPackage *WhatsApp* -ErrorAction SilentlyContinue) { 'yes' } else { 'no' }`
|
||||
case "Ardour":
|
||||
cmd = `if (Test-Path "C:\msys64\mingw64\bin\ardour8.exe" -or Test-Path "C:\msys64\mingw64\bin\ardour.exe" -or Test-Path "C:\msys64\mingw64\bin\ardour9.exe" -or Test-Path "C:\tools\msys64\mingw64\bin\ardour8.exe" -or Test-Path "C:\tools\msys64\mingw64\bin\ardour.exe" -or Test-Path "C:\tools\msys64\mingw64\bin\ardour9.exe") { 'yes' } else { 'no' }`
|
||||
case "Nvidia Broadcast":
|
||||
cmd = `if (Test-Path "${env:ProgramFiles}\NVIDIA Corporation\NVIDIA Broadcast\NVIDIA Broadcast.exe") { 'yes' } else { 'no' }`
|
||||
case "AMD Auto-Detect":
|
||||
cmd = `if (Test-Path "${env:ProgramFiles}\AMD\CNext\CNext\RadeonSoftware.exe" -or Test-Path "${env:ProgramFiles}\AMD\CNext\CNext\AMDRSServ.exe") { 'yes' } else { 'no' }`
|
||||
case "Intel DSA":
|
||||
cmd = `if (Test-Path "${env:ProgramFiles(x86)}\Intel\Driver and Support Assistant\DSAService.exe") { 'yes' } else { 'no' }`
|
||||
case "Nvidia Driver":
|
||||
model := current.pkg.ChocoParams
|
||||
driverType := current.pkg.InstallerArgs
|
||||
pfid, err := resolveNvidiaPFID(model)
|
||||
if err != nil {
|
||||
state.CheckErr = err
|
||||
state.Installed = false
|
||||
} else {
|
||||
_, apiVer, err := fetchNvidiaDriverURLFn(pfid, driverType)
|
||||
if err != nil {
|
||||
state.CheckErr = err
|
||||
state.Installed = false
|
||||
} else {
|
||||
installedVer, err := getInstalledNvidiaVersionFn(ctx, runner)
|
||||
if err != nil {
|
||||
state.CheckErr = err
|
||||
state.Installed = false
|
||||
} else {
|
||||
state.Installed = (installedVer != "" && installedVer == apiVer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cmd != "" {
|
||||
result := runner.Run(ctx, "powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", cmd)
|
||||
state.Installed = strings.Contains(strings.ToLower(result.CombinedOutput()), "yes")
|
||||
} else if current.pkg.Name != "Nvidia Driver" {
|
||||
state.Installed = false
|
||||
}
|
||||
} else {
|
||||
result := runner.Run(ctx, "powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Test-Path C:\\ProgramData\\chocolatey\\lib\\"+current.pkg.ChocoID)
|
||||
if result.Err != nil {
|
||||
state.CheckErr = result.Err
|
||||
} else {
|
||||
state.Installed = strings.Contains(strings.ToLower(result.CombinedOutput()), "true")
|
||||
}
|
||||
}
|
||||
states[current.index] = state
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i, pkg := range packages {
|
||||
jobs <- job{index: i, pkg: pkg}
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
return states
|
||||
}
|
||||
|
||||
func PendingPackages(states []PackageState) []Package {
|
||||
var pending []Package
|
||||
for _, state := range states {
|
||||
if !state.Installed {
|
||||
pending = append(pending, state.Package)
|
||||
}
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
func UnknownPackageStates(packages []Package) []PackageState {
|
||||
states := make([]PackageState, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
states = append(states, PackageState{Package: pkg})
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
func InstallPackages(ctx context.Context, runner Runner, packages []Package) Issues {
|
||||
var issues Issues
|
||||
for _, pkg := range packages {
|
||||
pkgTimeout := 5 * time.Minute
|
||||
if pkg.Name == "Ardour" || pkg.Name == "Nvidia Driver" || pkg.Name == "Nvidia Broadcast" || pkg.Name == "AMD Auto-Detect" || pkg.Name == "Intel DSA" {
|
||||
pkgTimeout = 30 * time.Minute
|
||||
} else if pkg.Name == "DaVinci Resolve" {
|
||||
pkgTimeout = 15 * time.Minute
|
||||
}
|
||||
if IsSessionZero() && pkg.Class == ClassGUI {
|
||||
if pkg.Name != "Nvidia Driver" && pkg.Name != "Nvidia Broadcast" && pkg.Name != "AMD Auto-Detect" && pkg.Name != "Intel DSA" {
|
||||
pkgTimeout = 10 * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
pkgCtx, pkgCancel := context.WithTimeout(ctx, pkgTimeout)
|
||||
|
||||
if isCustomPackage(pkg) {
|
||||
var script string
|
||||
switch pkg.Name {
|
||||
case "FilePilot":
|
||||
script = `$tempExe = Join-Path $env:TEMP 'FilePilotSetup.exe'; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; Invoke-WebRequest -Uri 'https://filepilot.tech/download/latest' -OutFile $tempExe -UseBasicParsing; Start-Process -FilePath $tempExe -ArgumentList '/S' -Wait`
|
||||
case "Affinity":
|
||||
script = `
|
||||
$tempMsix = Join-Path $env:TEMP 'Affinity_x64.msix'
|
||||
if (Test-Path $tempMsix) { Remove-Item -Force $tempMsix }
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Write-Host "Downloading Affinity MSIX..."
|
||||
Invoke-WebRequest -Uri 'https://downloads.affinity.studio/Affinity%20x64.msix' -OutFile $tempMsix -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host "Installing Affinity MSIX package..."
|
||||
Add-AppxProvisionedPackage -Online -PackagePath $tempMsix -SkipLicense
|
||||
`
|
||||
case "Tidal":
|
||||
script = `
|
||||
$tempExe = Join-Path $env:TEMP 'TIDALSetup.exe'
|
||||
if (Test-Path $tempExe) { Remove-Item -Force $tempExe }
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Write-Host "Downloading Tidal setup..."
|
||||
Invoke-WebRequest -Uri 'https://download.tidal.com/desktop/TIDALSetup.exe' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host "Installing Tidal silently..."
|
||||
Start-Process -FilePath $tempExe -ArgumentList '-s' -Wait
|
||||
`
|
||||
case "WhatsApp":
|
||||
script = `winget install --id WhatsApp.WhatsApp --silent --accept-package-agreements --accept-source-agreements`
|
||||
case "DaVinci Resolve":
|
||||
script = `winget install --id BlackmagicDesign.DaVinciResolve --silent --accept-package-agreements --accept-source-agreements`
|
||||
case "Ardour":
|
||||
script = `
|
||||
$msysDir = "C:\msys64"
|
||||
if (-not (Test-Path "$msysDir\usr\bin\bash.exe")) {
|
||||
$msysDir = "C:\tools\msys64"
|
||||
}
|
||||
if (-not (Test-Path "$msysDir\usr\bin\bash.exe")) {
|
||||
Write-Host "Installing MSYS2 via Chocolatey..."
|
||||
choco install msys2 -y --no-progress
|
||||
$msysDir = "C:\tools\msys64"
|
||||
if (-not (Test-Path "$msysDir\usr\bin\bash.exe")) {
|
||||
$msysDir = "C:\msys64"
|
||||
}
|
||||
}
|
||||
$env:MSYSTEM="MINGW64"
|
||||
$buildScript = @'
|
||||
set -e
|
||||
pacman -Sy --noconfirm
|
||||
pacman -S --noconfirm --needed git unzip make cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-boost mingw-w64-x86_64-glib2 mingw-w64-x86_64-glibmm mingw-w64-x86_64-libsndfile mingw-w64-x86_64-curl mingw-w64-x86_64-libarchive mingw-w64-x86_64-liblo mingw-w64-x86_64-taglib mingw-w64-x86_64-vamp-plugin-sdk mingw-w64-x86_64-rubberband mingw-w64-x86_64-aubio mingw-w64-x86_64-cairomm mingw-w64-x86_64-pangomm mingw-w64-x86_64-jack2 mingw-w64-x86_64-serd mingw-w64-x86_64-sord mingw-w64-x86_64-sratom mingw-w64-x86_64-lilv mingw-w64-x86_64-libwebsockets mingw-w64-x86_64-libusb python3
|
||||
if [ ! -d "/tmp/ardour" ]; then
|
||||
git clone --depth 1 https://github.com/Ardour/ardour.git /tmp/ardour
|
||||
fi
|
||||
cd /tmp/ardour
|
||||
python3 ./waf configure --prefix=/mingw64 --dist-target=mingw --optimize
|
||||
python3 ./waf build
|
||||
python3 ./waf install
|
||||
'@
|
||||
$scriptPath = "$msysDir\tmp\build_ardour.sh"
|
||||
[System.IO.Directory]::CreateDirectory("$msysDir\tmp") | Out-Null
|
||||
[System.IO.File]::WriteAllText($scriptPath, $buildScript)
|
||||
& "$msysDir\usr\bin\bash.exe" --login /tmp/build_ardour.sh
|
||||
`
|
||||
case "Nvidia Driver":
|
||||
model := pkg.ChocoParams
|
||||
driverType := pkg.InstallerArgs
|
||||
pfid, err := resolveNvidiaPFID(model)
|
||||
if err != nil {
|
||||
issues = append(issues, Issue{
|
||||
Step: "resolve Nvidia model",
|
||||
Err: err,
|
||||
})
|
||||
} else {
|
||||
downloadURL, _, err := fetchNvidiaDriverURLFn(pfid, driverType)
|
||||
if err != nil {
|
||||
issues = append(issues, Issue{
|
||||
Step: "fetch Nvidia driver URL",
|
||||
Err: err,
|
||||
})
|
||||
} else {
|
||||
script = fmt.Sprintf(`
|
||||
$tempExe = Join-Path $env:TEMP 'NvidiaDriver.exe'
|
||||
if (Test-Path $tempExe) { Remove-Item -Force $tempExe }
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Write-Host "Downloading Nvidia driver..."
|
||||
Invoke-WebRequest -Uri '%s' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host "Installing Nvidia driver silently..."
|
||||
$p = Start-Process -FilePath $tempExe -ArgumentList '-s' -Wait -PassThru
|
||||
Remove-Item -Force $tempExe
|
||||
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1) {
|
||||
throw "Nvidia driver installation failed with exit code " + $p.ExitCode
|
||||
}
|
||||
`, downloadURL)
|
||||
}
|
||||
}
|
||||
case "Nvidia Broadcast":
|
||||
downloadURL := "https://international.download.nvidia.com/Windows/broadcast/2.2.0/NVIDIA_Broadcast_v2.2.0.52896077.exe"
|
||||
script = fmt.Sprintf(`
|
||||
$tempExe = Join-Path $env:TEMP 'NvidiaBroadcast.exe'
|
||||
if (Test-Path $tempExe) { Remove-Item -Force $tempExe }
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Write-Host "Downloading Nvidia Broadcast..."
|
||||
Invoke-WebRequest -Uri '%s' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host "Installing Nvidia Broadcast silently..."
|
||||
$p = Start-Process -FilePath $tempExe -ArgumentList '/s' -Wait -PassThru
|
||||
Remove-Item -Force $tempExe
|
||||
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {
|
||||
throw "Nvidia Broadcast installation failed with exit code " + $p.ExitCode
|
||||
}
|
||||
`, downloadURL)
|
||||
case "AMD Auto-Detect":
|
||||
downloadURL := "https://drivers.amd.com/drivers/installer/26.10/whql/amd-software-adrenalin-edition-26.6.1-minimalsetup-260601_web.exe"
|
||||
script = fmt.Sprintf(`
|
||||
$tempExe = Join-Path $env:TEMP 'AMDMinimalSetup.exe'
|
||||
if (Test-Path $tempExe) { Remove-Item -Force $tempExe }
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Write-Host "Downloading AMD Auto-Detect tool..."
|
||||
Invoke-WebRequest -Uri '%s' -OutFile $tempExe -UseBasicParsing -Headers @{"Referer" = "https://www.amd.com/"} -UserAgent "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" -ErrorAction Stop
|
||||
Write-Host "Launching AMD Auto-Detect tool..."
|
||||
$p = Start-Process -FilePath $tempExe -Wait -PassThru
|
||||
Remove-Item -Force $tempExe
|
||||
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {
|
||||
throw "AMD Auto-Detect failed with exit code " + $p.ExitCode
|
||||
}
|
||||
`, downloadURL)
|
||||
case "Intel DSA":
|
||||
downloadURL := "https://dsadata.intel.com/installer"
|
||||
script = fmt.Sprintf(`
|
||||
$tempExe = Join-Path $env:TEMP 'IntelDSAInstaller.exe'
|
||||
if (Test-Path $tempExe) { Remove-Item -Force $tempExe }
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Write-Host "Downloading Intel Driver & Support Assistant..."
|
||||
Invoke-WebRequest -Uri '%s' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host "Installing Intel Driver & Support Assistant silently..."
|
||||
$p = Start-Process -FilePath $tempExe -ArgumentList '-s', '-norestart' -Wait -PassThru
|
||||
Remove-Item -Force $tempExe
|
||||
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {
|
||||
throw "Intel DSA installation failed with exit code " + $p.ExitCode
|
||||
}
|
||||
`, downloadURL)
|
||||
}
|
||||
if script != "" {
|
||||
result := runner.Run(pkgCtx, "powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
if result.Err != nil {
|
||||
issues = append(issues, Issue{
|
||||
Step: "install " + pkg.Name + " (custom download/install)",
|
||||
Err: fmt.Errorf("%w: %s", result.Err, result.CombinedOutput()),
|
||||
})
|
||||
}
|
||||
}
|
||||
pkgCancel()
|
||||
continue
|
||||
}
|
||||
|
||||
prepared, err := PreparePackageForInstall(pkg)
|
||||
if err != nil {
|
||||
issues = append(issues, Issue{Step: "prepare " + pkg.Name, Err: err})
|
||||
pkgCancel()
|
||||
continue
|
||||
}
|
||||
result := runner.Run(pkgCtx, chocoCommand, ChocoInstallArgs(prepared)...)
|
||||
if result.Err != nil {
|
||||
isSuccess := false
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(result.Err, &exitErr) {
|
||||
code := exitErr.ExitCode()
|
||||
if code == 3010 || code == 1641 {
|
||||
isSuccess = true
|
||||
}
|
||||
}
|
||||
if !isSuccess {
|
||||
out := result.CombinedOutput()
|
||||
if len(out) > 500 {
|
||||
out = out[:500] + "... (truncated)"
|
||||
}
|
||||
issues = append(issues, Issue{
|
||||
Step: "install " + pkg.Name,
|
||||
Err: fmt.Errorf("%w: %s", result.Err, out),
|
||||
})
|
||||
}
|
||||
}
|
||||
pkgCancel()
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
func PreparePackageForInstall(pkg Package) (Package, error) {
|
||||
if pkg.ChocoConfigAsset == "" {
|
||||
return pkg, nil
|
||||
}
|
||||
path, err := writeInstallAsset(pkg.ChocoConfigAsset)
|
||||
if err != nil {
|
||||
return pkg, err
|
||||
}
|
||||
pkg.InstallerArgs = strings.ReplaceAll(pkg.InstallerArgs, "{config}", `"`+path+`"`)
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func writeInstallAsset(asset string) (string, error) {
|
||||
data, err := managedAssets.ReadFile(asset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
root, err := os.UserCacheDir()
|
||||
if err != nil || root == "" {
|
||||
root = os.TempDir()
|
||||
}
|
||||
dir := filepath.Join(root, "bootstrap_windows_env")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
target := filepath.Join(dir, filepath.Base(asset))
|
||||
if err := os.WriteFile(target, data, 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func isCustomPackage(pkg Package) bool {
|
||||
return pkg.ChocoID == "" && (pkg.Name == "FilePilot" || pkg.Name == "Affinity" || pkg.Name == "Tidal" || pkg.Name == "DaVinci Resolve" || pkg.Name == "Ardour" || pkg.Name == "WhatsApp" || pkg.Name == "Nvidia Driver" || pkg.Name == "Nvidia Broadcast" || pkg.Name == "AMD Auto-Detect" || pkg.Name == "Intel DSA")
|
||||
}
|
||||
|
||||
var (
|
||||
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procProcessIdToSessionId = modkernel32.NewProc("ProcessIdToSessionId")
|
||||
procGetCurrentProcessId = modkernel32.NewProc("GetCurrentProcessId")
|
||||
)
|
||||
|
||||
func IsSessionZero() bool {
|
||||
pid, _, _ := procGetCurrentProcessId.Call()
|
||||
var sessionID uint32
|
||||
r, _, _ := procProcessIdToSessionId.Call(pid, uintptr(unsafe.Pointer(&sessionID)))
|
||||
if r == 0 {
|
||||
return false
|
||||
}
|
||||
return sessionID == 0
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
chocoCommand = "choco"
|
||||
}
|
||||
|
||||
func TestChocoInstallArgsUseExactNonInteractiveFlags(t *testing.T) {
|
||||
pkg := Package{Name: "Git", ChocoID: "git"}
|
||||
want := []string{"install", "git", "-y", "--no-progress"}
|
||||
if got := ChocoInstallArgs(pkg); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("args = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparePackageForInstallWritesVisualStudioConfigAndOverride(t *testing.T) {
|
||||
pkg := Package{
|
||||
Name: "Visual Studio Community 2026",
|
||||
ChocoID: "visualstudio2022community",
|
||||
InstallerArgs: "--passive --config {config}",
|
||||
ChocoConfigAsset: "assets/visual-studio-community.vsconfig",
|
||||
}
|
||||
prepared, err := PreparePackageForInstall(pkg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(prepared.InstallerArgs, "{config}") {
|
||||
t.Fatalf("override still has placeholder: %q", prepared.InstallerArgs)
|
||||
}
|
||||
args := ChocoInstallArgs(prepared)
|
||||
joined := strings.Join(args, " ")
|
||||
if !strings.Contains(joined, "--install-arguments") || !strings.Contains(joined, "visual-studio-community.vsconfig") {
|
||||
t.Fatalf("choco args missing VS config override: %#v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparePackageForInstallHandlesNoAssetAndMissingAsset(t *testing.T) {
|
||||
pkg := Package{Name: "Git", ChocoID: "git"}
|
||||
prepared, err := PreparePackageForInstall(pkg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(prepared, pkg) {
|
||||
t.Fatalf("prepared = %#v, want unchanged package", prepared)
|
||||
}
|
||||
|
||||
_, err = PreparePackageForInstall(Package{Name: "broken", ChocoConfigAsset: "assets/missing.vsconfig"})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing asset error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChocoInstalledStateParsing(t *testing.T) {
|
||||
pkg := Package{ChocoID: "git"}
|
||||
if !ChocoOutputShowsInstalled(pkg, "Chocolatey v1.2.1\ngit 2.40.1\n1 packages installed.") {
|
||||
t.Fatal("expected installed output to be detected")
|
||||
}
|
||||
if ChocoOutputShowsInstalled(pkg, "Chocolatey v1.2.1\n0 packages installed.") {
|
||||
t.Fatal("expected missing package output to be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackagesKeepsStableOrderAndAggregatesErrors(t *testing.T) {
|
||||
a := Package{Name: "A", ChocoID: "a"}
|
||||
b := Package{Name: "B", ChocoID: "b"}
|
||||
runner := &fakeRunner{responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Test-Path C:\\ProgramData\\chocolatey\\lib\\a"): {Stdout: "True\n"},
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Test-Path C:\\ProgramData\\chocolatey\\lib\\b"): {Err: errors.New("boom"), Stderr: "source unavailable"},
|
||||
}}
|
||||
states := CheckPackages(context.Background(), runner, []Package{a, b}, 2)
|
||||
if len(states) != 2 || states[0].Package.Name != "A" || states[1].Package.Name != "B" {
|
||||
t.Fatalf("states not in input order: %#v", states)
|
||||
}
|
||||
if !states[0].Installed {
|
||||
t.Fatal("first package should be installed")
|
||||
}
|
||||
if states[1].CheckErr == nil {
|
||||
t.Fatal("second package should keep check error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackagesUsesSingleWorkerMinimumAndMissingPackageIsPending(t *testing.T) {
|
||||
pkg := Package{Name: "Git", ChocoID: "git"}
|
||||
runner := &fakeRunner{responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Test-Path C:\\ProgramData\\chocolatey\\lib\\git"): {Stdout: "False\n"},
|
||||
}}
|
||||
states := CheckPackages(context.Background(), runner, []Package{pkg}, 0)
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("states = %d, want 1", len(states))
|
||||
}
|
||||
if states[0].Installed || states[0].CheckErr != nil {
|
||||
t.Fatalf("state = %#v, want pending without check error", states[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPackagesContinuesAfterFailure(t *testing.T) {
|
||||
a := Package{Name: "A", ChocoID: "a"}
|
||||
b := Package{Name: "B", ChocoID: "b"}
|
||||
runner := &fakeRunner{responses: map[string]CommandResult{
|
||||
commandKey("choco", ChocoInstallArgs(a)...): {Err: errors.New("boom"), Stderr: "failed"},
|
||||
commandKey("choco", ChocoInstallArgs(b)...): {},
|
||||
}, defaultErr: true}
|
||||
issues := InstallPackages(context.Background(), runner, []Package{a, b})
|
||||
if len(issues) != 1 {
|
||||
t.Fatalf("issues = %d, want 1", len(issues))
|
||||
}
|
||||
if !runner.called("choco", ChocoInstallArgs(b)...) {
|
||||
t.Fatal("second package should still be attempted after first failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPackagesReportsPrepareFailure(t *testing.T) {
|
||||
issues := InstallPackages(context.Background(), &fakeRunner{defaultErr: true}, []Package{{
|
||||
Name: "broken",
|
||||
ChocoConfigAsset: "assets/missing.vsconfig",
|
||||
}})
|
||||
if len(issues) != 1 || !strings.Contains(issues[0].Step, "prepare broken") {
|
||||
t.Fatalf("issues = %#v, want prepare failure", issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInstallAssetReportsCacheDirectoryError(t *testing.T) {
|
||||
temp := t.TempDir()
|
||||
cacheFile := filepath.Join(temp, "cache-file")
|
||||
if err := os.WriteFile(cacheFile, []byte("not a directory"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("LOCALAPPDATA", cacheFile)
|
||||
t.Setenv("LocalAppData", cacheFile)
|
||||
if _, err := writeInstallAsset("assets/visual-studio-community.vsconfig"); err == nil {
|
||||
t.Fatal("expected cache directory error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInstallAssetFallsBackToTempDirWhenUserCacheIsUnavailable(t *testing.T) {
|
||||
t.Setenv("LOCALAPPDATA", "")
|
||||
t.Setenv("LocalAppData", "")
|
||||
path, err := writeInstallAsset("assets/visual-studio-community.vsconfig")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if filepath.Base(path) != "visual-studio-community.vsconfig" {
|
||||
t.Fatalf("path = %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInstallAssetReportsWriteError(t *testing.T) {
|
||||
cache := t.TempDir()
|
||||
targetDir := filepath.Join(cache, "bootstrap_windows_env", "visual-studio-community.vsconfig")
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("LOCALAPPDATA", cache)
|
||||
t.Setenv("LocalAppData", cache)
|
||||
if _, err := writeInstallAsset("assets/visual-studio-community.vsconfig"); err == nil {
|
||||
t.Fatal("expected write error when target path is a directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackagesNvidiaDriver(t *testing.T) {
|
||||
// Backup original functions and defer restore
|
||||
oldFetch := fetchNvidiaDriverURLFn
|
||||
oldGetInstalled := getInstalledNvidiaVersionFn
|
||||
defer func() {
|
||||
fetchNvidiaDriverURLFn = oldFetch
|
||||
getInstalledNvidiaVersionFn = oldGetInstalled
|
||||
}()
|
||||
|
||||
// Mock fetchNvidiaDriverURLFn to return a version
|
||||
fetchNvidiaDriverURLFn = func(pfid string, driverType string) (string, string, error) {
|
||||
if pfid != "995" {
|
||||
t.Errorf("expected pfid 995, got %s", pfid)
|
||||
}
|
||||
if driverType != "Studio" {
|
||||
t.Errorf("expected driverType Studio, got %s", driverType)
|
||||
}
|
||||
return "https://uk.download.nvidia.com/Windows/610.47/610.47-desktop-win10-win11-64bit-international-nsd-dch-whql.exe", "610.47", nil
|
||||
}
|
||||
|
||||
// 1. Test case: Installed version matches API version -> should report installed = true
|
||||
getInstalledNvidiaVersionFn = func(ctx context.Context, runner Runner) (string, error) {
|
||||
return "610.47", nil
|
||||
}
|
||||
|
||||
pkg := Package{
|
||||
Name: "Nvidia Driver",
|
||||
ChocoParams: "RTX 4090",
|
||||
InstallerArgs: "Studio",
|
||||
}
|
||||
|
||||
runner := &fakeRunner{}
|
||||
states := CheckPackages(context.Background(), runner, []Package{pkg}, 1)
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("expected 1 state, got %d", len(states))
|
||||
}
|
||||
if !states[0].Installed {
|
||||
t.Error("expected Nvidia Driver to be reported as installed")
|
||||
}
|
||||
if states[0].CheckErr != nil {
|
||||
t.Errorf("expected no check error, got %v", states[0].CheckErr)
|
||||
}
|
||||
|
||||
// 2. Test case: Installed version does not match API version -> should report installed = false
|
||||
getInstalledNvidiaVersionFn = func(ctx context.Context, runner Runner) (string, error) {
|
||||
return "596.36", nil
|
||||
}
|
||||
|
||||
states = CheckPackages(context.Background(), runner, []Package{pkg}, 1)
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("expected 1 state, got %d", len(states))
|
||||
}
|
||||
if states[0].Installed {
|
||||
t.Error("expected Nvidia Driver to be reported as not installed (pending)")
|
||||
}
|
||||
if states[0].CheckErr != nil {
|
||||
t.Errorf("expected no check error, got %v", states[0].CheckErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPackagesNvidiaDriver(t *testing.T) {
|
||||
// Backup original functions and defer restore
|
||||
oldFetch := fetchNvidiaDriverURLFn
|
||||
defer func() {
|
||||
fetchNvidiaDriverURLFn = oldFetch
|
||||
}()
|
||||
|
||||
fetchNvidiaDriverURLFn = func(pfid string, driverType string) (string, string, error) {
|
||||
return "https://mock.nvidia.download/driver.exe", "610.47", nil
|
||||
}
|
||||
|
||||
pkg := Package{
|
||||
Name: "Nvidia Driver",
|
||||
ChocoParams: "RTX 4090",
|
||||
InstallerArgs: "Studio",
|
||||
}
|
||||
|
||||
runner := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "\n$tempExe = Join-Path $env:TEMP 'NvidiaDriver.exe'\nif (Test-Path $tempExe) { Remove-Item -Force $tempExe }\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072\nWrite-Host \"Downloading Nvidia driver...\"\nInvoke-WebRequest -Uri 'https://mock.nvidia.download/driver.exe' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop\nWrite-Host \"Installing Nvidia driver silently...\"\n$p = Start-Process -FilePath $tempExe -ArgumentList '-s' -Wait -PassThru\nRemove-Item -Force $tempExe\nif ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1) {\n throw \"Nvidia driver installation failed with exit code \" + $p.ExitCode\n}\n"): {},
|
||||
},
|
||||
}
|
||||
|
||||
issues := InstallPackages(context.Background(), runner, []Package{pkg})
|
||||
if len(issues) != 0 {
|
||||
t.Fatalf("expected no issues, got: %v", issues)
|
||||
}
|
||||
|
||||
if !runner.called("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "\n$tempExe = Join-Path $env:TEMP 'NvidiaDriver.exe'\nif (Test-Path $tempExe) { Remove-Item -Force $tempExe }\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072\nWrite-Host \"Downloading Nvidia driver...\"\nInvoke-WebRequest -Uri 'https://mock.nvidia.download/driver.exe' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop\nWrite-Host \"Installing Nvidia driver silently...\"\n$p = Start-Process -FilePath $tempExe -ArgumentList '-s' -Wait -PassThru\nRemove-Item -Force $tempExe\nif ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1) {\n throw \"Nvidia driver installation failed with exit code \" + $p.ExitCode\n}\n") {
|
||||
t.Error("expected Nvidia installer script to be executed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackagesNvidiaBroadcast(t *testing.T) {
|
||||
pkg := Package{
|
||||
Name: "Nvidia Broadcast",
|
||||
}
|
||||
|
||||
runner := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", `if (Test-Path "${env:ProgramFiles}\NVIDIA Corporation\NVIDIA Broadcast\NVIDIA Broadcast.exe") { 'yes' } else { 'no' }`): {
|
||||
Stdout: "yes\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
states := CheckPackages(context.Background(), runner, []Package{pkg}, 1)
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("expected 1 state, got %d", len(states))
|
||||
}
|
||||
if !states[0].Installed {
|
||||
t.Error("expected Nvidia Broadcast to be reported as installed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPackagesNvidiaBroadcast(t *testing.T) {
|
||||
pkg := Package{
|
||||
Name: "Nvidia Broadcast",
|
||||
}
|
||||
|
||||
runner := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "\n$tempExe = Join-Path $env:TEMP 'NvidiaBroadcast.exe'\nif (Test-Path $tempExe) { Remove-Item -Force $tempExe }\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072\nWrite-Host \"Downloading Nvidia Broadcast...\"\nInvoke-WebRequest -Uri 'https://international.download.nvidia.com/Windows/broadcast/2.2.0/NVIDIA_Broadcast_v2.2.0.52896077.exe' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop\nWrite-Host \"Installing Nvidia Broadcast silently...\"\n$p = Start-Process -FilePath $tempExe -ArgumentList '/s' -Wait -PassThru\nRemove-Item -Force $tempExe\nif ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {\n throw \"Nvidia Broadcast installation failed with exit code \" + $p.ExitCode\n}\n"): {},
|
||||
},
|
||||
}
|
||||
|
||||
issues := InstallPackages(context.Background(), runner, []Package{pkg})
|
||||
if len(issues) != 0 {
|
||||
t.Fatalf("expected no issues, got: %v", issues)
|
||||
}
|
||||
|
||||
if !runner.called("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "\n$tempExe = Join-Path $env:TEMP 'NvidiaBroadcast.exe'\nif (Test-Path $tempExe) { Remove-Item -Force $tempExe }\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072\nWrite-Host \"Downloading Nvidia Broadcast...\"\nInvoke-WebRequest -Uri 'https://international.download.nvidia.com/Windows/broadcast/2.2.0/NVIDIA_Broadcast_v2.2.0.52896077.exe' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop\nWrite-Host \"Installing Nvidia Broadcast silently...\"\n$p = Start-Process -FilePath $tempExe -ArgumentList '/s' -Wait -PassThru\nRemove-Item -Force $tempExe\nif ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {\n throw \"Nvidia Broadcast installation failed with exit code \" + $p.ExitCode\n}\n") {
|
||||
t.Error("expected Nvidia Broadcast installer script to be executed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackagesAmdAutoDetect(t *testing.T) {
|
||||
pkg := Package{
|
||||
Name: "AMD Auto-Detect",
|
||||
}
|
||||
|
||||
runner := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", `if (Test-Path "${env:ProgramFiles}\AMD\CNext\CNext\RadeonSoftware.exe" -or Test-Path "${env:ProgramFiles}\AMD\CNext\CNext\AMDRSServ.exe") { 'yes' } else { 'no' }`): {
|
||||
Stdout: "yes\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
states := CheckPackages(context.Background(), runner, []Package{pkg}, 1)
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("expected 1 state, got %d", len(states))
|
||||
}
|
||||
if !states[0].Installed {
|
||||
t.Error("expected AMD Auto-Detect to be reported as installed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPackagesAmdAutoDetect(t *testing.T) {
|
||||
pkg := Package{
|
||||
Name: "AMD Auto-Detect",
|
||||
}
|
||||
|
||||
runner := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "\n$tempExe = Join-Path $env:TEMP 'AMDMinimalSetup.exe'\nif (Test-Path $tempExe) { Remove-Item -Force $tempExe }\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072\nWrite-Host \"Downloading AMD Auto-Detect tool...\"\nInvoke-WebRequest -Uri 'https://drivers.amd.com/drivers/installer/26.10/whql/amd-software-adrenalin-edition-26.6.1-minimalsetup-260601_web.exe' -OutFile $tempExe -UseBasicParsing -Headers @{\"Referer\" = \"https://www.amd.com/\"} -UserAgent \"Mozilla/5.0 (Windows NT 10.0; Win64; x64)\" -ErrorAction Stop\nWrite-Host \"Launching AMD Auto-Detect tool...\"\n$p = Start-Process -FilePath $tempExe -Wait -PassThru\nRemove-Item -Force $tempExe\nif ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {\n throw \"AMD Auto-Detect failed with exit code \" + $p.ExitCode\n}\n"): {},
|
||||
},
|
||||
}
|
||||
|
||||
issues := InstallPackages(context.Background(), runner, []Package{pkg})
|
||||
if len(issues) != 0 {
|
||||
t.Fatalf("expected no issues, got: %v", issues)
|
||||
}
|
||||
|
||||
if !runner.called("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "\n$tempExe = Join-Path $env:TEMP 'AMDMinimalSetup.exe'\nif (Test-Path $tempExe) { Remove-Item -Force $tempExe }\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072\nWrite-Host \"Downloading AMD Auto-Detect tool...\"\nInvoke-WebRequest -Uri 'https://drivers.amd.com/drivers/installer/26.10/whql/amd-software-adrenalin-edition-26.6.1-minimalsetup-260601_web.exe' -OutFile $tempExe -UseBasicParsing -Headers @{\"Referer\" = \"https://www.amd.com/\"} -UserAgent \"Mozilla/5.0 (Windows NT 10.0; Win64; x64)\" -ErrorAction Stop\nWrite-Host \"Launching AMD Auto-Detect tool...\"\n$p = Start-Process -FilePath $tempExe -Wait -PassThru\nRemove-Item -Force $tempExe\nif ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {\n throw \"AMD Auto-Detect failed with exit code \" + $p.ExitCode\n}\n") {
|
||||
t.Error("expected AMD Auto-Detect installer script to be executed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackagesIntelDSA(t *testing.T) {
|
||||
pkg := Package{
|
||||
Name: "Intel DSA",
|
||||
}
|
||||
|
||||
runner := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", `if (Test-Path "${env:ProgramFiles(x86)}\Intel\Driver and Support Assistant\DSAService.exe") { 'yes' } else { 'no' }`): {
|
||||
Stdout: "yes\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
states := CheckPackages(context.Background(), runner, []Package{pkg}, 1)
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("expected 1 state, got %d", len(states))
|
||||
}
|
||||
if !states[0].Installed {
|
||||
t.Error("expected Intel DSA to be reported as installed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPackagesIntelDSA(t *testing.T) {
|
||||
pkg := Package{
|
||||
Name: "Intel DSA",
|
||||
}
|
||||
|
||||
runner := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "\n$tempExe = Join-Path $env:TEMP 'IntelDSAInstaller.exe'\nif (Test-Path $tempExe) { Remove-Item -Force $tempExe }\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072\nWrite-Host \"Downloading Intel Driver & Support Assistant...\"\nInvoke-WebRequest -Uri 'https://dsadata.intel.com/installer' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop\nWrite-Host \"Installing Intel Driver & Support Assistant silently...\"\n$p = Start-Process -FilePath $tempExe -ArgumentList '-s', '-norestart' -Wait -PassThru\nRemove-Item -Force $tempExe\nif ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {\n throw \"Intel DSA installation failed with exit code \" + $p.ExitCode\n}\n"): {},
|
||||
},
|
||||
}
|
||||
|
||||
issues := InstallPackages(context.Background(), runner, []Package{pkg})
|
||||
if len(issues) != 0 {
|
||||
t.Fatalf("expected no issues, got: %v", issues)
|
||||
}
|
||||
|
||||
if !runner.called("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "\n$tempExe = Join-Path $env:TEMP 'IntelDSAInstaller.exe'\nif (Test-Path $tempExe) { Remove-Item -Force $tempExe }\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072\nWrite-Host \"Downloading Intel Driver & Support Assistant...\"\nInvoke-WebRequest -Uri 'https://dsadata.intel.com/installer' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop\nWrite-Host \"Installing Intel Driver & Support Assistant silently...\"\n$p = Start-Process -FilePath $tempExe -ArgumentList '-s', '-norestart' -Wait -PassThru\nRemove-Item -Force $tempExe\nif ($p.ExitCode -ne 0 -and $p.ExitCode -ne 1 -and $p.ExitCode -ne 3010) {\n throw \"Intel DSA installation failed with exit code \" + $p.ExitCode\n}\n") {
|
||||
t.Error("expected Intel DSA installer script to be executed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -26,6 +27,7 @@ type Runner interface {
|
||||
|
||||
type ExecRunner struct {
|
||||
Timeout time.Duration
|
||||
Stream io.Writer
|
||||
}
|
||||
|
||||
func (r ExecRunner) Run(ctx context.Context, name string, args ...string) CommandResult {
|
||||
@@ -37,11 +39,23 @@ func (r ExecRunner) Run(ctx context.Context, name string, args ...string) Comman
|
||||
defer cancel()
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Cancel = func() error {
|
||||
if cmd.Process == nil {
|
||||
return nil
|
||||
}
|
||||
_ = exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", cmd.Process.Pid)).Run()
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
if r.Stream != nil {
|
||||
cmd.Stdout = io.MultiWriter(r.Stream, &stdout)
|
||||
cmd.Stderr = io.MultiWriter(r.Stream, &stderr)
|
||||
} else {
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
}
|
||||
err := cmd.Run()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = fmt.Errorf("%s timed out after %s", name, timeout)
|
||||
err = fmt.Errorf("%s timed out", name)
|
||||
}
|
||||
return CommandResult{
|
||||
Command: name + " " + strings.Join(args, " "),
|
||||
|
||||
@@ -2,7 +2,9 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -44,7 +46,12 @@ func EnableHyperV(ctx context.Context, runner Runner, status HyperVStatus) (stri
|
||||
}
|
||||
result := runner.Run(ctx, "dism.exe", "/Online", "/Enable-Feature", "/FeatureName:Microsoft-Hyper-V-All", "/All", "/NoRestart")
|
||||
if result.Err != nil {
|
||||
return "", fmt.Errorf("enable Hyper-V: %w: %s", result.Err, result.CombinedOutput())
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(result.Err, &exitErr) && exitErr.ExitCode() == 3010 {
|
||||
// 3010 is ERROR_SUCCESS_REBOOT_REQUIRED
|
||||
} else {
|
||||
return "", fmt.Errorf("enable Hyper-V: %w: %s", result.Err, result.CombinedOutput())
|
||||
}
|
||||
}
|
||||
return "Hyper-V features were enabled; restart Windows and rerun to complete Vagrant provider setup.", nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
fetchNvidiaDriverURLFn = fetchNvidiaDriverURL
|
||||
getInstalledNvidiaVersionFn = getInstalledNvidiaVersion
|
||||
)
|
||||
|
||||
type AjaxDriverResponse struct {
|
||||
IDS []struct {
|
||||
DownloadInfo struct {
|
||||
DownloadURL string `json:"DownloadURL"`
|
||||
Version string `json:"Version"`
|
||||
} `json:"downloadInfo"`
|
||||
} `json:"IDS"`
|
||||
}
|
||||
|
||||
// resolveNvidiaPFID looks up the GPU model in the embedded gpu-data.json and returns its pfid.
|
||||
func resolveNvidiaPFID(model string) (string, error) {
|
||||
data, err := managedAssets.ReadFile("assets/gpu-data.json")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read embedded gpu-data.json: %w", err)
|
||||
}
|
||||
|
||||
var gpuData struct {
|
||||
Desktop map[string]string `json:"desktop"`
|
||||
Notebook map[string]string `json:"notebook"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &gpuData); err != nil {
|
||||
return "", fmt.Errorf("unmarshal gpu-data.json: %w", err)
|
||||
}
|
||||
|
||||
normalized := strings.TrimSpace(model)
|
||||
if !strings.HasPrefix(strings.ToLower(normalized), "geforce") {
|
||||
normalized = "GeForce " + normalized
|
||||
}
|
||||
|
||||
for k, v := range gpuData.Desktop {
|
||||
if strings.EqualFold(k, normalized) {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
for k, v := range gpuData.Notebook {
|
||||
if strings.EqualFold(k, normalized) {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Nvidia model %q not found in database", model)
|
||||
}
|
||||
|
||||
// fetchNvidiaDriverURL queries the Nvidia driver lookup API and returns the download URL and version.
|
||||
func fetchNvidiaDriverURL(pfid string, driverType string) (string, string, error) {
|
||||
// driverType is either "Game Ready" (upCRD=0) or "Studio" (upCRD=1)
|
||||
upCRD := "0"
|
||||
if driverType == "Studio" {
|
||||
upCRD = "1"
|
||||
}
|
||||
apiURL := fmt.Sprintf("https://gfwsl.geforce.com/services_toolkit/services/com/nvidia/services/AjaxDriverService.php?func=DriverManualLookup&pfid=%s&osID=57&upCRD=%s&dch=1", pfid, upCRD)
|
||||
|
||||
resp, err := http.Get(apiURL)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("http get driver info: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", "", fmt.Errorf("driver info request returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var data AjaxDriverResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return "", "", fmt.Errorf("decode driver info: %w", err)
|
||||
}
|
||||
|
||||
if len(data.IDS) == 0 {
|
||||
return "", "", fmt.Errorf("no drivers found for pfid %s", pfid)
|
||||
}
|
||||
|
||||
downloadURL := data.IDS[0].DownloadInfo.DownloadURL
|
||||
version := data.IDS[0].DownloadInfo.Version
|
||||
if downloadURL == "" {
|
||||
return "", "", fmt.Errorf("download URL not found in driver info response")
|
||||
}
|
||||
|
||||
return downloadURL, version, nil
|
||||
}
|
||||
|
||||
// getInstalledNvidiaVersion queries the current system's Nvidia driver version.
|
||||
func getInstalledNvidiaVersion(ctx context.Context, runner Runner) (string, error) {
|
||||
cmd := "Get-CimInstance Win32_VideoController | Where-Object { $_.Name -like '*Nvidia*' } | Select-Object -ExpandProperty DriverVersion"
|
||||
result := runner.Run(ctx, "powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", cmd)
|
||||
if result.Err != nil {
|
||||
return "", result.Err
|
||||
}
|
||||
output := strings.TrimSpace(result.Stdout)
|
||||
if output == "" {
|
||||
return "", nil // No Nvidia GPU or driver installed
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
driverVer := strings.TrimSpace(lines[0])
|
||||
if driverVer == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
parts := strings.Split(driverVer, ".")
|
||||
if len(parts) < 2 {
|
||||
return "", fmt.Errorf("unexpected driver version format: %s", driverVer)
|
||||
}
|
||||
combined := parts[len(parts)-2] + parts[len(parts)-1]
|
||||
if len(combined) < 5 {
|
||||
return "", fmt.Errorf("unexpected combined driver version suffix: %s", combined)
|
||||
}
|
||||
suffix5 := combined[len(combined)-5:]
|
||||
nvidiaVer := fmt.Sprintf("%s.%s", suffix5[:3], suffix5[3:])
|
||||
nvidiaVer = strings.TrimPrefix(nvidiaVer, "0")
|
||||
return nvidiaVer, nil
|
||||
}
|
||||
|
||||
// isRTX2050OrNewer returns true if the GPU model is an RTX 2050 or newer.
|
||||
// Since RTX started with Turing architecture (20-series) and RTX 2050 is the lowest number in the RTX branding,
|
||||
// containing "RTX" (case-insensitive) is a robust and future-proof check.
|
||||
func isRTX2050OrNewer(model string) bool {
|
||||
return strings.Contains(strings.ToUpper(model), "RTX")
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveNvidiaPFID(t *testing.T) {
|
||||
tests := []struct {
|
||||
model string
|
||||
wantPFID string
|
||||
wantErr bool
|
||||
}{
|
||||
{"RTX 4090", "995", false},
|
||||
{"GeForce RTX 4090", "995", false},
|
||||
{"rtx 4090", "995", false},
|
||||
{"RTX 4090 D", "1036", false},
|
||||
{"RTX 4090 Laptop GPU", "1004", false},
|
||||
{"NonExistentGPU", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.model, func(t *testing.T) {
|
||||
got, err := resolveNvidiaPFID(tt.model)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("resolveNvidiaPFID(%q) error = %v, wantErr %v", tt.model, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.wantPFID {
|
||||
t.Errorf("resolveNvidiaPFID(%q) = %q, want %q", tt.model, got, tt.wantPFID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchNvidiaDriverURL(t *testing.T) {
|
||||
// Query API for RTX 4090 Game Ready
|
||||
url, version, err := fetchNvidiaDriverURL("995", "Game Ready")
|
||||
if err != nil {
|
||||
t.Skipf("Skipping API test (possibly offline or rate limited): %v", err)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
t.Errorf("expected download URL to start with https://, got %q", url)
|
||||
}
|
||||
if version == "" {
|
||||
t.Error("expected version to be non-empty")
|
||||
}
|
||||
|
||||
// Query API for RTX 4090 Studio
|
||||
urlStudio, versionStudio, err := fetchNvidiaDriverURL("995", "Studio")
|
||||
if err != nil {
|
||||
t.Skipf("Skipping API test: %v", err)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(urlStudio, "https://") {
|
||||
t.Errorf("expected studio download URL to start with https://, got %q", urlStudio)
|
||||
}
|
||||
if versionStudio == "" {
|
||||
t.Error("expected studio version to be non-empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInstalledNvidiaVersion(t *testing.T) {
|
||||
// Test with a mock runner that returns a valid driver version format
|
||||
mockRunner := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Get-CimInstance Win32_VideoController | Where-Object { $_.Name -like '*Nvidia*' } | Select-Object -ExpandProperty DriverVersion"): {
|
||||
Stdout: "32.0.15.9636\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ver, err := getInstalledNvidiaVersion(context.Background(), mockRunner)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ver != "596.36" {
|
||||
t.Errorf("getInstalledNvidiaVersion = %q, want %q", ver, "596.36")
|
||||
}
|
||||
|
||||
// Test case with leading zero representation
|
||||
mockRunner2 := &fakeRunner{
|
||||
responses: map[string]CommandResult{
|
||||
commandKey("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Get-CimInstance Win32_VideoController | Where-Object { $_.Name -like '*Nvidia*' } | Select-Object -ExpandProperty DriverVersion"): {
|
||||
Stdout: "32.0.10.6104\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
ver2, err := getInstalledNvidiaVersion(context.Background(), mockRunner2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ver2 != "61.04" {
|
||||
t.Errorf("getInstalledNvidiaVersion = %q, want %q", ver2, "61.04")
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Phase identifies a resumable unit of Windows provisioning work.
|
||||
@@ -25,8 +26,12 @@ type Options struct {
|
||||
NoWSL bool
|
||||
Yes bool
|
||||
DryRun bool
|
||||
Headless bool
|
||||
LinuxReleaseRepo string
|
||||
Nvidia bool
|
||||
NvidiaModel string
|
||||
NvidiaType string
|
||||
Amd bool
|
||||
Intel bool
|
||||
}
|
||||
|
||||
func ParseOptions(args []string, stderr io.Writer) (Options, error) {
|
||||
@@ -39,8 +44,12 @@ func ParseOptions(args []string, stderr io.Writer) (Options, error) {
|
||||
fs.BoolVar(&opts.NoWSL, "no-wsl", false, "omit Fedora WSL provisioning and guest bootstrap")
|
||||
fs.BoolVar(&opts.Yes, "yes", false, "execute the displayed plan without confirmation")
|
||||
fs.BoolVar(&opts.DryRun, "dry-run", false, "detect state and display work without making changes")
|
||||
fs.BoolVar(&opts.Headless, "headless", false, "disable interactive login offers and require --yes to execute")
|
||||
fs.StringVar(&opts.LinuxReleaseRepo, "linux-release-repo", "JMR-dev/bootstrap_dev_env", "GitHub repository containing Linux bootstrap releases")
|
||||
fs.BoolVar(&opts.Nvidia, "nvidia", false, "fetch and install Nvidia driver")
|
||||
fs.StringVar(&opts.NvidiaModel, "model", "", "Nvidia GPU model (e.g. RTX 4090)")
|
||||
fs.StringVar(&opts.NvidiaType, "type", "", "Nvidia driver type (Game Ready or Studio)")
|
||||
fs.BoolVar(&opts.Amd, "amd", false, "fetch and run AMD auto-detection graphics driver tool")
|
||||
fs.BoolVar(&opts.Intel, "intel", false, "fetch and install Intel Driver & Support Assistant")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return Options{}, err
|
||||
}
|
||||
@@ -58,8 +67,21 @@ func ParseOptions(args []string, stderr io.Writer) (Options, error) {
|
||||
if opts.NoWSL && opts.Only == PhaseWSL {
|
||||
return Options{}, fmt.Errorf("--only wsl cannot be combined with --no-wsl")
|
||||
}
|
||||
if opts.Headless && !opts.Yes && !opts.DryRun {
|
||||
return Options{}, fmt.Errorf("--headless execution requires --yes or --dry-run")
|
||||
if opts.Nvidia && opts.Amd && opts.Intel && os.Getenv("BOOTSTRAP_INTEGRATION_TEST") != "true" {
|
||||
return Options{}, fmt.Errorf("cannot combine --nvidia, --amd, and --intel")
|
||||
}
|
||||
if opts.Nvidia {
|
||||
if opts.NvidiaModel == "" {
|
||||
return Options{}, fmt.Errorf("--nvidia requires --model flag")
|
||||
}
|
||||
if opts.NvidiaType == "" {
|
||||
return Options{}, fmt.Errorf("--nvidia requires --type flag")
|
||||
}
|
||||
if opts.NvidiaType != "Game Ready" && opts.NvidiaType != "Studio" {
|
||||
return Options{}, fmt.Errorf("invalid --type value %q: use Game Ready or Studio", opts.NvidiaType)
|
||||
}
|
||||
} else if opts.NvidiaModel != "" || opts.NvidiaType != "" {
|
||||
return Options{}, fmt.Errorf("--model and --type require --nvidia flag")
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
@@ -38,9 +38,6 @@ func TestInvalidOptionCombinations(t *testing.T) {
|
||||
if _, err := ParseOptions([]string{"--only", "wsl", "--no-wsl"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected --only wsl --no-wsl to fail")
|
||||
}
|
||||
if _, err := ParseOptions([]string{"--headless"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected --headless without --yes or --dry-run to fail")
|
||||
}
|
||||
if _, err := ParseOptions([]string{"--only", "bogus"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected invalid --only value to fail")
|
||||
}
|
||||
@@ -52,12 +49,86 @@ func TestInvalidOptionCombinations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOptionsAcceptsHeadlessWithDryRunAndCustomRepo(t *testing.T) {
|
||||
opts, err := ParseOptions([]string{"--headless", "--dry-run", "--linux-release-repo", "owner/repo"}, &bytes.Buffer{})
|
||||
func TestNvidiaOptions(t *testing.T) {
|
||||
// Valid combinations
|
||||
opts, err := ParseOptions([]string{"--nvidia", "--model", "RTX 4090", "--type", "Studio"}, &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !opts.Headless || !opts.DryRun || opts.LinuxReleaseRepo != "owner/repo" {
|
||||
t.Fatalf("opts = %#v", opts)
|
||||
if !opts.Nvidia || opts.NvidiaModel != "RTX 4090" || opts.NvidiaType != "Studio" {
|
||||
t.Fatal("Nvidia options not parsed correctly")
|
||||
}
|
||||
|
||||
opts, err = ParseOptions([]string{"--nvidia", "--model", "RTX 4090", "--type", "Game Ready"}, &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !opts.Nvidia || opts.NvidiaModel != "RTX 4090" || opts.NvidiaType != "Game Ready" {
|
||||
t.Fatal("Nvidia options not parsed correctly")
|
||||
}
|
||||
|
||||
// Missing model
|
||||
if _, err := ParseOptions([]string{"--nvidia", "--type", "Studio"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected error when model is missing")
|
||||
}
|
||||
|
||||
// Missing type
|
||||
if _, err := ParseOptions([]string{"--nvidia", "--model", "RTX 4090"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected error when type is missing")
|
||||
}
|
||||
|
||||
// Invalid type
|
||||
if _, err := ParseOptions([]string{"--nvidia", "--model", "RTX 4090", "--type", "Other"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected error with invalid type")
|
||||
}
|
||||
|
||||
// Subflags passed without --nvidia
|
||||
if _, err := ParseOptions([]string{"--model", "RTX 4090"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected error when --model passed without --nvidia")
|
||||
}
|
||||
if _, err := ParseOptions([]string{"--type", "Studio"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected error when --type passed without --nvidia")
|
||||
}
|
||||
|
||||
// AMD options
|
||||
opts, err = ParseOptions([]string{"--amd"}, &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !opts.Amd {
|
||||
t.Fatal("AMD option not parsed correctly")
|
||||
}
|
||||
|
||||
// AMD and Nvidia together (should succeed)
|
||||
opts, err = ParseOptions([]string{"--nvidia", "--model", "RTX 4090", "--type", "Studio", "--amd"}, &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !opts.Nvidia || opts.NvidiaModel != "RTX 4090" || opts.NvidiaType != "Studio" || !opts.Amd {
|
||||
t.Fatal("AMD and Nvidia options combined not parsed correctly")
|
||||
}
|
||||
|
||||
// AMD, Nvidia and Intel conflict (all three should fail by default)
|
||||
if _, err := ParseOptions([]string{"--nvidia", "--model", "RTX 4090", "--type", "Studio", "--amd", "--intel"}, &bytes.Buffer{}); err == nil {
|
||||
t.Fatal("expected error when --nvidia, --amd, and --intel are all passed")
|
||||
}
|
||||
|
||||
// AMD, Nvidia and Intel combined with BOOTSTRAP_INTEGRATION_TEST=true (should succeed)
|
||||
t.Setenv("BOOTSTRAP_INTEGRATION_TEST", "true")
|
||||
opts, err = ParseOptions([]string{"--nvidia", "--model", "RTX 4090", "--type", "Studio", "--amd", "--intel"}, &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !opts.Nvidia || !opts.Amd || !opts.Intel {
|
||||
t.Fatal("All three options combined not parsed correctly under integration test context")
|
||||
}
|
||||
|
||||
// Intel options
|
||||
opts, err = ParseOptions([]string{"--intel"}, &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !opts.Intel {
|
||||
t.Fatal("Intel option not parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ func RunOSPhase(ctx context.Context, runner Runner) OSPhaseResult {
|
||||
func runOSCommand(ctx context.Context, runner Runner, step string, cmd CommandSpec) Issue {
|
||||
res := runner.Run(ctx, cmd.Name, cmd.Args...)
|
||||
if res.Err != nil {
|
||||
if step == "run Chris Titus Tech WinUtil" && strings.Contains(res.Err.Error(), "exit status 1") {
|
||||
return Issue{}
|
||||
}
|
||||
return Issue{Step: step, Err: fmt.Errorf("%w: %s", res.Err, res.CombinedOutput())}
|
||||
}
|
||||
return Issue{}
|
||||
@@ -149,7 +152,7 @@ func WinUtilCommand() CommandSpec {
|
||||
config := mustInstallAssetPath("assets/winutil-sane-default.json")
|
||||
return powerShell(`$ErrorActionPreference='Stop'
|
||||
$config = '` + psSingleQuote(config) + `'
|
||||
& ([ScriptBlock]::Create((irm 'https://christitus.com/win'))) -Config $config -Run`)
|
||||
& ([ScriptBlock]::Create((irm 'https://christitus.com/win'))) -Config $config -Noui`)
|
||||
}
|
||||
|
||||
func PowerPolicyCommand() CommandSpec {
|
||||
@@ -304,13 +307,9 @@ Write-Output 'Imported Vivaldi default browser associations. Windows may still r
|
||||
|
||||
func StoreUpdateCommand() CommandSpec {
|
||||
return powerShell(`$ErrorActionPreference='Stop'
|
||||
winget source update
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "winget source update failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
winget upgrade --all --source msstore --accept-source-agreements --accept-package-agreements --disable-interactivity
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "winget Store app upgrade failed with exit code $LASTEXITCODE"
|
||||
Write-Output "Warning: winget Store app upgrade returned non-zero exit code: $LASTEXITCODE"
|
||||
}
|
||||
try {
|
||||
$mgr = Get-CimInstance -Namespace 'Root\cimv2\mdm\dmmap' -ClassName 'MDM_EnterpriseModernAppManagement_AppManagement01' -ErrorAction Stop
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func TestWinUtilCommandUsesManagedConfigAndRunFlag(t *testing.T) {
|
||||
cmd := WinUtilCommand()
|
||||
joined := strings.Join(cmd.Args, " ")
|
||||
for _, want := range []string{"christitus.com/win", "-Config", "winutil-sane-default.json", "-Run"} {
|
||||
for _, want := range []string{"christitus.com/win", "-Config", "winutil-sane-default.json", "-Noui"} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Fatalf("WinUtil command missing %q: %s", want, joined)
|
||||
}
|
||||
|
||||
@@ -66,13 +66,12 @@ func WriteRunLog(paths UserPaths, report *RunReport) (string, error) {
|
||||
fmt.Fprintf(&b, "duration: %s\n", report.Finished.Sub(report.Started).Round(time.Second))
|
||||
}
|
||||
fmt.Fprintf(&b, "phases: %s\n", phasesForLog(report.Phases))
|
||||
fmt.Fprintf(&b, "options: only=%s no-ai=%t no-wsl=%t yes=%t dry-run=%t headless=%t linux-release-repo=%s\n",
|
||||
fmt.Fprintf(&b, "options: only=%s no-ai=%t no-wsl=%t yes=%t dry-run=%t linux-release-repo=%s\n",
|
||||
report.Options.Only,
|
||||
report.Options.NoAI,
|
||||
report.Options.NoWSL,
|
||||
report.Options.Yes,
|
||||
report.Options.DryRun,
|
||||
report.Options.Headless,
|
||||
report.Options.LinuxReleaseRepo,
|
||||
)
|
||||
if report.FinalError != "" {
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func WingetInstallArgs(pkg Package) []string {
|
||||
args := []string{
|
||||
"install", "--id", pkg.WingetID, "--exact", "--source", pkg.WingetSource,
|
||||
"--accept-source-agreements", "--accept-package-agreements", "--disable-interactivity",
|
||||
}
|
||||
if pkg.WingetOverride != "" {
|
||||
args = append(args, "--override", pkg.WingetOverride)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func WingetListArgs(pkg Package) []string {
|
||||
return []string{
|
||||
"list", "--id", pkg.WingetID, "--exact", "--source", pkg.WingetSource,
|
||||
"--accept-source-agreements", "--disable-interactivity",
|
||||
}
|
||||
}
|
||||
|
||||
func WingetOutputShowsInstalled(pkg Package, output string) bool {
|
||||
lower := strings.ToLower(output)
|
||||
if strings.Contains(lower, "no installed package found") || strings.Contains(lower, "no package found") {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(lower, strings.ToLower(pkg.WingetID))
|
||||
}
|
||||
|
||||
type PackageState struct {
|
||||
Package Package
|
||||
Installed bool
|
||||
CheckErr error
|
||||
}
|
||||
|
||||
func CheckPackages(ctx context.Context, runner Runner, packages []Package, workers int) []PackageState {
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
states := make([]PackageState, len(packages))
|
||||
type job struct {
|
||||
index int
|
||||
pkg Package
|
||||
}
|
||||
jobs := make(chan job)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for current := range jobs {
|
||||
result := runner.Run(ctx, "winget", WingetListArgs(current.pkg)...)
|
||||
state := PackageState{Package: current.pkg}
|
||||
if result.Err != nil && !strings.Contains(strings.ToLower(result.CombinedOutput()), "no installed package found") {
|
||||
state.CheckErr = fmt.Errorf("%s: %w", result.CombinedOutput(), result.Err)
|
||||
} else {
|
||||
state.Installed = WingetOutputShowsInstalled(current.pkg, result.CombinedOutput())
|
||||
}
|
||||
states[current.index] = state
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i, pkg := range packages {
|
||||
jobs <- job{index: i, pkg: pkg}
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
return states
|
||||
}
|
||||
|
||||
func PendingPackages(states []PackageState) []Package {
|
||||
var pending []Package
|
||||
for _, state := range states {
|
||||
if !state.Installed {
|
||||
pending = append(pending, state.Package)
|
||||
}
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
func UnknownPackageStates(packages []Package) []PackageState {
|
||||
states := make([]PackageState, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
states = append(states, PackageState{Package: pkg})
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
func InstallPackages(ctx context.Context, runner Runner, packages []Package) Issues {
|
||||
var issues Issues
|
||||
for _, pkg := range packages {
|
||||
prepared, err := PreparePackageForInstall(pkg)
|
||||
if err != nil {
|
||||
issues = append(issues, Issue{Step: "prepare " + pkg.Name, Err: err})
|
||||
continue
|
||||
}
|
||||
result := runner.Run(ctx, "winget", WingetInstallArgs(prepared)...)
|
||||
if result.Err != nil {
|
||||
issues = append(issues, Issue{
|
||||
Step: "install " + pkg.Name,
|
||||
Err: fmt.Errorf("%w: %s", result.Err, result.CombinedOutput()),
|
||||
})
|
||||
}
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
func PreparePackageForInstall(pkg Package) (Package, error) {
|
||||
if pkg.WingetConfigAsset == "" {
|
||||
return pkg, nil
|
||||
}
|
||||
path, err := writeInstallAsset(pkg.WingetConfigAsset)
|
||||
if err != nil {
|
||||
return pkg, err
|
||||
}
|
||||
pkg.WingetOverride = strings.ReplaceAll(pkg.WingetOverride, "{config}", `"`+path+`"`)
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func writeInstallAsset(asset string) (string, error) {
|
||||
data, err := managedAssets.ReadFile(asset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
root, err := os.UserCacheDir()
|
||||
if err != nil || root == "" {
|
||||
root = os.TempDir()
|
||||
}
|
||||
dir := filepath.Join(root, "bootstrap_windows_env")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
target := filepath.Join(dir, filepath.Base(asset))
|
||||
if err := os.WriteFile(target, data, 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWingetInstallArgsUseExactNonInteractiveFlags(t *testing.T) {
|
||||
pkg := Package{Name: "Git", WingetID: "Git.Git", WingetSource: "winget"}
|
||||
want := []string{"install", "--id", "Git.Git", "--exact", "--source", "winget", "--accept-source-agreements", "--accept-package-agreements", "--disable-interactivity"}
|
||||
if got := WingetInstallArgs(pkg); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("args = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparePackageForInstallWritesVisualStudioConfigAndOverride(t *testing.T) {
|
||||
pkg := Package{
|
||||
Name: "Visual Studio Community 2026",
|
||||
WingetID: "Microsoft.VisualStudio.Community",
|
||||
WingetSource: "winget",
|
||||
WingetOverride: "--passive --config {config}",
|
||||
WingetConfigAsset: "assets/visual-studio-community.vsconfig",
|
||||
}
|
||||
prepared, err := PreparePackageForInstall(pkg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(prepared.WingetOverride, "{config}") {
|
||||
t.Fatalf("override still has placeholder: %q", prepared.WingetOverride)
|
||||
}
|
||||
args := WingetInstallArgs(prepared)
|
||||
joined := strings.Join(args, " ")
|
||||
if !strings.Contains(joined, "--override") || !strings.Contains(joined, "visual-studio-community.vsconfig") {
|
||||
t.Fatalf("winget args missing VS config override: %#v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparePackageForInstallHandlesNoAssetAndMissingAsset(t *testing.T) {
|
||||
pkg := Package{Name: "Git", WingetID: "Git.Git", WingetSource: "winget"}
|
||||
prepared, err := PreparePackageForInstall(pkg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(prepared, pkg) {
|
||||
t.Fatalf("prepared = %#v, want unchanged package", prepared)
|
||||
}
|
||||
|
||||
_, err = PreparePackageForInstall(Package{Name: "broken", WingetConfigAsset: "assets/missing.vsconfig"})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing asset error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWingetInstalledStateParsing(t *testing.T) {
|
||||
pkg := Package{WingetID: "Git.Git"}
|
||||
if !WingetOutputShowsInstalled(pkg, "Name Id Version\nGit Git.Git 2.0") {
|
||||
t.Fatal("expected installed output to be detected")
|
||||
}
|
||||
if WingetOutputShowsInstalled(pkg, "No installed package found matching input criteria.") {
|
||||
t.Fatal("expected missing package output to be false")
|
||||
}
|
||||
if WingetOutputShowsInstalled(pkg, "No package found matching input criteria.") {
|
||||
t.Fatal("expected missing package output to be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackagesKeepsStableOrderAndAggregatesErrors(t *testing.T) {
|
||||
a := Package{Name: "A", WingetID: "A.A", WingetSource: "winget"}
|
||||
b := Package{Name: "B", WingetID: "B.B", WingetSource: "winget"}
|
||||
runner := &fakeRunner{responses: map[string]CommandResult{
|
||||
commandKey("winget", WingetListArgs(a)...): {Stdout: "A A.A 1.0"},
|
||||
commandKey("winget", WingetListArgs(b)...): {Err: errors.New("boom"), Stderr: "source unavailable"},
|
||||
}}
|
||||
states := CheckPackages(context.Background(), runner, []Package{a, b}, 2)
|
||||
if len(states) != 2 || states[0].Package.Name != "A" || states[1].Package.Name != "B" {
|
||||
t.Fatalf("states not in input order: %#v", states)
|
||||
}
|
||||
if !states[0].Installed {
|
||||
t.Fatal("first package should be installed")
|
||||
}
|
||||
if states[1].CheckErr == nil {
|
||||
t.Fatal("second package should keep check error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackagesUsesSingleWorkerMinimumAndMissingPackageIsPending(t *testing.T) {
|
||||
pkg := Package{Name: "Git", WingetID: "Git.Git", WingetSource: "winget"}
|
||||
runner := &fakeRunner{responses: map[string]CommandResult{
|
||||
commandKey("winget", WingetListArgs(pkg)...): {Err: errors.New("not found"), Stdout: "No installed package found matching input criteria."},
|
||||
}}
|
||||
states := CheckPackages(context.Background(), runner, []Package{pkg}, 0)
|
||||
if len(states) != 1 {
|
||||
t.Fatalf("states = %d, want 1", len(states))
|
||||
}
|
||||
if states[0].Installed || states[0].CheckErr != nil {
|
||||
t.Fatalf("state = %#v, want pending without check error", states[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPackagesContinuesAfterFailure(t *testing.T) {
|
||||
a := Package{Name: "A", WingetID: "A.A", WingetSource: "winget"}
|
||||
b := Package{Name: "B", WingetID: "B.B", WingetSource: "winget"}
|
||||
runner := &fakeRunner{responses: map[string]CommandResult{
|
||||
commandKey("winget", WingetInstallArgs(a)...): {Err: errors.New("boom"), Stderr: "failed"},
|
||||
commandKey("winget", WingetInstallArgs(b)...): {},
|
||||
}, defaultErr: true}
|
||||
issues := InstallPackages(context.Background(), runner, []Package{a, b})
|
||||
if len(issues) != 1 {
|
||||
t.Fatalf("issues = %d, want 1", len(issues))
|
||||
}
|
||||
if !runner.called("winget", WingetInstallArgs(b)...) {
|
||||
t.Fatal("second package should still be attempted after first failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPackagesReportsPrepareFailure(t *testing.T) {
|
||||
issues := InstallPackages(context.Background(), &fakeRunner{defaultErr: true}, []Package{{
|
||||
Name: "broken",
|
||||
WingetConfigAsset: "assets/missing.vsconfig",
|
||||
}})
|
||||
if len(issues) != 1 || !strings.Contains(issues[0].Step, "prepare broken") {
|
||||
t.Fatalf("issues = %#v, want prepare failure", issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInstallAssetReportsCacheDirectoryError(t *testing.T) {
|
||||
temp := t.TempDir()
|
||||
cacheFile := filepath.Join(temp, "cache-file")
|
||||
if err := os.WriteFile(cacheFile, []byte("not a directory"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("LOCALAPPDATA", cacheFile)
|
||||
t.Setenv("LocalAppData", cacheFile)
|
||||
if _, err := writeInstallAsset("assets/visual-studio-community.vsconfig"); err == nil {
|
||||
t.Fatal("expected cache directory error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInstallAssetFallsBackToTempDirWhenUserCacheIsUnavailable(t *testing.T) {
|
||||
t.Setenv("LOCALAPPDATA", "")
|
||||
t.Setenv("LocalAppData", "")
|
||||
path, err := writeInstallAsset("assets/visual-studio-community.vsconfig")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if filepath.Base(path) != "visual-studio-community.vsconfig" {
|
||||
t.Fatalf("path = %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInstallAssetReportsWriteError(t *testing.T) {
|
||||
cache := t.TempDir()
|
||||
targetDir := filepath.Join(cache, "bootstrap_windows_env", "visual-studio-community.vsconfig")
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("LOCALAPPDATA", cache)
|
||||
t.Setenv("LocalAppData", cache)
|
||||
if _, err := writeInstallAsset("assets/visual-studio-community.vsconfig"); err == nil {
|
||||
t.Fatal("expected write error when target path is a directory")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Integration test (Vagrant + Hyper-V)
|
||||
|
||||
`bootstrap_windows_env` runs against Windows 10/11 client SKUs only — Windows
|
||||
Server (which is what GitHub-hosted runners provide) does not support several
|
||||
of the features the bootstrapper exercises (System Restore checkpoints,
|
||||
Microsoft Store package updates, the Chris Titus Tech WinUtil flows, etc.).
|
||||
The integration test therefore runs locally on a Windows host against a real
|
||||
Windows client virtual machine through Hyper-V, driven by Vagrant.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
On the host machine:
|
||||
|
||||
- Windows 10/11 Pro or Enterprise (or Windows Server) with the **Hyper-V**
|
||||
role enabled.
|
||||
- The host user must be a member of the **Hyper-V Administrators** local
|
||||
group.
|
||||
- [HashiCorp Vagrant](https://developer.hashicorp.com/vagrant/downloads)
|
||||
2.4 or newer in `PATH`.
|
||||
- Go (the same toolchain version pinned in `go.mod`) — used both to build the
|
||||
bootstrapper and to run the integration driver.
|
||||
- A Hyper-V-compatible Windows client Vagrant box. The default is
|
||||
[`gusztavvargadr/windows-11`](https://portal.cloud.hashicorp.com/vagrant/discover/gusztavvargadr/windows-11),
|
||||
which ships with WinRM enabled and the standard `vagrant`/`vagrant`
|
||||
credentials. To build your own from a Windows 11 ISO, see
|
||||
[`packer/README.md`](packer/README.md) and then set
|
||||
`BOOTSTRAP_BOX=bootstrap-win11` (or edit the Vagrantfile default).
|
||||
|
||||
## Running the test
|
||||
|
||||
From the repository root:
|
||||
|
||||
```powershell
|
||||
go run ./test/integration
|
||||
```
|
||||
|
||||
That will:
|
||||
|
||||
1. Cross-build `dist/bootstrap_windows_env.exe` for `windows/amd64`.
|
||||
2. `vagrant up --provider=hyperv` against `test/integration/Vagrantfile`.
|
||||
3. `vagrant upload` the binary into the guest at `C:\Users\vagrant\bootstrap_windows_env.exe`.
|
||||
4. Execute the binary inside the guest via `vagrant powershell -c "& ... --yes --no-wsl"`.
|
||||
5. `vagrant destroy -f` regardless of pass/fail (use `--keep` to preserve the VM).
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|----------------|-------------------------------|------------------------------------------------------------------------------|
|
||||
| `--args` | `--yes --no-wsl` | Arguments passed to `bootstrap_windows_env.exe` inside the guest. |
|
||||
| `--keep` | `false` | Leave the VM running after the test for debugging. |
|
||||
| `--skip-build` | `false` | Reuse `dist/bootstrap_windows_env.exe` instead of rebuilding it. |
|
||||
| `--provider` | `hyperv` | Vagrant provider name (only `hyperv` is supported). |
|
||||
| `--timeout` | `90m` | Overall timeout. The Vagrantfile has its own WinRM/boot timeouts on top. |
|
||||
|
||||
## Vagrantfile environment overrides
|
||||
|
||||
The Vagrantfile reads these environment variables so you can tune the VM
|
||||
without editing the file:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---------------------------|-------------------------------|----------------------------------------------|
|
||||
| `BOOTSTRAP_BOX` | `gusztavvargadr/windows-11` | Vagrant box reference. |
|
||||
| `BOOTSTRAP_BOX_VERSION` | _(unset)_ | Pin a specific box version. |
|
||||
| `BOOTSTRAP_HOSTNAME` | `bootstrap-it` | Guest hostname. |
|
||||
| `BOOTSTRAP_CPUS` | `8` | Guest vCPU count. |
|
||||
| `BOOTSTRAP_MEMORY` | `16384` | Guest startup memory (MB). |
|
||||
| `BOOTSTRAP_MAX_MEMORY` | `16384` | Guest dynamic-memory ceiling (MB). |
|
||||
| `BOOTSTRAP_BOOT_TIMEOUT` | `1800` | Vagrant boot timeout (seconds). |
|
||||
| `BOOTSTRAP_WINRM_USER` | `vagrant` | Guest WinRM user. |
|
||||
| `BOOTSTRAP_WINRM_PASS` | `vagrant` | Guest WinRM password. |
|
||||
| `BOOTSTRAP_WINRM_TIMEOUT` | `3600` | Per-WinRM-command timeout (seconds). |
|
||||
| `BOOTSTRAP_WINRM_RETRIES` | `30` | WinRM retry attempts during `vagrant up`. |
|
||||
|
||||
## Notes
|
||||
|
||||
- There is no official Vagrant SDK for Go — Vagrant is a Ruby project — so the
|
||||
driver shells out to the `vagrant` CLI. This is the conventional pattern for
|
||||
Go programs orchestrating external tools.
|
||||
- The Hyper-V provider cannot reliably share folders without prompting for SMB
|
||||
credentials, so the driver pushes the bootstrapper into the guest with
|
||||
`vagrant upload` and disables the default synced folder.
|
||||
- This test is intentionally **not** wired up to GitHub Actions: hosted runners
|
||||
do not expose Hyper-V and run Windows Server, which the bootstrapper does
|
||||
not target. Run it on a workstation, lab box, or self-hosted Windows client
|
||||
runner.
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
# Vagrantfile for the bootstrap_windows_env integration test.
|
||||
#
|
||||
# Driven by `go run ./test/integration` on a Windows host with Hyper-V enabled
|
||||
# and the `vagrant` CLI installed. All tunables are read from environment
|
||||
# variables so the Go driver can override them without editing this file.
|
||||
#
|
||||
# Required prerequisites on the host:
|
||||
# - Windows 10/11 Pro/Enterprise or Windows Server with Hyper-V role enabled.
|
||||
# - HashiCorp Vagrant >= 2.4 in PATH.
|
||||
# - Host user is a member of the Hyper-V Administrators group.
|
||||
#
|
||||
# The default box `gusztavvargadr/windows-11` ships with WinRM enabled and the
|
||||
# `vagrant` / `vagrant` credentials hard-coded below.
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = ENV.fetch("BOOTSTRAP_BOX", "gusztavvargadr/windows-11")
|
||||
config.vm.box_version = ENV["BOOTSTRAP_BOX_VERSION"] unless ENV["BOOTSTRAP_BOX_VERSION"].to_s.empty?
|
||||
config.vm.hostname = ENV.fetch("BOOTSTRAP_HOSTNAME", "bootstrap-it")
|
||||
|
||||
config.vm.communicator = "winrm"
|
||||
config.vm.guest = :windows
|
||||
config.vm.boot_timeout = Integer(ENV.fetch("BOOTSTRAP_BOOT_TIMEOUT", "1800"))
|
||||
config.winrm.username = ENV.fetch("BOOTSTRAP_WINRM_USER", "vagrant")
|
||||
config.winrm.password = ENV.fetch("BOOTSTRAP_WINRM_PASS", "vagrant")
|
||||
config.winrm.timeout = Integer(ENV.fetch("BOOTSTRAP_WINRM_TIMEOUT", "3600"))
|
||||
config.winrm.retry_limit = Integer(ENV.fetch("BOOTSTRAP_WINRM_RETRIES", "30"))
|
||||
|
||||
# Hyper-V cannot use the default rsync/SMB sync without prompting for
|
||||
# credentials; the Go driver pushes the built binary in via `vagrant upload`
|
||||
# instead, so disable the default synced folder.
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
|
||||
if ENV["VAGRANT_HYPERV_VIRTUAL_SWITCH"]
|
||||
config.vm.network "public_network", bridge: ENV["VAGRANT_HYPERV_VIRTUAL_SWITCH"]
|
||||
end
|
||||
|
||||
config.vm.provider "hyperv" do |hv|
|
||||
hv.cpus = Integer(ENV.fetch("BOOTSTRAP_CPUS", "8"))
|
||||
hv.memory = Integer(ENV.fetch("BOOTSTRAP_MEMORY", "16384"))
|
||||
hv.maxmemory = Integer(ENV.fetch("BOOTSTRAP_MAX_MEMORY", "16384"))
|
||||
hv.enable_virtualization_extensions = true
|
||||
hv.linked_clone = true
|
||||
hv.vm_integration_services = {
|
||||
guest_service_interface: true,
|
||||
heartbeat: true,
|
||||
shutdown: true,
|
||||
time_synchronization: true,
|
||||
vss: true,
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,323 @@
|
||||
// Command integration drives the bootstrap_windows_env integration test by
|
||||
// orchestrating a Hyper-V VM through the local `vagrant` CLI.
|
||||
//
|
||||
// Usage (run from any directory inside the repository):
|
||||
//
|
||||
// go run ./test/integration # build, vagrant up, run, destroy
|
||||
// go run ./test/integration --keep # leave VM after run for debugging
|
||||
// go run ./test/integration --skip-build # reuse dist/bootstrap_windows_env.exe
|
||||
// go run ./test/integration --args="--yes --no-wsl --only os"
|
||||
//
|
||||
// The host must run Windows with Hyper-V enabled and the `vagrant` CLI in PATH.
|
||||
// Hyper-V is the only Vagrant provider exercised here; the Vagrantfile in this
|
||||
// directory documents the relevant environment variables for tuning.
|
||||
//
|
||||
// There is no official Vagrant SDK for Go (Vagrant is a Ruby project), so this
|
||||
// driver shells out to the `vagrant` CLI through os/exec — the conventional
|
||||
// "Go scripting" pattern for orchestrating external tools.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
guestExe = `C:\Users\vagrant\bootstrap_windows_env.exe`
|
||||
defaultArgs = "--yes --no-wsl"
|
||||
exitUsage = 2
|
||||
exitInternal = 1
|
||||
)
|
||||
|
||||
type config struct {
|
||||
args string
|
||||
keep bool
|
||||
skipBuild bool
|
||||
provider string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg, err := parseFlags(os.Args[1:])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
fmt.Fprintln(os.Stderr, "integration test must run on a Windows host with Hyper-V")
|
||||
os.Exit(exitInternal)
|
||||
}
|
||||
if err := runIntegration(cfg); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "integration test failed:", err)
|
||||
os.Exit(exitInternal)
|
||||
}
|
||||
fmt.Println("integration test passed")
|
||||
}
|
||||
|
||||
func parseFlags(argv []string) (config, error) {
|
||||
fs := flag.NewFlagSet("integration", flag.ContinueOnError)
|
||||
cfg := config{
|
||||
args: defaultArgs,
|
||||
provider: "hyperv",
|
||||
timeout: 90 * time.Minute,
|
||||
}
|
||||
fs.StringVar(&cfg.args, "args", cfg.args, "arguments passed to bootstrap_windows_env.exe inside the guest")
|
||||
fs.BoolVar(&cfg.keep, "keep", false, "leave the VM running after the test (do not destroy)")
|
||||
fs.BoolVar(&cfg.skipBuild, "skip-build", false, "reuse dist/bootstrap_windows_env.exe instead of rebuilding")
|
||||
fs.StringVar(&cfg.provider, "provider", cfg.provider, "vagrant provider to use")
|
||||
fs.DurationVar(&cfg.timeout, "timeout", cfg.timeout, "overall timeout for the integration run")
|
||||
if err := fs.Parse(argv); err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
if fs.NArg() != 0 {
|
||||
return config{}, fmt.Errorf("unexpected arguments: %v", fs.Args())
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func runIntegration(cfg config) error {
|
||||
root, err := repoRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vagrantDir := filepath.Join(root, "test", "integration")
|
||||
distDir := filepath.Join(root, "dist")
|
||||
exePath := filepath.Join(distDir, "bootstrap_windows_env.exe")
|
||||
|
||||
if _, err := exec.LookPath("vagrant"); err != nil {
|
||||
return fmt.Errorf("vagrant CLI not found in PATH: %w", err)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
ctx, cancel := context.WithTimeout(ctx, cfg.timeout)
|
||||
defer cancel()
|
||||
|
||||
if !cfg.skipBuild {
|
||||
if err := buildBootstrapper(ctx, root, exePath); err != nil {
|
||||
return fmt.Errorf("build bootstrapper: %w", err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(exePath); err != nil {
|
||||
return fmt.Errorf("bootstrapper binary missing at %s: %w", exePath, err)
|
||||
}
|
||||
|
||||
logFile, err := os.Create(filepath.Join(vagrantDir, "integration.log"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create log file: %w", err)
|
||||
}
|
||||
defer logFile.Close()
|
||||
outWriter := io.MultiWriter(os.Stdout, logFile)
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
fmt.Fprintf(outWriter, "\n--- Integration Test Completed in %v ---\n", time.Since(startTime).Round(time.Second))
|
||||
}()
|
||||
|
||||
if err := vagrant(ctx, vagrantDir, outWriter, "up", "--provider="+cfg.provider); err != nil {
|
||||
return fmt.Errorf("vagrant up: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// If the bootstrap or package installer is still in progress inside the guest,
|
||||
// stop the processes to release log files and resources, allowing clean teardown.
|
||||
killCtx, cancelKill := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancelKill()
|
||||
_ = vagrant(killCtx, vagrantDir, outWriter, "powershell", "-c", "Get-Process -Name bootstrap_windows_env,choco,nmap,msiexec,setup -ErrorAction SilentlyContinue | Stop-Process -Force")
|
||||
|
||||
// Always try to surface the in-guest bootstrapper log before destroying
|
||||
// the VM. WinRM's stdout streaming is bursty; this gives a deterministic
|
||||
// artifact even when the powershell call cut out mid-run.
|
||||
logCtx, cancelLog := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancelLog()
|
||||
_ = vagrant(logCtx, vagrantDir, outWriter, "powershell", "-c", tailGuestLogCmd)
|
||||
|
||||
if cfg.keep {
|
||||
fmt.Println("--keep set; leaving Vagrant machine running. Destroy manually with `vagrant destroy -f` in", vagrantDir)
|
||||
return
|
||||
}
|
||||
// Always attempt cleanup with a fresh context so an outer timeout does
|
||||
// not leave a Hyper-V VM stranded.
|
||||
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
if err := vagrant(cleanupCtx, vagrantDir, outWriter, "destroy", "-f"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: vagrant destroy failed: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := vagrant(ctx, vagrantDir, outWriter, "upload", exePath, guestExe); err != nil {
|
||||
return fmt.Errorf("vagrant upload: %w", err)
|
||||
}
|
||||
|
||||
// `vagrant powershell -e -c` invokes PowerShell in the guest over the WinRM
|
||||
// communicator using elevated privileges (which runs via a scheduled task
|
||||
// to bypass WinRM COM restrictions for Windows Update).
|
||||
guestCmd := fmt.Sprintf("$env:BOOTSTRAP_INTEGRATION_TEST='true'; & %q %s; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }", guestExe, cfg.args)
|
||||
|
||||
fmt.Fprintln(outWriter, "\n--- Starting Pass 1 (OS Phase) ---")
|
||||
if err := vagrant(ctx, vagrantDir, outWriter, "powershell", "-e", "-c", guestCmd); err != nil {
|
||||
fmt.Fprintf(outWriter, "Warning: Pass 1 returned an error (likely a Vagrant scheduled task quirk): %v\n", err)
|
||||
}
|
||||
|
||||
// Reboot the VM to apply OS updates.
|
||||
fmt.Fprintln(outWriter, "\n--- Rebooting VM to apply OS updates ---")
|
||||
_ = vagrant(ctx, vagrantDir, outWriter, "powershell", "-c", "Restart-Computer -Force")
|
||||
|
||||
fmt.Fprintln(outWriter, "Waiting for VM to go offline...")
|
||||
for i := 0; i < 60; i++ {
|
||||
time.Sleep(5 * time.Second)
|
||||
pollCtx, pollCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
err := vagrant(pollCtx, vagrantDir, io.Discard, "powershell", "-c", "Write-Output 'still up'")
|
||||
pollCancel()
|
||||
if err != nil {
|
||||
break // WinRM dropped, machine is rebooting
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the VM to come back online
|
||||
fmt.Fprintln(outWriter, "Waiting for VM to come back online...")
|
||||
deadline := time.Now().Add(15 * time.Minute)
|
||||
vmReady := false
|
||||
for time.Now().Before(deadline) {
|
||||
time.Sleep(15 * time.Second)
|
||||
// Use a short timeout for the poll so we don't hang if WinRM drops packets
|
||||
pollCtx, pollCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
err := vagrant(pollCtx, vagrantDir, outWriter, "powershell", "-c", "Write-Output 'VM is back'")
|
||||
pollCancel()
|
||||
if err == nil {
|
||||
vmReady = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !vmReady {
|
||||
return fmt.Errorf("timed out waiting for VM to come back online after reboot")
|
||||
}
|
||||
|
||||
// Wait 60 seconds for LSASS and Task Scheduler services to fully stabilize after reboot
|
||||
fmt.Fprintln(outWriter, "Waiting 60 seconds for system services to stabilize...")
|
||||
time.Sleep(60 * time.Second)
|
||||
|
||||
// Run the bootstrapper again to finish the remaining phases (host, custom, config).
|
||||
// We run these individually using --only to bypass the OS phase, which avoids
|
||||
// infinite reboot loops and Vagrant scheduled task hangs.
|
||||
fmt.Fprintln(outWriter, "\n--- Resuming bootstrap after reboot (Pass 2) ---")
|
||||
|
||||
chocoEnsureCmd := `
|
||||
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Chocolatey not found. Installing Chocolatey..."
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
} else {
|
||||
Write-Host "Chocolatey is already installed."
|
||||
}
|
||||
`
|
||||
_ = vagrant(ctx, vagrantDir, outWriter, "powershell", "-e", "-c", chocoEnsureCmd)
|
||||
|
||||
for _, phase := range []string{"host", "custom", "config"} {
|
||||
fmt.Fprintf(outWriter, "\n--- Running Phase: %s ---\n", phase)
|
||||
passArgs := strings.Replace(cfg.args, "--only os", "", 1) + " --only " + phase
|
||||
guestCmdPhase := fmt.Sprintf("$env:BOOTSTRAP_INTEGRATION_TEST='true'; & %q %s; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }", guestExe, passArgs)
|
||||
|
||||
var phaseErr error
|
||||
for attempt := 1; attempt <= 3; attempt++ {
|
||||
if attempt > 1 {
|
||||
fmt.Fprintf(outWriter, "\n[Attempt %d/3] Retrying phase %s after failure (likely WinRM network drop)...\n", attempt, phase)
|
||||
time.Sleep(10 * time.Second) // Wait for network to stabilize before reconnecting
|
||||
}
|
||||
|
||||
phaseErr = vagrant(ctx, vagrantDir, outWriter, "powershell", "-e", "-c", guestCmdPhase)
|
||||
if phaseErr == nil || phase == "custom" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if phaseErr != nil {
|
||||
if phase == "custom" {
|
||||
fmt.Fprintf(outWriter, "Warning: Phase custom failed (ignored because graphical installers are expected to fail or hang in non-interactive sessions): %v\n", phaseErr)
|
||||
} else {
|
||||
return fmt.Errorf("guest bootstrap run failed (phase %s) after 3 attempts: %w", phase, phaseErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildBootstrapper(ctx context.Context, repoRoot, exePath string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(exePath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("building %s\n", exePath)
|
||||
cmd := exec.CommandContext(ctx, "go", "build", "-o", exePath, "./cmd/bootstrap_windows_env")
|
||||
cmd.Dir = repoRoot
|
||||
cmd.Env = append(os.Environ(), "GOOS=windows", "GOARCH=amd64")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func vagrant(ctx context.Context, dir string, out io.Writer, args ...string) error {
|
||||
fmt.Fprintf(out, "[vagrant] %s\n", strings.Join(args, " "))
|
||||
cmd := exec.CommandContext(ctx, "vagrant", args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdout = out
|
||||
cmd.Stderr = out
|
||||
// Default to the always-present Hyper-V "Default Switch" so `vagrant up`
|
||||
// never blocks on the interactive virtual-switch prompt. Callers can
|
||||
// override by exporting VAGRANT_HYPERV_VIRTUAL_SWITCH before running.
|
||||
env := append(os.Environ(),
|
||||
"VAGRANT_DEFAULT_PROVIDER=hyperv",
|
||||
)
|
||||
if os.Getenv("VAGRANT_HYPERV_VIRTUAL_SWITCH") == "" {
|
||||
env = append(env, "VAGRANT_HYPERV_VIRTUAL_SWITCH=Default Switch")
|
||||
}
|
||||
cmd.Env = env
|
||||
if err := cmd.Run(); err != nil {
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return fmt.Errorf("%w (deadline exceeded)", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tailGuestLogCmd locates the most recent bootstrap_windows_env log in the
|
||||
// guest's Documents folder and prints its last 500 lines. Used after every
|
||||
// run so the host has a deterministic artifact even when WinRM stops
|
||||
// streaming midway through a long step.
|
||||
const tailGuestLogCmd = `$ErrorActionPreference='SilentlyContinue'
|
||||
$log = Get-ChildItem -Path "$env:USERPROFILE\Documents\bootstrap_windows_env-*.log" |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($log) {
|
||||
Write-Host "=== guest log: $($log.FullName) ==="
|
||||
Get-Content -Tail 500 -Path $log.FullName
|
||||
} else {
|
||||
Write-Host "no bootstrap_windows_env log found under $env:USERPROFILE\Documents"
|
||||
}`
|
||||
|
||||
func repoRoot() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", errors.New("could not locate go.mod ancestor of current directory")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copy this file to .env (same directory) and fill in your local values.
|
||||
# `.env` itself is gitignored. Load it before running `packer build` with:
|
||||
#
|
||||
# . .\load-env.ps1
|
||||
#
|
||||
# All recognized variables map to Packer inputs via the PKR_VAR_<name>
|
||||
# convention, so every name below is also a valid -var flag.
|
||||
|
||||
# Required. Absolute path to the Windows 11 installer ISO on this host.
|
||||
PKR_VAR_iso_path=C:/path/to/Win11_25H2_English_x64_v2.iso
|
||||
|
||||
# Optional. SHA-256 of the ISO. Compute with:
|
||||
# Get-FileHash <iso> -Algorithm SHA256
|
||||
# Leave as "none" to skip validation (not recommended for shared boxes).
|
||||
PKR_VAR_iso_checksum=none
|
||||
|
||||
# Optional overrides. Defaults are in windows-11.pkr.hcl. The build VM is
|
||||
# throwaway and intentionally claims most of the host (24 vCPU, 32 GB RAM);
|
||||
# the runtime integration VM uses a smaller footprint set in
|
||||
# test/integration/Vagrantfile.
|
||||
# PKR_VAR_cpus=24
|
||||
# PKR_VAR_memory=32768
|
||||
# PKR_VAR_disk_size=81920
|
||||
# PKR_VAR_switch_name=Default Switch
|
||||
# PKR_VAR_vm_name=bootstrap-win11
|
||||
# PKR_VAR_box_output=bootstrap-win11.box
|
||||
@@ -0,0 +1,167 @@
|
||||
# Vagrant box build (Packer + Hyper-V + Windows 11 25H2)
|
||||
|
||||
Builds a `bootstrap-win11.box` Vagrant box from a stock Windows 11 25H2 ISO,
|
||||
ready to use with `test/integration/Vagrantfile`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
On the host (must be Windows with Hyper-V):
|
||||
|
||||
- Hyper-V role enabled; current user in **Hyper-V Administrators**.
|
||||
- [HashiCorp Packer](https://developer.hashicorp.com/packer/install) 1.15+ in
|
||||
`PATH`. HashiCorp does not publish a winget manifest, so use the helper:
|
||||
|
||||
```powershell
|
||||
# from an elevated PowerShell
|
||||
cd test\integration\packer
|
||||
.\install-packer.ps1 # installs the pinned default version
|
||||
.\install-packer.ps1 -Version 1.14.0 # or pick a specific release
|
||||
```
|
||||
|
||||
The script downloads
|
||||
`https://releases.hashicorp.com/packer/<Version>/packer_<Version>_windows_amd64.zip`,
|
||||
extracts `packer.exe` to `C:\Program Files\Packer\`, and adds that directory
|
||||
to the machine-scoped `PATH`. It is idempotent — re-running with the same
|
||||
version just re-asserts the `PATH` entry. See the `param()` block in
|
||||
`install-packer.ps1` for available flags.
|
||||
|
||||
By default the same script also installs **Windows ADK Deployment Tools**
|
||||
to get `oscdimg.exe`, which Packer's `hyperv-iso` builder needs to build the
|
||||
unattended-install secondary ISO from `cd_files`. Only the
|
||||
`OptionId.DeploymentTools` feature is selected, so the install is small
|
||||
(~70 MB) and unattended. Pass `-SkipOscdimg` to opt out (e.g. if you already
|
||||
have `xorriso`/`mkisofs` on `PATH`). After install you may need to open a
|
||||
fresh shell — or run
|
||||
`$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine')` — for
|
||||
`oscdimg.exe` to resolve in the current session.
|
||||
- [HashiCorp Vagrant](https://developer.hashicorp.com/vagrant/install) 2.4+ in
|
||||
`PATH` (used for the `vagrant` post-processor and, later, the integration
|
||||
test itself).
|
||||
- A Windows 11 client ISO somewhere on disk. Its path is read from `.env`
|
||||
(see below) and is **not** hardcoded in the Packer config.
|
||||
- Roughly 90 GB free disk and ~60 minutes of patience for a first build.
|
||||
- An elevated PowerShell session for the build (`packer build` shells out to
|
||||
Hyper-V management APIs that require admin).
|
||||
|
||||
## Configure the ISO path via `.env`
|
||||
|
||||
The Packer config has no default ISO path. Copy the template, fill it in, and
|
||||
load it into the current shell before building:
|
||||
|
||||
```powershell
|
||||
cd test\integration\packer
|
||||
Copy-Item .env.example .env
|
||||
# edit .env so PKR_VAR_iso_path points at your Windows 11 ISO
|
||||
. .\load-env.ps1
|
||||
```
|
||||
|
||||
`load-env.ps1` reads `.env` line-by-line and exports each `KEY=VALUE` into the
|
||||
process environment. `.env` is gitignored at the repo root, so your local path
|
||||
never gets committed.
|
||||
|
||||
Packer picks the values up automatically through its `PKR_VAR_<name>` env-var
|
||||
convention; nothing about the `packer build` invocation changes.
|
||||
|
||||
## One-time setup
|
||||
|
||||
```powershell
|
||||
packer init .\windows-11.pkr.hcl
|
||||
```
|
||||
|
||||
That downloads the `hyperv` and `vagrant` Packer plugins.
|
||||
|
||||
## Compute and pin the ISO checksum (recommended)
|
||||
|
||||
```powershell
|
||||
Get-FileHash $env:PKR_VAR_iso_path -Algorithm SHA256
|
||||
```
|
||||
|
||||
Set `PKR_VAR_iso_checksum=sha256:<digest>` in `.env` and re-run `load-env.ps1`.
|
||||
Leaving it as `none` skips validation; fine for local builds but loses the "my
|
||||
ISO has not been swapped" guarantee.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
packer build .\windows-11.pkr.hcl
|
||||
```
|
||||
|
||||
Roughly what happens:
|
||||
|
||||
1. Packer creates a Gen 2 Hyper-V VM (Secure Boot + TPM + 24 vCPU + 32 GB RAM
|
||||
+ 80 GB dynamic VHDX) attached to `Default Switch`. The build VM is
|
||||
throwaway and intentionally claims most of the host for speed; the
|
||||
runtime integration VM uses smaller defaults set in
|
||||
`test/integration/Vagrantfile` (8 vCPU / 16 GB).
|
||||
2. It mounts the Windows ISO and a tiny `PROVISION` ISO containing
|
||||
`cd/autounattend.xml` and `cd/scripts/oobe-enable-winrm.ps1`.
|
||||
3. Windows Setup performs an unattended install (Pro edition, generic
|
||||
activation key) and creates user `vagrant`:`vagrant`.
|
||||
4. FirstLogonCommands enable WinRM (HTTP, basic auth) and set
|
||||
`LocalAccountTokenFilterPolicy=1` so remote admin tokens are not filtered.
|
||||
5. Packer connects over WinRM and runs the provisioners in `scripts/`:
|
||||
- `configure-os.ps1` — UAC token policy, DiagTrack off, NTP resync.
|
||||
- `disable-windows-updates.ps1` — fully disables WU and friends.
|
||||
- `disable-defender-cloud.ps1` — disables MAPS / cloud lookups.
|
||||
- `install-vagrant-key.ps1` — drops the vagrant insecure public key.
|
||||
- `compact.ps1` — cleans caches, zeros free space.
|
||||
6. Packer shuts the guest down cleanly and the `vagrant` post-processor
|
||||
packages the VHDX into `bootstrap-win11.box`.
|
||||
|
||||
## Register the resulting box with Vagrant
|
||||
|
||||
```powershell
|
||||
vagrant box add bootstrap-win11 .\bootstrap-win11.box
|
||||
```
|
||||
|
||||
Then in `test/integration/Vagrantfile` set:
|
||||
|
||||
```powershell
|
||||
$env:BOOTSTRAP_BOX = "bootstrap-win11"
|
||||
go run ..\..\test\integration
|
||||
```
|
||||
|
||||
(or just edit the default in the Vagrantfile to `bootstrap-win11`).
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
test/integration/packer/
|
||||
├── README.md ← this file
|
||||
├── .env.example ← template; copy to .env (gitignored)
|
||||
├── load-env.ps1 ← dot-source to export PKR_VAR_* into shell
|
||||
├── install-packer.ps1 ← elevated installer for packer.exe
|
||||
├── windows-11.pkr.hcl ← Packer HCL2 build definition
|
||||
├── vagrantfile-template.rb ← baked into the output box
|
||||
├── cd/
|
||||
│ ├── autounattend.xml ← Windows Setup answer file
|
||||
│ └── scripts/
|
||||
│ └── oobe-enable-winrm.ps1
|
||||
└── scripts/ ← provisioners run after WinRM is up
|
||||
├── configure-os.ps1
|
||||
├── disable-windows-updates.ps1
|
||||
├── disable-defender-cloud.ps1
|
||||
├── install-vagrant-key.ps1
|
||||
└── compact.ps1
|
||||
```
|
||||
|
||||
## Notes and gotchas
|
||||
|
||||
- **Run elevated.** `packer build` opens Hyper-V management APIs; non-elevated
|
||||
shells fail with confusing "access denied" messages midway through.
|
||||
- **First build is slow.** Windows Setup + updates trimming + cipher /w on a
|
||||
60 GB volume can take 45–90 min. Subsequent rebuilds reuse the parent VHDX
|
||||
via linked clone and are much faster.
|
||||
- **License compliance.** The box uses the public KMS client setup key
|
||||
(W269N-WFGWX-YVC9B-4J6C9-T83GX) to pick the Pro edition during install. The
|
||||
resulting VM is not activated; for short-lived integration runs the eval
|
||||
period is more than sufficient, but redistribute the box only under your
|
||||
own licensing terms.
|
||||
- **25H2 OOBE.** The autounattend sets `BypassNRO=1` during `specialize`
|
||||
because 24H2/25H2 removed the `BypassNRO.cmd` helper. If a future Windows
|
||||
update changes the OOBE flow again, the symptom will be a hung
|
||||
`vagrant up` waiting for WinRM; check by opening Hyper-V Manager and
|
||||
looking for an OOBE screen on the VM console.
|
||||
- **Secure Boot.** Enabled (`MicrosoftWindows` template). Switch off in the
|
||||
Packer source if you ever need to install unsigned kernel-mode drivers in
|
||||
the box.
|
||||
@@ -0,0 +1,200 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Unattend file for Windows 11 25H2 (Pro), Hyper-V Gen 2, UEFI + Secure Boot.
|
||||
|
||||
Goals:
|
||||
- Fully unattended install on the only VHDX presented by the VM.
|
||||
- Create local user `vagrant` (password `vagrant`) in Administrators.
|
||||
- Skip every OOBE wizard page (including the network-required screen).
|
||||
- Bring WinRM online with basic auth and unencrypted transport so Packer
|
||||
and Vagrant can talk to the guest.
|
||||
|
||||
The BypassNRO trick is set in `specialize` rather than relying on the
|
||||
removed BypassNRO.cmd script, so this works on 24H2/25H2.
|
||||
-->
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||
|
||||
<settings pass="windowsPE">
|
||||
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<SetupUILanguage>
|
||||
<UILanguage>en-US</UILanguage>
|
||||
</SetupUILanguage>
|
||||
<InputLocale>0409:00000409</InputLocale>
|
||||
<SystemLocale>en-US</SystemLocale>
|
||||
<UILanguage>en-US</UILanguage>
|
||||
<UserLocale>en-US</UserLocale>
|
||||
</component>
|
||||
|
||||
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<DiskConfiguration>
|
||||
<Disk wcm:action="add">
|
||||
<DiskID>0</DiskID>
|
||||
<WillWipeDisk>true</WillWipeDisk>
|
||||
<CreatePartitions>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Type>EFI</Type>
|
||||
<Size>300</Size>
|
||||
</CreatePartition>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Type>MSR</Type>
|
||||
<Size>128</Size>
|
||||
</CreatePartition>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Type>Primary</Type>
|
||||
<Extend>true</Extend>
|
||||
</CreatePartition>
|
||||
</CreatePartitions>
|
||||
<ModifyPartitions>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<PartitionID>1</PartitionID>
|
||||
<Format>FAT32</Format>
|
||||
<Label>System</Label>
|
||||
</ModifyPartition>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<PartitionID>2</PartitionID>
|
||||
</ModifyPartition>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<PartitionID>3</PartitionID>
|
||||
<Format>NTFS</Format>
|
||||
<Label>Windows</Label>
|
||||
<Letter>C</Letter>
|
||||
</ModifyPartition>
|
||||
</ModifyPartitions>
|
||||
</Disk>
|
||||
</DiskConfiguration>
|
||||
|
||||
<ImageInstall>
|
||||
<OSImage>
|
||||
<InstallTo>
|
||||
<DiskID>0</DiskID>
|
||||
<PartitionID>3</PartitionID>
|
||||
</InstallTo>
|
||||
<InstallFrom>
|
||||
<MetaData wcm:action="add">
|
||||
<Key>/IMAGE/NAME</Key>
|
||||
<Value>Windows 11 Pro</Value>
|
||||
</MetaData>
|
||||
</InstallFrom>
|
||||
</OSImage>
|
||||
</ImageInstall>
|
||||
|
||||
<UserData>
|
||||
<!-- KMS client setup key for Windows 11 Pro; lets the installer pick
|
||||
the right edition without prompting. Replaced at activation. -->
|
||||
<ProductKey>
|
||||
<Key>W269N-WFGWX-YVC9B-4J6C9-T83GX</Key>
|
||||
<WillShowUI>OnError</WillShowUI>
|
||||
</ProductKey>
|
||||
<AcceptEula>true</AcceptEula>
|
||||
<FullName>vagrant</FullName>
|
||||
<Organization>vagrant</Organization>
|
||||
</UserData>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<settings pass="specialize">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<ComputerName>bootstrap-win11</ComputerName>
|
||||
<TimeZone>UTC</TimeZone>
|
||||
</component>
|
||||
|
||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<RunSynchronous>
|
||||
<!-- BypassNRO: allows OOBE without a network/Microsoft account.
|
||||
24H2/25H2 removed the BypassNRO.cmd helper, so we set the
|
||||
registry key directly during specialize. -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 1 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
<!-- Allow WinRM remote-elevated tokens (#3 mitigation). -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||
<NetworkLocation>Work</NetworkLocation>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
<SkipMachineOOBE>true</SkipMachineOOBE>
|
||||
<SkipUserOOBE>true</SkipUserOOBE>
|
||||
</OOBE>
|
||||
|
||||
<UserAccounts>
|
||||
<LocalAccounts>
|
||||
<LocalAccount wcm:action="add">
|
||||
<Name>vagrant</Name>
|
||||
<Group>Administrators</Group>
|
||||
<DisplayName>vagrant</DisplayName>
|
||||
<Description>Vagrant Box Build</Description>
|
||||
<Password>
|
||||
<Value>vagrant</Value>
|
||||
<PlainText>true</PlainText>
|
||||
</Password>
|
||||
</LocalAccount>
|
||||
</LocalAccounts>
|
||||
</UserAccounts>
|
||||
|
||||
<AutoLogon>
|
||||
<Enabled>true</Enabled>
|
||||
<Username>vagrant</Username>
|
||||
<LogonCount>3</LogonCount>
|
||||
<Password>
|
||||
<Value>vagrant</Value>
|
||||
<PlainText>true</PlainText>
|
||||
</Password>
|
||||
</AutoLogon>
|
||||
|
||||
<FirstLogonCommands>
|
||||
<!-- Allow PowerShell scripts unsigned (machine scope). -->
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy Bypass -Force"</CommandLine>
|
||||
<Description>Set ExecutionPolicy Bypass</Description>
|
||||
</SynchronousCommand>
|
||||
<!-- Enable WinRM inline so we don't depend on the PROVISION ISO
|
||||
still being mounted at first logon (Windows Setup sometimes
|
||||
detaches secondary DVDs after install). Mirrors the contents
|
||||
of scripts/oobe-enable-winrm.ps1 (now unused) and writes a
|
||||
transcript to C:\Windows\Temp\firstlogon.log for debugging. -->
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<CommandLine>cmd.exe /c winrm quickconfig -force -q > C:\Windows\Temp\firstlogon.log 2>&1</CommandLine>
|
||||
<Description>WinRM quickconfig</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Set-NetConnectionProfile -NetworkCategory Private -ErrorAction SilentlyContinue; Set-Item WSMan:\localhost\Service\Auth\Basic $true; Set-Item WSMan:\localhost\Service\AllowUnencrypted $true; Set-Item WSMan:\localhost\Service\MaxConcurrentOperationsPerUser 12000; Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 1024" >> C:\Windows\Temp\firstlogon.log 2>&1</CommandLine>
|
||||
<Description>Configure WinRM auth and limits</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "New-NetFirewallRule -Name WinRM-HTTP -DisplayName 'WinRM HTTP' -Protocol TCP -LocalPort 5985 -Action Allow -Profile Any -ErrorAction SilentlyContinue" >> C:\Windows\Temp\firstlogon.log 2>&1</CommandLine>
|
||||
<Description>Open firewall for WinRM HTTP</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<CommandLine>cmd.exe /c sc config WinRM start= auto && net stop WinRM && net start WinRM >> C:\Windows\Temp\firstlogon.log 2>&1</CommandLine>
|
||||
<Description>Restart WinRM service</Description>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
|
||||
<TimeZone>UTC</TimeZone>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
@@ -0,0 +1,206 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Installs HashiCorp Packer on Windows by downloading the release zip,
|
||||
extracting it to Program Files, and adding that directory to the system
|
||||
PATH.
|
||||
|
||||
.DESCRIPTION
|
||||
Winget does not publish HashiCorp Packer, so this helper does the install
|
||||
by hand. It is idempotent: if the requested version is already installed
|
||||
at the target location it just re-asserts the PATH entry and exits.
|
||||
|
||||
Requires an elevated PowerShell session (writes under Program Files and
|
||||
modifies the machine-scoped PATH environment variable).
|
||||
|
||||
.PARAMETER Version
|
||||
Packer release to install (e.g. "1.15.3"). The download URL is computed
|
||||
as https://releases.hashicorp.com/packer/<Version>/packer_<Version>_windows_amd64.zip.
|
||||
|
||||
.PARAMETER InstallRoot
|
||||
Directory under which packer.exe is placed. Defaults to
|
||||
"$env:ProgramFiles\Packer". This directory is what gets added to the
|
||||
system PATH.
|
||||
|
||||
.PARAMETER SkipOscdimg
|
||||
Skip installing the Windows ADK Deployment Tools (which provide
|
||||
oscdimg.exe). Packer's hyperv-iso builder needs an ISO-creation tool
|
||||
(xorriso, mkisofs, hdiutil, or oscdimg) on PATH when using cd_files,
|
||||
so by default this script also installs oscdimg via the Windows ADK
|
||||
bootstrapper with only the DeploymentTools feature selected (~70 MB
|
||||
on disk).
|
||||
|
||||
.PARAMETER AdkSetupUrl
|
||||
Override the URL used to download the Windows ADK setup bootstrapper.
|
||||
Defaults to Microsoft's stable fwlink for the Windows 11 ADK.
|
||||
|
||||
.EXAMPLE
|
||||
# Install the default version into "C:\Program Files\Packer".
|
||||
.\install-packer.ps1
|
||||
|
||||
.EXAMPLE
|
||||
# Install a specific version.
|
||||
.\install-packer.ps1 -Version 1.14.0
|
||||
|
||||
.EXAMPLE
|
||||
# Install Packer only; skip the Windows ADK Deployment Tools install.
|
||||
.\install-packer.ps1 -SkipOscdimg
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Version = '1.15.3',
|
||||
[string] $InstallRoot = (Join-Path $env:ProgramFiles 'Packer'),
|
||||
[switch] $SkipOscdimg,
|
||||
[string] $AdkSetupUrl = 'https://go.microsoft.com/fwlink/?linkid=2289980'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-Elevated {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw "install-packer.ps1 must be run from an elevated PowerShell session (writes to Program Files and the machine PATH)."
|
||||
}
|
||||
}
|
||||
|
||||
function Get-InstalledPackerVersion {
|
||||
param([string] $Root)
|
||||
$exe = Join-Path $Root 'packer.exe'
|
||||
if (-not (Test-Path -LiteralPath $exe)) { return $null }
|
||||
try {
|
||||
$output = & $exe version 2>$null
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
foreach ($line in $output) {
|
||||
if ($line -match 'Packer\s+v?([0-9]+\.[0-9]+\.[0-9]+)') {
|
||||
return $Matches[1]
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Add-ToMachinePath {
|
||||
param([string] $Directory)
|
||||
$current = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$parts = @()
|
||||
if ($current) {
|
||||
$parts = $current.Split(';') | Where-Object { $_ -ne '' }
|
||||
}
|
||||
if ($parts -contains $Directory) {
|
||||
Write-Host "system PATH already contains $Directory"
|
||||
return
|
||||
}
|
||||
$newPath = ($parts + $Directory) -join ';'
|
||||
[Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine')
|
||||
# Make the change visible in the current session as well so the caller
|
||||
# can immediately run `packer` without opening a new shell.
|
||||
if (-not (($env:Path -split ';') -contains $Directory)) {
|
||||
$env:Path = "$env:Path;$Directory"
|
||||
}
|
||||
Write-Host "added $Directory to the system PATH (machine scope)."
|
||||
}
|
||||
|
||||
function Get-OscdimgPath {
|
||||
$candidates = @(
|
||||
(Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe'),
|
||||
(Join-Path ${env:ProgramFiles} 'Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe')
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if ($c -and (Test-Path -LiteralPath $c)) { return $c }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Install-Oscdimg {
|
||||
param([string] $SetupUrl)
|
||||
|
||||
$existing = Get-OscdimgPath
|
||||
if ($existing) {
|
||||
Write-Host "oscdimg.exe already present at $existing"
|
||||
Add-ToMachinePath -Directory (Split-Path -Parent $existing)
|
||||
return
|
||||
}
|
||||
|
||||
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("install-adk-" + [Guid]::NewGuid())
|
||||
$setup = Join-Path $tmp 'adksetup.exe'
|
||||
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
|
||||
try {
|
||||
Write-Host "downloading Windows ADK bootstrapper from $SetupUrl"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $SetupUrl -OutFile $setup
|
||||
|
||||
Write-Host "installing Windows ADK Deployment Tools (this may take several minutes)"
|
||||
# /features OptionId.DeploymentTools => oscdimg only (~70 MB)
|
||||
# /quiet /norestart /ceip off => unattended, no telemetry prompt
|
||||
$args = @('/features','OptionId.DeploymentTools','/quiet','/norestart','/ceip','off')
|
||||
$proc = Start-Process -FilePath $setup -ArgumentList $args -Wait -PassThru
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
throw "Windows ADK setup exited with code $($proc.ExitCode)."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$installed = Get-OscdimgPath
|
||||
if (-not $installed) {
|
||||
throw "Windows ADK setup completed but oscdimg.exe was not found in the expected location."
|
||||
}
|
||||
Write-Host "installed oscdimg.exe at $installed"
|
||||
Add-ToMachinePath -Directory (Split-Path -Parent $installed)
|
||||
}
|
||||
|
||||
Assert-Elevated
|
||||
|
||||
# Idempotency: if the target version is already installed, just make sure the
|
||||
# PATH entry is in place and return.
|
||||
$existing = Get-InstalledPackerVersion -Root $InstallRoot
|
||||
if ($existing -eq $Version) {
|
||||
Write-Host "packer $Version already installed at $InstallRoot."
|
||||
Add-ToMachinePath -Directory $InstallRoot
|
||||
if (-not $SkipOscdimg) { Install-Oscdimg -SetupUrl $AdkSetupUrl }
|
||||
return
|
||||
}
|
||||
if ($existing) {
|
||||
Write-Host "replacing packer $existing at $InstallRoot with $Version."
|
||||
}
|
||||
|
||||
$url = "https://releases.hashicorp.com/packer/$Version/packer_${Version}_windows_amd64.zip"
|
||||
$tmpRoot = Join-Path ([IO.Path]::GetTempPath()) ("install-packer-" + [Guid]::NewGuid())
|
||||
$zipPath = Join-Path $tmpRoot "packer_${Version}.zip"
|
||||
|
||||
New-Item -ItemType Directory -Path $tmpRoot -Force | Out-Null
|
||||
try {
|
||||
Write-Host "downloading $url"
|
||||
# TLS 1.2 is required to talk to releases.hashicorp.com on stock
|
||||
# Windows 10/Server 2019 PowerShell sessions.
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $zipPath
|
||||
|
||||
Write-Host "extracting to $tmpRoot"
|
||||
Expand-Archive -LiteralPath $zipPath -DestinationPath $tmpRoot -Force
|
||||
|
||||
$srcExe = Join-Path $tmpRoot 'packer.exe'
|
||||
if (-not (Test-Path -LiteralPath $srcExe)) {
|
||||
throw "packer.exe not found inside $zipPath after extraction."
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
|
||||
Copy-Item -LiteralPath $srcExe -Destination (Join-Path $InstallRoot 'packer.exe') -Force
|
||||
Write-Host "installed packer.exe to $InstallRoot"
|
||||
|
||||
Add-ToMachinePath -Directory $InstallRoot
|
||||
|
||||
$installed = Get-InstalledPackerVersion -Root $InstallRoot
|
||||
if ($installed -ne $Version) {
|
||||
Write-Warning "post-install version check returned '$installed' (expected $Version)."
|
||||
} else {
|
||||
Write-Host "verified: packer $installed available at $(Join-Path $InstallRoot 'packer.exe')."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (-not $SkipOscdimg) { Install-Oscdimg -SetupUrl $AdkSetupUrl }
|
||||
@@ -0,0 +1,34 @@
|
||||
# Loads variables from .env (in this directory) into the current PowerShell
|
||||
# session as environment variables. Dot-source it so the variables stick:
|
||||
#
|
||||
# . .\load-env.ps1
|
||||
#
|
||||
# Format:
|
||||
# - One KEY=VALUE per line.
|
||||
# - Lines starting with '#' and blank lines are ignored.
|
||||
# - Values are taken verbatim (no shell quoting / interpolation). Surround
|
||||
# with quotes only if you actually want quotes in the value.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$envFile = Join-Path $PSScriptRoot '.env'
|
||||
if (-not (Test-Path -LiteralPath $envFile)) {
|
||||
throw ".env not found at $envFile. Copy .env.example to .env and fill in PKR_VAR_iso_path."
|
||||
}
|
||||
|
||||
$loaded = 0
|
||||
foreach ($line in Get-Content -LiteralPath $envFile) {
|
||||
$trimmed = $line.Trim()
|
||||
if ($trimmed -eq '' -or $trimmed.StartsWith('#')) { continue }
|
||||
$idx = $trimmed.IndexOf('=')
|
||||
if ($idx -lt 1) {
|
||||
Write-Warning "skipping malformed line: $line"
|
||||
continue
|
||||
}
|
||||
$name = $trimmed.Substring(0, $idx).Trim()
|
||||
$value = $trimmed.Substring($idx + 1)
|
||||
[Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||
$loaded++
|
||||
}
|
||||
|
||||
Write-Host "loaded $loaded variable(s) from $envFile into the current session."
|
||||
@@ -0,0 +1,78 @@
|
||||
Warning: A checksum of 'none' was specified. Since ISO files are so big,
|
||||
a checksum is highly recommended.
|
||||
|
||||
on .\windows-11.pkr.hcl line 83:
|
||||
(source code not available)
|
||||
|
||||
|
||||
hyperv-iso.win11: output will be in this color.
|
||||
|
||||
==> hyperv-iso.win11: Creating build directory...
|
||||
==> hyperv-iso.win11: Retrieving ISO
|
||||
==> hyperv-iso.win11: Trying C:/Users/jasonross/Downloads/Win11_25H2_English_x64_v2(1).iso
|
||||
==> hyperv-iso.win11: Trying file://C:/Users/jasonross/Downloads/Win11_25H2_English_x64_v2%281%29.iso
|
||||
==> hyperv-iso.win11: file://C:/Users/jasonross/Downloads/Win11_25H2_English_x64_v2%281%29.iso => C:/Users/jasonross/Downloads/Win11_25H2_English_x64_v2(1).iso
|
||||
==> hyperv-iso.win11: Creating switch 'Default Switch' if required...
|
||||
==> hyperv-iso.win11: switch 'Default Switch' already exists. Will not delete on cleanup...
|
||||
==> hyperv-iso.win11: Creating virtual machine...
|
||||
==> hyperv-iso.win11: Enabling Integration Service...
|
||||
==> hyperv-iso.win11: Setting boot drive to os dvd drive C:/Users/jasonross/Downloads/Win11_25H2_English_x64_v2(1).iso ...
|
||||
==> hyperv-iso.win11: Mounting os dvd drive C:/Users/jasonross/Downloads/Win11_25H2_English_x64_v2(1).iso ...
|
||||
==> hyperv-iso.win11: Skipping mounting Integration Services Setup Disk...
|
||||
==> hyperv-iso.win11: Creating CD disk...
|
||||
==> hyperv-iso.win11: OSCDIMG 2.56 CD-ROM and DVD-ROM Premastering Utility
|
||||
==> hyperv-iso.win11: Copyright (C) Microsoft, 1993-2012. All rights reserved.
|
||||
==> hyperv-iso.win11: Licensed only for producing Microsoft authorized content.
|
||||
==> hyperv-iso.win11: Scanning source tree
|
||||
==> hyperv-iso.win11: Scanning source tree complete (2 files in 1 directories)
|
||||
==> hyperv-iso.win11: Computing directory information complete
|
||||
==> hyperv-iso.win11: Image file is 61440 bytes (before optimization)
|
||||
==> hyperv-iso.win11: Writing 2 files in 1 directories to C:\Users\jasonross\AppData\Local\Temp\packer2666332811.iso
|
||||
==> hyperv-iso.win11: Storage optimization saved 0 files, 0 bytes (0% of image)
|
||||
==> hyperv-iso.win11: After optimization, image file is 61440 bytes
|
||||
==> hyperv-iso.win11: Done.
|
||||
==> hyperv-iso.win11: 100% complete
|
||||
==> hyperv-iso.win11: Done copying paths from CD_dirs
|
||||
==> hyperv-iso.win11: Mounting secondary DVD images...
|
||||
==> hyperv-iso.win11: Mounting secondary dvd drive C:\Users\jasonross\AppData\Local\Temp\packer2666332811.iso ...
|
||||
==> hyperv-iso.win11: Configuring vlan...
|
||||
==> hyperv-iso.win11: Determine Host IP for HyperV machine...
|
||||
==> hyperv-iso.win11: Host IP for the HyperV machine: 172.25.16.1
|
||||
==> hyperv-iso.win11: Attempting to connect with vmconnect...
|
||||
==> hyperv-iso.win11: Starting the virtual machine...
|
||||
==> hyperv-iso.win11: Waiting 1s for boot...
|
||||
==> hyperv-iso.win11: Typing the boot command...
|
||||
==> hyperv-iso.win11: Waiting for WinRM to become available...
|
||||
==> hyperv-iso.win11: WinRM connected.
|
||||
==> hyperv-iso.win11: Connected to WinRM!
|
||||
==> hyperv-iso.win11: Provisioning with Powershell...
|
||||
==> hyperv-iso.win11: Provisioning with powershell script: ./scripts/configure-os.ps1
|
||||
==> hyperv-iso.win11: Script exited with non-zero exit status: 2147943462. Allowed exit codes are: [0]
|
||||
==> hyperv-iso.win11: Step "StepProvision" failed, aborting...
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepConnect"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepTypeBootCommand"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepRun"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepSetFirstBootDevice"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepSetBootOrder"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepConfigureVlan"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepMountSecondaryDvdImages"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepCreateCD"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepMountGuestAdditions"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepMountFloppydrive"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepMountDvdDrive"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepEnableIntegrationService"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepCreateVM"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepCreateSwitch"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepHTTPServer"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepCreateFloppy"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepDownload"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepOutputDir"
|
||||
==> hyperv-iso.win11: aborted: skipping cleanup of step "StepCreateBuildDir"
|
||||
Build 'hyperv-iso.win11' errored after 16 minutes 59 seconds: Script exited with non-zero exit status: 2147943462. Allowed exit codes are: [0]
|
||||
|
||||
==> Wait completed after 16 minutes 59 seconds
|
||||
|
||||
==> Some builds didn't complete successfully and had errors:
|
||||
--> hyperv-iso.win11: Script exited with non-zero exit status: 2147943462. Allowed exit codes are: [0]
|
||||
|
||||
==> Builds finished but no artifacts were created.
|
||||
@@ -0,0 +1 @@
|
||||
51192
|
||||
@@ -0,0 +1,25 @@
|
||||
# Zero out free space so the resulting .box file is small.
|
||||
#
|
||||
# Runs last, just before Packer initiates shutdown. Cleans caches and the WU
|
||||
# component store, which directly shrinks the on-disk footprint of the VHDX.
|
||||
# We intentionally do not call `cipher /w:C:\` here: it costs 15–30 minutes
|
||||
# and its only payoff is better tarball compression of unused space, which
|
||||
# only matters when shipping the box. Re-add it before publishing publicly.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# Clean Windows Update download cache and component store.
|
||||
Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "$env:WINDIR\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
& dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase /Quiet
|
||||
|
||||
# Empty Recycle Bin and temp dirs.
|
||||
Get-ChildItem -Path "$env:TEMP" -Force -ErrorAction SilentlyContinue |
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Clear-RecycleBin -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Light defrag pass to consolidate free extents. Quick on an SSD-backed VHDX.
|
||||
& defrag C: /U /V
|
||||
|
||||
# Drop hibernation file (Windows 11 has it on by default).
|
||||
& powercfg /h off
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generic OS tweaks applied after WinRM is reachable.
|
||||
#
|
||||
# Idempotent; safe to rerun.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Make sure the remote-elevated token policy survives any unattended changes.
|
||||
$policy = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||
New-ItemProperty -Path $policy -Name 'LocalAccountTokenFilterPolicy' `
|
||||
-PropertyType DWord -Value 1 -Force | Out-Null
|
||||
|
||||
# Disable telemetry "Connected User Experiences" service to avoid background
|
||||
# network noise that competes with the bootstrapper.
|
||||
$svc = Get-Service -Name DiagTrack -ErrorAction SilentlyContinue
|
||||
if ($svc) {
|
||||
Set-Service -Name DiagTrack -StartupType Disabled
|
||||
Stop-Service -Name DiagTrack -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Disable SmartScreen network checks (slows winget installs in the lab).
|
||||
$ss = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||
New-Item -Path $ss -Force | Out-Null
|
||||
New-ItemProperty -Path $ss -Name 'EnableSmartScreen' -PropertyType DWord -Value 0 -Force | Out-Null
|
||||
|
||||
# Force NTP resync; Hyper-V time-sync after sysprep can drift, which breaks
|
||||
# TLS handshakes against winget / GitHub.
|
||||
Start-Service -Name w32time -ErrorAction SilentlyContinue
|
||||
& w32tm /resync /force 2>&1 | Out-Null
|
||||
@@ -0,0 +1,12 @@
|
||||
# Turn off Defender cloud-delivered protection and sample submission.
|
||||
#
|
||||
# Cloud lookups add tens of seconds per winget install on a cold box and have
|
||||
# no value in an ephemeral integration VM.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
Set-MpPreference -MAPSReporting Disabled -ErrorAction SilentlyContinue
|
||||
Set-MpPreference -SubmitSamplesConsent NeverSend -ErrorAction SilentlyContinue
|
||||
Set-MpPreference -DisableBlockAtFirstSeen $true -ErrorAction SilentlyContinue
|
||||
Set-MpPreference -DisableIOAVProtection $true -ErrorAction SilentlyContinue
|
||||
Set-MpPreference -ScanAvgCPULoadFactor 5 -ErrorAction SilentlyContinue
|
||||
@@ -0,0 +1,26 @@
|
||||
# Disable Windows Update on the gold image so it doesn't fight with the
|
||||
# bootstrapper's own WindowsUpdateCommand step at integration-test time.
|
||||
#
|
||||
# This is the precise mitigation for the 6-hour CI hang investigation: by
|
||||
# the time the bootstrapper runs, no other process is holding the WU agent.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$au = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||
New-Item -Path $au -Force | Out-Null
|
||||
New-ItemProperty -Path $au -Name 'NoAutoUpdate' -PropertyType DWord -Value 1 -Force | Out-Null
|
||||
New-ItemProperty -Path $au -Name 'AUOptions' -PropertyType DWord -Value 1 -Force | Out-Null
|
||||
New-ItemProperty -Path $au -Name 'ScheduledInstallDay' -PropertyType DWord -Value 0 -Force | Out-Null
|
||||
New-ItemProperty -Path $au -Name 'NoAutoRebootWithLoggedOnUsers' -PropertyType DWord -Value 1 -Force | Out-Null
|
||||
|
||||
$wu = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
|
||||
New-Item -Path $wu -Force | Out-Null
|
||||
New-ItemProperty -Path $wu -Name 'DoNotConnectToWindowsUpdateInternetLocations' -PropertyType DWord -Value 1 -Force | Out-Null
|
||||
|
||||
foreach ($svc in 'wuauserv','UsoSvc','WaaSMedicSvc') {
|
||||
$s = Get-Service -Name $svc -ErrorAction SilentlyContinue
|
||||
if ($s) {
|
||||
Set-Service -Name $svc -StartupType Disabled -ErrorAction SilentlyContinue
|
||||
Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Install the Vagrant insecure SSH key marker so `vagrant ssh-config` and
|
||||
# tooling that expects the Vagrant-flavoured user account is happy. WinRM is
|
||||
# the actual communicator, but several Vagrant plugins probe for this layout.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$sshDir = "$env:USERPROFILE\.ssh"
|
||||
New-Item -ItemType Directory -Path $sshDir -Force | Out-Null
|
||||
|
||||
$keyUrl = 'https://raw.githubusercontent.com/hashicorp/vagrant/main/keys/vagrant.pub'
|
||||
try {
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $keyUrl -OutFile "$sshDir\authorized_keys"
|
||||
} catch {
|
||||
Write-Host "warning: could not fetch vagrant insecure public key: $($_.Exception.Message)"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Vagrantfile fragment baked into the output .box file by the
|
||||
# `packer-post-processor-vagrant` plugin. This is what every `vagrant up`
|
||||
# of a derived box inherits before our top-level Vagrantfile is applied.
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.communicator = "winrm"
|
||||
config.vm.guest = :windows
|
||||
config.winrm.username = "vagrant"
|
||||
config.winrm.password = "vagrant"
|
||||
config.winrm.transport = :plaintext
|
||||
config.winrm.basic_auth_only = true
|
||||
config.vm.boot_timeout = 1800
|
||||
|
||||
config.vm.provider "hyperv" do |hv|
|
||||
hv.enable_virtualization_extensions = false
|
||||
hv.linked_clone = true
|
||||
hv.vm_integration_services = {
|
||||
guest_service_interface: true,
|
||||
heartbeat: true,
|
||||
shutdown: true,
|
||||
time_synchronization: true,
|
||||
vss: true,
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,157 @@
|
||||
packer {
|
||||
required_plugins {
|
||||
hyperv = {
|
||||
version = ">= 1.1.4"
|
||||
source = "github.com/hashicorp/hyperv"
|
||||
}
|
||||
vagrant = {
|
||||
version = ">= 1.1.5"
|
||||
source = "github.com/hashicorp/vagrant"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Inputs ───────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "iso_path" {
|
||||
type = string
|
||||
description = "Absolute path to the Windows 11 installer ISO. Set via PKR_VAR_iso_path (load .env first; see README)."
|
||||
}
|
||||
|
||||
variable "iso_checksum" {
|
||||
type = string
|
||||
description = "Checksum of the ISO (e.g. 'sha256:<hex>'); 'none' skips validation. Set via PKR_VAR_iso_checksum."
|
||||
default = "none"
|
||||
}
|
||||
|
||||
variable "vm_name" {
|
||||
type = string
|
||||
default = "bootstrap-win11"
|
||||
}
|
||||
|
||||
variable "output_directory" {
|
||||
type = string
|
||||
default = "output-bootstrap-win11"
|
||||
}
|
||||
|
||||
variable "box_output" {
|
||||
type = string
|
||||
default = "bootstrap-win11.box"
|
||||
}
|
||||
|
||||
variable "cpus" {
|
||||
type = number
|
||||
default = 24
|
||||
description = "vCPUs allocated to the build VM. The Packer guest is throwaway and gets the full host budget for speed; the runtime integration VM keeps a smaller footprint in test/integration/Vagrantfile."
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
type = number
|
||||
default = 32768
|
||||
description = "Build-VM memory (MB). Throwaway VM, full host budget."
|
||||
}
|
||||
|
||||
variable "disk_size" {
|
||||
type = number
|
||||
default = 81920
|
||||
description = "Guest disk size in MB."
|
||||
}
|
||||
|
||||
variable "switch_name" {
|
||||
type = string
|
||||
default = "Default Switch"
|
||||
}
|
||||
|
||||
variable "winrm_username" {
|
||||
type = string
|
||||
default = "vagrant"
|
||||
}
|
||||
|
||||
variable "winrm_password" {
|
||||
type = string
|
||||
default = "vagrant"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# ─── Source ───────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Hyper-V Gen 2 + Secure Boot is required for modern Windows 11. The
|
||||
# unattend payload lives on a small ISO that Packer builds on the fly from
|
||||
# `cd_files` and attaches as a secondary drive; Windows Setup auto-discovers
|
||||
# `autounattend.xml` on any attached media at the root.
|
||||
|
||||
source "hyperv-iso" "win11" {
|
||||
vm_name = var.vm_name
|
||||
iso_url = var.iso_path
|
||||
iso_checksum = var.iso_checksum
|
||||
|
||||
cpus = var.cpus
|
||||
memory = var.memory
|
||||
disk_size = var.disk_size
|
||||
|
||||
generation = 2
|
||||
enable_secure_boot = true
|
||||
secure_boot_template = "MicrosoftWindows"
|
||||
enable_tpm = true
|
||||
enable_dynamic_memory = false
|
||||
enable_virtualization_extensions = false
|
||||
guest_additions_mode = "disable"
|
||||
switch_name = var.switch_name
|
||||
|
||||
output_directory = var.output_directory
|
||||
|
||||
communicator = "winrm"
|
||||
winrm_username = var.winrm_username
|
||||
winrm_password = var.winrm_password
|
||||
winrm_timeout = "2h"
|
||||
winrm_use_ssl = false
|
||||
winrm_insecure = true
|
||||
|
||||
# Gen 2 UEFI shows "Press any key to boot from CD or DVD..." for ~5s.
|
||||
# Default boot_wait is 10s (already past the prompt) and default
|
||||
# boot_command is empty, so without these the firmware falls through to
|
||||
# PXE and Windows Setup never starts. Hammer ENTER early to catch it.
|
||||
boot_wait = "1s"
|
||||
boot_command = ["<enter><wait><enter><wait><enter>"]
|
||||
|
||||
shutdown_command = "shutdown /s /t 10 /f /d p:4:1 /c \"Packer Shutdown\""
|
||||
shutdown_timeout = "30m"
|
||||
|
||||
# Build a tiny "PROVISION" ISO containing the unattend file. The
|
||||
# FirstLogonCommands in autounattend.xml enable WinRM inline (no helper
|
||||
# script needed) since Windows Setup may detach secondary DVDs after
|
||||
# install, leaving the PROVISION volume unreachable at first logon.
|
||||
cd_files = [
|
||||
"./cd/autounattend.xml",
|
||||
]
|
||||
cd_label = "PROVISION"
|
||||
}
|
||||
|
||||
# ─── Build ────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Provisioners run *after* WinRM is reachable, which is after OOBE has finished
|
||||
# and the autounattend's FirstLogonCommands have brought WinRM online. Each
|
||||
# script is small and single-purpose; see comments in each .ps1.
|
||||
|
||||
build {
|
||||
sources = ["source.hyperv-iso.win11"]
|
||||
|
||||
provisioner "powershell" {
|
||||
elevated_user = var.winrm_username
|
||||
elevated_password = var.winrm_password
|
||||
scripts = [
|
||||
"./scripts/configure-os.ps1",
|
||||
"./scripts/disable-windows-updates.ps1",
|
||||
"./scripts/disable-defender-cloud.ps1",
|
||||
"./scripts/install-vagrant-key.ps1",
|
||||
"./scripts/compact.ps1",
|
||||
]
|
||||
}
|
||||
|
||||
post-processor "vagrant" {
|
||||
output = var.box_output
|
||||
keep_input_artifact = false
|
||||
provider_override = "hyperv"
|
||||
vagrantfile_template = "./vagrantfile-template.rb"
|
||||
}
|
||||
}
|
||||
+1937
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user