Skip to content

Commit ec3dbd8

Browse files
authored
Merge pull request github#10815 from github/redsun82/cmake-generator-prototype
Swift: cmake generator for better IDE support
2 parents f0eabd4 + a20fdad commit ec3dbd8

File tree

7 files changed

+351
-1
lines changed

7 files changed

+351
-1
lines changed

misc/bazel/cmake/BUILD.bazel

Whitespace-only changes.

misc/bazel/cmake/cmake.bzl

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
CmakeInfo = provider(
2+
fields = {
3+
"name": "",
4+
"inputs": "",
5+
"kind": "",
6+
"modifier": "",
7+
"hdrs": "",
8+
"srcs": "",
9+
"deps": "",
10+
"system_includes": "",
11+
"includes": "",
12+
"quote_includes": "",
13+
"stripped_includes": "",
14+
"imported_static_libs": "",
15+
"imported_dynamic_libs": "",
16+
"copts": "",
17+
"linkopts": "",
18+
"force_cxx_compilation": "",
19+
"defines": "",
20+
"local_defines": "",
21+
"transitive_deps": "",
22+
},
23+
)
24+
25+
def _cmake_name(label):
26+
return ("%s_%s_%s" % (label.workspace_name, label.package, label.name)).replace("/", "_")
27+
28+
def _cmake_file(file):
29+
if not file.is_source:
30+
return "${BAZEL_EXEC_ROOT}/" + file.path
31+
return _cmake_path(file.path)
32+
33+
def _cmake_path(path):
34+
if path.startswith("external/"):
35+
return "${BAZEL_OUTPUT_BASE}/" + path
36+
return "${BAZEL_WORKSPACE}/" + path
37+
38+
def _file_kind(file):
39+
ext = file.extension
40+
if ext in ("c", "cc", "cpp"):
41+
return "src"
42+
if ext in ("h", "hh", "hpp", "def", "inc"):
43+
return "hdr"
44+
if ext == "a":
45+
return "static_lib"
46+
if ext in ("so", "dylib"):
47+
return "dynamic_lib"
48+
return None
49+
50+
def _get_includes(includes):
51+
# see strip prefix comment below to understand why we are skipping virtual includes here
52+
return [_cmake_path(i) for i in includes.to_list() if "/_virtual_includes/" not in i]
53+
54+
def _cmake_aspect_impl(target, ctx):
55+
if not ctx.rule.kind.startswith("cc_"):
56+
return [CmakeInfo(name = None, transitive_deps = depset())]
57+
58+
name = _cmake_name(ctx.label)
59+
60+
is_macos = "darwin" in ctx.var["TARGET_CPU"]
61+
62+
is_binary = ctx.rule.kind == "cc_binary"
63+
force_cxx_compilation = "force_cxx_compilation" in ctx.rule.attr.features
64+
attr = ctx.rule.attr
65+
srcs = attr.srcs + getattr(attr, "hdrs", []) + getattr(attr, "textual_hdrs", [])
66+
srcs = [f for src in srcs for f in src.files.to_list()]
67+
inputs = [f for f in srcs if not f.is_source or f.path.startswith("external/")]
68+
by_kind = {}
69+
for f in srcs:
70+
by_kind.setdefault(_file_kind(f), []).append(_cmake_file(f))
71+
hdrs = by_kind.get("hdr", [])
72+
srcs = by_kind.get("src", [])
73+
static_libs = by_kind.get("static_lib", [])
74+
dynamic_libs = by_kind.get("dynamic_lib", [])
75+
if not srcs and is_binary:
76+
empty = ctx.actions.declare_file(name + "_empty.cpp")
77+
ctx.actions.write(empty, "")
78+
inputs.append(empty)
79+
srcs = [_cmake_file(empty)]
80+
deps = ctx.rule.attr.deps if hasattr(ctx.rule.attr, "deps") else []
81+
82+
cxx_compilation = force_cxx_compilation or any([not src.endswith(".c") for src in srcs])
83+
84+
copts = ctx.fragments.cpp.copts + (ctx.fragments.cpp.cxxopts if cxx_compilation else ctx.fragments.cpp.conlyopts)
85+
copts += [ctx.expand_make_variables("copts", o, {}) for o in ctx.rule.attr.copts]
86+
87+
linkopts = ctx.fragments.cpp.linkopts
88+
linkopts += [ctx.expand_make_variables("linkopts", o, {}) for o in ctx.rule.attr.linkopts]
89+
90+
compilation_ctx = target[CcInfo].compilation_context
91+
system_includes = _get_includes(compilation_ctx.system_includes)
92+
93+
# move -I copts to includes
94+
includes = _get_includes(compilation_ctx.includes) + [_cmake_path(opt[2:]) for opt in copts if opt.startswith("-I")]
95+
copts = [opt for opt in copts if not opt.startswith("-I")]
96+
quote_includes = _get_includes(compilation_ctx.quote_includes)
97+
98+
# strip prefix is special, as in bazel it creates a _virtual_includes directory with symlinks
99+
# as we want to avoid relying on bazel having done that, we must undo that mechanism
100+
# also for some reason cmake fails to propagate these with target_include_directories,
101+
# so we propagate them ourselvels by using the stripped_includes field
102+
stripped_includes = []
103+
if getattr(ctx.rule.attr, "strip_include_prefix", ""):
104+
prefix = ctx.rule.attr.strip_include_prefix.strip("/")
105+
if ctx.label.workspace_name:
106+
stripped_includes = [
107+
"${BAZEL_OUTPUT_BASE}/external/%s/%s" % (ctx.label.workspace_name, prefix), # source
108+
"${BAZEL_EXEC_ROOT}/%s/external/%s/%s" % (ctx.var["BINDIR"], ctx.label.workspace_name, prefix), # generated
109+
]
110+
else:
111+
stripped_includes = [
112+
prefix, # source
113+
"${BAZEL_EXEC_ROOT}/%s/%s" % (ctx.var["BINDIR"], prefix), # generated
114+
]
115+
116+
deps = [dep[CmakeInfo] for dep in deps if CmakeInfo in dep]
117+
118+
# by the book this should be done with depsets, but so far the performance implication is negligible
119+
for dep in deps:
120+
if dep.name:
121+
stripped_includes += dep.stripped_includes
122+
includes += stripped_includes
123+
124+
return [
125+
CmakeInfo(
126+
name = name,
127+
inputs = inputs,
128+
kind = "executable" if is_binary else "library",
129+
modifier = "INTERFACE" if not srcs and not is_binary else "",
130+
hdrs = hdrs,
131+
srcs = srcs,
132+
deps = [dep for dep in deps if dep.name != None],
133+
includes = includes,
134+
system_includes = system_includes,
135+
quote_includes = quote_includes,
136+
stripped_includes = stripped_includes,
137+
imported_static_libs = static_libs,
138+
imported_dynamic_libs = dynamic_libs,
139+
copts = copts,
140+
linkopts = linkopts,
141+
defines = compilation_ctx.defines.to_list(),
142+
local_defines = compilation_ctx.local_defines.to_list(),
143+
force_cxx_compilation = force_cxx_compilation,
144+
transitive_deps = depset(deps, transitive = [dep.transitive_deps for dep in deps]),
145+
),
146+
]
147+
148+
cmake_aspect = aspect(
149+
implementation = _cmake_aspect_impl,
150+
attr_aspects = ["deps"],
151+
fragments = ["cpp"],
152+
)
153+
154+
def _map_cmake_info(info, is_windows):
155+
args = " ".join([info.name, info.modifier] + info.hdrs + info.srcs).strip()
156+
commands = [
157+
"add_%s(%s)" % (info.kind, args),
158+
]
159+
if info.imported_static_libs and info.imported_dynamic_libs:
160+
commands += [
161+
"if(BUILD_SHARED_LIBS)",
162+
" target_link_libraries(%s %s %s)" %
163+
(info.name, info.modifier or "PUBLIC", " ".join(info.imported_dynamic_libs)),
164+
"else()",
165+
" target_link_libraries(%s %s %s)" %
166+
(info.name, info.modifier or "PUBLIC", " ".join(info.imported_static_libs)),
167+
"endif()",
168+
]
169+
elif info.imported_static_libs or info.imported_dynamic_libs:
170+
commands += [
171+
"target_link_libraries(%s %s %s)" %
172+
(info.name, info.modifier or "PUBLIC", " ".join(info.imported_dynamic_lib + info.imported_static_libs)),
173+
]
174+
if info.deps:
175+
libs = {}
176+
if info.modifier == "INTERFACE":
177+
libs = {"INTERFACE": [lib.name for lib in info.deps]}
178+
else:
179+
for lib in info.deps:
180+
libs.setdefault(lib.modifier, []).append(lib.name)
181+
for modifier, names in libs.items():
182+
commands += [
183+
"target_link_libraries(%s %s %s)" % (info.name, modifier or "PUBLIC", " ".join(names)),
184+
]
185+
if info.includes:
186+
commands += [
187+
"target_include_directories(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.includes)),
188+
]
189+
if info.system_includes:
190+
commands += [
191+
"target_include_directories(%s SYSTEM %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.system_includes)),
192+
]
193+
if info.quote_includes:
194+
if is_windows:
195+
commands += [
196+
"target_include_directories(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.quote_includes)),
197+
]
198+
else:
199+
commands += [
200+
"target_compile_options(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(["-iquote%s" % i for i in info.quote_includes])),
201+
]
202+
if info.copts and info.modifier != "INTERFACE":
203+
commands += [
204+
"target_compile_options(%s PRIVATE %s)" % (info.name, " ".join(info.copts)),
205+
]
206+
if info.linkopts:
207+
commands += [
208+
"target_link_options(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.linkopts)),
209+
]
210+
if info.force_cxx_compilation and any([f.endswith(".c") for f in info.srcs]):
211+
commands += [
212+
"set_source_files_properties(%s PROPERTIES LANGUAGE CXX)" % " ".join([f for f in info.srcs if f.endswith(".c")]),
213+
]
214+
if info.defines:
215+
commands += [
216+
"target_compile_definitions(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.defines)),
217+
]
218+
if info.local_defines:
219+
commands += [
220+
"target_compile_definitions(%s %s %s)" % (info.name, info.modifier or "PRIVATE", " ".join(info.local_defines)),
221+
]
222+
return commands
223+
224+
GeneratedCmakeFiles = provider(
225+
fields = {
226+
"files": "",
227+
},
228+
)
229+
230+
def _generate_cmake_impl(ctx):
231+
commands = []
232+
inputs = []
233+
234+
infos = {}
235+
for dep in ctx.attr.targets:
236+
for info in [dep[CmakeInfo]] + dep[CmakeInfo].transitive_deps.to_list():
237+
if info.name != None:
238+
inputs += info.inputs
239+
infos[info.name] = info
240+
241+
is_windows = ctx.target_platform_has_constraint(ctx.attr._windows[platform_common.ConstraintValueInfo])
242+
243+
for info in infos.values():
244+
commands += _map_cmake_info(info, is_windows)
245+
commands.append("")
246+
247+
for include in ctx.attr.includes:
248+
for file in include[GeneratedCmakeFiles].files.to_list():
249+
inputs.append(file)
250+
commands.append("include(${BAZEL_EXEC_ROOT}/%s)" % file.path)
251+
252+
# we want to use a run or run_shell action to register a bunch of files like inputs, but we cannot write all
253+
# in a shell command as we would hit the command size limit. So we first write the file and then copy it with
254+
# the dummy inputs
255+
tmp_output = ctx.actions.declare_file(ctx.label.name + ".cmake~")
256+
output = ctx.actions.declare_file(ctx.label.name + ".cmake")
257+
ctx.actions.write(tmp_output, "\n".join(commands))
258+
ctx.actions.run_shell(outputs = [output], inputs = inputs + [tmp_output], command = "cp %s %s" % (tmp_output.path, output.path))
259+
260+
return [
261+
DefaultInfo(files = depset([output])),
262+
GeneratedCmakeFiles(files = depset([output])),
263+
]
264+
265+
generate_cmake = rule(
266+
implementation = _generate_cmake_impl,
267+
attrs = {
268+
"targets": attr.label_list(aspects = [cmake_aspect]),
269+
"includes": attr.label_list(providers = [GeneratedCmakeFiles]),
270+
"_windows": attr.label(default = "@platforms//os:windows"),
271+
},
272+
)

