aboutsummaryrefslogtreecommitdiff
path: root/cc/system_library.bzl
blob: f87bb35c8e4df6fe8b7c9c9efd27ed743ff469d8 (plain)
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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
"""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.
""",
)