Files
Vintner/internal/download/manifest.go
T
Cheviiot a1743e4435 Rebrand project to vintner
Renamed the GitHub repo, Go module path, binary, and default install
directory from msvc-go-wine to vintner. Updated every user-facing
string (usage text, error prefixes, README, LICENSE, CI/release
workflow) and the embedded compatibility patches' own header text to
match; the VINTNER_LANG env var replaces VSMC_GO_WINE_LANG.

Also fixes a real bug found while re-verifying the rename end-to-end:
Microsoft.Cpp.WindowsSDK.props.patch had LF-only line endings in its
hunk body while the real Microsoft-shipped file it targets is CRLF,
so `git apply` silently failed on every real install and the SDK
detection fix it's meant to provide was never actually taking effect.
Restored matching CRLF endings in the hunk (checked against the other
five patches, which already had this right). Left the patch's
internal MsvcGoWine_ExtraSdkRoots MSBuild property name alone rather
than renaming it too - changing hunk content, even just an identifier,
breaks reverse-apply idempotency for anyone re-running download
against an already-patched tree, which the fix above depends on. Added
a .remove marker so an existing install's old-named props file gets
cleaned up on the next download.

Re-verified end-to-end after the rename: general MSBuild/cl/link
still work, and a real KMDF driver build (compile, link, INF stamping,
Inf2Cat signability check) still succeeds under the renamed binary.

README also gets an accuracy pass: WDK support and the download/env
CLI flags it lists were out of date (WDK was previously listed under
"Known gaps" despite being implemented and verified), the full tool
list was missing mc/cmd/findstr, and the new command aliases and
VINTNER_LANG option are now documented.
2026-07-25 03:55:22 +10:00

284 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++ {
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)
}
}