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,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