feat: add integration test infra and bootstrapper updates
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user