Skip to content

Commit 1977cec

Browse files
committed
Add support for swift
https://github.com/hedronvision/bazel-compile-commands-extractor/pull/89\#discussion_r1037771523 refresh.template.py: omit warn_missing_generated and rename _file_exists to _warn_if_file_doesnt_exist #89 (comment) refresh.template.py: using endwith api #89 (comment) refresh.template.py: do not suppress error when locate xcrun toolchain #89 (comment) refresh.template.py: add some more explain for why need to remove the worker
1 parent a797a78 commit 1977cec

File tree

2 files changed

+101
-21
lines changed

2 files changed

+101
-21
lines changed

refresh.template.py

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,23 @@ def _print_header_finding_warning_once():
9393
Continuing gracefully...""")
9494
_print_header_finding_warning_once.has_logged = False
9595

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

97114
@functools.lru_cache(maxsize=None)
98115
def _get_bazel_cached_action_keys():
@@ -577,19 +594,7 @@ def _get_files(compile_action):
577594
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!"
578595

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

595600
# Note: We need to apply commands to headers and sources.
@@ -618,7 +623,6 @@ def _get_files(compile_action):
618623
compile_action.arguments.insert(1, lang_flag)
619624

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

665669

666-
def _get_apple_platform(compile_args: typing.List[str]):
670+
def _get_apple_platform(compile_args: typing.List[str], environmentVariables = None):
667671
"""Figure out which Apple platform a command is for.
668672
669673
Is the name used by Xcode in the SDK files, not the marketing name.
@@ -674,6 +678,13 @@ def _get_apple_platform(compile_args: typing.List[str]):
674678
match = re.search('/Platforms/([a-zA-Z]+).platform/Developer/', arg)
675679
if match:
676680
return match.group(1)
681+
if environmentVariables:
682+
match = next(
683+
filter(lambda x: x.key == "APPLE_SDK_PLATFORM", environmentVariables),
684+
None
685+
)
686+
if match:
687+
return match.value
677688
return None
678689

679690

@@ -685,7 +696,29 @@ def _get_apple_DEVELOPER_DIR():
685696
# Traditionally stored in DEVELOPER_DIR environment variable, but not provided by Bazel. See https://github.com/bazelbuild/bazel/issues/12852
686697

687698