misc/bazel/cmake/setup.cmake

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
option(BUILD_SHARED_LIBS "Build and use shared libraries" 0)
2+
option(CREATE_COMPILATION_DATABASE_LINK "Create compilation database link. Implies CMAKE_EXPORT_COMPILE_COMMANDS" 1)
3+
4+
if (CREATE_COMPILATION_DATABASE_LINK)
5+
set(CMAKE_EXPORT_COMPILE_COMMANDS 1)
6+
endif ()
7+
8+
macro(bazel)
9+
execute_process(COMMAND bazel ${ARGN} COMMAND_ERROR_IS_FATAL ANY OUTPUT_STRIP_TRAILING_WHITESPACE)
10+
endmacro()
11+
12+
bazel(info workspace OUTPUT_VARIABLE BAZEL_WORKSPACE)
13+
14+
bazel(info output_base OUTPUT_VARIABLE BAZEL_OUTPUT_BASE)
15+
string(REPLACE "-" "_" BAZEL_EXEC_ROOT ${PROJECT_NAME})
16+
set(BAZEL_EXEC_ROOT ${BAZEL_OUTPUT_BASE}/execroot/${BAZEL_EXEC_ROOT})
17+
18+
macro(include_generated BAZEL_TARGET)
19+
bazel(build ${BAZEL_TARGET})
20+
string(REPLACE "@" "/external/" BAZEL_TARGET_PATH ${BAZEL_TARGET})
21+
string(REPLACE "//" "/" BAZEL_TARGET_PATH ${BAZEL_TARGET_PATH})
22+
string(REPLACE ":" "/" BAZEL_TARGET_PATH ${BAZEL_TARGET_PATH})
23+
include(${BAZEL_WORKSPACE}/bazel-bin${BAZEL_TARGET_PATH}.cmake)
24+
endmacro()
25+
26+
if (CREATE_COMPILATION_DATABASE_LINK)
27+
file(CREATE_LINK ${PROJECT_BINARY_DIR}/compile_commands.json ${PROJECT_SOURCE_DIR}/compile_commands.json SYMBOLIC)
28+
endif ()

