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,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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user