Files
Vintner/internal/download/manifest.go
T
Cheviiot d11b534fa1 Stability pass: deterministic dependency order, retry backoff, input validation
Found via manual audit plus a staticcheck run:

- collectDependencyClosure iterated a package's dependencies map
  directly, so which package "won" a same-key collision (and the
  order things got downloaded/unpacked in) could vary between runs
  of the exact same download command. Sort the dependency targets
  first, matching what --print-deps-tree's tree-printer already did.
  Verified two consecutive --print-deps-tree runs now produce
  byte-identical output.
- HTTP retry loops (manifest fetch, payload download) retried
  immediately with no backoff, which just hammers a server harder
  during exactly the kind of transient failure retries exist for.
  Added a capped exponential backoff (1s/2s/4s/8s/10s).
- --architecture/--host-arch accepted any string silently; a typo'd
  value matched nothing during package selection and surfaced as a
  confusing downstream failure far from the actual mistake. Now
  rejected up front with a clear error.
- pumpLines' bufio.Scanner silently stops (dropping the rest of a
  tool's output) if a single line ever exceeds its buffer - narrow but
  real for pathological cases like heavily templated C++ diagnostics.
  Now at least reports that truncation happened instead of losing
  output with no trace.
- Removed select.go's unused off() helper (staticcheck U1000).

Re-verified end-to-end after these changes: a real KMDF driver build
and a plain cl/link build both still succeed.
2026-07-25 04:14:34 +10:00

287 lines
8.2 KiB
Go

