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
+45
View File
@@ -0,0 +1,45 @@
name: CI
on:
push:
branches: ["master"]
pull_request:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
test:
name: Build, vet & test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.23"
- name: gofmt
run: |
out="$(gofmt -l .)"
if [ -n "$out" ]; then
echo "gofmt would reformat:"
echo "$out"
exit 1
fi
- name: go vet
run: go vet ./...
- name: go build
run: go build ./...
- name: go test
run: go test ./...
+13 -5
View File
@@ -68,7 +68,15 @@ msvc-go-wine version print the vers
`--host-arch`, `--with-*` component toggles, `--ignore`, `--only-download`, `--host-arch`, `--with-*` component toggles, `--ignore`, `--only-download`,
`--only-unpack`, `--keep-unpack`, `--cache`, `--language`, `--only-unpack`, `--keep-unpack`, `--cache`, `--language`,
`--include-optional`, `--skip-recommended`, `--major`, `--preview`, `--include-optional`, `--skip-recommended`, `--major`, `--preview`,
`--manifest`. Run `msvc-go-wine download -h` for the full list. `--manifest`, `--list-workloads`, `--list-components`, `--print-deps-tree`.
Run `msvc-go-wine download -h` for the full list.
`--list-workloads`/`--list-components` print every workload/component id
(with its human-readable title) available in the fetched manifest and exit
without downloading anything - useful for discovering what to pass as a bare
package id or via `--with-*`. `--print-deps-tree` prints the dependency tree
of whatever would actually be selected (honoring every other flag), also
without downloading.
### Using clang-cl/lld-link instead of Wine ### Using clang-cl/lld-link instead of Wine
@@ -116,10 +124,10 @@ telemetry, and don't hard-fail devcmd setup when an optional component
## Known gaps ## Known gaps
- `download` doesn't yet support printing the dependency/reverse-dependency - `download` doesn't yet support installing the Windows Driver Kit via
tree, listing available workloads/components/packages, or installing the `--with-wdk-installers`; the core selection/download/unpack/install
Windows Driver Kit via `--with-wdk-installers`; the core selection/ pipeline, dependency tree printing, and workload/component listing are all
download/unpack/install pipeline is fully implemented. fully implemented.
## License ## License
+32 -1
View File
@@ -30,6 +30,9 @@ func runDownload(args []string) int {
onlyUnpack := fs.Bool("only-unpack", false, "unpack selected packages and keep everything, without pruning to just the CLI tools") onlyUnpack := fs.Bool("only-unpack", false, "unpack selected packages and keep everything, without pruning to just the CLI tools")
keepUnpack := fs.Bool("keep-unpack", false, "keep the scratch unpack dir instead of removing it after moving files into place") keepUnpack := fs.Bool("keep-unpack", false, "keep the scratch unpack dir instead of removing it after moving files into place")
skipPatch := fs.Bool("skip-patch", false, "don't apply the Wine compatibility patches") skipPatch := fs.Bool("skip-patch", false, "don't apply the Wine compatibility patches")
listWorkloads := fs.Bool("list-workloads", false, "list available workloads from the manifest and exit, without downloading anything")
listComponents := fs.Bool("list-components", false, "list available components from the manifest and exit, without downloading anything")
printDepsTree := fs.Bool("print-deps-tree", false, "print the dependency tree of the selected packages and exit, without downloading anything")
var archsFlag stringList var archsFlag stringList
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable") fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
var ignoreFlag stringList var ignoreFlag stringList
@@ -78,7 +81,17 @@ func runDownload(args []string) int {
idx := download.BuildIndex(manifest, opts.HostArch, opts.Language) idx := download.BuildIndex(manifest, opts.HostArch, opts.Language)
if !*acceptLicense { if *listWorkloads || *listComponents {
if *listWorkloads {
printPackageList("Workload", download.PackagesByType(idx, "Workload"), opts.Language)
}
if *listComponents {
printPackageList("Component", download.PackagesByType(idx, "Component"), opts.Language)
}
return 0
}
if !*acceptLicense && !*printDepsTree {
license := "the Visual Studio Build Tools license" license := "the Visual Studio Build Tools license"
if p := idx.Find("Microsoft.VisualStudio.Product.BuildTools", nil); p != nil && len(p.LocalizedResources) > 0 { if p := idx.Find("Microsoft.VisualStudio.Product.BuildTools", nil); p != nil && len(p.LocalizedResources) > 0 {
license = p.LocalizedResources[0].License license = p.LocalizedResources[0].License
@@ -93,6 +106,11 @@ func runDownload(args []string) int {
return 1 return 1
} }
if *printDepsTree {
download.PrintDependencyTree(os.Stdout, idx, opts)
return 0
}
selected, err := download.ExpandSelection(idx, opts) selected, err := download.ExpandSelection(idx, opts)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err) fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
@@ -184,6 +202,19 @@ func runDownload(args []string) int {
return 0 return 0
} }
// printPackageList prints one line per package: its ID, and (when the
// manifest carries one) its human-readable title in the requested language.
func printPackageList(kind string, pkgs []*download.Package, language string) {
fmt.Printf("Available %ss (%d):\n", kind, len(pkgs))
for _, p := range pkgs {
if lr := p.Localized(language); lr != nil && lr.Title != "" {
fmt.Printf(" %-65s %s\n", p.ID, lr.Title)
} else {
fmt.Printf(" %s\n", p.ID)
}
}
}
func detectHostArch() string { func detectHostArch() string {
if runtime.GOARCH == "arm64" { if runtime.GOARCH == "arm64" {
return "arm64" return "arm64"
+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
}
+48 -2
View File
@@ -40,10 +40,14 @@ type Dependency struct {
Type string // "", "Optional" or "Recommended" Type string // "", "Optional" or "Recommended"
} }
// LocalizedResource carries the license URL shown before accepting a // LocalizedResource carries a package's human-readable title/description
// package's terms. // (shown by --list-workloads/--list-components) and the license URL shown
// before accepting a package's terms.
type LocalizedResource struct { type LocalizedResource struct {
Language string `json:"language"` Language string `json:"language"`
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
License string `json:"license"` License string `json:"license"`
} }
@@ -108,6 +112,39 @@ func (p *Package) Key() string {
return key 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 { func (p *Package) InstalledSize() int64 {
var sum int64 var sum int64
for _, v := range p.InstallSizes { for _, v := range p.InstallSizes {
@@ -147,6 +184,15 @@ type Manifest struct {
// restart of `download`. // restart of `download`.
var httpClient = &http.Client{Timeout: 5 * time.Minute} 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 const maxManifestAttempts = 5
func httpGet(url string) ([]byte, error) { func httpGet(url string) ([]byte, error) {
+72
View File
@@ -2,7 +2,9 @@ package download
import ( import (
"fmt" "fmt"
"io"
"regexp" "regexp"
"sort"
"strings" "strings"
) )
@@ -367,3 +369,73 @@ func ExpandSelection(idx Index, opts *Options) ([]*Package, error) {
} }
return ret, nil 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)
}
}