Files
fleetforge/agents/forge-agent/internal/pkgmanager/pkgmanager_windows.go
T

163 lines
4.9 KiB
Go

//go:build windows
package pkgmanager
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/sys/windows/registry"
)
func listWindowsPackages() ([]PackageInfo, error) {
seen := make(map[string]PackageInfo)
if err := enumerateRegistryUninstall(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil {
return nil, fmt.Errorf("enumerate current user uninstall failed: %w", err)
}
if err := enumerateRegistryUninstall(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil {
// non-fatal; 32-bit view may not exist
_ = err
}
if err := enumerateRegistryUninstall(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil {
_ = err
}
if _, err := exec.LookPath(resolveManagerPath("winget", interactiveProfile())); err == nil {
if wingetPkgs, err := listWingetPackages(); err == nil {
for _, p := range wingetPkgs {
key := p.Name + "@" + p.Manager
if _, ok := seen[key]; !ok {
seen[key] = p
}
}
}
}
packages := make([]PackageInfo, 0, len(seen))
for _, p := range seen {
packages = append(packages, p)
}
return packages, nil
}
func enumerateRegistryUninstall(root registry.Key, path string, seen map[string]PackageInfo) error {
k, err := registry.OpenKey(root, path, registry.READ)
if err != nil {
return err
}
defer k.Close()
subKeys, err := k.ReadSubKeyNames(0)
if err != nil {
return err
}
for _, name := range subKeys {
sk, err := registry.OpenKey(k, name, registry.READ)
if err != nil {
continue
}
displayName, _, err := sk.GetStringValue("DisplayName")
if err != nil || displayName == "" {
sk.Close()
continue
}
displayVersion, _, _ := sk.GetStringValue("DisplayVersion")
version := strings.TrimSpace(displayVersion)
if version == "" {
version = "unknown"
}
key := displayName + "@windows"
if _, ok := seen[key]; !ok {
seen[key] = PackageInfo{
Name: displayName,
Version: version,
Manager: "windows",
}
}
sk.Close()
}
return nil
}
func BootstrapManagers() error {
var errs []string
if !lookupWinget() {
if err := installWingetViaAppInstaller(); err != nil {
errs = append(errs, "winget: "+err.Error())
}
}
if _, err := exec.LookPath("choco"); err != nil {
if err := installChocolatey(); err != nil {
errs = append(errs, "chocolatey: "+err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("bootstrap failed: %s", strings.Join(errs, "; "))
}
return nil
}
func installChocolatey() error {
// Official Chocolatey install script (requires admin; agent runs as LocalSystem).
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
"Set-ExecutionPolicy Bypass -Scope Process -Force; "+
"[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; "+
"iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("chocolatey install script failed: %w\nOutput: %s", err, string(out))
}
return nil
}
func lookupWinget() bool {
if _, err := exec.LookPath(resolveManagerPath("winget", interactiveProfile())); err == nil {
return true
}
appInstaller := `Microsoft.DesktopAppInstaller_8wekyb3d8bbwe`
appxReg, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore`,
registry.READ|registry.ENUMERATE_SUB_KEYS)
if err == nil {
defer appxReg.Close()
if keys, err := appxReg.ReadSubKeyNames(0); err == nil {
for _, keyName := range keys {
if strings.Contains(keyName, "DesktopAppInstaller") {
_ = appInstaller
return true
}
}
}
}
return false
}
func installWingetViaAppInstaller() error {
// Microsoft App Installer download link (redirects to latest winget bundle)
installerURL := "https://aka.ms/getwinget"
appInstallerPath := filepath.Join(os.TempDir(), "DesktopAppInstaller.msixbundle")
// Download App Installer bundle
downloadCmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command",
"[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; "+
"Invoke-WebRequest -Uri '"+installerURL+"' -OutFile '"+appInstallerPath+"' -UseBasicParsing")
if out, err := downloadCmd.CombinedOutput(); err != nil {
return fmt.Errorf("download App Installer failed: %w\nOutput: %s", err, string(out))
}
// Install bundle (dependencies may need to be allowed; add-AppxPackage on msixbundle handles it)
installCmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command",
"Add-AppxPackage -Path '"+appInstallerPath+"' -ForceUpdateFromAnyVersion")
if out, err := installCmd.CombinedOutput(); err != nil {
return fmt.Errorf("install App Installer failed: %w\nOutput: %s", err, string(out))
}
return nil
}