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.
This commit is contained in:
Cheviiot
2026-07-25 03:55:22 +10:00
parent 23ea620ce2
commit a1743e4435
28 changed files with 183 additions and 160 deletions
+299
View File
@@ -0,0 +1,299 @@
package main
import (
"bufio"
"flag"
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/Cheviiot/vintner/internal/download"
"github.com/Cheviiot/vintner/internal/i18n"
)
func runDownload(args []string) int {
fs := flag.NewFlagSet("download", flag.ContinueOnError)
dest := fs.String("dest", "", "directory to install into (default: ~/.vintner)")
cacheDir := fs.String("cache", "", "directory to use as a persistent download cache (default: a temp dir, removed afterwards)")
major := fs.Int("major", 18, "the major VS version to download")
preview := fs.Bool("preview", false, "download the preview/insiders channel instead of release/stable")
manifestFile := fs.String("manifest", "", "use a predownloaded installer manifest file instead of fetching one")
acceptLicense := fs.Bool("accept-license", false, "don't prompt for accepting the license")
msvcVersion := fs.String("msvc-version", "", "install a specific MSVC toolchain version, e.g. 17.10")
sdkVersion := fs.String("sdk-version", "", "install a specific Windows SDK version")
hostArch := fs.String("host-arch", "", "host architecture of packages to install (x86, x64, arm64; auto-detected)")
onlyHost := fs.Bool("only-host", true, "only download packages matching the host architecture")
language := fs.String("language", "en", "preferred language code for packages available in multiple languages")
includeOptional := fs.Bool("include-optional", false, "include all optional dependencies")
skipRecommended := fs.Bool("skip-recommended", false, "don't include recommended dependencies")
onlyDownload := fs.Bool("only-download", false, "stop after downloading package files")
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")
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")
withWDK := fs.Bool("with-wdk", false, "also fetch and install the Windows Driver Kit (headers, libs and MSBuild driver PlatformToolsets, for building KMDF/UMDF drivers)")
var archsFlag stringList
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
var ignoreFlag stringList
fs.Var(&ignoreFlag, "ignore", "package id to skip; repeatable")
if err := fs.Parse(args); err != nil {
return 2
}
packages := fs.Args()
opts := &download.Options{
Package: packages,
Ignore: []string(ignoreFlag),
Architecture: []string(archsFlag),
HostArch: *hostArch,
OnlyHost: *onlyHost,
MSVCVersion: *msvcVersion,
SDKVersion: *sdkVersion,
IncludeOptional: *includeOptional,
SkipRecommended: *skipRecommended,
Language: *language,
WithWDK: *withWDK,
}
manifestURL := *manifestFile
if manifestURL == "" {
url, err := download.FetchChannelManifest(*major, *preview)
if err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
manifestURL = url
} else {
manifestURL = "file:" + manifestURL
}
manifest, err := download.FetchInstallerManifest(manifestURL)
if err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
if opts.HostArch == "" {
opts.HostArch = detectHostArch()
}
fmt.Println(i18n.T("download.host_arch", opts.HostArch))
idx := download.BuildIndex(manifest, opts.HostArch, opts.Language)
if *listWorkloads || *listComponents {
if *listWorkloads {
printPackageList("download.workloads_header", download.PackagesByType(idx, "Workload"), opts.Language)
}
if *listComponents {
printPackageList("download.components_header", download.PackagesByType(idx, "Component"), opts.Language)
}
return 0
}
if !*acceptLicense && !*printDepsTree {
license := "the Visual Studio Build Tools license"
if p := idx.Find("Microsoft.VisualStudio.Product.BuildTools", nil); p != nil && len(p.LocalizedResources) > 0 {
license = p.LocalizedResources[0].License
}
if !promptAcceptLicense(license) {
return 0
}
}
if err := download.ResolveSelection(opts, idx); err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
if *printDepsTree {
download.PrintDependencyTree(os.Stdout, idx, opts)
return 0
}
selected, err := download.ExpandSelection(idx, opts)
if err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
var downloadSize, installSize int64
for _, p := range selected {
downloadSize += p.DownloadSize()
installSize += p.InstalledSize()
}
fmt.Print(i18n.T("download.selected",
len(selected), download.HumanizeBytes(downloadSize), download.HumanizeBytes(installSize)))
cache := *cacheDir
removeCache := false
if cache == "" {
tmp, err := os.MkdirTemp("", "vintner-cache-")
if err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
cache = tmp
removeCache = true
}
if removeCache {
defer os.RemoveAll(cache)
}
if !*onlyDownload && *dest == "" {
def, err := defaultToolchainDir()
if err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
*dest = def
fmt.Println(i18n.T("download.default_dest", *dest))
}
if err := download.FetchPayloads(selected, cache, *onlyDownload); err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
if *onlyDownload {
return 0
}
destAbs, err := filepath.Abs(*dest)
if err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
unpack := destAbs
if !*onlyUnpack {
unpack = filepath.Join(destAbs, "unpack")
}
if err := download.UnpackSelectedPackages(selected, cache, unpack); err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
// Wine doesn't honor .exe.config <dependentAssembly> redirects, so copy
// MSBuild's redirected assemblies next to it directly.
for _, hostArch := range []string{"amd64", "arm64"} {
msbuildExe := filepath.Join(unpack, "MSBuild", "Current", "Bin", hostArch, "MSBuild.exe")
if err := download.CopyRedirectedAssemblies(msbuildExe); err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
}
if !*onlyUnpack {
if err := download.RelocateBuildTools(unpack, destAbs); err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
if !*keepUnpack {
os.RemoveAll(unpack)
}
if !*skipPatch && *major == 18 {
if err := download.ApplyCompatibilityFixes(destAbs); err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
}
}
if opts.WithWDK && !*onlyUnpack {
if err := downloadWDK(opts, selected, cache, destAbs, *major); err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
}
fmt.Println(i18n.T("download.done", destAbs))
return 0
}
// downloadWDK fetches the WDK NuGet package(s) matching opts.Architecture
// (only x64 and arm64 have one - there's no WDK package for x86/arm
// targets) into destAbs/wdk/<arch>, preferring a version matching the
// Windows SDK actually selected. See wdk.go for why this is a separate
// download path from the rest of ExpandSelection/FetchPayloads/Unpack.
func downloadWDK(opts *download.Options, selected []*download.Package, cache, destAbs string, major int) error {
sdkBuild := download.SDKBuildPrefix(selected)
vsVersion := fmt.Sprintf("%d.0", major)
var archs []string
for _, a := range []string{"x64", "arm64"} {
if contains(opts.Architecture, a) {
archs = append(archs, a)
}
}
if len(archs) == 0 {
fmt.Println(i18n.T("download.wdk_skip"))
return nil
}
for _, arch := range archs {
version, err := download.FetchLatestWDKVersion(arch, sdkBuild)
if err != nil {
return err
}
wdkDir, err := download.DownloadWDK(arch, version, cache, destAbs, vsVersion)
if err != nil {
return err
}
fmt.Print(i18n.T("download.wdk_installed", arch, version, wdkDir))
}
return nil
}
func contains(list []string, v string) bool {
for _, s := range list {
if s == v {
return true
}
}
return false
}
// printPackageList prints one line per package: its ID, and (when the
// manifest carries one) its human-readable title in the requested language.
// headerKey is an i18n catalog key taking the package count as its one arg.
func printPackageList(headerKey string, pkgs []*download.Package, language string) {
fmt.Print(i18n.T(headerKey, 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 {
if runtime.GOARCH == "arm64" {
return "arm64"
}
return "x64"
}
func promptAcceptLicense(license string) bool {
fmt.Print(i18n.T("download.license_prompt", license))
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
switch scanner.Text() {
case "yes":
return true
case "no":
return false
}
fmt.Print(i18n.T("download.license_reprompt"))
}
return false
}
// stringList implements flag.Value to collect a repeatable string flag.
type stringList []string
func (s *stringList) String() string { return fmt.Sprint([]string(*s)) }
func (s *stringList) Set(v string) error {
*s = append(*s, v)
return nil
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/Cheviiot/vintner/internal/i18n"
"github.com/Cheviiot/vintner/internal/wineenv"
)
// runEnv prints shell `export` statements for INCLUDE/LIB (converted from
// wine's "z:\..." notation to plain unix paths) and TARGET_TRIPLE, for
// driving clang-cl/lld-link directly without Wine.
// Usage: eval "$(vintner env --bin <dest>/bin/<arch>)"
func runEnv(args []string) int {
fs := flag.NewFlagSet("env", flag.ContinueOnError)
bin := fs.String("bin", "", "the <dest>/bin/<arch> directory produced by `vintner install`")
if err := fs.Parse(args); err != nil {
return 2
}
if *bin == "" {
fmt.Fprintln(os.Stderr, i18n.T("env.usage"))
return 1
}
cfg, err := wineenv.Load(*bin)
if err != nil {
fmt.Fprintln(os.Stderr, "vintner env:", err)
return 1
}
baseUnix, err := wineenv.FindBaseUnix(*bin)
if err != nil {
fmt.Fprintln(os.Stderr, "vintner env:", err)
return 1
}
paths := wineenv.NewPaths(cfg, baseUnix)
triple, ok := targetTriples[cfg.Arch]
if !ok {
fmt.Fprint(os.Stderr, i18n.T("env.unknown_arch", cfg.Arch))
return 1
}
fmt.Printf("export INCLUDE=%q\n", toUnixPathList(paths.Include))
fmt.Printf("export LIB=%q\n", toUnixPathList(paths.Lib))
fmt.Printf("export TARGET_TRIPLE=%q\n", triple)
return 0
}
var targetTriples = map[string]string{
"x86": "i686-windows-msvc",
"x64": "x86_64-windows-msvc",
"arm": "armv7-windows-msvc",
"arm64": "aarch64-windows-msvc",
}
// toUnixPathList does a blanket removal of the "z:" drive prefix and
// backslash->slash conversion across the whole semicolon-joined path list.
func toUnixPathList(s string) string {
s = strings.ReplaceAll(s, "z:", "")
s = strings.ReplaceAll(s, `\`, "/")
return s
}
+42
View File
@@ -0,0 +1,42 @@
package main
import (
"fmt"
"os"
"github.com/Cheviiot/vintner/internal/i18n"
"github.com/Cheviiot/vintner/internal/install"
)
func runInstall(args []string) int {
if len(args) > 1 || (len(args) == 1 && (args[0] == "-h" || args[0] == "--help")) {
fmt.Fprintln(os.Stderr, i18n.T("install.usage"))
return 1
}
var dest string
if len(args) == 1 {
dest = args[0]
} else {
def, err := defaultToolchainDir()
if err != nil {
fmt.Fprintln(os.Stderr, "vintner install:", err)
return 1
}
dest = def
fmt.Println(i18n.T("install.default_dir", dest))
}
self, err := os.Executable()
if err != nil {
fmt.Fprintln(os.Stderr, "vintner install:", err)
return 1
}
if err := install.Install(dest, self); err != nil {
fmt.Fprintln(os.Stderr, "vintner install:", err)
return 1
}
fmt.Println(i18n.T("install.done", dest+"/bin/<arch>"))
return 0
}
+65
View File
@@ -0,0 +1,65 @@
// Command vintner cross compiles with the real MSVC toolchain on Linux
// via Wine. It's a multi-call binary that behaves as `cl`, `link`, `lib`,
// `rc`, `midl`, `mt`, `dumpbin`, `msbuild`, etc. when invoked under one of
// those names (via symlinks set up by `vintner install`), and
// otherwise exposes the `download`/`install`/`env`/`version` management
// subcommands.
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Cheviiot/vintner/internal/i18n"
"github.com/Cheviiot/vintner/internal/wrapper"
)
// version is set at build time via -ldflags "-X main.version=X.Y.Z";
// left as "dev" for plain `go build`/`go run`.
var version = "dev"
func main() {
base := filepath.Base(os.Args[0])
name := strings.TrimSuffix(strings.ToLower(base), ".exe")
if _, ok := wrapper.Tools[name]; ok {
os.Exit(wrapper.Run(name, os.Args[1:]))
}
if name == "cmd" || name == "findstr" {
os.Exit(wrapper.Run(name, os.Args[1:]))
}
os.Exit(runCLI(os.Args[1:]))
}
func runCLI(args []string) int {
if len(args) == 0 {
printUsage()
return 1
}
switch args[0] {
case "download", "dl":
return runDownload(args[1:])
case "install", "i":
return runInstall(args[1:])
case "env", "e":
return runEnv(args[1:])
case "version", "v", "--version":
fmt.Println("vintner " + version)
return 0
case "-h", "--help", "help", "h":
printUsage()
return 0
default:
fmt.Fprint(os.Stderr, i18n.T("main.unknown_subcommand", args[0]))
printUsage()
return 1
}
}
func printUsage() {
fmt.Fprint(os.Stderr, i18n.T("main.usage"))
}
+17
View File
@@ -0,0 +1,17 @@
package main
import (
"os"
"path/filepath"
)
// defaultToolchainDir is where `download`/`install` operate when the user
// doesn't specify a directory: a hidden ~/.vintner, so it doesn't
// clutter a plain `ls ~`.
func defaultToolchainDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".vintner"), nil
}