// Package download fetches and unpacks MSVC/WinSDK using the same installer
// manifests Visual Studio's own installer uses.
package download
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// Payload is one downloadable file belonging to a Package.
type Payload struct {
FileName string `json:"fileName"`
URL string `json:"url"`
SHA256 string `json:"sha256"`
Size int64 `json:"size"`
}
// Name returns the payload's bare file name, stripping any directory
// components the manifest's fileName might carry.
func (p Payload) Name() string {
name := p.FileName
if i := strings.LastIndexByte(name, '\\'); i >= 0 {
name = name[i+1:]
}
if i := strings.LastIndexByte(name, '/'); i >= 0 {
name = name[i+1:]
}
return name
}
// Dependency is a normalized package dependency: manifests encode these
// either as a bare version string or as an object with version/type/id.
type Dependency struct {
TargetID string // the id to depend on (overrides the map key if set)
Version string
Type string // "", "Optional" or "Recommended"
}
// LocalizedResource carries a package's human-readable title/description
// (shown by --list-workloads/--list-components) and the license URL shown
// before accepting a package's terms.
type LocalizedResource struct {
Language string `json:"language"`
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
License string `json:"license"`
}
// Package is one entry from the installer manifest's "packages" array.
type Package struct {
ID string `json:"id"`
Type string `json:"type"`
Version string `json:"version"`
Chip string `json:"chip"`
MachineArch string `json:"machineArch"`
ProductArch string `json:"productArch"`
Language string `json:"language"`
Payloads []Payload `json:"payloads"`
InstallSizes map[string]int64 `json:"installSizes"`
LocalizedResources []LocalizedResource `json:"localizedResources"`
DependenciesRaw map[string]json.RawMessage `json:"dependencies"`
dependencies map[string]Dependency
}
// Dependencies lazily normalizes DependenciesRaw into Dependency values.
func (p *Package) Dependencies() map[string]Dependency {
if p.dependencies != nil {
return p.dependencies
}
p.dependencies = map[string]Dependency{}
for key, raw := range p.DependenciesRaw {
var version string
if err := json.Unmarshal(raw, &version); err == nil {
p.dependencies[key] = Dependency{Version: version}
continue
}
var d struct {
Version string `json:"version"`
Type string `json:"type"`
ID string `json:"id"`
}
if err := json.Unmarshal(raw, &d); err == nil {
p.dependencies[key] = Dependency{TargetID: d.ID, Version: d.Version, Type: d.Type}
}
}
return p.dependencies
}
// Key uniquely identifies a specific package variant (id + version + arch),
// used to dedupe an already-included package during dependency resolution
// and to name its cache directory. Mirrors getPackageKey.
func (p *Package) Key() string {
key := p.ID
if p.Version != "" {
key += "-" + p.Version
}
if p.Chip != "" {
key += "-chip." + p.Chip
}
if p.MachineArch != "" {
key += "-machineArch." + p.MachineArch
}
if p.ProductArch != "" {
key += "-productArch." + p.ProductArch
}
return key
}
// Localized returns p's LocalizedResource best matching language ("" means
// "en"), preferring an exact match, then any en-* entry, then whatever's
// first. Returns nil if p has no localized resources at all.
func (p *Package) Localized(language string) *LocalizedResource {
if len(p.LocalizedResources) == 0 {
return nil
}
if language == "" {
language = "en"
}
language = strings.ToLower(language)
best := &p.LocalizedResources[0]
bestScore := -1
for i := range p.LocalizedResources {
lr := &p.LocalizedResources[i]
lang := strings.ToLower(lr.Language)
score := 0
switch {
case lang == language:
score = 3
case strings.HasPrefix(lang, language+"-"):
score = 2
case strings.HasPrefix(lang, "en"):
score = 1
}
if score > bestScore {
bestScore = score
best = lr
}
}
return best
}
func (p *Package) InstalledSize() int64 {
var sum int64
for _, v := range p.InstallSizes {
sum += v
}
return sum
}
func (p *Package) DownloadSize() int64 {
var sum int64
for _, pl := range p.Payloads {
sum += pl.Size
}
return sum
}
// ChannelItem is one entry of a channel manifest's "channelItems", used only
// to locate the installer manifest URL.
type ChannelItem struct {
Type string `json:"type"`
Payloads []Payload `json:"payloads"`
}
// Manifest is the top-level installer manifest (or channel manifest, which
// shares the "info" field used for logging).
type Manifest struct {
Info struct {
ProductDisplayVersion string `json:"productDisplayVersion"`
} `json:"info"`
Packages []Package `json:"packages"`
ChannelItems []ChannelItem `json:"channelItems"`
}
// Manifests (particularly the installer manifest, a single JSON file
// listing every package) can be tens of MB, so give this a generous timeout
// and a few retries - transient network hiccups shouldn't need a full
// restart of `download`.
var httpClient = &http.Client{Timeout: 5 * time.Minute}
func init() {
// --manifest points at a local file, fetched through this same client
// via a "file:" URL (see cmd/vintner's runDownload) - so it needs a
// registered "file" handler alongside the default http/https transport.
t := http.DefaultTransport.(*http.Transport).Clone()
t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
httpClient.Transport = t
}
const maxManifestAttempts = 5
func httpGet(url string) ([]byte, error) {
var lastErr error
for attempt := 0; attempt < maxManifestAttempts; attempt++ {
if attempt > 0 {
time.Sleep(retryBackoff(attempt))
}
data, err := tryHTTPGet(url)
if err == nil {
return data, nil
}
lastErr = err
fmt.Printf("GET %s: %v (retrying)\n", url, err)
}
return nil, fmt.Errorf("GET %s: giving up after %d attempts: %w", url, maxManifestAttempts, lastErr)
}
func tryHTTPGet(url string) ([]byte, error) {
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s", resp.Status)
}
return io.ReadAll(resp.Body)
}
// FetchChannelManifest downloads the top-level channel manifest for the
// given major VS version ("18" and up use the "stable"/"insiders" channel
// naming, earlier ones use "release"/"pre"), and returns the URL of the
// installer manifest it references.
func FetchChannelManifest(major int, preview bool) (string, error) {
kind := "stable"
if major < 18 {
kind = "release"
}
if preview {
kind = "insiders"
if major < 18 {
kind = "pre"
}
}
url := fmt.Sprintf("https://aka.ms/vs/%d/%s/channel", major, kind)
fmt.Println("Fetching", url)
data, err := httpGet(url)
if err != nil {
return "", err
}
var m Manifest
if err := json.Unmarshal(data, &m); err != nil {
return "", fmt.Errorf("parsing channel manifest: %w", err)
}
fmt.Printf("Got toplevel manifest for %s\n", m.Info.ProductDisplayVersion)
for _, item := range m.ChannelItems {
if item.Type == "Manifest" && len(item.Payloads) > 0 {
return item.Payloads[0].URL, nil
}
}
return "", fmt.Errorf("unable to find an installer manifest")
}
// FetchInstallerManifest downloads and parses the installer manifest at url.
func FetchInstallerManifest(url string) (*Manifest, error) {
data, err := httpGet(url)
if err != nil {
return nil, err
}
var m Manifest
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("parsing installer manifest: %w", err)
}
fmt.Printf("Loaded installer manifest for %s\n", m.Info.ProductDisplayVersion)
return &m, nil
}
// HumanizeBytes renders a byte count as a friendly "1.2 GB" style string.
func HumanizeBytes(s int64) string {
switch {
case s > 900*1024*1024:
return fmt.Sprintf("%.1f GB", float64(s)/(1024*1024*1024))
case s > 900*1024:
return fmt.Sprintf("%.1f MB", float64(s)/(1024*1024))
case s > 1024:
return fmt.Sprintf("%.1f KB", float64(s)/1024)
default:
return fmt.Sprintf("%d bytes", s)
}
}