xref: /aosp_15_r20/external/bazelbuild-rules_cc/cc/system_library.bzl (revision eed53cd41c5909d05eedc7ad9720bb158fd93452)
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