1# Starlark utilities for working with other build systems 2 3load("@rules_pkg//:providers.bzl", "PackageFilegroupInfo", "PackageFilesInfo") 4 5################################################################################ 6# Macro to create CMake and Automake source lists. 7################################################################################ 8 9def gen_file_lists(name, out_stem, **kwargs): 10 gen_cmake_file_lists( 11 name = name + "_cmake", 12 out = out_stem + ".cmake", 13 source_prefix = "${protobuf_SOURCE_DIR}/", 14 **kwargs 15 ) 16 gen_automake_file_lists( 17 name = name + "_automake", 18 out = out_stem + ".am", 19 source_prefix = "$(top_srcdir)/", 20 **kwargs 21 ) 22 native.filegroup( 23 name = name, 24 srcs = [ 25 out_stem + ".cmake", 26 out_stem + ".am", 27 ], 28 ) 29 30################################################################################ 31# Aspect that extracts srcs, hdrs, etc. 32################################################################################ 33 34CcFileList = provider( 35 doc = "List of files to be built into a library.", 36 fields = { 37 # As a rule of thumb, `hdrs` and `textual_hdrs` are the files that 38 # would be installed along with a prebuilt library. 39 "hdrs": "public header files, including those used by generated code", 40 "textual_hdrs": "files which are included but are not self-contained", 41 42 # The `internal_hdrs` are header files which appear in `srcs`. 43 # These are only used when compiling the library. 44 "internal_hdrs": "internal header files (only used to build .cc files)", 45 "srcs": "source files", 46 }, 47) 48 49ProtoFileList = provider( 50 doc = "List of proto files and generated code to be built into a library.", 51 fields = { 52 # Proto files: 53 "proto_srcs": "proto file sources", 54 55 # Generated sources: 56 "hdrs": "header files that are expected to be generated", 57 "srcs": "source files that are expected to be generated", 58 }, 59) 60 61def _flatten_target_files(targets): 62 files = [] 63 for target in targets: 64 for tfile in target.files.to_list(): 65 files.append(tfile) 66 return files 67 68def _combine_cc_file_lists(file_lists): 69 hdrs = {} 70 textual_hdrs = {} 71 internal_hdrs = {} 72 srcs = {} 73 for file_list in file_lists: 74 hdrs.update({f: 1 for f in file_list.hdrs}) 75 textual_hdrs.update({f: 1 for f in file_list.textual_hdrs}) 76 internal_hdrs.update({f: 1 for f in file_list.internal_hdrs}) 77 srcs.update({f: 1 for f in file_list.srcs}) 78 return CcFileList( 79 hdrs = sorted(hdrs.keys()), 80 textual_hdrs = sorted(textual_hdrs.keys()), 81 internal_hdrs = sorted(internal_hdrs.keys()), 82 srcs = sorted(srcs.keys()), 83 ) 84 85def _file_list_aspect_impl(target, ctx): 86 # We're going to reach directly into the attrs on the traversed rule. 87 rule_attr = ctx.rule.attr 88 providers = [] 89 90 # Extract sources from a `cc_library` (or similar): 91 if CcInfo in target: 92 # CcInfo is a proxy for what we expect this rule to look like. 93 # However, some deps may expose `CcInfo` without having `srcs`, 94 # `hdrs`, etc., so we use `getattr` to handle that gracefully. 95 96 internal_hdrs = [] 97 srcs = [] 98 99 # Filter `srcs` so it only contains source files. Headers will go 100 # into `internal_headers`. 101 for src in _flatten_target_files(getattr(rule_attr, "srcs", [])): 102 if src.extension.lower() in ["c", "cc", "cpp", "cxx"]: 103 srcs.append(src) 104 else: 105 internal_hdrs.append(src) 106 107 providers.append(CcFileList( 108 hdrs = _flatten_target_files(getattr(rule_attr, "hdrs", [])), 109 textual_hdrs = _flatten_target_files(getattr( 110 rule_attr, 111 "textual_hdrs", 112 [], 113 )), 114 internal_hdrs = internal_hdrs, 115 srcs = srcs, 116 )) 117 118 # Extract sources from a `proto_library`: 119 if ProtoInfo in target: 120 proto_srcs = [] 121 srcs = [] 122 hdrs = [] 123 for src in _flatten_target_files(rule_attr.srcs): 124 proto_srcs.append(src) 125 srcs.append("%s/%s.pb.cc" % (src.dirname, src.basename)) 126 hdrs.append("%s/%s.pb.h" % (src.dirname, src.basename)) 127 128 providers.append(ProtoFileList( 129 proto_srcs = proto_srcs, 130 srcs = srcs, 131 hdrs = hdrs, 132 )) 133 134 return providers 135 136file_list_aspect = aspect( 137 doc = """ 138Aspect to provide the list of sources and headers from a rule. 139 140Output is CcFileList and/or ProtoFileList. Example: 141 142 cc_library( 143 name = "foo", 144 srcs = [ 145 "foo.cc", 146 "foo_internal.h", 147 ], 148 hdrs = ["foo.h"], 149 textual_hdrs = ["foo_inl.inc"], 150 ) 151 # produces: 152 # CcFileList( 153 # hdrs = [File("foo.h")], 154 # textual_hdrs = [File("foo_inl.inc")], 155 # internal_hdrs = [File("foo_internal.h")], 156 # srcs = [File("foo.cc")], 157 # ) 158 159 proto_library( 160 name = "bar_proto", 161 srcs = ["bar.proto"], 162 ) 163 # produces: 164 # ProtoFileList( 165 # proto_srcs = ["bar.proto"], 166 # # Generated filenames are synthesized: 167 # hdrs = ["bar.pb.h"], 168 # srcs = ["bar.pb.cc"], 169 # ) 170""", 171 implementation = _file_list_aspect_impl, 172) 173 174################################################################################ 175# Generic source lists generation 176# 177# This factory creates a rule implementation that is parameterized by a 178# fragment generator function. 179################################################################################ 180 181def _create_file_list_impl(fragment_generator): 182 # `fragment_generator` is a function like: 183 # def fn(originating_rule: Label, 184 # varname: str, 185 # source_prefix: str, 186 # path_strings: [str]) -> str 187 # 188 # It returns a string that defines `varname` to `path_strings`, each 189 # prepended with `source_prefix`. 190 # 191 # When dealing with `File` objects, the `short_path` is used to strip 192 # the output prefix for generated files. 193 194 def _impl(ctx): 195 out = ctx.outputs.out 196 197 fragments = [] 198 for srcrule, libname in ctx.attr.src_libs.items(): 199 if CcFileList in srcrule: 200 cc_file_list = srcrule[CcFileList] 201 fragments.extend([ 202 fragment_generator( 203 srcrule.label, 204 libname + "_srcs", 205 ctx.attr.source_prefix, 206 [f.short_path for f in cc_file_list.srcs], 207 ), 208 fragment_generator( 209 srcrule.label, 210 libname + "_hdrs", 211 ctx.attr.source_prefix, 212 [f.short_path for f in (cc_file_list.hdrs + 213 cc_file_list.textual_hdrs)], 214 ), 215 ]) 216 217 if ProtoFileList in srcrule: 218 proto_file_list = srcrule[ProtoFileList] 219 fragments.extend([ 220 fragment_generator( 221 srcrule.label, 222 libname + "_proto_srcs", 223 ctx.attr.source_prefix, 224 [f.short_path for f in proto_file_list.proto_srcs], 225 ), 226 fragment_generator( 227 srcrule.label, 228 libname + "_srcs", 229 ctx.attr.source_prefix, 230 proto_file_list.srcs, 231 ), 232 fragment_generator( 233 srcrule.label, 234 libname + "_hdrs", 235 ctx.attr.source_prefix, 236 proto_file_list.hdrs, 237 ), 238 ]) 239 240 files = {} 241 242 if PackageFilegroupInfo in srcrule: 243 for pkg_files_info, origin in srcrule[PackageFilegroupInfo].pkg_files: 244 # keys are the destination path: 245 files.update(pkg_files_info.dest_src_map) 246 247 if PackageFilesInfo in srcrule: 248 # keys are the destination: 249 files.update(srcrule[PackageFilesInfo].dest_src_map) 250 251 if files == {} and DefaultInfo in srcrule and CcInfo not in srcrule: 252 # This could be an individual file or filegroup. 253 # We explicitly ignore rules with CcInfo, since their 254 # output artifacts are libraries or binaries. 255 files.update( 256 { 257 f.short_path: 1 258 for f in srcrule[DefaultInfo].files.to_list() 259 }, 260 ) 261 262 if files: 263 fragments.append( 264 fragment_generator( 265 srcrule.label, 266 libname + "_files", 267 ctx.attr.source_prefix, 268 sorted(files.keys()), 269 ), 270 ) 271 272 ctx.actions.write( 273 output = out, 274 content = (ctx.attr._header % ctx.label) + "\n".join(fragments), 275 ) 276 277 return [DefaultInfo(files = depset([out]))] 278 279 return _impl 280 281# Common rule attrs for rules that use `_create_file_list_impl`: 282# (note that `_header` is also required) 283_source_list_common_attrs = { 284 "out": attr.output( 285 doc = ( 286 "The generated filename. This should usually have a build " + 287 "system-specific extension, like `out.am` or `out.cmake`." 288 ), 289 mandatory = True, 290 ), 291 "src_libs": attr.label_keyed_string_dict( 292 doc = ( 293 "A dict, {target: libname} of libraries to include. " + 294 "Targets can be C++ rules (like `cc_library` or `cc_test`), " + 295 "`proto_library` rules, files, `filegroup` rules, `pkg_files` " + 296 "rules, or `pkg_filegroup` rules. " + 297 "The libname is a string, and used to construct the variable " + 298 "name in the `out` file holding the target's sources. " + 299 "For generated files, the output root (like `bazel-bin/`) is not " + 300 "included. " + 301 "For `pkg_files` and `pkg_filegroup` rules, the destination path " + 302 "is used." 303 ), 304 mandatory = True, 305 providers = [ 306 [CcFileList], 307 [DefaultInfo], 308 [PackageFilegroupInfo], 309 [PackageFilesInfo], 310 [ProtoFileList], 311 ], 312 aspects = [file_list_aspect], 313 ), 314 "source_prefix": attr.string( 315 doc = "String to prepend to each source path.", 316 ), 317} 318 319################################################################################ 320# CMake source lists generation 321################################################################################ 322 323def _cmake_var_fragment(owner, varname, prefix, entries): 324 """Returns a single `set(varname ...)` fragment (CMake syntax). 325 326 Args: 327 owner: Label, the rule that owns these srcs. 328 varname: str, the var name to set. 329 prefix: str, prefix to prepend to each of `entries`. 330 entries: [str], the entries in the list. 331 332 Returns: 333 A string. 334 """ 335 return ( 336 "# {owner}\n" + 337 "set({varname}\n" + 338 "{entries}\n" + 339 ")\n" 340 ).format( 341 owner = owner, 342 varname = varname, 343 entries = "\n".join([" %s%s" % (prefix, f) for f in entries]), 344 ) 345 346gen_cmake_file_lists = rule( 347 doc = """ 348Generates a CMake-syntax file with lists of files. 349 350The generated file defines variables with lists of files from `srcs`. The 351intent is for these files to be included from a non-generated CMake file 352which actually defines the libraries based on these lists. 353 354For C++ rules, the following are generated: 355 {libname}_srcs: contains srcs. 356 {libname}_hdrs: contains hdrs and textual_hdrs. 357 358For proto_library, the following are generated: 359 {libname}_proto_srcs: contains the srcs from the `proto_library` rule. 360 {libname}_srcs: contains syntesized paths for generated C++ sources. 361 {libname}_hdrs: contains syntesized paths for generated C++ headers. 362 363""", 364 implementation = _create_file_list_impl(_cmake_var_fragment), 365 attrs = dict( 366 _source_list_common_attrs, 367 _header = attr.string( 368 default = """\ 369# Auto-generated by %s 370# 371# This file contains lists of sources based on Bazel rules. It should 372# be included from a hand-written CMake file that defines targets. 373# 374# Changes to this file will be overwritten based on Bazel definitions. 375 376if(${CMAKE_VERSION} VERSION_GREATER 3.10 OR ${CMAKE_VERSION} VERSION_EQUAL 3.10) 377 include_guard() 378endif() 379 380""", 381 ), 382 ), 383) 384 385################################################################################ 386# Automake source lists generation 387################################################################################ 388 389def _automake_var_fragment(owner, varname, prefix, entries): 390 """Returns a single variable assignment fragment (Automake syntax). 391 392 Args: 393 owner: Label, the rule that owns these srcs. 394 varname: str, the var name to set. 395 prefix: str, prefix to prepend to each of `entries`. 396 entries: [str], the entries in the list. 397 398 Returns: 399 A string. 400 """ 401 if len(entries) == 0: 402 # A backslash followed by a blank line is illegal. We still want 403 # to emit the variable, though. 404 return "# {owner}\n{varname} =\n".format( 405 owner = owner, 406 varname = varname, 407 ) 408 fragment = ( 409 "# {owner}\n" + 410 "{varname} = \\\n" + 411 "{entries}" 412 ).format( 413 owner = owner, 414 varname = varname, 415 entries = " \\\n".join([" %s%s" % (prefix, f) for f in entries]), 416 ) 417 return fragment.rstrip("\\ ") + "\n" 418 419gen_automake_file_lists = rule( 420 doc = """ 421Generates an Automake-syntax file with lists of files. 422 423The generated file defines variables with lists of files from `srcs`. The 424intent is for these files to be included from a non-generated Makefile.am 425file which actually defines the libraries based on these lists. 426 427For C++ rules, the following are generated: 428 {libname}_srcs: contains srcs. 429 {libname}_hdrs: contains hdrs and textual_hdrs. 430 431For proto_library, the following are generated: 432 {libname}_proto_srcs: contains the srcs from the `proto_library` rule. 433 {libname}_srcs: contains syntesized paths for generated C++ sources. 434 {libname}_hdrs: contains syntesized paths for generated C++ headers. 435 436""", 437 implementation = _create_file_list_impl(_automake_var_fragment), 438 attrs = dict( 439 _source_list_common_attrs.items(), 440 _header = attr.string( 441 default = """\ 442# Auto-generated by %s 443# 444# This file contains lists of sources based on Bazel rules. It should 445# be included from a hand-written Makefile.am that defines targets. 446# 447# Changes to this file will be overwritten based on Bazel definitions. 448 449""", 450 ), 451 ), 452) 453