Skip to content

Commit

Permalink
Add support for swift
Browse files Browse the repository at this point in the history
  • Loading branch information
xinzhengzhang authored and ilibilib committed Nov 24, 2022
1 parent 3bb6d78 commit 78500d0
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 21 deletions.
124 changes: 104 additions & 20 deletions refresh.template.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ def _print_header_finding_warning_once():
Continuing gracefully...""")
_print_header_finding_warning_once.has_logged = False

def _file_exists(source_file, warn_missing_generated = True):
if not os.path.isfile(source_file):
if not _file_exists.has_logged_missing_file_error and True == warn_missing_generated: # Just log once; subsequent messages wouldn't add anything.
_file_exists.has_logged_missing_file_error = True
log_warning(f""">>> A source file you compile doesn't (yet) exist: {source_file}
It's probably a generated file, and you haven't yet run a build to generate it.
That's OK; your code doesn't even have to compile for this tool to work.
If you can, though, you might want to run a build of your code.
That way everything is generated, browsable and indexed for autocomplete.
However, if you have *already* built your code, and generated the missing file...
Please make sure you're supplying this tool with the same flags you use to build.
You can either use a refresh_compile_commands rule or the special -- syntax. Please see the README.
[Supplying flags normally won't work. That just causes this tool to be built with those flags.]
Continuing gracefully...""")
return False
return True
_file_exists.has_logged_missing_file_error = False

@functools.lru_cache(maxsize=None)
def _get_bazel_cached_action_keys():
Expand Down Expand Up @@ -564,19 +581,7 @@ def _get_files(compile_action):
assert source_file.endswith(_get_files.source_extensions), f"Source file candidate, {source_file}, seems to be wrong.\nSelected from {compile_action.arguments}.\nPlease file an issue with this information!"

# Warn gently about missing files
if not os.path.isfile(source_file):
if not _get_files.has_logged_missing_file_error: # Just log once; subsequent messages wouldn't add anything.
_get_files.has_logged_missing_file_error = True
log_warning(f""">>> A source file you compile doesn't (yet) exist: {source_file}
It's probably a generated file, and you haven't yet run a build to generate it.
That's OK; your code doesn't even have to compile for this tool to work.
If you can, though, you might want to run a build of your code.
That way everything is generated, browsable and indexed for autocomplete.
However, if you have *already* built your code, and generated the missing file...
Please make sure you're supplying this tool with the same flags you use to build.
You can either use a refresh_compile_commands rule or the special -- syntax. Please see the README.
[Supplying flags normally won't work. That just causes this tool to be built with those flags.]
Continuing gracefully...""")
if not _file_exists(source_file):
return {source_file}, set()

# Note: We need to apply commands to headers and sources.
Expand Down Expand Up @@ -605,7 +610,6 @@ def _get_files(compile_action):
compile_action.arguments.insert(1, lang_flag)

return {source_file}, header_files
_get_files.has_logged_missing_file_error = False
# Setup extensions and flags for the whole C-language family.
# Clang has a list: https://github.com/llvm/llvm-project/blob/b9f3b7f89a4cb4cf541b7116d9389c73690f78fa/clang/lib/Driver/Types.cpp#L293
_get_files.c_source_extensions = ('.c', '.i')
Expand Down Expand Up @@ -650,7 +654,7 @@ def _get_apple_SDKROOT(SDK_name: str):
# Traditionally stored in SDKROOT environment variable, but not provided by Bazel. See https://github.com/bazelbuild/bazel/issues/12852


def _get_apple_platform(compile_args: typing.List[str]):
def _get_apple_platform(compile_args: typing.List[str], environmentVariables = None):
"""Figure out which Apple platform a command is for.
Is the name used by Xcode in the SDK files, not the marketing name.
Expand All @@ -661,6 +665,13 @@ def _get_apple_platform(compile_args: typing.List[str]):
match = re.search('/Platforms/([a-zA-Z]+).platform/Developer/', arg)
if match:
return match.group(1)
if environmentVariables:
match = next(
filter(lambda x: x.key == "APPLE_SDK_PLATFORM", environmentVariables),
None
)
if match:
return match.value
return None


Expand All @@ -683,7 +694,36 @@ def _get_apple_active_clang():
# Unless xcode-select has been invoked (like for a beta) we'd expect, e.g., '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang' or '/Library/Developer/CommandLineTools/usr/bin/clang'.


def _apple_platform_patch(compile_args: typing.List[str]):
@functools.lru_cache(maxsize=None)
def _get_apple_active_swiftc():
"""Get path to xcode-select'd clang version."""
return subprocess.check_output(
('xcrun', '--find', 'swiftc'),
stderr=subprocess.DEVNULL, # Suppress superfluous error messages like "Requested but did not find extension point with identifier..."
encoding=locale.getpreferredencoding()
).rstrip()


def _apple_swift_patch(compile_args: typing.List[str]):
"""De-Bazel(rule_swift) the command into something sourcekit-lsp can parse."""

# Remove build_bazel_rules_swift/tools/worker/worker
compile_args.pop(0)

# Expand -debug-prefix-pwd-is-dot
match = next((i for i,v in enumerate(compile_args) if v == "-Xwrapped-swift=-debug-prefix-pwd-is-dot"), None)
if match:
compile_args[match] = "-debug-prefix-map"
compile_args.insert(match + 1, os.environ["BUILD_WORKSPACE_DIRECTORY"] + "=.")

# Remove other -Xwrapped-swift arguments like `-ephemeral-module-cache` `-global-index-store-import-path`
# We could override index-store by config sourcekit-lsp
compile_args = [arg for arg in compile_args if not arg.startswith('-Xwrapped-swift')]

return compile_args


def _apple_platform_patch(compile_args: typing.List[str], environmentVariables = None):
"""De-Bazel the command into something clangd can parse.
This function has fixes specific to Apple platforms, but you should call it on all platforms. It'll determine whether the fixes should be applied or not.
Expand All @@ -694,7 +734,12 @@ def _apple_platform_patch(compile_args: typing.List[str]):
# Undo Bazel's Apple platform compiler wrapping.
# Bazel wraps the compiler as `external/local_config_cc/wrapped_clang` and exports that wrapped compiler in the proto. However, we need a clang call that clangd can introspect. (See notes in "how clangd uses compile_commands.json" in ImplementationReadme.md for more.)
# Removing the wrapper is also important because Bazel's Xcode (but not CommandLineTools) wrapper crashes if you don't specify particular environment variables (replaced below). We'd need the wrapper to be invokable by clangd's --query-driver if we didn't remove the wrapper.
compile_args[0] = _get_apple_active_clang()
# Bazel wrapps the swiftc as `external/build_bazel_rules_swift/tools/worker/worker swiftc ` and worker has been removed in apple_swift_patch

if compile_args[0][-6:] == "swiftc":
compile_args[0] = _get_apple_active_swiftc()
else:
compile_args[0] = _get_apple_active_clang()

# We have to manually substitute out Bazel's macros so clang can parse the command
# Code this mirrors is in https://github.com/bazelbuild/bazel/blob/master/tools/osx/crosstool/wrapped_clang.cc
Expand All @@ -703,7 +748,7 @@ def _apple_platform_patch(compile_args: typing.List[str]):
# We also have to manually figure out the values of SDKROOT and DEVELOPER_DIR, since they're missing from the environment variables Bazel provides.
# Filed Bazel issue about the missing environment variables: https://github.com/bazelbuild/bazel/issues/12852
compile_args = [arg.replace('__BAZEL_XCODE_DEVELOPER_DIR__', _get_apple_DEVELOPER_DIR()) for arg in compile_args]
apple_platform = _get_apple_platform(compile_args)
apple_platform = _get_apple_platform(compile_args, environmentVariables)
assert apple_platform, f"Apple platform not detected in CMD: {compile_args}"
compile_args = [arg.replace('__BAZEL_XCODE_SDKROOT__', _get_apple_SDKROOT(apple_platform)) for arg in compile_args]

Expand Down Expand Up @@ -761,6 +806,40 @@ def _get_cpp_command_for_files(compile_action):
return source_files, header_files, compile_action.arguments


def _get_swift_command_for_files(compile_action):
"""Reformat compile_action into a compile command sourcekit-lsp (https://github.com/apple/sourcekit-lsp) can understand.
Compile_action was produced by rule_swift (https://github.com/bazelbuild/rules_swift)
"""
# Patch command
compile_action.arguments = _all_platform_patch(compile_action.arguments)
compile_action.arguments = _apple_swift_patch(compile_action.arguments)
compile_action.arguments = _apple_platform_patch(compile_action.arguments, compile_action.environmentVariables)

# Source files is end with `.swift`
source_files = set(filter(lambda x: x[-6:] == ".swift", compile_action.arguments))

for source_file in source_files:
_file_exists(source_file)

return source_files, set(), compile_action.arguments


def _get_command_for_files(compile_action):
"""Routing to correspond parser
"""

assert compile_action.mnemonic in compile_action.mnemonic, f"Expecting mnemonic is one of (Objc|Cpp|Swift)Compile. Found mnemonic {compile_action.mnemonic}, target {compile_action}"

return _get_command_map[compile_action.mnemonic](compile_action)

_get_command_map = {
"ObjcCompile": _get_cpp_command_for_files,
"CppCompile": _get_cpp_command_for_files,
"SwiftCompile": _get_swift_command_for_files,
}


def _convert_compile_commands(aquery_output):
"""Converts from Bazel's aquery format to de-Bazeled compile_commands.json entries.
Expand Down Expand Up @@ -794,7 +873,7 @@ def _amend_action_as_external(action):
with concurrent.futures.ThreadPoolExecutor(
max_workers=min(32, (os.cpu_count() or 1) + 4) # Backport. Default in MIN_PY=3.8. See "using very large resources implicitly on many-core machines" in https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor
) as threadpool:
outputs = threadpool.map(_get_cpp_command_for_files, aquery_output.actions)
outputs = threadpool.map(_get_command_for_files, aquery_output.actions)

# Yield as compile_commands.json entries
header_files_already_written = set()
Expand Down Expand Up @@ -843,14 +922,19 @@ def _get_commands(target: str, flags: str):
Try adding them as flags in your refresh_compile_commands rather than targets.
In a moment, Bazel will likely fail to parse.""")

support_mnemonics = ["Objc", "Cpp"]
if {enable_swift}:
support_mnemonics += ["Swift"]
mnemonics_string = '|'.join(support_mnemonics)

# First, query Bazel's C-family compile actions for that configured target
aquery_args = [
'bazel',
'aquery',
# Aquery docs if you need em: https://docs.bazel.build/versions/master/aquery.html
# Aquery output proto reference: https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/analysis_v2.proto
# One bummer, not described in the docs, is that aquery filters over *all* actions for a given target, rather than just those that would be run by a build to produce a given output. This mostly isn't a problem, but can sometimes surface extra, unnecessary, misconfigured actions. Chris has emailed the authors to discuss and filed an issue so anyone reading this could track it: https://github.com/bazelbuild/bazel/issues/14156.
f"mnemonic('(Objc|Cpp)Compile',deps({target}))",
f"mnemonic('({mnemonics_string})Compile',deps({target}))",
# We switched to jsonproto instead of proto because of https://github.com/bazelbuild/bazel/issues/13404. We could change back when fixed--reverting most of the commit that added this line and tweaking the build file to depend on the target in that issue. That said, it's kinda nice to be free of the dependency, unless (OPTIMNOTE) jsonproto becomes a performance bottleneck compated to binary protos.
'--output=jsonproto',
# We'll disable artifact output for efficiency, since it's large and we don't use them. Small win timewise, but dramatically less json output from aquery.
Expand Down
5 changes: 4 additions & 1 deletion refresh_compile_commands.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def refresh_compile_commands(
targets = None,
exclude_headers = None,
exclude_external_sources = False,
enable_swift = False,
**kwargs): # For the other common attributes. Tags, compatible_with, etc. https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes.
# Convert the various, acceptable target shorthands into the dictionary format
# In Python, `type(x) == y` is an antipattern, but [Starlark doesn't support inheritance](https://bazel.build/rules/language), so `isinstance` doesn't exist, and this is the correct way to switch on type.
Expand All @@ -75,7 +76,7 @@ def refresh_compile_commands(

# Generate runnable python script from template
script_name = name + ".py"
_expand_template(name = script_name, labels_to_flags = targets, exclude_headers = exclude_headers, exclude_external_sources = exclude_external_sources, **kwargs)
_expand_template(name = script_name, labels_to_flags = targets, exclude_headers = exclude_headers, exclude_external_sources = exclude_external_sources, enable_swift = enable_swift, **kwargs)
native.py_binary(name = name, srcs = [script_name], **kwargs)

def _expand_template_impl(ctx):
Expand All @@ -91,6 +92,7 @@ def _expand_template_impl(ctx):
" {windows_default_include_paths}": "\n".join([" %r," % path for path in find_cpp_toolchain(ctx).built_in_include_directories]), # find_cpp_toolchain is from https://docs.bazel.build/versions/main/integrating-with-rules-cc.html
"{exclude_headers}": '"' + str(ctx.attr.exclude_headers) + '"',
"{exclude_external_sources}": str(ctx.attr.exclude_external_sources),
"{enable_swift}": str(ctx.attr.enable_swift),
},
)
return DefaultInfo(files = depset([script]))
Expand All @@ -100,6 +102,7 @@ _expand_template = rule(
"labels_to_flags": attr.string_dict(mandatory = True), # string keys instead of label_keyed because Bazel doesn't support parsing wildcard target patterns (..., *, :all) in BUILD attributes.
"exclude_external_sources": attr.bool(default = False),
"exclude_headers": attr.string(values = ["all", "external", ""]), # "" needed only for compatibility with Bazel < 3.6.0
"enable_swift": attr.bool(default = False),
"_script_template": attr.label(allow_single_file = True, default = "refresh.template.py"),
"_cc_toolchain": attr.label(default = "@bazel_tools//tools/cpp:current_cc_toolchain"), # For Windows INCLUDE. If this were eliminated, for example by the resolution of https://github.com/clangd/clangd/issues/123, we'd be able to just use a macro and skylib's expand_template rule: https://github.com/bazelbuild/bazel-skylib/pull/330
},
Expand Down

0 comments on commit 78500d0

Please sign in to comment.