swift/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,13 @@
33

44
# output files created by running tests
55
*.o
6+
7+
# compilation database
8+
compile_commands.json
9+
10+
# CLion project data and build directories
11+
/.idea
12+
/cmake*
13+
14+
# VSCode default build directory
15+
/build

swift/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# this uses generated cmake files to setup cmake compilation of the swift extractor
2+
# this is provided solely for IDE integration
3+
4+
cmake_minimum_required(VERSION 3.21)
5+
6+
set(CMAKE_CXX_STANDARD 17)
7+
set(CMAKE_CXX_EXTENSIONS OFF)
8+
9+
set(CMAKE_C_COMPILER clang)
10+
set(CMAKE_CXX_COMPILER clang++)
11+
12+
project(codeql)
13+
14+
include(../misc/bazel/cmake/setup.cmake)
15+
16+
include_generated(//swift/extractor:cmake)

swift/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,20 @@ bazel run //swift/codegen
3333

3434
to update generated files. This can be shortened to
3535
`bazel run codegen` if you are in the `swift` directory.
36+
37+
## IDE setup
38+
39+
### CLion and the native bazel plugin
40+
41+
You can use [CLion][1] with the official [IntelliJ Bazel plugin][2], creating the project from scratch with default
42+
options. This is known to have issues on non-Linux platforms.
43+
44+
[1]: https://www.jetbrains.com/clion/
45+
46+
[2]: https://ij.bazel.build/
47+
48+
### CMake project
49+
50+
The `CMakeLists.txt` file allows to load the Swift extractor as a CMake project, which allows integration into a wider
51+
variety of IDEs. Building with CMake also creates a `compile_commands.json` compilation database that can be picked up
52+
by even more IDEs. In particular, opening the `swift` directory in VSCode should work.

swift/extractor/BUILD.bazel

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("//swift:rules.bzl", "swift_cc_binary")
2+
load("//misc/bazel/cmake:cmake.bzl", "generate_cmake")
23

34
swift_cc_binary(
45
name = "extractor",
@@ -9,8 +10,14 @@ swift_cc_binary(
910
visibility = ["//swift:__pkg__"],
1011
deps = [
1112
"//swift/extractor/infra",
12-
"//swift/extractor/visitors",
1313
"//swift/extractor/remapping",
14+
"//swift/extractor/visitors",
1415
"//swift/tools/prebuilt:swift-llvm-support",
1516
],
1617
)
18+
19+
generate_cmake(
20+
name = "cmake",
21+
targets = [":extractor"],
22+
visibility = ["//visibility:public"],
23+
)

0 commit comments

Comments
 (0)