mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
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.
310 lines
7.8 KiB
Go
310 lines
7.8 KiB
Go
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
|
|
}
|