163 lines
4.9 KiB
Go
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
|
|
} |