-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathcmake.bzl
More file actions
273 lines (244 loc) · 10.6 KB
/
cmake.bzl
File metadata and controls
273 lines (244 loc) · 10.6 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
CmakeInfo = provider(
fields = {
"name": "",
"inputs": "",
"kind": "",
"modifier": "",
"hdrs": "",
"srcs": "",
"deps": "",
"system_includes": "",
"includes": "",
"quote_includes": "",
"stripped_includes": "",
"imported_libs": "",
"copts": "",
"linkopts": "",
"force_cxx_compilation": "",
"defines": "",
"local_defines": "",
"transitive_deps": "",
},
)
def _cmake_name(label):
# strip away the bzlmod module version for now
workspace_name, _, _ = label.workspace_name.partition("~")
ret = ("%s_%s_%s" % (workspace_name, label.package, label.name)).replace("/", "_")
internal_transition_suffix = "_INTERNAL_TRANSITION"
if ret.endswith(internal_transition_suffix):
ret = ret[:-len(internal_transition_suffix)]
return ret
def _cmake_file(file):
if not file.is_source:
return "${BAZEL_EXEC_ROOT}/" + file.path
return _cmake_path(file.path)
def _cmake_path(path):
if path.startswith("external/"):
return "${BAZEL_OUTPUT_BASE}/" + path
return "${BAZEL_WORKSPACE}/" + path
def _file_kind(file):
ext = file.extension
if ext in ("c", "cc", "cpp"):
return "src"
if ext in ("h", "hh", "hpp", "def", "inc"):
return "hdr"
if ext in ("a", "so", "dylib"):
return "lib"
return None
def _get_includes(includes):
# see strip prefix comment below to understand why we are skipping virtual includes here
return [_cmake_path(i) for i in includes.to_list() if "/_virtual_includes/" not in i]
def _cmake_aspect_impl(target, ctx):
if not ctx.rule.kind.startswith(("cc_", "_cc_add_features")):
return [CmakeInfo(name = None, transitive_deps = depset())]
# TODO: remove cc_binary_add_features once we remove it from internal repo
if ctx.rule.kind in ("cc_binary_add_features", "_cc_add_features_binary", "_cc_add_features_test"):
dep = ctx.rule.attr.dep[0][CmakeInfo]
return [CmakeInfo(
name = None,
transitive_deps = depset([dep], transitive = [dep.transitive_deps]),
)]
name = _cmake_name(ctx.label)
is_macos = "darwin" in ctx.var["TARGET_CPU"]
is_binary = ctx.rule.kind in ("cc_binary", "cc_test")
force_cxx_compilation = "force_cxx_compilation" in ctx.rule.attr.features
attr = ctx.rule.attr
srcs = getattr(attr, "srcs", []) + getattr(attr, "hdrs", []) + getattr(attr, "textual_hdrs", [])
srcs = [f for src in srcs for f in src.files.to_list()]
inputs = [f for f in srcs if not f.is_source or f.path.startswith("external/")]
by_kind = {}
for f in srcs:
by_kind.setdefault(_file_kind(f), []).append(_cmake_file(f))
hdrs = by_kind.get("hdr", [])
srcs = by_kind.get("src", [])
libs = by_kind.get("lib", [])
if not srcs and is_binary:
empty = ctx.actions.declare_file(name + "_empty.cpp")
ctx.actions.write(empty, "")
inputs.append(empty)
srcs = [_cmake_file(empty)]
deps = ctx.rule.attr.deps if hasattr(ctx.rule.attr, "deps") else []
cxx_compilation = force_cxx_compilation or any([not src.endswith(".c") for src in srcs])
copts = ctx.fragments.cpp.copts + (ctx.fragments.cpp.cxxopts if cxx_compilation else ctx.fragments.cpp.conlyopts)
copts += [ctx.expand_make_variables("copts", o, {}) for o in getattr(ctx.rule.attr, "copts", [])]
linkopts = ctx.fragments.cpp.linkopts
linkopts += [ctx.expand_make_variables("linkopts", o, {}) for o in getattr(ctx.rule.attr, "linkopts", [])]
compilation_ctx = target[CcInfo].compilation_context
system_includes = _get_includes(compilation_ctx.system_includes)
# move -I copts to includes
includes = _get_includes(compilation_ctx.includes) + [_cmake_path(opt[2:]) for opt in copts if opt.startswith("-I")]
copts = [opt for opt in copts if not opt.startswith("-I")]
quote_includes = _get_includes(compilation_ctx.quote_includes)
# strip prefix is special, as in bazel it creates a _virtual_includes directory with symlinks
# as we want to avoid relying on bazel having done that, we must undo that mechanism
# also for some reason cmake fails to propagate these with target_include_directories,
# so we propagate them ourselvels by using the stripped_includes field
stripped_includes = []
if getattr(ctx.rule.attr, "strip_include_prefix", ""):
prefix = ctx.rule.attr.strip_include_prefix.strip("/")
if ctx.label.workspace_name:
stripped_includes = [
"${BAZEL_OUTPUT_BASE}/external/%s/%s" % (ctx.label.workspace_name, prefix), # source
"${BAZEL_EXEC_ROOT}/%s/external/%s/%s" % (ctx.var["BINDIR"], ctx.label.workspace_name, prefix), # generated
]
else:
stripped_includes = [
prefix, # source
"${BAZEL_EXEC_ROOT}/%s/%s" % (ctx.var["BINDIR"], prefix), # generated
]
deps = [dep[CmakeInfo] for dep in deps if CmakeInfo in dep]
# by the book this should be done with depsets, but so far the performance implication is negligible
for dep in deps:
if dep.name:
stripped_includes += dep.stripped_includes
includes += stripped_includes
return [
CmakeInfo(
name = name,
inputs = inputs,
kind = "executable" if is_binary else "library",
modifier = "INTERFACE" if not srcs and not is_binary else "",
hdrs = hdrs,
srcs = srcs,
deps = [dep for dep in deps if dep.name != None],
includes = includes,
system_includes = system_includes,
quote_includes = quote_includes,
stripped_includes = stripped_includes,
imported_libs = libs,
copts = copts,
linkopts = linkopts,
defines = compilation_ctx.defines.to_list(),
local_defines = [
d
for d in compilation_ctx.local_defines.to_list()
if not d.startswith("BAZEL_CURRENT_REPOSITORY")
],
force_cxx_compilation = force_cxx_compilation,
transitive_deps = depset(deps, transitive = [dep.transitive_deps for dep in deps]),
),
]
cmake_aspect = aspect(
implementation = _cmake_aspect_impl,
attr_aspects = ["deps", "dep"],
fragments = ["cpp"],
)
def _map_cmake_info(info, is_windows):
args = " ".join([info.name, info.modifier] + info.hdrs + info.srcs).strip()
commands = [
"add_%s(%s)" % (info.kind, args),
]
if info.imported_libs:
commands.append(
"target_link_libraries(%s %s %s)" %
(info.name, info.modifier or "PUBLIC", " ".join(info.imported_libs)),
)
if info.deps:
libs = {}
if info.modifier == "INTERFACE":
libs = {"INTERFACE": [lib.name for lib in info.deps]}
else:
for lib in info.deps:
libs.setdefault(lib.modifier, []).append(lib.name)
for modifier, names in libs.items():
commands.append(
"target_link_libraries(%s %s %s)" % (info.name, modifier or "PUBLIC", " ".join(names)),
)
if info.includes:
commands.append(
"target_include_directories(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.includes)),
)
if info.system_includes:
commands.append(
"target_include_directories(%s SYSTEM %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.system_includes)),
)
if info.quote_includes:
if is_windows:
commands.append(
"target_include_directories(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.quote_includes)),
)
else:
commands.append(
"target_compile_options(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(["-iquote%s" % i for i in info.quote_includes])),
)
if info.copts and info.modifier != "INTERFACE":
commands.append(
"target_compile_options(%s PRIVATE %s)" % (info.name, " ".join(info.copts)),
)
if info.linkopts:
commands.append(
"target_link_options(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.linkopts)),
)
if info.force_cxx_compilation and any([f.endswith(".c") for f in info.srcs]):
commands.append(
"set_source_files_properties(%s PROPERTIES LANGUAGE CXX)" % " ".join([f for f in info.srcs if f.endswith(".c")]),
)
if info.defines:
commands.append(
"target_compile_definitions(%s %s %s)" % (info.name, info.modifier or "PUBLIC", " ".join(info.defines)),
)
if info.local_defines:
commands.append(
"target_compile_definitions(%s %s %s)" % (info.name, info.modifier or "PRIVATE", " ".join(info.local_defines)),
)
return commands
GeneratedCmakeFiles = provider(
fields = {
"targets": "",
},
)
def _generate_cmake_impl(ctx):
commands = []
inputs = []
infos = {}
targets = list(ctx.attr.targets)
for include in ctx.attr.includes:
targets += include[GeneratedCmakeFiles].targets.to_list()
for dep in targets:
for info in [dep[CmakeInfo]] + dep[CmakeInfo].transitive_deps.to_list():
if info.name != None:
inputs += info.inputs
infos[info.name] = info
is_windows = ctx.target_platform_has_constraint(ctx.attr._windows[platform_common.ConstraintValueInfo])
for info in infos.values():
commands += _map_cmake_info(info, is_windows)
commands.append("")
# we want to use a run or run_shell action to register a bunch of files like inputs, but we cannot write all
# in a shell command as we would hit the command size limit. So we first write the file and then copy it with
# the dummy inputs
tmp_output = ctx.actions.declare_file(ctx.label.name + ".cmake~")
output = ctx.actions.declare_file(ctx.label.name + ".cmake")
ctx.actions.write(tmp_output, "\n".join(commands))
ctx.actions.run_shell(outputs = [output], inputs = inputs + [tmp_output], command = "cp %s %s" % (tmp_output.path, output.path))
return [
DefaultInfo(files = depset([output])),
GeneratedCmakeFiles(targets = depset(ctx.attr.targets)),
]
generate_cmake = rule(
implementation = _generate_cmake_impl,
attrs = {
"targets": attr.label_list(aspects = [cmake_aspect]),
"includes": attr.label_list(providers = [GeneratedCmakeFiles]),
"_windows": attr.label(default = "@platforms//os:windows"),
},
)