mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
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:
@@ -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 ./...
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user