mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Initial implementation of msvc-go-wine
A single-binary Go tool for cross compiling with the real MSVC toolchain on Linux via Wine. Behaves as cl/link/lib/rc/midl/mt/dumpbin/msbuild/ nmake/ml/ml64/armasm/armasm64/cmd/findstr depending on the name it's invoked as, plus download/install/env/version management subcommands. - download: fetches the MSVC/WinSDK installer manifest, resolves package selection and dependencies, downloads and verifies payloads, unpacks VSIX/MSI packages, and applies a handful of compatibility patches so VsDevCmd.bat and MSBuild's SDK detection work without a Windows Registry (which doesn't exist under Wine). - install: locates the installed toolchain/SDK versions, normalizes header/library name casing, lays out per-architecture tool symlinks with an env.json config each, and compiles a small native launcher (toolrelay.exe) that lets mt.exe's CMake-compatibility exit code survive Wine's own exit-code truncation. - The wrapper runtime rewrites absolute unix paths in tool arguments into Wine's z:\... form, runs the real .exe under wine, and rewrites the tool's output back to plain unix paths. Verified end-to-end against a real MSVC/WinSDK download: cl, link, mt and the resulting hello.exe all work under Wine, including through the toolrelay.exe relay path and with paths containing non-ASCII characters. Offline unit tests cover the wrapper's path-rewrite/output-filter logic, install-time header lowercasing, and download package-selection/ dependency-resolution.
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UnpackSelectedPackages unpacks every selected package's cached payloads
|
||||
// into unpack (VSIX packages as plain zips, Win10SDK/Win11SDK via the
|
||||
// external msiextract).
|
||||
func UnpackSelectedPackages(selected []*Package, cacheDir, unpack string) error {
|
||||
if err := os.MkdirAll(unpack, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(unpack, "MSBuild"), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range selected {
|
||||
dir := filepath.Join(cacheDir, p.Key())
|
||||
switch p.Type {
|
||||
case "Component", "Workload", "Group":
|
||||
continue
|
||||
case "Vsix":
|
||||
fmt.Println("Unpacking", p.ID)
|
||||
for _, pl := range p.Payloads {
|
||||
listing := filepath.Join(unpack, p.Key()+"-listing.txt")
|
||||
if err := extractVSIXPackage(filepath.Join(dir, pl.Name()), unpack, listing); err != nil {
|
||||
return fmt.Errorf("unpacking %s: %w", p.ID, err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
if strings.HasPrefix(p.ID, "Win10SDK") || strings.HasPrefix(p.ID, "Win11SDK") {
|
||||
fmt.Println("Unpacking", p.ID)
|
||||
if err := extractWindowsSDKPackage(dir, p.Payloads, unpack); err != nil {
|
||||
return fmt.Errorf("unpacking %s: %w", p.ID, err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Skipping unpacking of", p.ID, "of type", p.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractVSIXPackage extracts a VSIX (plain zip) into a scratch dir under
|
||||
// dest, then merges its "Contents" (and WDK's "$MSBuild") subtree into
|
||||
// dest.
|
||||
func extractVSIXPackage(file, dest, listingPath string) error {
|
||||
r, err := zip.OpenReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
tmp := filepath.Join(dest, "vsix")
|
||||
if err := os.RemoveAll(tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, f := range r.File {
|
||||
names = append(names, f.Name)
|
||||
name, err := url.PathUnescape(f.Name)
|
||||
if err != nil {
|
||||
name = f.Name
|
||||
}
|
||||
target := filepath.Join(tmp, name)
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := extractZipEntry(f, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(listingPath, []byte(strings.Join(names, "\n")+"\n"), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if contents := filepath.Join(tmp, "Contents"); isDir(contents) {
|
||||
if err := combineDirTrees(contents, dest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// This archive directory structure is used by the WDK.vsix.
|
||||
if msbuild := filepath.Join(tmp, "$MSBuild"); isDir(msbuild) {
|
||||
if err := combineDirTrees(msbuild, filepath.Join(dest, "MSBuild")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return os.RemoveAll(tmp)
|
||||
}
|
||||
|
||||
func extractZipEntry(f *zip.File, dest string) error {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
mode := f.Mode()
|
||||
if mode == 0 {
|
||||
mode = 0o644
|
||||
}
|
||||
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, rc)
|
||||
return err
|
||||
}
|
||||
|
||||
// extractWindowsSDKPackage extracts every .msi payload of a WinSDK package
|
||||
// via msiextract, and symlinks "Program Files" to "." so files msiextract
|
||||
// unpacks there land at the unpack root (matching msiexec's own behavior on
|
||||
// Windows).
|
||||
func extractWindowsSDKPackage(src string, payloads []Payload, dest string) error {
|
||||
pf := filepath.Join(dest, "Program Files")
|
||||
if !exists(pf) {
|
||||
if err := os.Symlink(".", pf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, pl := range payloads {
|
||||
name := pl.Name()
|
||||
if !strings.HasSuffix(strings.ToLower(name), ".msi") {
|
||||
continue
|
||||
}
|
||||
fmt.Println("Extracting", name)
|
||||
srcFile := filepath.Join(src, name)
|
||||
logPath := filepath.Join(dest, "WinSDK-"+name+"-listing.txt")
|
||||
if err := runMsiExtract(srcFile, dest, logPath); err != nil {
|
||||
return fmt.Errorf("msiextract %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMsiExtract(srcFile, dest, logPath string) error {
|
||||
if _, err := exec.LookPath("msiextract"); err != nil {
|
||||
return fmt.Errorf("msiextract not found in PATH (install the msitools package): %w", err)
|
||||
}
|
||||
logFile, err := os.Create(logPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logFile.Close()
|
||||
cmd := exec.Command("msiextract", "-C", dest, srcFile)
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// RelocateBuildTools relocates the components CLI tools actually need (VC,
|
||||
// Windows Kits, DIA SDK, MSBuild, Common7/Tools) from the scratch unpack
|
||||
// dir into dest, so the rest of unpack can be discarded.
|
||||
func RelocateBuildTools(unpack, dest string) error {
|
||||
components := []string{"VC", "Windows Kits", "DIA SDK", "MSBuild", filepath.Join("Common7", "Tools")}
|
||||
for _, c := range components {
|
||||
if err := combineDirTrees(filepath.Join(unpack, c), filepath.Join(dest, c)); err != nil {
|
||||
return fmt.Errorf("moving %s: %w", c, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// combineDirTrees moves src into dest, recursively merging where a
|
||||
// same-named directory already exists in dest (matching case-insensitively)
|
||||
// rather than clobbering it - MSVC/WinSDK packages aren't casing-consistent
|
||||
// about where they put things.
|
||||
func combineDirTrees(src, dest string) error {
|
||||
if !isDir(src) {
|
||||
return nil
|
||||
}
|
||||
if !isDir(dest) {
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(src, dest)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destEntries, err := os.ReadDir(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destNames := map[string]string{}
|
||||
for _, e := range destEntries {
|
||||
destNames[strings.ToLower(e.Name())] = e.Name()
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
n := e.Name()
|
||||
srcName := filepath.Join(src, n)
|
||||
destName := filepath.Join(dest, n)
|
||||
if e.IsDir() {
|
||||
if isDir(destName) {
|
||||
if err := combineDirTrees(srcName, destName); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if actual, ok := destNames[strings.ToLower(n)]; ok {
|
||||
if err := combineDirTrees(srcName, filepath.Join(dest, actual)); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.Rename(srcName, destName); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.Rename(srcName, destName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyRedirectedAssemblies works around Wine not honoring <dependentAssembly>
|
||||
// codeBase redirects in an .exe.config file, by copying the referenced DLL
|
||||
// next to the executable directly.
|
||||
func CopyRedirectedAssemblies(app string) error {
|
||||
cfgPath := app + ".config"
|
||||
if !isFile(cfgPath) {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cfg struct {
|
||||
Runtime struct {
|
||||
AssemblyBinding struct {
|
||||
DependentAssembly []struct {
|
||||
CodeBase struct {
|
||||
Href string `xml:"href,attr"`
|
||||
} `xml:"codeBase"`
|
||||
} `xml:"dependentAssembly"`
|
||||
} `xml:"assemblyBinding"`
|
||||
} `xml:"runtime"`
|
||||
}
|
||||
if err := xml.Unmarshal(data, &cfg); err != nil {
|
||||
return fmt.Errorf("parsing %s: %w", cfgPath, err)
|
||||
}
|
||||
|
||||
dest := filepath.Dir(app)
|
||||
for _, da := range cfg.Runtime.AssemblyBinding.DependentAssembly {
|
||||
href := strings.ReplaceAll(da.CodeBase.Href, "\\", "/")
|
||||
if href == "" {
|
||||
continue
|
||||
}
|
||||
src := filepath.Join(dest, href)
|
||||
if isFile(src) {
|
||||
if err := copyFile(src, filepath.Join(dest, filepath.Base(src)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
fi, err := os.Stat(path)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func isFile(path string) bool {
|
||||
fi, err := os.Stat(path)
|
||||
return err == nil && !fi.IsDir()
|
||||
}
|
||||
|
||||
func exists(path string) bool {
|
||||
_, err := os.Lstat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
_ = os.Remove(dst)
|
||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maxConcurrentDownloads = 5
|
||||
const maxDownloadAttempts = 5
|
||||
|
||||
// FetchPayloads fetches every payload of every selected package into
|
||||
// cacheDir/<packageKey>/<payloadName>, verifying sha256 and skipping files
|
||||
// already present and correct. allowHashMismatch (used for --only-download)
|
||||
// warns instead of failing on a hash mismatch.
|
||||
func FetchPayloads(selected []*Package, cacheDir string, allowHashMismatch bool) error {
|
||||
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type task struct {
|
||||
payload Payload
|
||||
dest string
|
||||
fileID string
|
||||
}
|
||||
var tasks []task
|
||||
for _, p := range selected {
|
||||
if len(p.Payloads) == 0 {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(cacheDir, p.Key())
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pl := range p.Payloads {
|
||||
name := pl.Name()
|
||||
tasks = append(tasks, task{
|
||||
payload: pl,
|
||||
dest: filepath.Join(dir, name),
|
||||
fileID: filepath.Join(p.Key(), name),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sem := make(chan struct{}, maxConcurrentDownloads)
|
||||
var wg sync.WaitGroup
|
||||
var totalDownloaded int64
|
||||
errCh := make(chan error, len(tasks))
|
||||
|
||||
for _, t := range tasks {
|
||||
t := t
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
n, err := fetchOnePayloadWithRetries(t.payload, t.dest, t.fileID, allowHashMismatch)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&totalDownloaded, n)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Downloaded %s in total\n", HumanizeBytes(totalDownloaded))
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchOnePayloadWithRetries(payload Payload, dest, fileID string, allowHashMismatch bool) (int64, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < maxDownloadAttempts; attempt++ {
|
||||
n, err := tryDownloadPayload(payload, dest, fileID, allowHashMismatch)
|
||||
if err == nil {
|
||||
return n, nil
|
||||
}
|
||||
lastErr = err
|
||||
fmt.Printf("%v\n", err)
|
||||
}
|
||||
return 0, fmt.Errorf("giving up on %s after %d attempts: %w", fileID, maxDownloadAttempts, lastErr)
|
||||
}
|
||||
|
||||
func tryDownloadPayload(payload Payload, dest, fileID string, allowHashMismatch bool) (int64, error) {
|
||||
if fi, err := os.Stat(dest); err == nil && fi.Mode().IsRegular() {
|
||||
if payload.SHA256 != "" {
|
||||
sum, err := sha256File(dest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !equalFoldHex(sum, payload.SHA256) {
|
||||
fmt.Printf("Incorrect existing file %s, removing\n", fileID)
|
||||
os.Remove(dest)
|
||||
} else {
|
||||
fmt.Printf("Using existing file %s\n", fileID)
|
||||
return 0, nil
|
||||
}
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading %s (%s)\n", fileID, HumanizeBytes(payload.Size))
|
||||
if err := httpDownloadFile(payload.URL, dest); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if payload.SHA256 != "" {
|
||||
sum, err := sha256File(dest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !equalFoldHex(sum, payload.SHA256) {
|
||||
if allowHashMismatch {
|
||||
fmt.Printf("WARNING: incorrect hash for downloaded file %s\n", fileID)
|
||||
} else {
|
||||
return 0, fmt.Errorf("incorrect hash for downloaded file %s, aborting", fileID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return payload.Size, nil
|
||||
}
|
||||
|
||||
var downloadHTTPClient = &http.Client{Timeout: 30 * time.Minute}
|
||||
|
||||
func httpDownloadFile(url, dest string) error {
|
||||
resp, err := downloadHTTPClient.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("GET %s: %s", url, resp.Status)
|
||||
}
|
||||
tmp := dest + ".part"
|
||||
out, err := os.Create(tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, resp.Body); err != nil {
|
||||
out.Close()
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, dest)
|
||||
}
|
||||
|
||||
func sha256File(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func equalFoldHex(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
ca, cb := a[i], b[i]
|
||||
if ca >= 'A' && ca <= 'Z' {
|
||||
ca += 'a' - 'A'
|
||||
}
|
||||
if cb >= 'A' && cb <= 'Z' {
|
||||
cb += 'a' - 'A'
|
||||
}
|
||||
if ca != cb {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Index maps a lowercased package id to every variant sharing that id,
|
||||
// sorted by arch/language priority (best match first).
|
||||
type Index map[string][]*Package
|
||||
|
||||
// BuildIndex indexes every package in m, prioritizing variants matching
|
||||
// hostArch and language ("" language means no language preference beyond
|
||||
// the default "en").
|
||||
func BuildIndex(m *Manifest, hostArch, language string) Index {
|
||||
if language == "" {
|
||||
language = "en"
|
||||
}
|
||||
idx := Index{}
|
||||
for i := range m.Packages {
|
||||
p := &m.Packages[i]
|
||||
key := strings.ToLower(p.ID)
|
||||
idx[key] = append(idx[key], p)
|
||||
}
|
||||
for key, list := range idx {
|
||||
list := list
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return comparePackages(hostArch, language, list[i], list[j]) < 0
|
||||
})
|
||||
idx[key] = list
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// comparePackages ranks arch match first, then language match.
|
||||
func comparePackages(arch, language string, a, b *Package) int {
|
||||
archOrd := func(field string, x *Package) int {
|
||||
if arch == "" {
|
||||
return 0
|
||||
}
|
||||
var v string
|
||||
switch field {
|
||||
case "chip":
|
||||
v = x.Chip
|
||||
case "machineArch":
|
||||
v = x.MachineArch
|
||||
case "productArch":
|
||||
v = x.ProductArch
|
||||
}
|
||||
v = strings.ToLower(v)
|
||||
if v == "" || v == "neutral" {
|
||||
return 0
|
||||
}
|
||||
if v == arch {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
for _, field := range []string{"chip", "machineArch", "productArch"} {
|
||||
if r := archOrd(field, a) - archOrd(field, b); r != 0 {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
lang := strings.ToLower(language)
|
||||
countryLang := strings.Contains(lang, "-")
|
||||
langPriority := func(x *Package) int {
|
||||
xl := strings.ToLower(x.Language)
|
||||
if xl == "" {
|
||||
return 0
|
||||
}
|
||||
if (countryLang && xl == lang) || (!countryLang && strings.HasPrefix(xl, lang+"-")) {
|
||||
return 2
|
||||
}
|
||||
if strings.HasPrefix(xl, "en-") {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
ap, bp := langPriority(a), langPriority(b)
|
||||
if ap > bp {
|
||||
return -1
|
||||
}
|
||||
if ap < bp {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Find looks up id (case-insensitive), preferring a candidate matching
|
||||
// constraints' chip/machineArch, falling back to the highest-priority
|
||||
// variant. Returns nil if id isn't in the index at all.
|
||||
func (idx Index) Find(id string, constraints map[string]string) *Package {
|
||||
candidates, ok := idx[strings.ToLower(id)]
|
||||
if !ok || len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, p := range candidates {
|
||||
matched := true
|
||||
for _, k := range []string{"chip", "machineArch"} {
|
||||
want, has := constraints[k]
|
||||
if !has {
|
||||
continue
|
||||
}
|
||||
var got string
|
||||
if k == "chip" {
|
||||
got = p.Chip
|
||||
} else {
|
||||
got = p.MachineArch
|
||||
}
|
||||
if !strings.EqualFold(got, want) {
|
||||
matched = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
var knownHostArchs = []string{"x86", "x64", "arm64"}
|
||||
|
||||
// HostArchCompatible reports whether p can run on host: some packages
|
||||
// encode their host arch in the id itself (e.g.
|
||||
// Microsoft.VisualCpp.Tools.HostARM64.*).
|
||||
func HostArchCompatible(p *Package, host string) bool {
|
||||
if host == "" {
|
||||
return true
|
||||
}
|
||||
id := strings.ToLower(p.ID)
|
||||
for _, a := range knownHostArchs {
|
||||
if strings.Contains(id, "host"+a) {
|
||||
return a == host
|
||||
}
|
||||
}
|
||||
for _, v := range []string{p.Chip, p.MachineArch, p.ProductArch} {
|
||||
v = strings.ToLower(v)
|
||||
if v == "" || v == "neutral" {
|
||||
continue
|
||||
}
|
||||
if v != host {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var reTargetArch = regexp.MustCompile(`\.target(x86|x64|arm64|arm)(\W|$)`)
|
||||
|
||||
// TargetArchCompatible reports whether p is wanted for archs: packages
|
||||
// naming a target arch in their id (e.g. ...HostX64.TargetX64) are only
|
||||
// wanted when that arch was requested.
|
||||
func TargetArchCompatible(p *Package, archs []string) bool {
|
||||
if archs == nil {
|
||||
return true
|
||||
}
|
||||
m := reTargetArch.FindStringSubmatch(strings.ToLower(p.ID))
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
for _, a := range archs {
|
||||
if a == m[1] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// 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 the license URL shown before accepting a
|
||||
// package's terms.
|
||||
type LocalizedResource struct {
|
||||
Language string `json:"language"`
|
||||
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
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/assets"
|
||||
)
|
||||
|
||||
// ApplyCompatibilityFixes applies the embedded Wine compatibility patches
|
||||
// (assets/patches) against a freshly unpacked+moved dest tree: a
|
||||
// "foo.props.patch" is git-applied over dest/foo.props (skipped if already
|
||||
// applied), a "foo.patch"-less file is copied verbatim, and a "foo.remove"
|
||||
// marker deletes dest/foo.
|
||||
func ApplyCompatibilityFixes(dest string) error {
|
||||
return fs.WalkDir(assets.Patches, "patches", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel := strings.TrimPrefix(path, "patches/")
|
||||
ext := filepath.Ext(rel)
|
||||
target := strings.TrimSuffix(rel, ext)
|
||||
|
||||
switch ext {
|
||||
case ".patch":
|
||||
return applyGitPatch(dest, path, target)
|
||||
case ".remove":
|
||||
full := filepath.Join(dest, target)
|
||||
if isFile(full) {
|
||||
fmt.Println("Removing", target)
|
||||
return os.Remove(full)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
fmt.Println("Copying", rel)
|
||||
data, err := assets.Patches.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
full := filepath.Join(dest, rel)
|
||||
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(full, data, 0o644)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func applyGitPatch(dest, embeddedPath, target string) error {
|
||||
full := filepath.Join(dest, target)
|
||||
if !isFile(full) {
|
||||
return nil
|
||||
}
|
||||
data, err := assets.Patches.ReadFile(embeddedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "msvc-go-wine-*.patch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
// Skip if the patch has already been applied (reverse-apply check),
|
||||
// so re-running download/install stays idempotent.
|
||||
check := exec.Command("git", "--work-tree=.", "apply", "--quiet", "--reverse", "--check", tmp.Name())
|
||||
check.Dir = dest
|
||||
if check.Run() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Patching", target)
|
||||
apply := exec.Command("git", "--work-tree=.", "apply", tmp.Name())
|
||||
apply.Dir = dest
|
||||
apply.Stderr = os.Stderr
|
||||
return apply.Run()
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var reSDKVersion = regexp.MustCompile(`^\d+\.\d+\.\d+`)
|
||||
|
||||
// TriState represents a `--with-*` style flag: nil means "unset, use the
|
||||
// applicable default"; a set value means the user (or a higher-level
|
||||
// default) explicitly chose to include/exclude the component.
|
||||
type TriState = *bool
|
||||
|
||||
func on() TriState { v := true; return &v }
|
||||
func off() TriState { v := false; return &v }
|
||||
|
||||
// Options holds every flag that feeds package selection and download.
|
||||
type Options struct {
|
||||
Package []string
|
||||
Ignore []string
|
||||
Architecture []string // subset of x86,x64,arm,arm64,host
|
||||
HostArch string // x86, x64 or arm64
|
||||
OnlyHost bool
|
||||
|
||||
MSVCVersion string // "", "preview", "16.0".."18.0", "15.4".."15.9"
|
||||
SDKVersion string
|
||||
|
||||
WithDefault TriState
|
||||
WithWorkload TriState
|
||||
WithMSVC TriState
|
||||
WithASAN TriState
|
||||
WithSDK TriState
|
||||
WithATL TriState
|
||||
WithDIA TriState
|
||||
WithMSBuild TriState
|
||||
WithDevCmd TriState
|
||||
|
||||
IncludeOptional bool
|
||||
SkipRecommended bool
|
||||
Language string
|
||||
|
||||
WithWDKInstallers string
|
||||
}
|
||||
|
||||
func addIfWanted(opts *Options, flag TriState, pkg string) {
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
if *flag {
|
||||
opts.Package = append(opts.Package, pkg)
|
||||
} else {
|
||||
opts.Ignore = append(opts.Ignore, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
type msvcVersionEntry struct {
|
||||
gen string // "15" or "16" - which package-name scheme applies
|
||||
sdk string
|
||||
toolVersion string
|
||||
}
|
||||
|
||||
// msvcVersionTable maps a --msvc-version value to the SDK it pulls in by
|
||||
// default and the toolset version fragment used in package ids.
|
||||
var msvcVersionTable = map[string]msvcVersionEntry{
|
||||
"preview": {"16", "", "Preview"},
|
||||
|
||||
"16.0": {"16", "10.0.17763", "14.20"},
|
||||
"16.1": {"16", "10.0.18362", "14.21"},
|
||||
"16.2": {"16", "10.0.18362", "14.22"},
|
||||
"16.3": {"16", "10.0.18362", "14.23"},
|
||||
"16.4": {"16", "10.0.18362", "14.24"},
|
||||
"16.5": {"16", "10.0.18362", "14.25"},
|
||||
"16.6": {"16", "10.0.18362", "14.26"},
|
||||
"16.7": {"16", "10.0.18362", "14.27"},
|
||||
"16.8": {"16", "10.0.18362", "14.28"},
|
||||
"16.9": {"16", "10.0.19041", "14.28.16.9"},
|
||||
"16.10": {"16", "10.0.19041", "14.29.16.10"},
|
||||
"16.11": {"16", "10.0.19041", "14.29.16.11"},
|
||||
"17.0": {"16", "10.0.19041", "14.30.17.0"},
|
||||
"17.1": {"16", "10.0.19041", "14.31.17.1"},
|
||||
"17.2": {"16", "10.0.19041", "14.32.17.2"},
|
||||
"17.3": {"16", "10.0.19041", "14.33.17.3"},
|
||||
"17.4": {"16", "10.0.22621", "14.34.17.4"},
|
||||
"17.5": {"16", "10.0.22621", "14.35.17.5"},
|
||||
"17.6": {"16", "10.0.22621", "14.36.17.6"},
|
||||
"17.7": {"16", "10.0.22621", "14.37.17.7"},
|
||||
"17.8": {"16", "10.0.22621", "14.38.17.8"},
|
||||
"17.9": {"16", "10.0.22621", "14.39.17.9"},
|
||||
"17.10": {"16", "10.0.22621", "14.40.17.10"},
|
||||
"17.11": {"16", "10.0.22621", "14.41.17.11"},
|
||||
"17.12": {"16", "10.0.22621", "14.42.17.12"},
|
||||
"17.13": {"16", "10.0.22621", "14.43.17.13"},
|
||||
"17.14": {"16", "10.0.26100", "14.44.17.14"},
|
||||
"18.0": {"16", "10.0.26100", "14.50.18.0"},
|
||||
|
||||
"15.4": {"15", "10.0.16299", "14.11"},
|
||||
"15.5": {"15", "10.0.16299", "14.12"},
|
||||
"15.6": {"15", "10.0.16299", "14.13"},
|
||||
"15.7": {"15", "10.0.17134", "14.14"},
|
||||
"15.8": {"15", "10.0.17134", "14.15"},
|
||||
"15.9": {"15", "10.0.17763", "14.16"},
|
||||
}
|
||||
|
||||
func selectToolsetV16(opts *Options, idx Index, userVersion, sdk, toolVersion string, defaultPkgs, defaultIgnores []string) {
|
||||
ext := ""
|
||||
if toolVersion == "Preview" {
|
||||
ext = ".Tools"
|
||||
}
|
||||
base := "Microsoft.VisualStudio.Component.VC." + toolVersion + ext
|
||||
if idx.Find(base+".x86.x64", nil) != nil {
|
||||
if contains(opts.Architecture, "x86") || contains(opts.Architecture, "x64") {
|
||||
addIfWanted(opts, opts.WithMSVC, base+".x86.x64")
|
||||
addIfWanted(opts, opts.WithASAN, "Microsoft.VC."+toolVersion+".ASAN.X86")
|
||||
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ATL")
|
||||
}
|
||||
if contains(opts.Architecture, "arm") {
|
||||
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ARM")
|
||||
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ATL.ARM")
|
||||
}
|
||||
if contains(opts.Architecture, "arm64") {
|
||||
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ARM64")
|
||||
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ATL.ARM64")
|
||||
}
|
||||
if opts.SDKVersion == "" {
|
||||
opts.SDKVersion = sdk
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Didn't find exact version packages for %s, assuming this is provided by the default/latest version\n", userVersion)
|
||||
opts.Package = append(opts.Package, defaultPkgs...)
|
||||
opts.Ignore = append(opts.Ignore, defaultIgnores...)
|
||||
}
|
||||
}
|
||||
|
||||
func selectToolsetV15(opts *Options, idx Index, userVersion, sdk, toolVersion string, defaultPkgs, defaultIgnores []string) {
|
||||
id := "Microsoft.VisualStudio.Component.VC.Tools." + toolVersion
|
||||
if idx.Find(id, nil) != nil {
|
||||
addIfWanted(opts, opts.WithMSVC, id)
|
||||
if opts.SDKVersion == "" {
|
||||
opts.SDKVersion = sdk
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Didn't find exact version packages for %s, assuming this is provided by the default/latest version\n", userVersion)
|
||||
opts.Package = append(opts.Package, defaultPkgs...)
|
||||
opts.Ignore = append(opts.Ignore, defaultIgnores...)
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveSelection turns Options' high-level flags (--with-*,
|
||||
// --msvc-version, --sdk-version, explicit packages) into the final
|
||||
// opts.Package/opts.Ignore lists ready for ExpandSelection.
|
||||
func ResolveSelection(opts *Options, idx Index) error {
|
||||
if len(opts.Architecture) == 0 {
|
||||
opts.Architecture = []string{"host", "x86", "x64", "arm", "arm64"}
|
||||
}
|
||||
if opts.HostArch != "" && contains(opts.Architecture, "host") {
|
||||
opts.Architecture = append(opts.Architecture, opts.HostArch)
|
||||
}
|
||||
|
||||
if opts.MSVCVersion != "" {
|
||||
if opts.WithMSVC == nil {
|
||||
opts.WithMSVC = on()
|
||||
}
|
||||
if opts.WithASAN == nil {
|
||||
opts.WithASAN = on()
|
||||
}
|
||||
if opts.WithATL == nil {
|
||||
opts.WithATL = on()
|
||||
}
|
||||
if opts.WithSDK == nil {
|
||||
opts.WithSDK = on()
|
||||
}
|
||||
}
|
||||
if opts.SDKVersion != "" && opts.WithSDK == nil {
|
||||
opts.WithSDK = on()
|
||||
}
|
||||
|
||||
if opts.WithDefault == nil && opts.MSVCVersion == "" && len(opts.Package) == 0 {
|
||||
opts.WithDefault = on()
|
||||
}
|
||||
if opts.WithDefault != nil {
|
||||
for _, pair := range []struct {
|
||||
flag *TriState
|
||||
}{
|
||||
{&opts.WithWorkload}, {&opts.WithMSVC}, {&opts.WithASAN}, {&opts.WithSDK},
|
||||
{&opts.WithATL}, {&opts.WithDIA}, {&opts.WithMSBuild}, {&opts.WithDevCmd},
|
||||
} {
|
||||
if *pair.flag == nil {
|
||||
v := *opts.WithDefault
|
||||
*pair.flag = &v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultPkgs, defaultIgnores := opts.Package, opts.Ignore
|
||||
opts.Package, opts.Ignore = nil, nil
|
||||
|
||||
addIfWanted(opts, opts.WithWorkload, "Microsoft.VisualStudio.Workload.VCTools")
|
||||
if contains(opts.Architecture, "x86") || contains(opts.Architecture, "x64") {
|
||||
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
|
||||
addIfWanted(opts, opts.WithASAN, "Microsoft.VisualCpp.ASAN.X86")
|
||||
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC.ATL")
|
||||
}
|
||||
if contains(opts.Architecture, "arm") {
|
||||
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC.Tools.ARM")
|
||||
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC.ATL.ARM")
|
||||
}
|
||||
if contains(opts.Architecture, "arm64") {
|
||||
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC.Tools.ARM64")
|
||||
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC.ATL.ARM64")
|
||||
}
|
||||
|
||||
defaultPkgs, opts.Package = opts.Package, defaultPkgs
|
||||
defaultIgnores, opts.Ignore = opts.Ignore, defaultIgnores
|
||||
|
||||
switch {
|
||||
case opts.MSVCVersion == "":
|
||||
opts.Package = append(opts.Package, defaultPkgs...)
|
||||
opts.Ignore = append(opts.Ignore, defaultIgnores...)
|
||||
default:
|
||||
entry, ok := msvcVersionTable[opts.MSVCVersion]
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported MSVC toolchain version %s", opts.MSVCVersion)
|
||||
}
|
||||
if entry.gen == "15" {
|
||||
selectToolsetV15(opts, idx, opts.MSVCVersion, entry.sdk, entry.toolVersion, defaultPkgs, defaultIgnores)
|
||||
} else {
|
||||
selectToolsetV16(opts, idx, opts.MSVCVersion, entry.sdk, entry.toolVersion, defaultPkgs, defaultIgnores)
|
||||
}
|
||||
}
|
||||
|
||||
if err := selectSDK(opts, idx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addIfWanted(opts, opts.WithDIA, "Microsoft.VisualCpp.DIA.SDK")
|
||||
addIfWanted(opts, opts.WithMSBuild, "Microsoft.Build")
|
||||
addIfWanted(opts, opts.WithMSBuild, "Microsoft.Build.Dependencies")
|
||||
addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.VC.vcvars")
|
||||
addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.PackageGroup.VsDevCmd")
|
||||
|
||||
if opts.WithWDKInstallers != "" {
|
||||
opts.Package = append(opts.Package, "Component.Microsoft.Windows.DriverKit.BuildTools")
|
||||
}
|
||||
|
||||
normalizeIgnoreCase(opts)
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectSDK(opts *Options, idx Index) error {
|
||||
switch {
|
||||
case opts.WithSDK == nil:
|
||||
return nil
|
||||
case !*opts.WithSDK:
|
||||
for key := range idx {
|
||||
if strings.HasPrefix(key, "win10sdk") || strings.HasPrefix(key, "win11sdk") {
|
||||
opts.Ignore = append(opts.Ignore, key)
|
||||
}
|
||||
}
|
||||
case opts.SDKVersion == "":
|
||||
saved := *opts
|
||||
opts.Package = []string{"Microsoft.VisualStudio.Workload.VCTools"}
|
||||
opts.IncludeOptional = false
|
||||
opts.SkipRecommended = false
|
||||
recommended, err := ExpandSelection(idx, opts)
|
||||
*opts = saved
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range recommended {
|
||||
key := strings.ToLower(p.ID)
|
||||
if strings.HasPrefix(key, "win10sdk") || strings.HasPrefix(key, "win11sdk") {
|
||||
opts.Package = append(opts.Package, key)
|
||||
}
|
||||
}
|
||||
default:
|
||||
found := false
|
||||
var versions []string
|
||||
for key := range idx {
|
||||
if !strings.HasPrefix(key, "win10sdk") && !strings.HasPrefix(key, "win11sdk") {
|
||||
continue
|
||||
}
|
||||
version := key[9:]
|
||||
if reSDKVersion.MatchString(version) {
|
||||
versions = append(versions, version)
|
||||
}
|
||||
if key == key[:8]+"_"+opts.SDKVersion {
|
||||
found = true
|
||||
opts.Package = append(opts.Package, key)
|
||||
} else {
|
||||
opts.Ignore = append(opts.Ignore, key)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("WinSDK version %s not found (available: %s)", opts.SDKVersion, strings.Join(versions, ", "))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeIgnoreCase(opts *Options) {
|
||||
for i, s := range opts.Ignore {
|
||||
opts.Ignore[i] = strings.ToLower(s)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(list []string, v string) bool {
|
||||
for _, s := range list {
|
||||
if s == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// collectDependencyClosure recursively resolves target's dependency tree,
|
||||
// honoring arch matching, --ignore, and Optional/Recommended filtering.
|
||||
func collectDependencyClosure(idx Index, included map[string]bool, target string, constraints map[string]string, opts *Options) []*Package {
|
||||
if contains(opts.Ignore, strings.ToLower(target)) {
|
||||
return nil
|
||||
}
|
||||
p := idx.Find(target, constraints)
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
if opts.OnlyHost && !HostArchCompatible(p, opts.HostArch) {
|
||||
return nil
|
||||
}
|
||||
if !TargetArchCompatible(p, opts.Architecture) {
|
||||
return nil
|
||||
}
|
||||
key := p.Key()
|
||||
if included[key] {
|
||||
return nil
|
||||
}
|
||||
included[key] = true
|
||||
|
||||
ret := []*Package{p}
|
||||
for target, dep := range p.Dependencies() {
|
||||
id := target
|
||||
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
|
||||
}
|
||||
ret = append(ret, collectDependencyClosure(idx, included, id, c, opts)...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ExpandSelection resolves opts.Package (and everything they transitively
|
||||
// depend on) into a flat, deduped package list.
|
||||
func ExpandSelection(idx Index, opts *Options) ([]*Package, error) {
|
||||
included := map[string]bool{}
|
||||
var ret []*Package
|
||||
for _, id := range opts.Package {
|
||||
ret = append(ret, collectDependencyClosure(idx, included, id, nil, opts)...)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fixtureManifest is a trimmed-down stand-in for a real installer manifest,
|
||||
// covering: a workload -> component -> {required, Optional, Recommended}
|
||||
// dependency chain, host-arch variants of the same package id, and
|
||||
// language variants - enough to exercise BuildIndex/collectDependencyClosure
|
||||
// offline, without ever hitting the network.
|
||||
const fixtureManifestJSON = `{
|
||||
"info": {"productDisplayVersion": "test"},
|
||||
"packages": [
|
||||
{
|
||||
"id": "Microsoft.VisualStudio.Workload.VCTools",
|
||||
"type": "Workload",
|
||||
"dependencies": {
|
||||
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||
"type": "Component",
|
||||
"dependencies": {
|
||||
"Microsoft.VC.Tools.Core": "1.0",
|
||||
"Microsoft.VC.Tools.Optional": {"version": "1.0", "type": "Optional"},
|
||||
"Microsoft.VC.Tools.Recommended": {"version": "1.0", "type": "Recommended"}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Microsoft.VC.Tools.Core",
|
||||
"type": "Msi",
|
||||
"machineArch": "x86",
|
||||
"payloads": [{"fileName": "core-x86.msi", "url": "https://example.invalid/core-x86.msi", "sha256": "aa", "size": 100}]
|
||||
},
|
||||
{
|
||||
"id": "Microsoft.VC.Tools.Core",
|
||||
"type": "Msi",
|
||||
"machineArch": "arm64",
|
||||
"payloads": [{"fileName": "core-arm64.msi", "url": "https://example.invalid/core-arm64.msi", "sha256": "bb", "size": 200}]
|
||||
},
|
||||
{
|
||||
"id": "Microsoft.VC.Tools.Optional",
|
||||
"type": "Msi",
|
||||
"payloads": [{"fileName": "optional.msi", "url": "https://example.invalid/optional.msi", "size": 10}]
|
||||
},
|
||||
{
|
||||
"id": "Microsoft.VC.Tools.Recommended",
|
||||
"type": "Msi",
|
||||
"payloads": [{"fileName": "recommended.msi", "url": "https://example.invalid/recommended.msi", "size": 20}]
|
||||
},
|
||||
{
|
||||
"id": "Microsoft.VisualStudio.Resources",
|
||||
"type": "Msi",
|
||||
"language": "en-US",
|
||||
"payloads": [{"fileName": "res-en.msi", "url": "https://example.invalid/res-en.msi", "size": 1}]
|
||||
},
|
||||
{
|
||||
"id": "Microsoft.VisualStudio.Resources",
|
||||
"type": "Msi",
|
||||
"language": "de-DE",
|
||||
"payloads": [{"fileName": "res-de.msi", "url": "https://example.invalid/res-de.msi", "size": 1}]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
func fixtureIndex(t *testing.T, hostArch string) Index {
|
||||
t.Helper()
|
||||
var m Manifest
|
||||
if err := json.Unmarshal([]byte(fixtureManifestJSON), &m); err != nil {
|
||||
t.Fatalf("parsing fixture manifest: %v", err)
|
||||
}
|
||||
return BuildIndex(&m, hostArch, "en")
|
||||
}
|
||||
|
||||
func TestBuildIndexPrioritizesHostArch(t *testing.T) {
|
||||
idx := fixtureIndex(t, "x86")
|
||||
p := idx.Find("Microsoft.VC.Tools.Core", nil)
|
||||
if p == nil {
|
||||
t.Fatal("expected to find Microsoft.VC.Tools.Core")
|
||||
}
|
||||
if p.MachineArch != "x86" {
|
||||
t.Errorf("expected x86 variant to sort first for host x86, got machineArch=%q", p.MachineArch)
|
||||
}
|
||||
|
||||
idx64 := fixtureIndex(t, "arm64")
|
||||
p64 := idx64.Find("Microsoft.VC.Tools.Core", nil)
|
||||
if p64.MachineArch != "arm64" {
|
||||
t.Errorf("expected arm64 variant to sort first for host arm64, got machineArch=%q", p64.MachineArch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIndexPrioritizesLanguage(t *testing.T) {
|
||||
idx := fixtureIndex(t, "x86")
|
||||
p := idx.Find("Microsoft.VisualStudio.Resources", nil)
|
||||
if p == nil {
|
||||
t.Fatal("expected to find Microsoft.VisualStudio.Resources")
|
||||
}
|
||||
if p.Language != "en-US" {
|
||||
t.Errorf("expected en-US variant to sort first for language=en, got %q", p.Language)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateDependsDefaultExcludesOptionalOnly(t *testing.T) {
|
||||
// Optional deps are skipped unless --include-optional is passed, but
|
||||
// Recommended deps are pulled in unless --skip-recommended is passed -
|
||||
// it's "recommended", not "extra".
|
||||
idx := fixtureIndex(t, "x86")
|
||||
opts := &Options{
|
||||
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||
HostArch: "x86",
|
||||
OnlyHost: true,
|
||||
}
|
||||
selected, err := ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := idsOf(selected)
|
||||
|
||||
mustContain(t, ids, "Microsoft.VisualStudio.Workload.VCTools")
|
||||
mustContain(t, ids, "Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
|
||||
mustContain(t, ids, "Microsoft.VC.Tools.Core")
|
||||
mustContain(t, ids, "Microsoft.VC.Tools.Recommended")
|
||||
mustNotContain(t, ids, "Microsoft.VC.Tools.Optional")
|
||||
}
|
||||
|
||||
func TestAggregateDependsIncludeOptional(t *testing.T) {
|
||||
idx := fixtureIndex(t, "x86")
|
||||
opts := &Options{
|
||||
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||
HostArch: "x86",
|
||||
OnlyHost: true,
|
||||
IncludeOptional: true,
|
||||
}
|
||||
selected, err := ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := idsOf(selected)
|
||||
mustContain(t, ids, "Microsoft.VC.Tools.Optional")
|
||||
// Recommended is included by default (only --skip-recommended excludes it).
|
||||
mustContain(t, ids, "Microsoft.VC.Tools.Recommended")
|
||||
}
|
||||
|
||||
func TestAggregateDependsSkipRecommended(t *testing.T) {
|
||||
idx := fixtureIndex(t, "x86")
|
||||
opts := &Options{
|
||||
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||
HostArch: "x86",
|
||||
OnlyHost: true,
|
||||
SkipRecommended: true,
|
||||
}
|
||||
selected, err := ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := idsOf(selected)
|
||||
mustNotContain(t, ids, "Microsoft.VC.Tools.Recommended")
|
||||
}
|
||||
|
||||
func TestAggregateDependsRespectsIgnore(t *testing.T) {
|
||||
idx := fixtureIndex(t, "x86")
|
||||
opts := &Options{
|
||||
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||
Ignore: []string{"microsoft.vc.tools.core"},
|
||||
HostArch: "x86",
|
||||
OnlyHost: true,
|
||||
}
|
||||
selected, err := ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := idsOf(selected)
|
||||
mustNotContain(t, ids, "Microsoft.VC.Tools.Core")
|
||||
mustContain(t, ids, "Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
|
||||
}
|
||||
|
||||
func TestAggregateDependsHostArchMismatchExcludes(t *testing.T) {
|
||||
idx := fixtureIndex(t, "x86")
|
||||
opts := &Options{
|
||||
Package: []string{"Microsoft.VC.Tools.Core"},
|
||||
HostArch: "arm", // no arm variant exists, only x86/arm64
|
||||
OnlyHost: true,
|
||||
}
|
||||
selected, err := ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(selected) != 0 {
|
||||
t.Errorf("expected no packages selected for mismatched host arch, got %v", idsOf(selected))
|
||||
}
|
||||
}
|
||||
|
||||
func idsOf(pkgs []*Package) []string {
|
||||
var ids []string
|
||||
for _, p := range pkgs {
|
||||
ids = append(ids, p.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func mustContain(t *testing.T, ids []string, want string) {
|
||||
t.Helper()
|
||||
for _, id := range ids {
|
||||
if id == want {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("expected %v to contain %q", ids, want)
|
||||
}
|
||||
|
||||
func mustNotContain(t *testing.T, ids []string, unwanted string) {
|
||||
t.Helper()
|
||||
for _, id := range ids {
|
||||
if id == unwanted {
|
||||
t.Errorf("expected %v to NOT contain %q", ids, unwanted)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FixIncludeOptions controls how FixInclude rewrites include directives.
|
||||
type FixIncludeOptions struct {
|
||||
// MapWinSDK restores the canonical "GL/" casing after lowercasing,
|
||||
// since that's the cross-platform spelling for that header directory.
|
||||
MapWinSDK bool
|
||||
}
|
||||
|
||||
// reIncludeLine matches `#include <foo/bar.h>` or `#include "foo.h"`, but
|
||||
// not `#include IDENTIFIER` (macro-expanded includes).
|
||||
var reIncludeLine = regexp.MustCompile(`^\s*#\s*include\s+["<][\w.\\/]+[">]`)
|
||||
|
||||
// FixInclude rewrites #include directives under root to reference the
|
||||
// lowercase header names produced by Lowercase, since MSVC/WinSDK headers
|
||||
// reference each other with casing that's internally inconsistent (but
|
||||
// self-consistent once lowercased). Every text file under root is
|
||||
// rewritten with normalized (LF) line endings as a side effect.
|
||||
func FixInclude(root string, opts FixIncludeOptions) error {
|
||||
var doDir func(dir string) error
|
||||
doDir = func(dir string) error {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
path := filepath.Join(dir, e.Name())
|
||||
if e.Type()&os.ModeSymlink != 0 {
|
||||
continue
|
||||
}
|
||||
if e.IsDir() {
|
||||
if err := doDir(path); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := fixFile(path, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return doDir(root)
|
||||
}
|
||||
|
||||
func fixFile(path string, opts FixIncludeOptions) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for i, line := range lines {
|
||||
if reIncludeLine.MatchString(line) {
|
||||
code, comment := line, ""
|
||||
if idx := strings.Index(line, "//"); idx >= 0 {
|
||||
code, comment = line[:idx], line[idx:]
|
||||
}
|
||||
code = lowercaseAndSlash(code)
|
||||
if opts.MapWinSDK {
|
||||
code = strings.Replace(code, "gl/", "GL/", 1)
|
||||
}
|
||||
line = code + comment
|
||||
}
|
||||
lines[i] = strings.TrimRight(line, "\r\n")
|
||||
}
|
||||
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644)
|
||||
}
|
||||
|
||||
// lowercaseAndSlash lowercases ASCII letters and turns backslashes into
|
||||
// forward slashes, leaving everything else (including non-ASCII bytes)
|
||||
// untouched.
|
||||
func lowercaseAndSlash(s string) string {
|
||||
b := []byte(s)
|
||||
for i, c := range b {
|
||||
switch {
|
||||
case c >= 'A' && c <= 'Z':
|
||||
b[i] = c - 'A' + 'a'
|
||||
case c == '\\':
|
||||
b[i] = '/'
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// Package install wires up a downloaded MSVC/WinSDK tree so the msvc-go-wine
|
||||
// wrapper commands can find it: locating the installed toolchain/SDK
|
||||
// versions, fixing up header/library name casing, laying out the
|
||||
// per-architecture tool symlinks, and building the toolrelay helper.
|
||||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/assets"
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||
)
|
||||
|
||||
var archs = []string{"x86", "x64", "arm", "arm64"}
|
||||
|
||||
// Install wires up dest (a directory previously populated by
|
||||
// `msvc-go-wine download --dest dest`) with the tool wrapper symlinks and
|
||||
// env.json config the wrapper runtime expects. selfBinary is the path to
|
||||
// the currently running msvc-go-wine executable, copied into dest/bin so
|
||||
// the arch-specific tool symlinks have something to point at.
|
||||
func Install(dest, selfBinary string) error {
|
||||
dest, err := filepath.Abs(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi, err := os.Stat(dest); err != nil || !fi.IsDir() {
|
||||
return fmt.Errorf("destination %q is not a directory", dest)
|
||||
}
|
||||
|
||||
// Targets are relative so the whole installed tree stays relocatable -
|
||||
// moving or renaming dest doesn't break these symlinks the way an
|
||||
// absolute target baked in at install time would.
|
||||
if err := lnS("Windows Kits", filepath.Join(dest, "kits")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lnS("VC", filepath.Join(dest, "vc")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lnS("Tools", filepath.Join(dest, "vc", "tools")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lnS("MSVC", filepath.Join(dest, "vc", "tools", "msvc")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msvcRoot := filepath.Join(dest, "vc", "tools", "msvc")
|
||||
msvcVer, err := findMSVCVersion(msvcRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Using MSVC version", msvcVer)
|
||||
msvcDir := filepath.Join(msvcRoot, msvcVer)
|
||||
|
||||
if err := fixLibCasing(filepath.Join(msvcDir, "lib")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
includeDir := filepath.Join(msvcDir, "include")
|
||||
if err := Lowercase(includeDir, LowercaseOptions{Symlink: true}); err != nil {
|
||||
return fmt.Errorf("lowercasing %s: %w", includeDir, err)
|
||||
}
|
||||
if err := FixInclude(includeDir, FixIncludeOptions{}); err != nil {
|
||||
return fmt.Errorf("fixing includes in %s: %w", includeDir, err)
|
||||
}
|
||||
atlIncludeDir := filepath.Join(msvcDir, "atlmfc", "include")
|
||||
if isDir(atlIncludeDir) {
|
||||
if err := FixInclude(atlIncludeDir, FixIncludeOptions{}); err != nil {
|
||||
return fmt.Errorf("fixing includes in %s: %w", atlIncludeDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
binDir := filepath.Join(msvcDir, "bin")
|
||||
if err := removeVctip(binDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := renameHostDirs(binDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kits10 := filepath.Join(dest, "kits", "10")
|
||||
if !isDir(kits10) {
|
||||
return fmt.Errorf("%s not found - expected a Windows SDK already unpacked by `msvc-go-wine download`", kits10)
|
||||
}
|
||||
if err := lnS("Lib", filepath.Join(kits10, "lib")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lnS("Include", filepath.Join(kits10, "include")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sdkVer, err := findSDKVersion(filepath.Join(kits10, "include"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Using SDK version", sdkVer)
|
||||
|
||||
for _, sub := range []string{"um", "shared", "winrt", "km"} {
|
||||
dir := filepath.Join(kits10, "include", sdkVer, sub)
|
||||
if !isDir(dir) {
|
||||
continue
|
||||
}
|
||||
if err := Lowercase(dir, LowercaseOptions{Symlink: true, MapWinSDK: true}); err != nil {
|
||||
return fmt.Errorf("lowercasing %s: %w", dir, err)
|
||||
}
|
||||
if err := FixInclude(dir, FixIncludeOptions{MapWinSDK: true}); err != nil {
|
||||
return fmt.Errorf("fixing includes in %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
wdfDir := filepath.Join(kits10, "include", "wdf")
|
||||
if isDir(wdfDir) {
|
||||
if err := Lowercase(wdfDir, LowercaseOptions{Symlink: true, MapWinSDK: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FixInclude(wdfDir, FixIncludeOptions{MapWinSDK: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, arch := range archs {
|
||||
for _, sub := range []string{"um", "km"} {
|
||||
dir := filepath.Join(kits10, "lib", sdkVer, sub, arch)
|
||||
if !isDir(dir) {
|
||||
continue
|
||||
}
|
||||
if err := Lowercase(dir, LowercaseOptions{Symlink: true}); err != nil {
|
||||
return fmt.Errorf("lowercasing %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host, dotnetHost := hostArch()
|
||||
|
||||
modulesRel := filepath.Join("VC", "Tools", "MSVC", msvcVer, "modules")
|
||||
if isDir(filepath.Join(dest, modulesRel)) {
|
||||
if err := lnS(modulesRel, filepath.Join(dest, "modules")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
destBin := filepath.Join(dest, "bin")
|
||||
if err := os.MkdirAll(destBin, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
sharedBinary := filepath.Join(destBin, "msvc-go-wine")
|
||||
if err := copyFile(selfBinary, sharedBinary, 0o755); err != nil {
|
||||
return fmt.Errorf("installing shared binary: %w", err)
|
||||
}
|
||||
|
||||
installed := 0
|
||||
for _, arch := range archs {
|
||||
clExe := filepath.Join(msvcDir, "bin", "Host"+host, arch, "cl.exe")
|
||||
if !isFile(clExe) {
|
||||
continue
|
||||
}
|
||||
if err := setupWrapperDir(destBin, selfBinary, arch, host, dotnetHost, msvcVer, sdkVer); err != nil {
|
||||
return err
|
||||
}
|
||||
installed++
|
||||
fmt.Println("Installed tool wrappers for", arch)
|
||||
}
|
||||
if installed == 0 {
|
||||
return fmt.Errorf("no target architecture found under %s/bin/Host%s/*", msvcDir, host)
|
||||
}
|
||||
|
||||
if bootstrapWine() {
|
||||
// Best-effort: if this fails, the wrapper runtime just falls back
|
||||
// to invoking tools directly through wine, without toolrelay.exe's
|
||||
// mt.exe/CMake exit-code fixup.
|
||||
if err := buildToolRelay(dest, destBin, host); err != nil {
|
||||
fmt.Println("Building toolrelay failed (continuing without it):", err)
|
||||
} else {
|
||||
fmt.Println("Build toolrelay done.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMSVCVersion(msvcRoot string) (string, error) {
|
||||
entries, err := os.ReadDir(msvcRoot)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading %s: %w", msvcRoot, err)
|
||||
}
|
||||
var names []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
}
|
||||
// `ls -r`: reverse of the default (lexical) sort order.
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(names)))
|
||||
for _, name := range names {
|
||||
dir := filepath.Join(msvcRoot, name)
|
||||
if isDir(filepath.Join(dir, "bin")) && isDir(filepath.Join(dir, "include")) && isDir(filepath.Join(dir, "lib")) {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no suitable MSVC version found under %s", msvcRoot)
|
||||
}
|
||||
|
||||
func findSDKVersion(includeDir string) (string, error) {
|
||||
entries, err := os.ReadDir(includeDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading %s: %w", includeDir, err)
|
||||
}
|
||||
var versions []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() && strings.HasPrefix(e.Name(), "10.") {
|
||||
versions = append(versions, e.Name())
|
||||
}
|
||||
}
|
||||
if len(versions) == 0 {
|
||||
return "", fmt.Errorf("no Windows SDK version (10.*) found under %s", includeDir)
|
||||
}
|
||||
sort.Strings(versions)
|
||||
return versions[len(versions)-1], nil
|
||||
}
|
||||
|
||||
// fixLibCasing adds uppercase symlinks (LIBCMT.lib -> libcmt.lib, etc) so
|
||||
// lld-link can resolve the /DEFAULTLIB directives cl.exe emits, which name
|
||||
// these libs in upper case, on a case-sensitive filesystem.
|
||||
func fixLibCasing(libDir string) error {
|
||||
names := []string{"libcmt", "libcmtd", "msvcrt", "msvcrtd", "oldnames"}
|
||||
for _, arch := range archs {
|
||||
dir := filepath.Join(libDir, arch)
|
||||
if !isDir(dir) {
|
||||
continue
|
||||
}
|
||||
for _, n := range names {
|
||||
lower := filepath.Join(dir, n+".lib")
|
||||
if !isFile(lower) {
|
||||
continue
|
||||
}
|
||||
upper := filepath.Join(dir, strings.ToUpper(n)+".lib")
|
||||
if err := lnS(n+".lib", upper); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeVctip deletes any vctip.exe found under dir: it phones home to
|
||||
// Microsoft and is known to cause problems under Wine.
|
||||
func removeVctip(dir string) error {
|
||||
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && strings.EqualFold(d.Name(), "vctip.exe") {
|
||||
return os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// renameHostDirs normalizes the several casings different MSVC releases
|
||||
// have used for their host-arch bin directories.
|
||||
func renameHostDirs(binDir string) error {
|
||||
renames := []struct{ from, to string }{
|
||||
{"HostX64", "Hostx64"}, // 15.x - 16.4
|
||||
{"HostARM64", "Hostarm64"}, // 17.2 - 17.3
|
||||
{"HostArm64", "Hostarm64"}, // 17.4
|
||||
}
|
||||
for _, r := range renames {
|
||||
from := filepath.Join(binDir, r.from)
|
||||
to := filepath.Join(binDir, r.to)
|
||||
if isDir(from) && !exists(to) {
|
||||
if err := os.Rename(from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
oldArm64 := filepath.Join(binDir, "Hostarm64", "ARM64")
|
||||
newArm64 := filepath.Join(binDir, "Hostarm64", "arm64")
|
||||
if isDir(oldArm64) && !exists(newArm64) {
|
||||
if err := os.Rename(oldArm64, newArm64); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupWrapperDir creates <destBin>/<arch> with its own local copy of the
|
||||
// msvc-go-wine binary (not a symlink to the shared one in destBin), and
|
||||
// symlinks every tool name to that LOCAL copy.
|
||||
//
|
||||
// This matters: the wrapper runtime locates its own install root via
|
||||
// os.Executable(), which fully resolves symlinks (like /proc/self/exe does)
|
||||
// - it can't rely on os.Args[0], since not every shell passes a
|
||||
// PATH-resolved absolute path as argv[0] (some just pass the bare command
|
||||
// name, e.g. "cl", which would make a naive argv[0]-based lookup resolve
|
||||
// against the caller's cwd instead of the install dir). A same-directory
|
||||
// symlink (cl -> msvc-go-wine) resolves to a binary that's still in the
|
||||
// right arch dir; a symlink to a binary one level up (cl -> ../msvc-go-wine)
|
||||
// would not be.
|
||||
func setupWrapperDir(destBin, selfBinary, arch, host, dotnetHost, msvcVer, sdkVer string) error {
|
||||
archDir := filepath.Join(destBin, arch)
|
||||
if err := os.MkdirAll(archDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
localBinary := filepath.Join(archDir, "msvc-go-wine")
|
||||
if err := copyFile(selfBinary, localBinary, 0o755); err != nil {
|
||||
return fmt.Errorf("installing per-arch binary: %w", err)
|
||||
}
|
||||
for name := range toolNames {
|
||||
if err := lnS("msvc-go-wine", filepath.Join(archDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lnS("msvc-go-wine", filepath.Join(archDir, name+".exe")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cfg := &wineenv.Config{
|
||||
Arch: arch,
|
||||
Host: host,
|
||||
DotnetHost: dotnetHost,
|
||||
MSVCVer: msvcVer,
|
||||
SDKVer: sdkVer,
|
||||
}
|
||||
return cfg.Save(archDir)
|
||||
}
|
||||
|
||||
// toolNames is the set of tool wrapper symlinks created in every arch dir.
|
||||
var toolNames = map[string]bool{
|
||||
"cl": true, "link": true, "lib": true, "ml": true, "ml64": true,
|
||||
"mc": true, "midl": true, "mt": true, "rc": true, "dumpbin": true,
|
||||
"msbuild": true, "nmake": true, "armasm": true, "armasm64": true,
|
||||
"cmd": true, "findstr": true,
|
||||
}
|
||||
|
||||
func hostArch() (host, dotnetHost string) {
|
||||
switch runtime.GOARCH {
|
||||
case "arm64":
|
||||
return "arm64", "arm64"
|
||||
default:
|
||||
return "x64", "amd64"
|
||||
}
|
||||
}
|
||||
|
||||
// bootstrapWine runs `wineboot --init` to set up the wine prefix ahead of
|
||||
// time, and reports whether wine is available at all (so callers can decide
|
||||
// whether to attempt building toolrelay.exe).
|
||||
func bootstrapWine() bool {
|
||||
wine, err := wineenv.FindWine()
|
||||
if err != nil {
|
||||
fmt.Println("wine not found, skipping wineboot bootstrap and toolrelay build (install wine before running the tools)")
|
||||
return false
|
||||
}
|
||||
fmt.Println("Bootstrapping wine prefix...")
|
||||
if err := runQuiet(wine, "wineboot", "--init"); err != nil {
|
||||
fmt.Println("wineboot --init failed (continuing):", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// buildToolRelay compiles the vendored toolrelay.cpp helper using the
|
||||
// freshly-installed host-arch cl wrapper. toolrelay.exe is what lets the
|
||||
// wrapper runtime give mt.exe's CMake-compatibility exit code
|
||||
// (0x41020001 -> 0xbb) a chance to survive Wine's own exit-code truncation -
|
||||
// see internal/wrapper's runViaToolRelay. Best-effort: any failure here just
|
||||
// means the wrapper runtime falls back to a plain wine invocation without
|
||||
// that fixup.
|
||||
func buildToolRelay(dest, destBin, host string) error {
|
||||
clWrapper := filepath.Join(destBin, host, "cl")
|
||||
if !isFile(clWrapper) {
|
||||
return fmt.Errorf("no cl wrapper for host arch %s at %s", host, clWrapper)
|
||||
}
|
||||
|
||||
srcPath := filepath.Join(dest, "toolrelay.cpp")
|
||||
if err := os.WriteFile(srcPath, assets.ToolRelaySource, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(srcPath)
|
||||
|
||||
fmt.Println("Build toolrelay ...")
|
||||
cmd := exec.Command(clWrapper, "/EHsc", "/O2", srcPath)
|
||||
cmd.Dir = dest
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("compiling toolrelay.cpp: %w", err)
|
||||
}
|
||||
|
||||
exePath := filepath.Join(dest, "toolrelay.exe")
|
||||
objPath := filepath.Join(dest, "toolrelay.obj")
|
||||
defer os.Remove(objPath)
|
||||
if !isFile(exePath) {
|
||||
return fmt.Errorf("cl did not produce %s", exePath)
|
||||
}
|
||||
return os.Rename(exePath, filepath.Join(destBin, "toolrelay.exe"))
|
||||
}
|
||||
|
||||
func runQuiet(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Env = append(os.Environ(), "WINEDEBUG=-all")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func lnS(target, link string) error {
|
||||
if _, err := os.Lstat(link); err == nil {
|
||||
return nil
|
||||
}
|
||||
return os.Symlink(target, link)
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
fi, err := os.Stat(path)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func isFile(path string) bool {
|
||||
fi, err := os.Stat(path)
|
||||
return err == nil && !fi.IsDir()
|
||||
}
|
||||
|
||||
func exists(path string) bool {
|
||||
_, err := os.Lstat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
_ = os.Remove(dst)
|
||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LowercaseOptions controls how Lowercase renames entries.
|
||||
type LowercaseOptions struct {
|
||||
// Symlink adds lowercase-named symlinks alongside the original entries
|
||||
// instead of renaming them in place (used for WinSDK/MSVC headers so
|
||||
// both casings stay available; renaming would break other packages
|
||||
// that reference the original casing).
|
||||
Symlink bool
|
||||
// MapWinSDK is shorthand for path-keyed overrides that keep the "GL"
|
||||
// header directory's canonical uppercase spelling.
|
||||
MapWinSDK bool
|
||||
}
|
||||
|
||||
// Lowercase recursively lowercases every file/dir name under root, merging
|
||||
// into an existing same-named lowercase directory on collision.
|
||||
func Lowercase(root string, opts LowercaseOptions) error {
|
||||
mapPaths := map[string]string{}
|
||||
if opts.MapWinSDK {
|
||||
mapPaths["gl"] = "GL"
|
||||
}
|
||||
|
||||
remap := func(relPath string) string {
|
||||
rp := strings.TrimSuffix(relPath, "/")
|
||||
base := rp
|
||||
if idx := strings.LastIndexByte(rp, '/'); idx >= 0 {
|
||||
base = rp[idx+1:]
|
||||
}
|
||||
if opts.MapWinSDK {
|
||||
if v, ok := mapPaths[strings.ToLower(rp)]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return strings.ToLower(base)
|
||||
}
|
||||
|
||||
var doDir func(dir, relPath string) error
|
||||
doDir = func(dir, relPath string) error {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
childPath := filepath.Join(dir, name)
|
||||
if e.IsDir() {
|
||||
if err := doDir(childPath, relPath+name+"/"); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
newName := remap(relPath + name)
|
||||
if newName != name {
|
||||
if err := renameOrSymlink(childPath, dir, newName, opts.Symlink); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newName string
|
||||
if relPath == "" {
|
||||
newName = strings.ToLower(filepath.Base(dir))
|
||||
} else {
|
||||
newName = remap(relPath)
|
||||
}
|
||||
oldName := filepath.Base(dir)
|
||||
if oldName == newName {
|
||||
return nil
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
newPath := filepath.Join(parent, newName)
|
||||
if fi, err := os.Stat(newPath); err == nil && fi.IsDir() {
|
||||
return combineIntoDir(dir, newPath, opts.Symlink)
|
||||
}
|
||||
return renameOrSymlink(dir, parent, newName, opts.Symlink)
|
||||
}
|
||||
|
||||
return doDir(root, "")
|
||||
}
|
||||
|
||||
func renameOrSymlink(src, destDir, destName string, symlink bool) error {
|
||||
dest := filepath.Join(destDir, destName)
|
||||
if symlink {
|
||||
if _, err := os.Lstat(dest); err == nil {
|
||||
// A conflicting entry already exists at dest - this happens on
|
||||
// case-insensitive filesystems where dest and src are the same
|
||||
// path, so treat it as already done rather than failing.
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(destDir, src)
|
||||
if err != nil {
|
||||
rel = src
|
||||
}
|
||||
return os.Symlink(rel, dest)
|
||||
}
|
||||
return os.Rename(src, dest)
|
||||
}
|
||||
|
||||
// combineIntoDir moves (or symlinks) every entry of src into the already-existing
|
||||
// dest, recursing into same-named subdirectories, then removes src (unless
|
||||
// symlink mode, where src's real content must be left in place).
|
||||
func combineIntoDir(src, dest string, symlink bool) error {
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
srcChild := filepath.Join(src, name)
|
||||
destChild := filepath.Join(dest, name)
|
||||
if fi, err := os.Stat(destChild); err == nil && fi.IsDir() && e.IsDir() {
|
||||
if err := combineIntoDir(srcChild, destChild, symlink); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := renameOrSymlink(srcChild, dest, name, symlink); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !symlink {
|
||||
return os.Remove(src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustMkdirAll(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteFile(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
mustMkdirAll(t, filepath.Dir(path))
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowercaseRenameMode(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
// The top-level dir passed to Lowercase is itself lowercased too when
|
||||
// not already lowercase (matching the original perl script's dodir,
|
||||
// which lowercases relpath=="" using the dir's own basename).
|
||||
tree := filepath.Join(root, "Include")
|
||||
mustWriteFile(t, filepath.Join(tree, "Foo", "Bar.H"), "content")
|
||||
|
||||
if err := Lowercase(tree, LowercaseOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lowerTree := filepath.Join(root, "include")
|
||||
if !isFile(filepath.Join(lowerTree, "foo", "bar.h")) {
|
||||
t.Fatalf("expected lowercased path to exist under %s", lowerTree)
|
||||
}
|
||||
if isDir(tree) {
|
||||
t.Fatalf("original-cased top dir should have been renamed away")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowercaseSymlinkMode(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
tree := filepath.Join(root, "include")
|
||||
mustWriteFile(t, filepath.Join(tree, "Foo", "Bar.h"), "content")
|
||||
|
||||
if err := Lowercase(tree, LowercaseOptions{Symlink: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Original casing must still be reachable (symlink mode never deletes).
|
||||
if !isFile(filepath.Join(tree, "Foo", "Bar.h")) {
|
||||
t.Fatalf("original-cased file should still exist in symlink mode")
|
||||
}
|
||||
// Lowercase alias must resolve to the same content.
|
||||
data, err := os.ReadFile(filepath.Join(tree, "foo", "bar.h"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected lowercase alias to resolve: %v", err)
|
||||
}
|
||||
if string(data) != "content" {
|
||||
t.Errorf("got %q", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowercaseMergeOnCollision(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
tree := filepath.Join(root, "include")
|
||||
// Both "GL" and "gl" exist as siblings; lowercasing "GL" must merge its
|
||||
// contents into the already-lowercase "gl" rather than clobbering it.
|
||||
mustWriteFile(t, filepath.Join(tree, "gl", "existing.h"), "existing")
|
||||
mustWriteFile(t, filepath.Join(tree, "GL", "New.h"), "new")
|
||||
|
||||
if err := Lowercase(tree, LowercaseOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !isFile(filepath.Join(tree, "gl", "existing.h")) {
|
||||
t.Errorf("pre-existing lowercase file lost during merge")
|
||||
}
|
||||
if !isFile(filepath.Join(tree, "gl", "new.h")) {
|
||||
t.Errorf("merged file not found at lowercase destination")
|
||||
}
|
||||
if isDir(filepath.Join(tree, "GL")) {
|
||||
t.Errorf("source dir should have been removed after merge (non-symlink mode)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowercaseMapWinSDKPreservesGL(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
tree := filepath.Join(root, "um")
|
||||
mustWriteFile(t, filepath.Join(tree, "GL", "gl.h"), "content")
|
||||
|
||||
if err := Lowercase(tree, LowercaseOptions{Symlink: true, MapWinSDK: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !isDir(filepath.Join(tree, "GL")) {
|
||||
t.Errorf("GL directory casing should be preserved under -map_winsdk")
|
||||
}
|
||||
if isDir(filepath.Join(tree, "gl")) {
|
||||
t.Errorf("no lowercase alias should be created for GL under -map_winsdk (name maps back to itself)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixIncludeLowercasesAndConvertsSlashes(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
header := filepath.Join(root, "foo.h")
|
||||
mustWriteFile(t, header, "#include <Some\\Path.H>\r\n#include \"Other.h\" // comment\r\nplain line\r\n")
|
||||
|
||||
if err := FixInclude(root, FixIncludeOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(header)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "#include <some/path.h>\n#include \"other.h\" // comment\nplain line\n"
|
||||
if string(data) != want {
|
||||
t.Errorf("got %q want %q", data, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixIncludeMapWinSDKPreservesGL(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
header := filepath.Join(root, "foo.h")
|
||||
mustWriteFile(t, header, "#include <GL/gl.h>\n")
|
||||
|
||||
if err := FixInclude(root, FixIncludeOptions{MapWinSDK: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(header)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "#include <GL/gl.h>\n"
|
||||
if string(data) != want {
|
||||
t.Errorf("got %q want %q", data, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixIncludeSkipsSymlinks(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
real := filepath.Join(root, "Real.h")
|
||||
mustWriteFile(t, real, "#include <Foo.h>\n")
|
||||
link := filepath.Join(root, "alias.h")
|
||||
if err := os.Symlink(real, link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := FixInclude(root, FixIncludeOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The symlink itself must be untouched (still a symlink to the same
|
||||
// target); only Real.h's contents get rewritten.
|
||||
target, err := os.Readlink(link)
|
||||
if err != nil {
|
||||
t.Fatalf("alias.h should still be a symlink: %v", err)
|
||||
}
|
||||
if target != real {
|
||||
t.Errorf("symlink target changed: %q", target)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestBuildToolRelay exercises the file shuffling around the actual
|
||||
// compiler invocation (write embedded source, run "cl", move the resulting
|
||||
// .exe into bin/, clean up the .obj and temp source) using a fake `cl`
|
||||
// shell script standing in for the real Wine-hosted compiler - this can
|
||||
// run fully offline, without wine or a real MSVC install.
|
||||
func TestBuildToolRelay(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
destBin := filepath.Join(dest, "bin")
|
||||
hostDir := filepath.Join(destBin, "x64")
|
||||
if err := os.MkdirAll(hostDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fake `cl`: just drop toolrelay.exe/.obj next to the source it was
|
||||
// given, like the real cl.exe would when invoked without /Fe or /Fo.
|
||||
fakeCl := filepath.Join(hostDir, "cl")
|
||||
script := "#!/bin/sh\ntouch toolrelay.exe toolrelay.obj\n"
|
||||
if err := os.WriteFile(fakeCl, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := buildToolRelay(dest, destBin, "x64"); err != nil {
|
||||
t.Fatalf("buildToolRelay: %v", err)
|
||||
}
|
||||
|
||||
if !isFile(filepath.Join(destBin, "toolrelay.exe")) {
|
||||
t.Errorf("expected %s/toolrelay.exe to exist", destBin)
|
||||
}
|
||||
if exists(filepath.Join(dest, "toolrelay.obj")) {
|
||||
t.Errorf("toolrelay.obj should have been removed from %s", dest)
|
||||
}
|
||||
if exists(filepath.Join(dest, "toolrelay.cpp")) {
|
||||
t.Errorf("temp toolrelay.cpp source should have been removed from %s", dest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildToolRelayNoClWrapper(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
destBin := filepath.Join(dest, "bin")
|
||||
if err := os.MkdirAll(destBin, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := buildToolRelay(dest, destBin, "x64"); err == nil {
|
||||
t.Fatal("expected an error when no host-arch cl wrapper exists")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Package wineenv computes the environment (INCLUDE/LIB/WINEPATH/...) needed
|
||||
// to run a Wine-hosted MSVC tool.
|
||||
package wineenv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ConfigFileName is the per-architecture config dropped next to the tool
|
||||
// symlinks by `msvc-go-wine install`.
|
||||
const ConfigFileName = "env.json"
|
||||
|
||||
// Config is the per-architecture info generated at install time.
|
||||
type Config struct {
|
||||
Arch string `json:"arch"` // x86, x64, arm, arm64 (target arch)
|
||||
Host string `json:"host"` // x64 or arm64 (Host<Host> bin dir suffix)
|
||||
DotnetHost string `json:"dotnet_host"` // amd64 or arm64 (.NET tool host dir suffix)
|
||||
MSVCVer string `json:"msvc_ver"`
|
||||
SDKVer string `json:"sdk_ver"`
|
||||
}
|
||||
|
||||
// Load reads env.json from dir (the directory the running binary was
|
||||
// invoked from, i.e. <dest>/bin/<arch>).
|
||||
func Load(dir string) (*Config, error) {
|
||||
data, err := os.ReadFile(filepath.Join(dir, ConfigFileName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s: %w", ConfigFileName, err)
|
||||
}
|
||||
var c Config
|
||||
if err := json.Unmarshal(data, &c); err != nil {
|
||||
return nil, fmt.Errorf("parsing %s: %w", ConfigFileName, err)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// Save writes env.json into dir.
|
||||
func (c *Config) Save(dir string) error {
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(dir, ConfigFileName), data, 0o644)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package wineenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Paths holds every path/env-var derived from a Config plus the on-disk
|
||||
// installation root - INCLUDE, LIB, WINEPATH and friends, in the Windows
|
||||
// notation Wine expects.
|
||||
type Paths struct {
|
||||
BaseUnix string // <dest>, absolute unix path
|
||||
MSVCDirUnix string // <dest>/vc/tools/msvc/<ver>
|
||||
BinDir string // <dest>/vc/tools/msvc/<ver>/bin/Host<host>/<arch> - cl/link/lib/ml/nmake/armasm live here
|
||||
SDKBinDir string // <dest>/kits/10/bin/<sdkver>/<host> - mc/midl/mt/rc live here
|
||||
MSBuildBinDir string // <dest>/MSBuild/Current/Bin/<dotnetHost> - MSBuild.exe lives here
|
||||
|
||||
Include string
|
||||
Lib string
|
||||
LibPath string
|
||||
WinePath string
|
||||
WineDLLOverrides string
|
||||
}
|
||||
|
||||
// FindBaseUnix locates the installation root starting from the directory the
|
||||
// tool wrapper was invoked from (scriptDir), searching one or two levels up
|
||||
// for a "vc" entry - matching the original wrappers' `dirname "$0"`/".." walk,
|
||||
// which tolerates wrappers living either directly in <dest>/bin/<arch> or
|
||||
// nested one level deeper.
|
||||
func FindBaseUnix(scriptDir string) (string, error) {
|
||||
base, err := filepath.Abs(filepath.Join(scriptDir, ".."))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(base, "vc")); err != nil {
|
||||
base = filepath.Join(base, "..")
|
||||
}
|
||||
return filepath.Abs(base)
|
||||
}
|
||||
|
||||
// NewPaths computes all derived paths/env-vars for cfg installed at baseUnix.
|
||||
func NewPaths(cfg *Config, baseUnix string) *Paths {
|
||||
winBase := "z:" + toWindowsSlashes(baseUnix)
|
||||
msvcBase := winBase + `\vc`
|
||||
sdkBase := winBase + `\kits\10`
|
||||
msvcDirWin := msvcBase + `\tools\msvc\` + cfg.MSVCVer
|
||||
sdkIncludeWin := sdkBase + `\include\` + cfg.SDKVer
|
||||
sdkLibWin := sdkBase + `\lib\` + cfg.SDKVer
|
||||
|
||||
msvcDirUnix := filepath.Join(baseUnix, "vc", "tools", "msvc", cfg.MSVCVer)
|
||||
binDir := filepath.Join(msvcDirUnix, "bin", "Host"+cfg.Host, cfg.Arch)
|
||||
sdkBinDir := filepath.Join(baseUnix, "kits", "10", "bin", cfg.SDKVer, cfg.Host)
|
||||
msbuildBinDir := filepath.Join(baseUnix, "MSBuild", "Current", "Bin", cfg.DotnetHost)
|
||||
|
||||
include := strings.Join([]string{
|
||||
msvcDirWin + `\atlmfc\include`,
|
||||
msvcDirWin + `\include`,
|
||||
sdkIncludeWin + `\shared`,
|
||||
sdkIncludeWin + `\ucrt`,
|
||||
sdkIncludeWin + `\um`,
|
||||
sdkIncludeWin + `\winrt`,
|
||||
sdkIncludeWin + `\km`,
|
||||
}, ";")
|
||||
|
||||
lib := strings.Join([]string{
|
||||
msvcDirWin + `\atlmfc\lib\` + cfg.Arch,
|
||||
msvcDirWin + `\lib\` + cfg.Arch,
|
||||
sdkLibWin + `\ucrt\` + cfg.Arch,
|
||||
sdkLibWin + `\um\` + cfg.Arch,
|
||||
sdkLibWin + `\km\` + cfg.Arch,
|
||||
}, ";")
|
||||
|
||||
// WINEPATH: unix dirs with slashes swapped for backslashes (no drive
|
||||
// letter - Wine accepts this for locating DLLs), plus the MSVC host bin
|
||||
// dir in full windows notation, deliberately always the *host* (not
|
||||
// target) x64/arm64 tool dir for the third entry - that's where DLLs
|
||||
// like mspdbcore.dll actually live.
|
||||
winePath := strings.Join([]string{
|
||||
toWindowsSlashes(binDir),
|
||||
toWindowsSlashes(sdkBinDir),
|
||||
msvcDirWin + `\bin\Host` + cfg.Host + `\` + cfg.Host,
|
||||
}, ";")
|
||||
|
||||
return &Paths{
|
||||
BaseUnix: baseUnix,
|
||||
MSVCDirUnix: msvcDirUnix,
|
||||
BinDir: binDir,
|
||||
SDKBinDir: sdkBinDir,
|
||||
MSBuildBinDir: msbuildBinDir,
|
||||
|
||||
Include: include,
|
||||
Lib: lib,
|
||||
LibPath: lib,
|
||||
WinePath: winePath,
|
||||
WineDLLOverrides: "vcruntime140=n;vcruntime140_1=n",
|
||||
}
|
||||
}
|
||||
|
||||
func toWindowsSlashes(p string) string {
|
||||
return strings.ReplaceAll(p, "/", `\`)
|
||||
}
|
||||
|
||||
// ToWinPath prefixes a unix absolute path with the "z:" drive Wine maps the
|
||||
// unix root to, converting slashes to backslashes.
|
||||
func ToWinPath(unixPath string) string {
|
||||
return "z:" + toWindowsSlashes(unixPath)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package wineenv
|
||||
|
||||
import "testing"
|
||||
|
||||
// Values lifted from the original wrappers/cl template
|
||||
// (MSVCVER=14.13.26128, SDKVER=10.0.16299.0, ARCH=x86) to cross-check the Go
|
||||
// port produces byte-identical INCLUDE/LIB strings.
|
||||
func TestNewPathsMatchesOriginalTemplate(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Arch: "x86",
|
||||
Host: "x64",
|
||||
DotnetHost: "amd64",
|
||||
MSVCVer: "14.13.26128",
|
||||
SDKVer: "10.0.16299.0",
|
||||
}
|
||||
p := NewPaths(cfg, "/opt/msvc")
|
||||
|
||||
wantInclude := `z:\opt\msvc\vc\tools\msvc\14.13.26128\atlmfc\include;` +
|
||||
`z:\opt\msvc\vc\tools\msvc\14.13.26128\include;` +
|
||||
`z:\opt\msvc\kits\10\include\10.0.16299.0\shared;` +
|
||||
`z:\opt\msvc\kits\10\include\10.0.16299.0\ucrt;` +
|
||||
`z:\opt\msvc\kits\10\include\10.0.16299.0\um;` +
|
||||
`z:\opt\msvc\kits\10\include\10.0.16299.0\winrt;` +
|
||||
`z:\opt\msvc\kits\10\include\10.0.16299.0\km`
|
||||
if p.Include != wantInclude {
|
||||
t.Errorf("INCLUDE mismatch:\n got: %s\nwant: %s", p.Include, wantInclude)
|
||||
}
|
||||
|
||||
wantLib := `z:\opt\msvc\vc\tools\msvc\14.13.26128\atlmfc\lib\x86;` +
|
||||
`z:\opt\msvc\vc\tools\msvc\14.13.26128\lib\x86;` +
|
||||
`z:\opt\msvc\kits\10\lib\10.0.16299.0\ucrt\x86;` +
|
||||
`z:\opt\msvc\kits\10\lib\10.0.16299.0\um\x86;` +
|
||||
`z:\opt\msvc\kits\10\lib\10.0.16299.0\km\x86`
|
||||
if p.Lib != wantLib {
|
||||
t.Errorf("LIB mismatch:\n got: %s\nwant: %s", p.Lib, wantLib)
|
||||
}
|
||||
|
||||
if p.WineDLLOverrides != "vcruntime140=n;vcruntime140_1=n" {
|
||||
t.Errorf("WINEDLLOVERRIDES mismatch: %s", p.WineDLLOverrides)
|
||||
}
|
||||
|
||||
wantBinDir := "/opt/msvc/vc/tools/msvc/14.13.26128/bin/Hostx64/x86"
|
||||
if p.BinDir != wantBinDir {
|
||||
t.Errorf("BinDir mismatch:\n got: %s\nwant: %s", p.BinDir, wantBinDir)
|
||||
}
|
||||
|
||||
wantSDKBinDir := "/opt/msvc/kits/10/bin/10.0.16299.0/x64"
|
||||
if p.SDKBinDir != wantSDKBinDir {
|
||||
t.Errorf("SDKBinDir mismatch:\n got: %s\nwant: %s", p.SDKBinDir, wantSDKBinDir)
|
||||
}
|
||||
|
||||
wantMSBuildBinDir := "/opt/msvc/MSBuild/Current/Bin/amd64"
|
||||
if p.MSBuildBinDir != wantMSBuildBinDir {
|
||||
t.Errorf("MSBuildBinDir mismatch:\n got: %s\nwant: %s", p.MSBuildBinDir, wantMSBuildBinDir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package wineenv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// FindWine locates the wine binary to use, preferring wine64 like the
|
||||
// original wrappers (`command -v wine64 || command -v wine`).
|
||||
func FindWine() (string, error) {
|
||||
if p, err := exec.LookPath("wine64"); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
if p, err := exec.LookPath("wine"); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
return "", fmt.Errorf("neither wine64 nor wine found in PATH")
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// clPostProcess mirrors cl's post-run fixup for `/P /Fi<file>` (preprocess-
|
||||
// to-file): the generated file still has CRLF endings and z:-prefixed
|
||||
// #line directives, so run it through the same line-directive rewrite used
|
||||
// for stdout.
|
||||
func clPostProcess(origArgs []string) {
|
||||
var hasP bool
|
||||
var fiFile string
|
||||
for _, a := range origArgs {
|
||||
switch {
|
||||
case a == "-P" || a == "/P":
|
||||
hasP = true
|
||||
case strings.HasPrefix(a, "-Fi") || strings.HasPrefix(a, "/Fi"):
|
||||
fiFile = a[3:]
|
||||
}
|
||||
}
|
||||
if !hasP || fiFile == "" {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(fiFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for i, line := range lines {
|
||||
line = stripCR(line)
|
||||
if reLineDirective.MatchString(line) {
|
||||
line = strings.ReplaceAll(stripFirstZDrive(line), `\\`, `/`)
|
||||
}
|
||||
lines[i] = line
|
||||
}
|
||||
_ = os.WriteFile(fiFile, []byte(strings.Join(lines, "\n")), 0o644)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// lineFilter rewrites a single line of tool output. nil means "no extra
|
||||
// rewriting" (CR-stripping still always happens, see stripCR).
|
||||
type lineFilter func(line string) string
|
||||
|
||||
// reZDrive matches the wine "z:" drive prefix followed by a path separator,
|
||||
// case-insensitively, anywhere in the line - mirroring sed's `s/z:([\\/])/\1/i`
|
||||
// (no /g flag: only the first occurrence is touched).
|
||||
var reZDrive = regexp.MustCompile(`(?i)z:([\\/])`)
|
||||
|
||||
// stripFirstZDrive removes the first "z:" preceding a path separator,
|
||||
// keeping the separator itself, matching the original's un-global sed rule.
|
||||
func stripFirstZDrive(line string) string {
|
||||
loc := reZDrive.FindStringSubmatchIndex(line)
|
||||
if loc == nil {
|
||||
return line
|
||||
}
|
||||
sep := line[loc[2]:loc[3]]
|
||||
return line[:loc[0]] + sep + line[loc[1]:]
|
||||
}
|
||||
|
||||
var (
|
||||
reNoteIncluding = regexp.MustCompile(`^Note: including file: `)
|
||||
reLineDirective = regexp.MustCompile(`^[ \t]*#[ \t]*line[ \t]`)
|
||||
reNoteErrorWarning = regexp.MustCompile(`(?i)^z:.*\([0-9]+\): (note|error c[0-9]{4}|warning c[0-9]{4}): `)
|
||||
reDumpbinPath = regexp.MustCompile(`^(Dump of file | PDB file found at )`)
|
||||
)
|
||||
|
||||
// clStdoutFilter rewrites cl's "Note: including file:", "#line", and
|
||||
// note/warning/error diagnostic lines from wine's z:\... notation back to
|
||||
// plain unix paths.
|
||||
func clStdoutFilter(line string) string {
|
||||
switch {
|
||||
case reNoteIncluding.MatchString(line):
|
||||
return strings.ReplaceAll(stripFirstZDrive(line), `\`, `/`)
|
||||
case reLineDirective.MatchString(line):
|
||||
return strings.ReplaceAll(stripFirstZDrive(line), `\\`, `/`)
|
||||
case reNoteErrorWarning.MatchString(line):
|
||||
return strings.ReplaceAll(stripFirstZDrive(line), `\`, `/`)
|
||||
default:
|
||||
return line
|
||||
}
|
||||
}
|
||||
|
||||
// clStderrFilter only rewrites "Note: including file:" lines - cl's stderr
|
||||
// diagnostics don't carry the z:\... prefix the stdout ones do.
|
||||
func clStderrFilter(line string) string {
|
||||
if reNoteIncluding.MatchString(line) {
|
||||
return strings.ReplaceAll(stripFirstZDrive(line), `\`, `/`)
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// dumpbinStdoutFilter mirrors dumpbin's unixify_path variant.
|
||||
func dumpbinStdoutFilter(line string) string {
|
||||
if reDumpbinPath.MatchString(line) {
|
||||
return strings.ReplaceAll(stripFirstZDrive(line), `\`, `/`)
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// stripCR mirrors the base `s/\r//` rule every wrapper prepends: removes the
|
||||
// first carriage return in the line (CRLF line endings only ever carry one).
|
||||
func stripCR(line string) string {
|
||||
return strings.Replace(line, "\r", "", 1)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package wrapper
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClStdoutFilterNoteIncluding(t *testing.T) {
|
||||
in := `Note: including file: z:\home\user\project\test.h`
|
||||
want := `Note: including file: /home/user/project/test.h`
|
||||
if got := clStdoutFilter(in); got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClStdoutFilterLineDirective(t *testing.T) {
|
||||
in := `#line 5 "z:\\home\\user\\project\\test.c"`
|
||||
want := `#line 5 "/home/user/project/test.c"`
|
||||
if got := clStdoutFilter(in); got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClStdoutFilterErrorLine(t *testing.T) {
|
||||
in := `z:\home\user\project\test.c(5): warning C4996: 'foo' was declared deprecated`
|
||||
want := `/home/user/project/test.c(5): warning C4996: 'foo' was declared deprecated`
|
||||
if got := clStdoutFilter(in); got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClStdoutFilterNoteLine(t *testing.T) {
|
||||
in := `z:\home\user\project\test.h(3): note: see declaration of 'foo'`
|
||||
want := `/home/user/project/test.h(3): note: see declaration of 'foo'`
|
||||
if got := clStdoutFilter(in); got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClStdoutFilterPassthrough(t *testing.T) {
|
||||
in := "test.c"
|
||||
if got := clStdoutFilter(in); got != in {
|
||||
t.Errorf("got %q want %q", got, in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClStderrFilter(t *testing.T) {
|
||||
in := `Note: including file: z:\a\b.h`
|
||||
want := `Note: including file: /a/b.h`
|
||||
if got := clStderrFilter(in); got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
// stderr does NOT get the error/warning/line-directive rewrite.
|
||||
in2 := `z:\a\b.c(1): error C2065: undeclared identifier`
|
||||
if got := clStderrFilter(in2); got != in2 {
|
||||
t.Errorf("stderr should pass through error lines unchanged, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDumpbinStdoutFilter(t *testing.T) {
|
||||
in := ` PDB file found at z:\a\b\file.pdb`
|
||||
want := ` PDB file found at /a/b/file.pdb`
|
||||
if got := dumpbinStdoutFilter(in); got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
|
||||
in2 := `Dump of file z:\a\b\file.exe`
|
||||
want2 := `Dump of file /a/b/file.exe`
|
||||
if got := dumpbinStdoutFilter(in2); got != want2 {
|
||||
t.Errorf("got %q want %q", got, want2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripCR(t *testing.T) {
|
||||
if got := stripCR("hello\r"); got != "hello" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
if got := stripCR("no cr here"); got != "no cr here" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// runNative handles the two tool names that need no Wine/MSVC install at
|
||||
// all: `cmd` (strips a leading "//c" and execs the rest) and `findstr`
|
||||
// (delegates to grep).
|
||||
func runNative(tool string, args []string) int {
|
||||
switch tool {
|
||||
case "cmd":
|
||||
if len(args) > 0 && args[0] == "//c" {
|
||||
args = args[1:]
|
||||
}
|
||||
return execInherit(args)
|
||||
case "findstr":
|
||||
return execInherit(append([]string{"grep"}, args...))
|
||||
}
|
||||
return 127
|
||||
}
|
||||
|
||||
func execInherit(args []string) int {
|
||||
if len(args) == 0 {
|
||||
return 0
|
||||
}
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
return 127
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Argument path rewriting: MSVC/Wine sometimes fails to resolve relative-
|
||||
// looking includes passed as plain unix absolute paths (see
|
||||
// https://bugs.winehq.org/show_bug.cgi?id=55200); the fix is to rewrite
|
||||
// `-I/abs/path` into `-Iz:/abs/path` and similar, trying each option-prefix
|
||||
// shape in priority order until one matches.
|
||||
var (
|
||||
reOpt1 = regexp.MustCompile(`^[-/][A-Za-z](/.*)$`) // -I/path, /I/path
|
||||
reOpt2 = regexp.MustCompile(`^[-/][A-Za-z][A-Za-z](/.*)$`) // -Fo/path
|
||||
reOpt3 = regexp.MustCompile(`^[-/][A-Za-z][A-Za-z][A-Za-z]*:(/.*)$`) // -MANIFESTINPUT:/path
|
||||
reBare = regexp.MustCompile(`^(/.*)$`) // /abs/path alone
|
||||
)
|
||||
|
||||
// RewriteArgs rewrites absolute unix paths embedded in tool arguments into
|
||||
// "z:/abs/path" form, only when the path's parent directory actually exists
|
||||
// on disk and isn't "/" - matching the original bash's `[ -d "$(dirname ..)" ]`
|
||||
// guard, which keeps short flags like "/P" or "/D" untouched.
|
||||
func RewriteArgs(args []string) []string {
|
||||
out := make([]string, len(args))
|
||||
for i, a := range args {
|
||||
out[i] = rewriteArg(a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func rewriteArg(a string) string {
|
||||
var path string
|
||||
switch {
|
||||
case reOpt1.MatchString(a):
|
||||
path = reOpt1.FindStringSubmatch(a)[1]
|
||||
case reOpt2.MatchString(a):
|
||||
path = reOpt2.FindStringSubmatch(a)[1]
|
||||
case reOpt3.MatchString(a):
|
||||
path = reOpt3.FindStringSubmatch(a)[1]
|
||||
case reBare.MatchString(a):
|
||||
path = reBare.FindStringSubmatch(a)[1]
|
||||
default:
|
||||
return a
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
if dir == "/" {
|
||||
return a
|
||||
}
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil || !fi.IsDir() {
|
||||
return a
|
||||
}
|
||||
|
||||
prefix := a[:len(a)-len(path)]
|
||||
return prefix + "z:" + path
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRewriteArg(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
sub := filepath.Join(dir, "inc")
|
||||
if err := os.Mkdir(sub, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file := filepath.Join(sub, "foo.h")
|
||||
if err := os.WriteFile(file, nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"single-letter option + abs dir", "-I" + sub, "-Iz:" + sub},
|
||||
{"two-letter option + abs file", "-Fo" + file, "-Foz:" + file},
|
||||
{"long colon option + abs file", "-MANIFESTINPUT:" + file, "-MANIFESTINPUT:z:" + file},
|
||||
{"bare absolute path", file, "z:" + file},
|
||||
{"plain flag untouched", "-nologo", "-nologo"},
|
||||
{"nonexistent dir untouched", "-I/does/not/exist/at/all", "-I/does/not/exist/at/all"},
|
||||
{"root-level bare path untouched", "/nologo", "/nologo"},
|
||||
{"relative path untouched", "test.c", "test.c"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := rewriteArg(tc.in); got != tc.want {
|
||||
t.Errorf("rewriteArg(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteArgsPreservesOrderAndLength(t *testing.T) {
|
||||
in := []string{"/nologo", "-c", "test.c"}
|
||||
out := RewriteArgs(in)
|
||||
if len(out) != len(in) {
|
||||
t.Fatalf("length changed: %v -> %v", in, out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||
)
|
||||
|
||||
// toolRelayName is where `msvc-go-wine install` places the compiled
|
||||
// toolrelay.exe helper, shared across all arch bin dirs.
|
||||
const toolRelayName = "toolrelay.exe"
|
||||
|
||||
// Run executes the named multi-call tool with args, exactly as the original
|
||||
// bash wrappers would, and returns the process exit code.
|
||||
func Run(tool string, args []string) int {
|
||||
if nativeTools[tool] {
|
||||
return runNative(tool, args)
|
||||
}
|
||||
|
||||
s, ok := Tools[tool]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "msvc-go-wine: unknown tool %q\n", tool)
|
||||
return 127
|
||||
}
|
||||
|
||||
// os.Executable() (backed by /proc/self/exe on Linux) fully resolves
|
||||
// symlinks, unlike os.Args[0]: not every shell passes a PATH-resolved
|
||||
// absolute path as argv[0] (some just pass the bare command name), which
|
||||
// would make an argv[0]-based lookup resolve against the caller's cwd
|
||||
// instead of the actual install dir. `install` sets each arch dir up
|
||||
// with its own local copy of the binary precisely so this resolves to
|
||||
// <dest>/bin/<arch>, not <dest>/bin.
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
scriptDir := filepath.Dir(exePath)
|
||||
|
||||
cfg, err := wineenv.Load(scriptDir)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine: loading install config:", err)
|
||||
return 1
|
||||
}
|
||||
baseUnix, err := wineenv.FindBaseUnix(scriptDir)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine: locating installation root:", err)
|
||||
return 1
|
||||
}
|
||||
paths := wineenv.NewPaths(cfg, baseUnix)
|
||||
|
||||
toolExePath := filepath.Join(s.exeDir(paths), s.exeName)
|
||||
|
||||
wineBin, err := wineenv.FindWine()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
rewritten := RewriteArgs(args)
|
||||
|
||||
var exitCode int
|
||||
switch {
|
||||
case s.rawStdout:
|
||||
// MSBuild: skip all filtering/toolrelay, inherit stdio directly.
|
||||
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
|
||||
cmd.Env = buildEnv(paths)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
exitCode = runAndWait(cmd)
|
||||
default:
|
||||
relay := filepath.Join(paths.BaseUnix, "bin", toolRelayName)
|
||||
if fi, err := os.Stat(relay); err == nil && !fi.IsDir() {
|
||||
exitCode = runViaToolRelay(wineBin, relay, toolExePath, rewritten, paths, s.stdoutFilter, s.stderrFilter)
|
||||
} else {
|
||||
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
|
||||
cmd.Env = buildEnv(paths)
|
||||
cmd.Stdin = os.Stdin
|
||||
exitCode = runFiltered(cmd, s.stdoutFilter, s.stderrFilter)
|
||||
}
|
||||
}
|
||||
|
||||
if s.postProcess != nil {
|
||||
s.postProcess(args)
|
||||
}
|
||||
|
||||
return exitCode
|
||||
}
|
||||
|
||||
// runViaToolRelay runs exePath through the compiled toolrelay.exe helper:
|
||||
// toolrelay.exe spawns the real tool natively under Windows, redirecting
|
||||
// its stdio to two named FIFOs we create and read from here. This is what
|
||||
// lets `mt.exe`'s CMake-compatibility exit code translation (0x41020001 ->
|
||||
// 0xbb) survive Wine's own exit-code truncation, since toolrelay.exe
|
||||
// observes the real 32-bit exit code via Win32 before translating and
|
||||
// re-exiting with a value that fits in a byte.
|
||||
func runViaToolRelay(wineBin, relayExe, exePath string, args []string, paths *wineenv.Paths, stdoutF, stderrF lineFilter) int {
|
||||
stdoutFifo := filepath.Join(os.TempDir(), fmt.Sprintf("msvc-go-wine.stdout.%d", os.Getpid()))
|
||||
stderrFifo := filepath.Join(os.TempDir(), fmt.Sprintf("msvc-go-wine.stderr.%d", os.Getpid()))
|
||||
os.Remove(stdoutFifo)
|
||||
os.Remove(stderrFifo)
|
||||
if err := syscall.Mkfifo(stdoutFifo, 0o600); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
defer os.Remove(stdoutFifo)
|
||||
if err := syscall.Mkfifo(stderrFifo, 0o600); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
defer os.Remove(stderrFifo)
|
||||
|
||||
cmdArgs := append([]string{relayExe, exePath}, args...)
|
||||
cmd := exec.Command(wineBin, cmdArgs...)
|
||||
cmd.Env = append(buildEnv(paths), "MSVCGOWINE_STDOUT="+stdoutFifo, "MSVCGOWINE_STDERR="+stderrFifo)
|
||||
if devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {
|
||||
defer devNull.Close()
|
||||
cmd.Stdout = devNull
|
||||
cmd.Stderr = devNull
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
f, err := os.Open(stdoutFifo) // blocks until toolrelay.exe opens its end
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
pumpLines(f, os.Stdout, stdoutF)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
f, err := os.Open(stderrFifo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
pumpLines(f, os.Stderr, stderrF)
|
||||
}()
|
||||
|
||||
err := cmd.Wait()
|
||||
wg.Wait()
|
||||
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func buildEnv(p *wineenv.Paths) []string {
|
||||
overrides := map[string]string{
|
||||
"INCLUDE": p.Include,
|
||||
"LIB": p.Lib,
|
||||
"LIBPATH": p.LibPath,
|
||||
"WINEPATH": p.WinePath,
|
||||
"WINEDLLOVERRIDES": p.WineDLLOverrides,
|
||||
}
|
||||
base := os.Environ()
|
||||
if _, set := os.LookupEnv("WINEDEBUG"); !set {
|
||||
overrides["WINEDEBUG"] = "-all"
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(base)+len(overrides))
|
||||
for _, kv := range base {
|
||||
key := kv[:strings.IndexByte(kv, '=')]
|
||||
if _, skip := overrides[key]; skip {
|
||||
continue
|
||||
}
|
||||
out = append(out, kv)
|
||||
}
|
||||
for k, v := range overrides {
|
||||
out = append(out, k+"="+v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// runFiltered streams stdout/stderr line by line through the tool's
|
||||
// filters (CR-stripping always applied first), then waits for completion.
|
||||
func runFiltered(cmd *exec.Cmd, stdoutF, stderrF lineFilter) int {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); pumpLines(stdout, os.Stdout, stdoutF) }()
|
||||
go func() { defer wg.Done(); pumpLines(stderr, os.Stderr, stderrF) }()
|
||||
wg.Wait()
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// pumpLines reads r line by line, CR-stripping and applying filter (if
|
||||
// non-nil) before writing each line to w.
|
||||
func pumpLines(r io.Reader, w *os.File, filter lineFilter) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := stripCR(scanner.Text())
|
||||
if filter != nil {
|
||||
line = filter(line)
|
||||
}
|
||||
fmt.Fprintln(w, line)
|
||||
}
|
||||
}
|
||||
|
||||
func runAndWait(cmd *exec.Cmd) int {
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Package wrapper implements the per-tool command dispatch (cl, link, lib,
|
||||
// ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd,
|
||||
// findstr): loading each tool's environment, invoking it under Wine, and
|
||||
// filtering its output.
|
||||
package wrapper
|
||||
|
||||
import "github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||
|
||||
// dirKind selects which install directory a tool's real .exe lives in.
|
||||
type dirKind int
|
||||
|
||||
const (
|
||||
dirBin dirKind = iota // <bindir> - vc/tools/msvc/<ver>/bin/Host<host>/<arch>
|
||||
dirSDK // <sdkbindir> - kits/10/bin/<sdkver>/<host>
|
||||
dirMSBuild
|
||||
)
|
||||
|
||||
type spec struct {
|
||||
exeName string
|
||||
dir dirKind
|
||||
rawStdout bool // MSBuild: skip line filtering, inherit stdio directly
|
||||
stdoutFilter lineFilter
|
||||
stderrFilter lineFilter
|
||||
postProcess func(origArgs []string) // cl: fix up /P /Fi<file> output
|
||||
}
|
||||
|
||||
// Tools lists every multi-call name this binary answers to, besides its own
|
||||
// management-CLI name.
|
||||
var Tools = map[string]spec{
|
||||
"cl": {exeName: "cl.exe", dir: dirBin, stdoutFilter: clStdoutFilter, stderrFilter: clStderrFilter, postProcess: clPostProcess},
|
||||
"link": {exeName: "link.exe", dir: dirBin},
|
||||
"lib": {exeName: "lib.exe", dir: dirBin},
|
||||
"ml": {exeName: "ml.exe", dir: dirBin},
|
||||
"ml64": {exeName: "ml64.exe", dir: dirBin},
|
||||
"nmake": {exeName: "nmake.exe", dir: dirBin},
|
||||
"armasm": {exeName: "armasm.exe", dir: dirBin},
|
||||
"armasm64": {exeName: "armasm64.exe", dir: dirBin},
|
||||
"dumpbin": {exeName: "dumpbin.exe", dir: dirBin, stdoutFilter: dumpbinStdoutFilter},
|
||||
"mc": {exeName: "mc.exe", dir: dirSDK},
|
||||
"midl": {exeName: "midl.exe", dir: dirSDK},
|
||||
"mt": {exeName: "mt.exe", dir: dirSDK},
|
||||
"rc": {exeName: "rc.exe", dir: dirSDK},
|
||||
"msbuild": {exeName: "MSBuild.exe", dir: dirMSBuild, rawStdout: true},
|
||||
}
|
||||
|
||||
// nativeTools are handled entirely without Wine.
|
||||
var nativeTools = map[string]bool{"cmd": true, "findstr": true}
|
||||
|
||||
func (s spec) exeDir(p *wineenv.Paths) string {
|
||||
switch s.dir {
|
||||
case dirSDK:
|
||||
return p.SDKBinDir
|
||||
case dirMSBuild:
|
||||
return p.MSBuildBinDir
|
||||
default:
|
||||
return p.BinDir
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user