blob: f87bb35c8e4df6fe8b7c9c9efd27ed743ff469d8 [file] [log] [blame]
"""system_library is a repository rule for importing system libraries"""
BAZEL_LIB_ADDITIONAL_PATHS_ENV_VAR = "BAZEL_LIB_ADDITIONAL_PATHS"
BAZEL_LIB_OVERRIDE_PATHS_ENV_VAR = "BAZEL_LIB_OVERRIDE_PATHS"
BAZEL_INCLUDE_ADDITIONAL_PATHS_ENV_VAR = "BAZEL_INCLUDE_ADDITIONAL_PATHS"
BAZEL_INCLUDE_OVERRIDE_PATHS_ENV_VAR = "BAZEL_INCLUDE_OVERRIDE_PATHS"
ENV_VAR_SEPARATOR = ","
ENV_VAR_ASSIGNMENT = "="
def _make_flags(flag_values, flag):
flags = []
if flag_values:
for s in flag_values:
flags.append(flag + s)
return " ".join(flags)
def _split_env_var(repo_ctx, var_name):
value = repo_ctx.os.environ.get(var_name)
if value:
assignments = value.split(ENV_VAR_SEPARATOR)
dict = {}
for assignment in assignments:
pair = assignment.split(ENV_VAR_ASSIGNMENT)
if len(pair) != 2:
fail(
"Assignments should have form 'name=value', " +
"but encountered {} in env variable {}"
.format(assignment, var_name),
)
key, value = pair[0], pair[1]
if not dict.get(key):
dict[key] = []
dict[key].append(value)
return dict
else:
return {}
def _get_list_from_env_var(repo_ctx, var_name, key):
return _split_env_var(repo_ctx, var_name).get(key, default = [])
def _execute_bash(repo_ctx, cmd):
return repo_ctx.execute(["/bin/bash", "-c", cmd]).stdout.strip("\n")
def _find_linker(repo_ctx):
ld = _execute_bash(repo_ctx, "which ld")
lld = _execute_bash(repo_ctx, "which lld")
if ld:
return ld
elif lld:
return lld
else:
fail("No linker found")
def _find_compiler(repo_ctx):
gcc = _execute_bash(repo_ctx, "which g++")
clang = _execute_bash(repo_ctx, "which clang++")
if gcc:
return gcc
elif clang:
return clang
else:
fail("No compiler found")
def _find_lib_path(repo_ctx, lib_name, archive_names, lib_path_hints):
override_paths = _get_list_from_env_var(
repo_ctx,
BAZEL_LIB_OVERRIDE_PATHS_ENV_VAR,
lib_name,
)
additional_paths = _get_list_from_env_var(
repo_ctx,
BAZEL_LIB_ADDITIONAL_PATHS_ENV_VAR,
lib_name,
)
# Directories will be searched in order
path_flags = _make_flags(
override_paths + lib_path_hints + additional_paths,
"-L",
)
linker = _find_linker(repo_ctx)
for archive_name in archive_names:
cmd = """
{} -verbose -l:{} {} 2>/dev/null | \\
grep succeeded | \\
head -1 | \\
sed -e 's/^\\s*attempt to open //' -e 's/ succeeded\\s*$//'
""".format(
linker,
archive_name,
path_flags,
)
path = _execute_bash(repo_ctx, cmd)
if path:
return (archive_name, path)
return (None, None)
def _find_header_path(repo_ctx, lib_name, header_name, includes):
override_paths = _get_list_from_env_var(
repo_ctx,
BAZEL_INCLUDE_OVERRIDE_PATHS_ENV_VAR,
lib_name,
)
additional_paths = _get_list_from_env_var(
repo_ctx,
BAZEL_INCLUDE_ADDITIONAL_PATHS_ENV_VAR,
lib_name,
)
compiler = _find_compiler(repo_ctx)
cmd = """
print | \\
{} -Wp,-v -x c++ - -fsyntax-only 2>&1 | \\
sed -n -e '/^\\s\\+/p' | \\
sed -e 's/^[ \t]*//'
""".format(compiler)
system_includes = _execute_bash(repo_ctx, cmd).split("\n")
all_includes = (override_paths + includes +
system_includes + additional_paths)
for directory in all_includes:
cmd = """
test -f "{dir}/{hdr}" && echo "{dir}/{hdr}"
""".format(dir = directory, hdr = header_name)
result = _execute_bash(repo_ctx, cmd)
if result:
return result
return None
def _system_library_impl(repo_ctx):
repo_name = repo_ctx.attr.name
includes = repo_ctx.attr.includes
hdrs = repo_ctx.attr.hdrs
optional_hdrs = repo_ctx.attr.optional_hdrs
deps = repo_ctx.attr.deps
lib_path_hints = repo_ctx.attr.lib_path_hints
static_lib_names = repo_ctx.attr.static_lib_names
shared_lib_names = repo_ctx.attr.shared_lib_names
static_lib_name, static_lib_path = _find_lib_path(
repo_ctx,
repo_name,
static_lib_names,
lib_path_hints,
)
shared_lib_name, shared_lib_path = _find_lib_path(
repo_ctx,
repo_name,
shared_lib_names,
lib_path_hints,
)
if not static_lib_path and not shared_lib_path:
fail("Library {} could not be found".format(repo_name))
hdr_names = []
hdr_paths = []
for hdr in hdrs:
hdr_path = _find_header_path(repo_ctx, repo_name, hdr, includes)
if hdr_path:
repo_ctx.symlink(hdr_path, hdr)
hdr_names.append(hdr)
hdr_paths.append(hdr_path)
else:
fail("Could not find required header {}".format(hdr))
for hdr in optional_hdrs:
hdr_path = _find_header_path(repo_ctx, repo_name, hdr, includes)
if hdr_path:
repo_ctx.symlink(hdr_path, hdr)
hdr_names.append(hdr)
hdr_paths.append(hdr_path)
hdrs_param = "hdrs = {},".format(str(hdr_names))
# This is needed for the case when quote-includes and system-includes
# alternate in the include chain, i.e.
# #include <SDL2/SDL.h> -> #include "SDL_main.h"
# -> #include <SDL2/_real_SDL_config.h> -> #include "SDL_platform.h"
# The problem is that the quote-includes are assumed to be
# in the same directory as the header they are included from -
# they have no subdir prefix ("SDL2/") in their paths
include_subdirs = {}
for hdr in hdr_names:
path_segments = hdr.split("/")
path_segments.pop()
current_path_segments = ["external", repo_name]
for segment in path_segments:
current_path_segments.append(segment)
current_path = "/".join(current_path_segments)
include_subdirs.update({current_path: None})
includes_param = "includes = {},".format(str(include_subdirs.keys()))
deps_names = []
for dep in deps:
dep_name = repr("@" + dep)
deps_names.append(dep_name)
deps_param = "deps = [{}],".format(",".join(deps_names))
link_hdrs_command = "mkdir -p $(RULEDIR)/remote \n"
remote_hdrs = []
for path, hdr in zip(hdr_paths, hdr_names):
remote_hdr = "remote/" + hdr
remote_hdrs.append(remote_hdr)
link_hdrs_command += "cp {path} $(RULEDIR)/{hdr}\n ".format(
path = path,
hdr = remote_hdr,
)
link_remote_static_lib_genrule = ""
link_remote_shared_lib_genrule = ""
remote_static_library_param = ""
remote_shared_library_param = ""
static_library_param = ""
shared_library_param = ""
if static_lib_path:
repo_ctx.symlink(static_lib_path, static_lib_name)
static_library_param = "static_library = \"{}\",".format(
static_lib_name,
)
remote_static_library = "remote/" + static_lib_name
link_library_command = """
mkdir -p $(RULEDIR)/remote && cp {path} $(RULEDIR)/{lib}""".format(
path = static_lib_path,
lib = remote_static_library,
)
remote_static_library_param = """
static_library = "remote_link_static_library","""
link_remote_static_lib_genrule = """
genrule(
name = "remote_link_static_library",
outs = ["{remote_static_library}"],
cmd = {link_library_command}
)
""".format(
link_library_command = repr(link_library_command),
remote_static_library = remote_static_library,
)
if shared_lib_path:
repo_ctx.symlink(shared_lib_path, shared_lib_name)
shared_library_param = "shared_library = \"{}\",".format(
shared_lib_name,
)
remote_shared_library = "remote/" + shared_lib_name
link_library_command = """
mkdir -p $(RULEDIR)/remote && cp {path} $(RULEDIR)/{lib}""".format(
path = shared_lib_path,
lib = remote_shared_library,
)
remote_shared_library_param = """
shared_library = "remote_link_shared_library","""
link_remote_shared_lib_genrule = """
genrule(
name = "remote_link_shared_library",
outs = ["{remote_shared_library}"],
cmd = {link_library_command}
)
""".format(
link_library_command = repr(link_library_command),
remote_shared_library = remote_shared_library,
)
repo_ctx.file(
"BUILD",
executable = False,
content =
"""
load("@bazel_tools//tools/build_defs/cc:cc_import.bzl", "cc_import")
cc_import(
name = "local_includes",
{static_library}
{shared_library}
{hdrs}
{deps}
{includes}
)
genrule(
name = "remote_link_headers",
outs = {remote_hdrs},
cmd = {link_hdrs_command}
)
{link_remote_static_lib_genrule}
{link_remote_shared_lib_genrule}
cc_import(
name = "remote_includes",
hdrs = [":remote_link_headers"],
{remote_static_library}
{remote_shared_library}
{deps}
{includes}
)
alias(
name = "{name}",
actual = select({{
"@bazel_tools//src/conditions:remote": "remote_includes",
"//conditions:default": "local_includes",
}}),
visibility = ["//visibility:public"],
)
""".format(
static_library = static_library_param,
shared_library = shared_library_param,
hdrs = hdrs_param,
deps = deps_param,
hdr_names = str(hdr_names),
link_hdrs_command = repr(link_hdrs_command),
name = repo_name,
includes = includes_param,
remote_hdrs = remote_hdrs,
link_remote_static_lib_genrule = link_remote_static_lib_genrule,
link_remote_shared_lib_genrule = link_remote_shared_lib_genrule,
remote_static_library = remote_static_library_param,
remote_shared_library = remote_shared_library_param,
),
)
system_library = repository_rule(
implementation = _system_library_impl,
local = True,
remotable = True,
environ = [
BAZEL_INCLUDE_ADDITIONAL_PATHS_ENV_VAR,
BAZEL_INCLUDE_OVERRIDE_PATHS_ENV_VAR,
BAZEL_LIB_ADDITIONAL_PATHS_ENV_VAR,
BAZEL_LIB_OVERRIDE_PATHS_ENV_VAR,
],
attrs = {
"deps": attr.string_list(doc = """
List of names of system libraries this target depends upon.
"""),
"hdrs": attr.string_list(
mandatory = True,
allow_empty = False,
doc = """
List of the library's public headers which must be imported.
""",
),
"includes": attr.string_list(doc = """
List of directories that should be browsed when looking for headers.
"""),
"lib_path_hints": attr.string_list(doc = """
List of directories that should be browsed when looking for library archives.
"""),
"optional_hdrs": attr.string_list(doc = """
List of library's private headers.
"""),
"shared_lib_names": attr.string_list(doc = """
List of possible shared library names in order of preference.
"""),
"static_lib_names": attr.string_list(doc = """
List of possible static library names in order of preference.
"""),
},
doc =
"""system_library is a repository rule for importing system libraries
`system_library` is a repository rule for safely depending on system-provided
libraries on Linux. It can be used with remote caching and remote execution.
Under the hood it uses gcc/clang for finding the library files and headers
and symlinks them into the build directory. Symlinking allows Bazel to take
these files into account when it calculates a checksum of the project.
This prevents cache poisoning from happening.
Currently `system_library` requires two exeperimental flags:
--experimental_starlark_cc_import
--experimental_repo_remote_exec
A typical usage looks like this:
WORKSPACE
```
system_library(
name = "jpeg",
hdrs = ["jpeglib.h"],
shared_lib_names = ["libjpeg.so, libjpeg.so.62"],
static_lib_names = ["libjpeg.a"],
includes = ["/usr/additional_includes"],
lib_path_hints = ["/usr/additional_libs", "/usr/some/other_path"]
optional_hdrs = [
"jconfig.h",
"jmorecfg.h",
],
)
system_library(
name = "bar",
hdrs = ["bar.h"],
shared_lib_names = ["libbar.so"],
deps = ["jpeg"]
)
```
BUILD
```
cc_binary(
name = "foo",
srcs = ["foo.cc"],
deps = ["@bar"]
)
```
foo.cc
```
#include "jpeglib.h"
#include "bar.h"
[code using symbols from jpeglib and bar]
```
`system_library` requires users to specify at least one header
(as it makes no sense to import a library without headers).
Public headers of a library (i.e. those included in the user-written code,
like `jpeglib.h` in the example above) should be put in `hdrs` param, as they
are required for the library to work. However, some libraries may use more
"private" headers. They should be imported as well, but their names may differ
from system to system. They should be specified in the `optional_hdrs` param.
The build will not fail if some of them are not found, so it's safe to put a
superset there, containing all possible combinations of names for different
versions/distributions. It's up to the user to determine which headers are
required for the library to work.
One `system_library` target always imports exactly one library.
Users can specify many potential names for the library file,
as these names can differ from system to system. The order of names establishes
the order of preference. As some libraries can be linked both statically
and dynamically, the names of files of each kind can be specified separately.
`system_library` rule will try to find library archives of both kinds, but it's
up to the top-level target (for example, `cc_binary`) to decide which kind of
linking will be used.
`system_library` rule depends on gcc/clang (whichever is installed) for
finding the actual locations of library archives and headers.
Libraries installed in a standard way by a package manager
(`sudo apt install libjpeg-dev`) are usually placed in one of directories
searched by the compiler/linker by default - on Ubuntu library most archives
are stored in `/usr/lib/x86_64-linux-gnu/` and their headers in
`/usr/include/`. If the maintainer of a project expects the files
to be installed in a non-standard location, they can use the `includes`
parameter to add directories to the search path for headers
and `lib_path_hints` to add directories to the search path for library
archives.
User building the project can override or extend these search paths by
providing these environment variables to the build:
BAZEL_INCLUDE_ADDITIONAL_PATHS, BAZEL_INCLUDE_OVERRIDE_PATHS,
BAZEL_LIB_ADDITIONAL_PATHS, BAZEL_LIB_OVERRIDE_PATHS.
The syntax for setting the env variables is:
`<library>=<path>,<library>=<path2>`.
Users can provide multiple paths for one library by repeating this segment:
`<library>=<path>`.
So in order to build the example presented above but with custom paths for the
jpeg lib, one would use the following command:
```
bazel build //:foo \
--experimental_starlark_cc_import \
--experimental_repo_remote_exec \
--action_env=BAZEL_LIB_OVERRIDE_PATHS=jpeg=/custom/libraries/path \
--action_env=BAZEL_INCLUDE_OVERRIDE_PATHS=jpeg=/custom/include/path,jpeg=/inc
```
Some libraries can depend on other libraries. `system_library` rule provides
a `deps` parameter for specifying such relationships. `system_library` targets
can depend only on other system libraries.
""",
)