688-
def _apple_platform_patch(compile_args: typing.List[str]):
699+
def _apple_swift_patch(compile_args: typing.List[str]):
700+
"""De-Bazel(rule_swift) the command into something sourcekit-lsp can parse."""
701+
702+
# rules_swift add a worker for wrapping if enable --persistent_worker flag (https://bazel.build/remote/persistent)
703+
# https://github.com/bazelbuild/rules_swift/blob/master/swift/internal/actions.bzl#L236
704+
# We need to remove it (build_bazel_rules_swift/tools/worker/worker)
705+
compile_args.pop(0)
706+
707+
# The worker also expand the arguments defined in swift_rule which was start with -Xwrapped-swift
708+
# Expand -debug-prefix-pwd-is-dot
709+
match = next((i for i,v in enumerate(compile_args) if v == "-Xwrapped-swift=-debug-prefix-pwd-is-dot"), None)
710+
if match:
711+
compile_args[match] = "-debug-prefix-map"
712+
compile_args.insert(match + 1, os.environ["BUILD_WORKSPACE_DIRECTORY"] + "=.")
713+
714+
# Remove other -Xwrapped-swift arguments like `-ephemeral-module-cache` `-global-index-store-import-path`
715+
# We could override index-store by config sourcekit-lsp
716+
compile_args = [arg for arg in compile_args if not arg.startswith('-Xwrapped-swift')]
717+
718+
return compile_args
719+
720+
721+
def _apple_platform_patch(compile_args: typing.List[str], environmentVariables = None):
689722
"""De-Bazel the command into something clangd can parse.
690723
691724
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.
@@ -695,8 +728,13 @@ def _apple_platform_patch(compile_args: typing.List[str]):
695728
if any('__BAZEL_XCODE_' in arg for arg in compile_args):
696729
# Undo Bazel's Apple platform compiler wrapping.
697730
# 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.)
731+
# Bazel wrapps the swiftc as `external/build_bazel_rules_swift/tools/worker/worker swiftc ` and worker has been removed in apple_swift_patch
698732
# 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.
699-
compile_args[0] = 'clang'
733+
734+
if compile_args[0].endswith('swiftc'):
735+
compile_args[0] = 'swiftc'
736+
else:
737+
compile_args[0] = 'clang'
700738

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

@@ -763,6 +801,40 @@ def _get_cpp_command_for_files(compile_action):
763801
return source_files, header_files, compile_action.arguments
764802

765803

804+
def _get_swift_command_for_files(compile_action):
805+
"""Reformat compile_action into a compile command sourcekit-lsp (https://github.com/apple/sourcekit-lsp) can understand.
806+
807+
Compile_action was produced by rule_swift (https://github.com/bazelbuild/rules_swift)
808+
"""
809+
# Patch command
810+
compile_action.arguments = _all_platform_patch(compile_action.arguments)
811+
compile_action.arguments = _apple_swift_patch(compile_action.arguments)
812+
compile_action.arguments = _apple_platform_patch(compile_action.arguments, compile_action.environmentVariables)
813+
814+
# Source files is end with `.swift`
815+
source_files = set(filter(lambda arg: arg.endswith('.swift'), compile_action.arguments))
816+
817+
for source_file in source_files:
818+
_warn_if_file_doesnt_exist(source_file)
819+
820+
return source_files, set(), compile_action.arguments
821+
822+
823+
def _get_command_for_files(compile_action):
824+
"""Routing to correspond parser
825+
"""
826+
827+
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}"
828+
829+
return _get_command_map[compile_action.mnemonic](compile_action)
830+
831+
_get_command_map = {
832+
"ObjcCompile": _get_cpp_command_for_files,
833+
"CppCompile": _get_cpp_command_for_files,
834+
"SwiftCompile": _get_swift_command_for_files,
835+
}
836+
837+
766838
def _convert_compile_commands(aquery_output):
767839
"""Converts from Bazel's aquery format to de-Bazeled compile_commands.json entries.
768840
@@ -796,7 +868,7 @@ def _amend_action_as_external(action):
796868
with concurrent.futures.ThreadPoolExecutor(
797869
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
798870
) as threadpool:
799-
outputs = threadpool.map(_get_cpp_command_for_files, aquery_output.actions)
871+
outputs = threadpool.map(_get_command_for_files, aquery_output.actions)
800872

801873
# Yield as compile_commands.json entries
802874
header_files_already_written = set()
@@ -845,14 +917,19 @@ def _get_commands(target: str, flags: str):
845917
Try adding them as flags in your refresh_compile_commands rather than targets.
846918
In a moment, Bazel will likely fail to parse.""")
847919

920+
support_mnemonics = ["Objc", "Cpp"]
921+
if {enable_swift}:
922+
support_mnemonics += ["Swift"]
923+
mnemonics_string = '|'.join(support_mnemonics)
924+
848925
# First, query Bazel's C-family compile actions for that configured target
849926
aquery_args = [
850927
'bazel',
851928
'aquery',
852929
# Aquery docs if you need em: https://docs.bazel.build/versions/master/aquery.html
853930
# Aquery output proto reference: https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/analysis_v2.proto
854931
# 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.
855-
f"mnemonic('(Objc|Cpp)Compile',deps({target}))",
932+
f"mnemonic('({mnemonics_string})Compile',deps({target}))",
856933
# 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.
857934
'--output=jsonproto',
858935
# 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.

refresh_compile_commands.bzl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def refresh_compile_commands(
6363
targets = None,
6464
exclude_headers = None,
6565
exclude_external_sources = False,
66+
enable_swift = False,
6667
**kwargs): # For the other common attributes. Tags, compatible_with, etc. https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes.
6768
# Convert the various, acceptable target shorthands into the dictionary format
6869
# 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.
@@ -79,7 +80,7 @@ def refresh_compile_commands(
7980

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

8586
def _expand_template_impl(ctx):
@@ -95,6 +96,7 @@ def _expand_template_impl(ctx):
9596
" {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
9697
"{exclude_headers}": '"' + str(ctx.attr.exclude_headers) + '"',
9798
"{exclude_external_sources}": str(ctx.attr.exclude_external_sources),
99+
"{enable_swift}": str(ctx.attr.enable_swift),
98100
},
99101
)
100102
return DefaultInfo(files = depset([script]))
@@ -104,6 +106,7 @@ _expand_template = rule(
104106
"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.
105107
"exclude_external_sources": attr.bool(default = False),
106108
"exclude_headers": attr.string(values = ["all", "external", ""]), # "" needed only for compatibility with Bazel < 3.6.0
109+
"enable_swift": attr.bool(default = False),
107110
"_script_template": attr.label(allow_single_file = True, default = "refresh.template.py"),
108111
"_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
109112
},

0 commit comments

Comments
 (0)