Add download --list-workloads/--list-components/--print-deps-tree, and CI

- download --list-workloads / --list-components print every workload or
  component id (with its manifest title) without downloading anything, for
  discovering what to pass as a package id or --with-* toggle.
- download --print-deps-tree prints the dependency tree of whatever the
  current flags would actually select, sharing the exact same
  arch/--ignore/Optional/Recommended filtering ExpandSelection uses so the
  output matches a real download; diamond dependencies are shown once and
  referenced as "(see above)" afterwards to keep it finite.
- Fixed --manifest (offline/predownloaded manifest testing) never having
  worked at all: it builds a "file:" URL but the shared http.Client had no
  handler registered for that scheme.
- Added a CI workflow running gofmt/vet/build/test on every push and PR;
  previously only the tag-triggered release workflow existed.
This commit is contained in:
Cheviiot
2026-07-25 02:47:24 +10:00
parent 47471e007c
commit 44fee57ddd
6 changed files with 230 additions and 10 deletions
+18
View File
@@ -0,0 +1,18 @@
package download
import "sort"
// PackagesByType returns the best (arch/language-matching) variant of every
// package in idx whose Type equals kind (e.g. "Workload" or "Component"),
// sorted by ID.
func PackagesByType(idx Index, kind string) []*Package {
var ret []*Package
for _, variants := range idx {
p := variants[0]
if p.Type == kind {
ret = append(ret, p)
}
}
sort.Slice(ret, func(i, j int) bool { return ret[i].ID < ret[j].ID })
return ret
}
+50 -4
View File
@@ -40,11 +40,15 @@ type Dependency struct {
Type string // "", "Optional" or "Recommended"
}
// LocalizedResource carries the license URL shown before accepting a
// package's terms.
// 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"`
License string `json:"license"`
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.
@@ -108,6 +112,39 @@ func (p *Package) Key() string {
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 {
@@ -147,6 +184,15 @@ type Manifest struct {
// 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/msvc-go-wine'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) {
+72
View File
@@ -2,7 +2,9 @@ package download
import (
"fmt"
"io"
"regexp"
"sort"
"strings"
)
@@ -367,3 +369,73 @@ func ExpandSelection(idx Index, opts *Options) ([]*Package, error) {
}
return ret, nil
}
// PrintDependencyTree writes an indented tree of opts.Package and everything
// they transitively depend on to w, applying the exact same
// arch/--ignore/Optional/Recommended filtering collectDependencyClosure
// (used by ExpandSelection) does, so what's printed matches what an actual
// download would select. A package already printed once elsewhere in the
// tree is shown again as a leaf ("(see above)") rather than re-expanded, to
// keep the output finite for packages multiple components depend on.
func PrintDependencyTree(w io.Writer, idx Index, opts *Options) {
printed := map[string]bool{}
for _, id := range opts.Package {
printDepNode(w, idx, id, nil, "", opts, 0, printed)
}
}
func printDepNode(w io.Writer, idx Index, target string, constraints map[string]string, depType string, opts *Options, depth int, printed map[string]bool) {
if contains(opts.Ignore, strings.ToLower(target)) {
return
}
indent := strings.Repeat(" ", depth)
annotation := ""
if depType != "" {
annotation = " [" + depType + "]"
}
p := idx.Find(target, constraints)
if p == nil {
fmt.Fprintf(w, "%s%s (not found)%s\n", indent, target, annotation)
return
}
if opts.OnlyHost && !HostArchCompatible(p, opts.HostArch) {
return
}
if !TargetArchCompatible(p, opts.Architecture) {
return
}
key := p.Key()
if printed[key] {
fmt.Fprintf(w, "%s%s%s (see above)\n", indent, p.ID, annotation)
return
}
printed[key] = true
fmt.Fprintf(w, "%s%s@%s%s\n", indent, p.ID, p.Version, annotation)
deps := p.Dependencies()
targets := make([]string, 0, len(deps))
for t := range deps {
targets = append(targets, t)
}
sort.Strings(targets)
for _, depTarget := range targets {
dep := deps[depTarget]
id := depTarget
if dep.TargetID != "" {
id = dep.TargetID
}
if dep.Type == "Optional" && !opts.IncludeOptional {
continue
}
if dep.Type == "Recommended" && opts.SkipRecommended {
continue
}
c := map[string]string{}
if dep.Version != "" {
c["version"] = dep.Version
}
printDepNode(w, idx, id, c, dep.Type, opts, depth+1, printed)
}
}