Skip to content

Commit 705d779

Browse files
committed
refresh.py: basic windows support
1 parent 1b886f7 commit 705d779

File tree

1 file changed

+74
-13
lines changed

1 file changed

+74
-13
lines changed

refresh.template.py

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import json
2424
import os
2525
import pathlib
26+
import platform
2627
import re
2728
import shlex
2829
import subprocess
@@ -37,19 +38,7 @@
3738
# Implementation: skip source files we've already seen in _get_files, shortcutting a bunch of slow preprocessor runs in _get_headers and output. We'd need a threadsafe set, or one set per thread, because header finding is already multithreaded for speed (same magnitudespeed win as single-threaded set).
3839
# Anticipated speedup: ~2x (30s to 15s.)
3940

40-
41-
def _get_headers(compile_args: typing.List[str], source_path_for_sanity_check: typing.Optional[str] = None):
42-
"""Gets the headers used by a particular compile command.
43-
44-
Relatively slow. Requires running the C preprocessor.
45-
"""
46-
# Hacky, but hopefully this is a temporary workaround for the clangd issue mentioned in the caller (https://github.com/clangd/clangd/issues/123)
47-
# Runs a modified version of the compile command to piggyback on the compiler's preprocessing and header searching.
48-
# Flags reference here: https://clang.llvm.org/docs/ClangCommandLineReference.html
49-
50-
# As an alternative approach, you might consider trying to get the headers by inspecing the Middlemen actions in the aquery output, but I don't see a way to get just the ones actually #included--or an easy way to get the system headers--without invoking the preprocessor's header search logic.
51-
# For more on this, see https://github.com/hedronvision/bazel-compile-commands-extractor/issues/5#issuecomment-1031148373
52-
41+
def _get_headers_gcc(compile_args: typing.List[str], source_path_for_sanity_check: typing.Optional[str] = None):
5342
# Strip out existing dependency file generation that could interfere with ours.
5443
# Clang on Apple doesn't let later flags override earlier ones, unfortunately.
5544
# These flags are prefixed with M for "make", because that's their output format.
@@ -90,6 +79,70 @@ def _get_headers(compile_args: typing.List[str], source_path_for_sanity_check: t
9079
return headers
9180

9281

82+
def _get_headers_msvc(compile_args: typing.List[str]):
83+
# We don't want to compile, but only pre-process and show all the included header files. We don't want to have the preprocessed results written to a file, so we get them printed to `stderr`.
84+
header_cmd = list(compile_args) + ["/showIncludes", "/EP"]
85+
86+
# cl.exe expects `INCLUDE` (and normally `LIB` but that is irrelevant here) to be set so that it can find the system headers. We have no idea what the correct paths are. So in case these variables are not set, we need to infer them somehow. These variables might not be set if the user has a custom `cc_toolchain()` configured with `bazel` and does not rely on having sourced vcvarsall.bat.
87+
environ = dict(os.environ)
88+
if "INCLUDE" not in environ:
89+
# We assume the base MSVC install based based on the `cl.exe` path in our command.
90+
base = pathlib.Path(header_cmd[0]).parent.parent.parent.parent
91+
# And we find the newest Windows 10 kits version that exists.
92+
windows_kits = sorted( pathlib.Path("C:\\Program Files (x86)\\Windows Kits\\10\\include").glob("*"))[-1]
93+
environ["INCLUDE"] = os.pathsep.join(
94+
(
95+
os.fspath(base / "ATLMFC/include"),
96+
os.fspath(base / "include"),
97+
os.fspath(windows_kits / "ucrt"),
98+
os.fspath(windows_kits / "shared"),
99+
os.fspath(windows_kits / "um"),
100+
os.fspath(windows_kits / "winrt"),
101+
os.fspath(windows_kits / "cppwinrt"),
102+
)
103+
)
104+
105+
header_search_process = subprocess.run(
106+
header_cmd,
107+
cwd=os.environ["BUILD_WORKSPACE_DIRECTORY"],
108+
stderr=subprocess.PIPE,
109+
stdout=subprocess.DEVNULL,
110+
env=environ,
111+
encoding="utf-8",
112+
check=False, # We explicitly ignore errors and carry on.
113+
)
114+
115+
headers = set()
116+
117+
# TODO This is not enough. Based on the locale, `cl.exe` will emit different marker strings. This should be extendend to multiple languages.
118+
include_marker = "Note: including file:"
119+
error_marker = " fatal error "
120+
for line in header_search_process.stderr.splitlines():
121+
if include_marker in line:
122+
headers.add(line.replace(include_marker, "").strip())
123+
if error_marker in line:
124+
print(line, file=sys.stderr)
125+
126+
return list(headers)
127+
128+
129+
def _get_headers(compile_args: typing.List[str], source_path_for_sanity_check: typing.Optional[str] = None):
130+
"""Gets the headers used by a particular compile command.
131+
132+
Relatively slow. Requires running the C preprocessor.
133+
"""
134+
# Hacky, but hopefully this is a temporary workaround for the clangd issue mentioned in the caller (https://github.com/clangd/clangd/issues/123)
135+
# Runs a modified version of the compile command to piggyback on the compiler's preprocessing and header searching.
136+
# Flags reference here: https://clang.llvm.org/docs/ClangCommandLineReference.html
137+
138+
# As an alternative approach, you might consider trying to get the headers by inspecing the Middlemen actions in the aquery output, but I don't see a way to get just the ones actually #included--or an easy way to get the system headers--without invoking the preprocessor's header search logic.
139+
# For more on this, see https://github.com/hedronvision/bazel-compile-commands-extractor/issues/5#issuecomment-1031148373
140+
141+
if "cl.exe" in compile_args[0]:
142+
return _get_headers_msvc(compile_args)
143+
return _get_headers_gcc(compile_args, source_path_for_sanity_check)
144+
145+
93146
def _get_files(compile_args: typing.List[str]):
94147
"""Gets the ([source files], [header files]) clangd should be told the command applies to."""
95148
source_files = [arg for arg in compile_args if arg.endswith(_get_files.source_extensions)]
@@ -222,6 +275,13 @@ def _all_platform_patch(compile_args: typing.List[str]):
222275
return list(compile_args)
223276

224277

278+
def _windows_platform_patch(compile_args: typing.List[str]):
279+
if platform.system() == "Windows":
280+
# `bazel` returns paths as "posix" paths with a `/` as the separator. We want to pass the usual `\` as a path separator to `cl.exe`. But we have arguments for `cl.exe` in the form of `/EP`, so when we have arguments beginning with a `/` we should keep that arround. In the extrem we have something like `/Isome/path` where we want `/Isome\path`.
281+
return list(arg[0] + arg[1:].replace("/", "\\") for arg in compile_args)
282+
return compile_args
283+
284+
225285
def _shlex_join_backport(args: typing.List[str]) -> str:
226286
"""shlex.join not available until PY_MIN=3.8. Use this instead for now."""
227287
return " ".join(shlex.quote(arg) for arg in args)
@@ -236,6 +296,7 @@ def _get_cpp_command_for_files(compile_action):
236296

237297
# Patch command by platform
238298
args = _all_platform_patch(args)
299+
args = _windows_platform_patch(args)
239300
args = _apple_platform_patch(args)
240301
# Android and Linux and grailbio LLVM toolchains: Fine as is; no special patching needed.
241302

0 commit comments

Comments
 (0)