1"""system_library is a repository rule for importing system libraries""" 2 3BAZEL_LIB_ADDITIONAL_PATHS_ENV_VAR = "BAZEL_LIB_ADDITIONAL_PATHS" 4BAZEL_LIB_OVERRIDE_PATHS_ENV_VAR = "BAZEL_LIB_OVERRIDE_PATHS" 5BAZEL_INCLUDE_ADDITIONAL_PATHS_ENV_VAR = "BAZEL_INCLUDE_ADDITIONAL_PATHS" 6BAZEL_INCLUDE_OVERRIDE_PATHS_ENV_VAR = "BAZEL_INCLUDE_OVERRIDE_PATHS" 7ENV_VAR_SEPARATOR = "," 8ENV_VAR_ASSIGNMENT = "=" 9 10def _make_flags(flag_values, flag): 11 flags = [] 12 if flag_values: 13 for s in flag_values: 14 flags.append(flag + s) 15 return " ".join(flags) 16 17def _split_env_var(repo_ctx, var_name): 18 value = repo_ctx.os.environ.get(var_name) 19 if value: 20 assignments = value.split(ENV_VAR_SEPARATOR) 21 dict = {} 22 for assignment in assignments: 23 pair = assignment.split(ENV_VAR_ASSIGNMENT) 24 if len(pair) != 2: 25 fail( 26 "Assignments should have form 'name=value', " + 27 "but encountered {} in env variable {}" 28 .format(assignment, var_name), 29 ) 30 key, value = pair[0], pair[1] 31 if not dict.get(key): 32 dict[key] = [] 33 dict[key].append(value) 34 return dict 35 else: 36 return {} 37 38def _get_list_from_env_var(repo_ctx, var_name, key): 39 return _split_env_var(repo_ctx, var_name).get(key, default = []) 40 41def _execute_bash(repo_ctx, cmd): 42 return repo_ctx.execute(["/bin/bash", "-c", cmd]).stdout.strip("\n") 43 44def _find_linker(repo_ctx): 45 ld = _execute_bash(repo_ctx, "which ld") 46 lld = _execute_bash(repo_ctx, "which lld") 47 if ld: 48 return ld 49 elif lld: 50 return lld 51 else: 52 fail("No linker found") 53 54def _find_compiler(repo_ctx): 55 gcc = _execute_bash(repo_ctx, "which g++") 56 clang = _execute_bash(repo_ctx, "which clang++") 57 if gcc: 58 return gcc 59 elif clang: 60 return clang 61 else: 62 fail("No compiler found") 63 64def _find_lib_path(repo_ctx, lib_name, archive_names, lib_path_hints): 65 override_paths = _get_list_from_env_var( 66 repo_ctx, 67 BAZEL_LIB_OVERRIDE_PATHS_ENV_VAR, 68 lib_name, 69 ) 70 additional_paths = _get_list_from_env_var( 71 repo_ctx, 72 BAZEL_LIB_ADDITIONAL_PATHS_ENV_VAR, 73 lib_name, 74 ) 75 76 # Directories will be searched in order 77 path_flags = _make_flags( 78 override_paths + lib_path_hints + additional_paths, 79 "-L", 80 ) 81 linker = _find_linker(repo_ctx) 82 for archive_name in archive_names: 83 cmd = """ 84 {} -verbose -l:{} {} 2>/dev/null | \\ 85 grep succeeded | \\ 86 head -1 | \\ 87 sed -e 's/^\\s*attempt to open //' -e 's/ succeeded\\s*$//' 88 """.format( 89 linker, 90 archive_name, 91 path_flags, 92 ) 93 path = _execute_bash(repo_ctx, cmd) 94 if path: 95 return (archive_name, path) 96 return (None, None) 97 98def _find_header_path(repo_ctx, lib_name, header_name, includes): 99 override_paths = _get_list_from_env_var( 100 repo_ctx, 101 BAZEL_INCLUDE_OVERRIDE_PATHS_ENV_VAR, 102 lib_name, 103 ) 104 additional_paths = _get_list_from_env_var( 105 repo_ctx, 106 BAZEL_INCLUDE_ADDITIONAL_PATHS_ENV_VAR, 107 lib_name, 108 ) 109 110 compiler = _find_compiler(repo_ctx) 111 cmd = """ 112 print | \\ 113 {} -Wp,-v -x c++ - -fsyntax-only 2>&1 | \\ 114 sed -n -e '/^\\s\\+/p' | \\ 115 sed -e 's/^[ \t]*//' 116 """.format(compiler) 117 system_includes = _execute_bash(repo_ctx, cmd).split("\n") 118 all_includes = (override_paths + includes + 119 system_includes + additional_paths) 120 121 for directory in all_includes: 122 cmd = """ 123 test -f "{dir}/{hdr}" && echo "{dir}/{hdr}" 124 """.format(dir = directory, hdr = header_name) 125 result = _execute_bash(repo_ctx, cmd) 126 if result: 127 return result 128 return None 129 130def _system_library_impl(repo_ctx): 131 repo_name = repo_ctx.attr.name 132 includes = repo_ctx.attr.includes 133 hdrs = repo_ctx.attr.hdrs 134 optional_hdrs = repo_ctx.attr.optional_hdrs 135 deps = repo_ctx.attr.deps 136 lib_path_hints = repo_ctx.attr.lib_path_hints 137 static_lib_names = repo_ctx.attr.static_lib_names 138 shared_lib_names = repo_ctx.attr.shared_lib_names 139 140 static_lib_name, static_lib_path = _find_lib_path( 141 repo_ctx, 142 repo_name, 143 static_lib_names, 144 lib_path_hints, 145 ) 146 shared_lib_name, shared_lib_path = _find_lib_path( 147 repo_ctx, 148 repo_name, 149 shared_lib_names, 150 lib_path_hints, 151 ) 152 153 if not static_lib_path and not shared_lib_path: 154 fail("Library {} could not be found".format(repo_name)) 155 156 hdr_names = [] 157 hdr_paths = [] 158 for hdr in hdrs: 159 hdr_path = _find_header_path(repo_ctx, repo_name, hdr, includes) 160 if hdr_path: 161 repo_ctx.symlink(hdr_path, hdr) 162 hdr_names.append(hdr) 163 hdr_paths.append(hdr_path) 164 else: 165 fail("Could not find required header {}".format(hdr)) 166 167 for hdr in optional_hdrs: 168 hdr_path = _find_header_path(repo_ctx, repo_name, hdr, includes) 169 if hdr_path: 170 repo_ctx.symlink(hdr_path, hdr) 171 hdr_names.append(hdr) 172 hdr_paths.append(hdr_path) 173 174 hdrs_param = "hdrs = {},".format(str(hdr_names)) 175 176 # This is needed for the case when quote-includes and system-includes 177 # alternate in the include chain, i.e. 178 # #include <SDL2/SDL.h> -> #include "SDL_main.h" 179 # -> #include <SDL2/_real_SDL_config.h> -> #include "SDL_platform.h" 180 # The problem is that the quote-includes are assumed to be 181 # in the same directory as the header they are included from - 182 # they have no subdir prefix ("SDL2/") in their paths 183 include_subdirs = {} 184 for hdr in hdr_names: 185 path_segments = hdr.split("/") 186 path_segments.pop() 187 current_path_segments = ["external", repo_name] 188 for segment in path_segments: 189 current_path_segments.append(segment) 190 current_path = "/".join(current_path_segments) 191 include_subdirs.update({current_path: None}) 192 193 includes_param = "includes = {},".format(str(include_subdirs.keys())) 194 195 deps_names = [] 196 for dep in deps: 197 dep_name = repr("@" + dep) 198 deps_names.append(dep_name) 199 deps_param = "deps = [{}],".format(",".join(deps_names)) 200 201 link_hdrs_command = "mkdir -p $(RULEDIR)/remote \n" 202 remote_hdrs = [] 203 for path, hdr in zip(hdr_paths, hdr_names): 204 remote_hdr = "remote/" + hdr 205 remote_hdrs.append(remote_hdr) 206 link_hdrs_command += "cp {path} $(RULEDIR)/{hdr}\n ".format( 207 path = path, 208 hdr = remote_hdr, 209 ) 210 211 link_remote_static_lib_genrule = "" 212 link_remote_shared_lib_genrule = "" 213 remote_static_library_param = "" 214 remote_shared_library_param = "" 215 static_library_param = "" 216 shared_library_param = "" 217 218 if static_lib_path: 219 repo_ctx.symlink(static_lib_path, static_lib_name) 220 static_library_param = "static_library = \"{}\",".format( 221 static_lib_name, 222 ) 223 remote_static_library = "remote/" + static_lib_name 224 link_library_command = """ 225mkdir -p $(RULEDIR)/remote && cp {path} $(RULEDIR)/{lib}""".format( 226 path = static_lib_path, 227 lib = remote_static_library, 228 ) 229 remote_static_library_param = """ 230static_library = "remote_link_static_library",""" 231 link_remote_static_lib_genrule = """ 232genrule( 233 name = "remote_link_static_library", 234 outs = ["{remote_static_library}"], 235 cmd = {link_library_command} 236) 237""".format( 238 link_library_command = repr(link_library_command), 239 remote_static_library = remote_static_library, 240 ) 241 242 if shared_lib_path: 243 repo_ctx.symlink(shared_lib_path, shared_lib_name) 244 shared_library_param = "shared_library = \"{}\",".format( 245 shared_lib_name, 246 ) 247 remote_shared_library = "remote/" + shared_lib_name 248 link_library_command = """ 249mkdir -p $(RULEDIR)/remote && cp {path} $(RULEDIR)/{lib}""".format( 250 path = shared_lib_path, 251 lib = remote_shared_library, 252 ) 253 remote_shared_library_param = """ 254shared_library = "remote_link_shared_library",""" 255 link_remote_shared_lib_genrule = """ 256genrule( 257 name = "remote_link_shared_library", 258 outs = ["{remote_shared_library}"], 259 cmd = {link_library_command} 260) 261""".format( 262 link_library_command = repr(link_library_command), 263 remote_shared_library = remote_shared_library, 264 ) 265 266 repo_ctx.file( 267 "BUILD", 268 executable = False, 269 content = 270 """ 271load("@bazel_tools//tools/build_defs/cc:cc_import.bzl", "cc_import") 272cc_import( 273 name = "local_includes", 274 {static_library} 275 {shared_library} 276 {hdrs} 277 {deps} 278 {includes} 279) 280 281genrule( 282 name = "remote_link_headers", 283 outs = {remote_hdrs}, 284 cmd = {link_hdrs_command} 285) 286 287{link_remote_static_lib_genrule} 288 289{link_remote_shared_lib_genrule} 290 291cc_import( 292 name = "remote_includes", 293 hdrs = [":remote_link_headers"], 294 {remote_static_library} 295 {remote_shared_library} 296 {deps} 297 {includes} 298) 299 300alias( 301 name = "{name}", 302 actual = select({{ 303 "@bazel_tools//src/conditions:remote": "remote_includes", 304 "//conditions:default": "local_includes", 305 }}), 306 visibility = ["//visibility:public"], 307) 308""".format( 309 static_library = static_library_param, 310 shared_library = shared_library_param, 311 hdrs = hdrs_param, 312 deps = deps_param, 313 hdr_names = str(hdr_names), 314 link_hdrs_command = repr(link_hdrs_command), 315 name = repo_name, 316 includes = includes_param, 317 remote_hdrs = remote_hdrs, 318 link_remote_static_lib_genrule = link_remote_static_lib_genrule, 319 link_remote_shared_lib_genrule = link_remote_shared_lib_genrule, 320 remote_static_library = remote_static_library_param, 321 remote_shared_library = remote_shared_library_param, 322 ), 323 ) 324 325system_library = repository_rule( 326 implementation = _system_library_impl, 327 local = True, 328 remotable = True, 329 environ = [ 330 BAZEL_INCLUDE_ADDITIONAL_PATHS_ENV_VAR, 331 BAZEL_INCLUDE_OVERRIDE_PATHS_ENV_VAR, 332 BAZEL_LIB_ADDITIONAL_PATHS_ENV_VAR, 333 BAZEL_LIB_OVERRIDE_PATHS_ENV_VAR, 334 ], 335 attrs = { 336 "deps": attr.string_list(doc = """ 337List of names of system libraries this target depends upon. 338"""), 339 "hdrs": attr.string_list( 340 mandatory = True, 341 allow_empty = False, 342 doc = """ 343List of the library's public headers which must be imported. 344""", 345 ), 346 "includes": attr.string_list(doc = """ 347List of directories that should be browsed when looking for headers. 348"""), 349 "lib_path_hints": attr.string_list(doc = """ 350List of directories that should be browsed when looking for library archives. 351"""), 352 "optional_hdrs": attr.string_list(doc = """ 353List of library's private headers. 354"""), 355 "shared_lib_names": attr.string_list(doc = """ 356List of possible shared library names in order of preference. 357"""), 358 "static_lib_names": attr.string_list(doc = """ 359List of possible static library names in order of preference. 360"""), 361 }, 362 doc = 363 """system_library is a repository rule for importing system libraries 364 365`system_library` is a repository rule for safely depending on system-provided 366libraries on Linux. It can be used with remote caching and remote execution. 367Under the hood it uses gcc/clang for finding the library files and headers 368and symlinks them into the build directory. Symlinking allows Bazel to take 369these files into account when it calculates a checksum of the project. 370This prevents cache poisoning from happening. 371 372Currently `system_library` requires two exeperimental flags: 373--experimental_starlark_cc_import 374--experimental_repo_remote_exec 375 376A typical usage looks like this: 377WORKSPACE 378``` 379system_library( 380 name = "jpeg", 381 hdrs = ["jpeglib.h"], 382 shared_lib_names = ["libjpeg.so, libjpeg.so.62"], 383 static_lib_names = ["libjpeg.a"], 384 includes = ["/usr/additional_includes"], 385 lib_path_hints = ["/usr/additional_libs", "/usr/some/other_path"] 386 optional_hdrs = [ 387 "jconfig.h", 388 "jmorecfg.h", 389 ], 390) 391 392system_library( 393 name = "bar", 394 hdrs = ["bar.h"], 395 shared_lib_names = ["libbar.so"], 396 deps = ["jpeg"] 397 398) 399``` 400 401BUILD 402``` 403cc_binary( 404 name = "foo", 405 srcs = ["foo.cc"], 406 deps = ["@bar"] 407) 408``` 409 410foo.cc 411``` 412#include "jpeglib.h" 413#include "bar.h" 414 415[code using symbols from jpeglib and bar] 416``` 417 418`system_library` requires users to specify at least one header 419(as it makes no sense to import a library without headers). 420Public headers of a library (i.e. those included in the user-written code, 421like `jpeglib.h` in the example above) should be put in `hdrs` param, as they 422are required for the library to work. However, some libraries may use more 423"private" headers. They should be imported as well, but their names may differ 424from system to system. They should be specified in the `optional_hdrs` param. 425The build will not fail if some of them are not found, so it's safe to put a 426superset there, containing all possible combinations of names for different 427versions/distributions. It's up to the user to determine which headers are 428required for the library to work. 429 430One `system_library` target always imports exactly one library. 431Users can specify many potential names for the library file, 432as these names can differ from system to system. The order of names establishes 433the order of preference. As some libraries can be linked both statically 434and dynamically, the names of files of each kind can be specified separately. 435`system_library` rule will try to find library archives of both kinds, but it's 436up to the top-level target (for example, `cc_binary`) to decide which kind of 437linking will be used. 438 439`system_library` rule depends on gcc/clang (whichever is installed) for 440finding the actual locations of library archives and headers. 441Libraries installed in a standard way by a package manager 442(`sudo apt install libjpeg-dev`) are usually placed in one of directories 443searched by the compiler/linker by default - on Ubuntu library most archives 444are stored in `/usr/lib/x86_64-linux-gnu/` and their headers in 445`/usr/include/`. If the maintainer of a project expects the files 446to be installed in a non-standard location, they can use the `includes` 447parameter to add directories to the search path for headers 448and `lib_path_hints` to add directories to the search path for library 449archives. 450 451User building the project can override or extend these search paths by 452providing these environment variables to the build: 453BAZEL_INCLUDE_ADDITIONAL_PATHS, BAZEL_INCLUDE_OVERRIDE_PATHS, 454BAZEL_LIB_ADDITIONAL_PATHS, BAZEL_LIB_OVERRIDE_PATHS. 455The syntax for setting the env variables is: 456`<library>=<path>,<library>=<path2>`. 457Users can provide multiple paths for one library by repeating this segment: 458`<library>=<path>`. 459 460So in order to build the example presented above but with custom paths for the 461jpeg lib, one would use the following command: 462 463``` 464bazel build //:foo \ 465 --experimental_starlark_cc_import \ 466 --experimental_repo_remote_exec \ 467 --action_env=BAZEL_LIB_OVERRIDE_PATHS=jpeg=/custom/libraries/path \ 468 --action_env=BAZEL_INCLUDE_OVERRIDE_PATHS=jpeg=/custom/include/path,jpeg=/inc 469``` 470 471Some libraries can depend on other libraries. `system_library` rule provides 472a `deps` parameter for specifying such relationships. `system_library` targets 473can depend only on other system libraries. 474""", 475) 476