-
Notifications
You must be signed in to change notification settings - Fork 65
Expand file tree
/
Copy pathcl-proxy
More file actions
executable file
·312 lines (275 loc) · 11.4 KB
/
Copy pathcl-proxy
File metadata and controls
executable file
·312 lines (275 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#!/usr/bin/env python3
###############################################################################
#
# Copyright (c) 2024, Intel Corporation
# SPDX-License-Identifier: Apache-2.0
#
###############################################################################
# cl-proxy presents a GCC/Clang-style command-line interface backed by MSVC's
# cl.exe, so YARPGen's testing harness can drive MSVC from WSL without any
# changes to the (GCC-flag-centric) Makefile generator. It is the MSVC
# counterpart of ispc-proxy / ispc-disp.
#
# It does three things:
# 1. Translates the few GCC-style flags the scripts generate (-c, -o, -std=,
# -D, -I) into cl.exe syntax, and drops GCC-only flags that have no MSVC
# equivalent.
# It deliberately does NOT translate optimization or feature flags: what
# you write in the testing-set config is what cl.exe gets. Spell those
# flags the MSVC way (/Od, /O1, /O2, /Os, /Ot, /arch:AVX2, ...); they are
# passed through verbatim. A GCC-style flag it does not recognize is a
# hard error rather than something silently dropped or reinterpreted.
# 2. Invokes cl.exe through cmd.exe with the MSVC environment loaded
# (vcvars64.bat).
# 3. Wraps the produced Windows .exe in a tiny shell launcher that also
# strips CRLF from its output, so the rest of the harness can treat it as
# an ordinary native binary and its checksum matches other compilers'.
#
# REQUIREMENTS / GOTCHAS
# * Source/object/include paths that are plain WSL paths (leading '/', e.g.
# /home/... or /mnt/c/...) are translated to a Windows-visible form via
# `wslpath -w` before being handed to cl.exe. cmd.exe itself still refuses
# a UNC working directory (\\wsl$\...), so keep the process cwd (and
# YARPGen's --out-dir / run_gen target) under /mnt/<drive>/.
# * Put this scripts/ directory on your PATH (same as ispc-proxy).
# * Set YARPGEN_VCVARS to your vcvars64.bat if auto-detection fails.
# * C++ only. C / ISPC / SYCL are not handled.
###############################################################################
import json
import os
import re
import subprocess
import sys
# Candidate locations for vcvars64.bat (Build Tools and full VS, 2026/2022/2019,
# both Program Files roots). YARPGEN_VCVARS overrides all of them.
# Built from version-dirs x editions x install roots. Newest-first so
# find_vcvars() defaults to the newest installed toolchain. Note the install
# directory is a version number, not the marketing year: VS 2026 is "18", VS
# 2022 is "2022"/"17". We list both the version and the year spelling to be safe.
_VS_YEARS = ["18", "2026", "17", "2022", "2019"]
_VS_EDITIONS = ["BuildTools", "Community", "Professional", "Enterprise"]
_VS_ROOTS = ["C:\\Program Files", "C:\\Program Files (x86)"]
_VCVARS_CANDIDATES = [
"%s\\Microsoft Visual Studio\\%s\\%s\\VC\\Auxiliary\\Build\\vcvars64.bat"
% (root, year, edition)
for year in _VS_YEARS
for edition in _VS_EDITIONS
for root in _VS_ROOTS
]
def win_path_to_wsl(win_path):
p = win_path.strip().strip('"')
m = re.match(r"^([A-Za-z]):\\(.*)$", p)
if not m:
return None
drive, rest = m.group(1).lower(), m.group(2).replace("\\", "/")
return "/mnt/%s/%s" % (drive, rest)
def to_win_path(path):
"""Translate a WSL path (native /home/... or /mnt/<drive>/...) to a
Windows-visible path so cl.exe, a native Win32 process, can open it.
Falls back to the original string if wslpath is unavailable or fails."""
try:
out = subprocess.check_output(["wslpath", "-w", path],
stderr=subprocess.DEVNULL)
return out.decode("utf-8").strip()
except (OSError, subprocess.CalledProcessError):
return path
def find_vcvars():
env = os.environ.get("YARPGEN_VCVARS")
if env:
return env
for cand in _VCVARS_CANDIDATES:
wsl = win_path_to_wsl(cand)
if wsl and os.path.isfile(wsl):
return cand
# Fall back to the first candidate so we produce a clear error from cmd.exe.
return _VCVARS_CANDIDATES[0]
VCVARS = find_vcvars()
# Exact flags with no MSVC equivalent that we intentionally drop.
DROP_EXACT = {
"-fPIC", "-fpermissive", "-fno-strict-aliasing", "-w",
"-fno-sanitize-recover=undefined", "--pic",
}
# Flag prefixes to drop (arch selection, sanitizers, linker libs, warnings...).
DROP_PREFIX = (
"-march=", "-mcmodel", "--mcmodel", "-x", "-W", "-fsanitize", "-mllvm",
"--target", "-woff", "-rtlib", "-l",
)
def map_std(std):
# MSVC /std: accepts c++14, c++17, c++20, c++latest (min is c++14).
table = {
"c++98": "c++14", "c++03": "c++14", "c++11": "c++14", "c++0x": "c++14",
"c++14": "c++14", "c++1y": "c++14", "c++17": "c++17", "c++1z": "c++17",
"c++20": "c++20", "c++2a": "c++20", "c++23": "c++latest",
"c++2b": "c++latest",
}
return table.get(std, "c++17")
class UnknownFlag(Exception):
def __init__(self, flag):
Exception.__init__(self, flag)
self.flag = flag
def translate(argv):
"""GCC-style argv -> (cl args, is_compile_only, output_target).
Only the flags generated in code are rewritten; '/'-prefixed flags from the
config are passed through untouched.
"""
out = ["/nologo", "/EHsc", "/w", "/permissive-"]
compiling = "-c" in argv
# A testing set may pin the standard itself (/std:c++20). That is more
# specific than the -std= the harness derives from --std, so let it win
# outright instead of emitting both and relying on cl's last-one-wins
# (which also costs a D9025 override warning per compile).
explicit_std = any(a.startswith("/std:") for a in argv)
target = None
i = 0
while i < len(argv):
a = argv[i]
if a == "-c":
out.append("/c")
elif a == "-o":
i += 1
target = argv[i]
elif a.startswith("-std="):
if not explicit_std:
out.append("/std:" + map_std(a[len("-std="):]))
elif a.startswith("-D"):
out.append("/D" + a[2:])
elif a.startswith("-I"):
out.append("/I" + to_win_path(a[2:]))
elif a in DROP_EXACT or any(a.startswith(p) for p in DROP_PREFIX):
pass
elif a.startswith("/") and not os.path.exists(a):
out.append(a) # already a cl flag (/O2, /std:c++20, ...)
elif a.startswith("-"):
raise UnknownFlag(a)
else:
# source/object file. A leading '/' here is a WSL path (the
# branch above already filtered out real cl.exe flags), which
# cl.exe can't open directly -> translate it.
out.append(to_win_path(a) if a.startswith("/") else a)
i += 1
return out, compiling, target
def make_launcher(name, exe):
# `exe` is always a sibling of `name` (built as target + ".exe"), so only
# its basename belongs in the relative reference below -- an absolute
# `exe` (e.g. check_isa's -o path) would otherwise produce a bogus
# dirname($0)/absolute-path concatenation.
with open(name, "w") as f:
f.write("#!/bin/bash\n")
f.write("# Auto-generated by cl-proxy: run the MSVC-built binary and\n")
f.write("# normalize CRLF so its checksum matches other compilers.\n")
f.write("set -o pipefail\n")
f.write('"$(dirname "$0")/%s" "$@" | tr -d "\\r"\n' % os.path.basename(exe))
os.chmod(name, 0o755)
# Cache of the MSVC environment so we do not pay the ~25s vcvars cost per call.
CACHE = os.environ.get("YARPGEN_MSVC_ENV_CACHE",
os.path.expanduser("~/.cache/yarpgen_msvc_env.json"))
def capture_msvc_env():
"""Run vcvars once (via a .bat, to dodge cmd.exe quoting) and record cl.exe's
path plus the INCLUDE/LIB/LIBPATH it sets. Returns {} on failure."""
bat = "_clproxy_env_%d.bat" % os.getpid()
try:
with open(bat, "w", newline="\r\n") as f:
f.write("@echo off\n")
f.write('call "%s" >nul\n' % VCVARS)
f.write("set\n")
out = subprocess.check_output(["cmd.exe", "/C", bat],
stderr=subprocess.DEVNULL)
except (OSError, subprocess.CalledProcessError):
return {}
finally:
try:
os.remove(bat)
except OSError:
pass
env = {}
for line in out.decode("utf-8", "replace").splitlines():
if "=" in line:
k, v = line.rstrip("\r\n").split("=", 1)
env[k] = v
cl = None
for d in env.get("Path", env.get("PATH", "")).split(";"):
wd = win_path_to_wsl(d)
if wd and os.path.isfile(os.path.join(wd, "cl.exe")):
cl = os.path.join(wd, "cl.exe")
break
if not cl:
return {}
data = {"cl": cl,
"vars": {k: env[k] for k in ("INCLUDE", "LIB", "LIBPATH")
if k in env}}
try:
os.makedirs(os.path.dirname(CACHE), exist_ok=True)
with open(CACHE, "w") as f:
json.dump(data, f)
except OSError:
pass
return data
def load_msvc_env():
try:
with open(CACHE) as f:
data = json.load(f)
if data.get("cl") and os.path.isfile(data["cl"]):
return data
except (OSError, ValueError):
pass
return capture_msvc_env()
def run_via_bat(cl_args):
"""Slow fallback: run cl through cmd.exe + vcvars for every invocation."""
cl_cmd = "cl " + " ".join(cl_args)
bat = "_clproxy_%d.bat" % os.getpid()
with open(bat, "w", newline="\r\n") as f:
f.write("@echo off\n")
f.write('call "%s" >nul\n' % VCVARS)
f.write(cl_cmd + "\n")
f.write("exit /b %errorlevel%\n")
try:
return subprocess.call(["cmd.exe", "/C", bat])
finally:
try:
os.remove(bat)
except OSError:
pass
def run_direct(data, cl_args):
"""Fast path: invoke cl.exe directly, forwarding the cached INCLUDE/LIB/
LIBPATH to the Win32 process via WSLENV (verbatim, no path translation)."""
env = dict(os.environ)
for k, v in data["vars"].items():
env[k] = v
names = ":".join(data["vars"].keys())
if names:
prev = os.environ.get("WSLENV", "")
env["WSLENV"] = (prev + ":" + names) if prev else names
return subprocess.call([data["cl"]] + cl_args, env=env)
def main():
try:
args, compiling, target = translate(sys.argv[1:])
except UnknownFlag as e:
sys.stderr.write(
"cl-proxy: unknown flag '%s'.\n" % e.flag)
return 2
exe_out = None
if target is not None:
if compiling:
args.append("/Fo:" + to_win_path(target))
else:
exe_out = target + ".exe"
args.append("/Fe:" + to_win_path(exe_out))
data = load_msvc_env()
if data.get("cl"):
rc = run_direct(data, args)
else:
# Could not locate cl.exe; fall back to the slow cmd.exe + vcvars path.
rc = run_via_bat(args)
if rc == 0 and exe_out is not None:
# cl.exe wrote exe_out as a native Win32 process. When the target
# directory is a plain WSL path (reached via \\wsl.localhost, not
# /mnt/<drive>), that write doesn't carry the POSIX execute bit, so
# set it explicitly before wrapping it in the launcher.
try:
os.chmod(exe_out, 0o755)
except OSError:
pass
make_launcher(target, exe_out)
return rc
if __name__ == "__main__":
sys.exit(main())