Bazel re-export headers in a cc_library from it's dependencies to pass `layering_check`

98 views Asked by At

I'm trying to Bazel-wrap a vendor-provided SDK for an embedded project. Part of that SDK includes a hardware abstraction layer (HAL), which has a set of identically named C headers and sources, separate for each part supported by the vendor. I was hoping to be able to create targets for each part, and then use select to include them into a meta-target, which always choses the right dependency, based on the chosen platform. However, depending on the meta-target is not equivalent: Because of the indirection, the HAL headers are not considered properly depended on, and thus will fail the layering_check. Is there a way to explicitly re-export the headers of its dependencies in the meta-target?

Sketch:

cc_library(
    name = "hal_part_A",
    hdrs = ["part_A/hal_regs.h"],
    strip_include_prefix = "part_A/",
    target_compatible_with = ["//part:part_A"],
)

cc_library(
    name = "hal_part_B",
    hdrs = ["part_B/hal_regs.h"],
    strip_include_prefix = "part_B/",
    target_compatible_with = ["//part:part_B"],
)

cc_library(
    name = "hal",
    deps = select({
        "//part:part_A": [":hal_part_A"],
        "//part:part_B": [":hal_part_B"],
    }),
    # <- add in some 'magic' way to re-export the headers declared by the dependencies
)

cc_binary(
    name = "main",
    srcs = ["main.c"],
    deps = [":hal"],
)

If the source file main.c tries to include hal_regs.h provided by the HAL, bazel complains with Compiling main.c failed: undeclared inclusion(s) in rule '//:main': 'part_A/hal_regs.h' (assuming a platform using //part:part_A was selected), because //:main doesn't directly depend on hal_part_A, which exposes that header.

Is there a way to tell bazel that hal should re-export the headers exported from it's dependencies?

Workarounds that are suboptimal:

  • Not using any meta targets like hal: Instead main could directly depend on a select linking to the corresponding HAL implementations. Disadvantage: The select logic needs to be copied to every dependant.
  • Explicitly re-exporting all relevant headers from the meta target: The meta target could have explicit hdrs = select(...), again listing all the headers. Things like strip_include_prefix complicate this, but can probably be solved. Still, suboptimal due to the duplication between the implementation targets and the meta target.
1

There are 1 answers

7
Ondrej K. On BEST ANSWER

For quick and dirty... and probably correct to use in this instance, scroll to the bottom for using alias instead of wrapping library.


Something else isn't probably right, cc_library dependencies should be transitive... and as a matter of fact I took your example... filling in few blanks (adding trivial source examples and defined platforms providing constraint_value //part:part_A and //part:part_B) leaving your BUILD file intact... and it "does work":

$ bazel run main --platforms //part:A
INFO: Analyzed target //:main (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:main up-to-date:
  bazel-bin/main
INFO: Elapsed time: 0.053s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/main
PartA

$ bazel run main --platforms //part:B
INFO: Build option --platforms has changed, discarding analysis cache.
INFO: Analyzed target //:main (0 packages loaded, 162 targets configured).
INFO: Found 1 target...
Target //:main up-to-date:
  bazel-bin/main
INFO: Elapsed time: 0.181s, Critical Path: 0.05s
INFO: 4 processes: 2 internal, 2 linux-sandbox.
INFO: Build completed successfully, 4 total actions
INFO: Running command line: bazel-bin/main
PartB

So, perhaps try to debug around a little: platform resolution, select() results and even C sources themselves... in fact...

Tracing back to your question and the reported failure complaining about... Well, I suspect the problem is how stuff got included? Since strip_include_prefix = "part_A/" for part_A (for instance) was used, you should no longer say:

#include "part_A/hal_regs.h"

but just:

#include "hal_regs.h"

I would presume by some coincidence (and perhaps a little oddity of tree layout) and the fact the sandbox isn't as hermetic as one would sometimes tend to hope and believe (RBE setup is a good way to test the assumptions and expose accidental leaks with much higher probability)... Preprocessor still managed to find hal_regs.h searching for part_A/hal_regs.h... but bazel (correctly here) detected this as undeclared dependency (which I think is where / what your error is emitted from / for).


I may have assumed too much about the context / setup, sorry about that. One quick hack (workaround) that comes to mind (regardless of exact mode of failure) would be to provide macro for the HAL selection, to make the first option in your question a little more palatable. E.g. something like hal.bzl:

def hal_deps():
    return select({
        "//part:part_A": [":hal_part_A"],
        "//part:part_B": [":hal_part_B"],
    })

And then consume like:

load(":hal.bzl", "hal")
...
cc_library(
    name = "hal",
    deps = [...] + hal_deps(),
)

For the less quick (and probably more correct) answer now considering a little more understanding of the context. This really is a question about layering_check feature and how it generates "intermediate" module maps... namely in this case it would do for instance something like this for //:hal target:

module "//:hal" {
  export *
  use "//:hal_part_A"
  use "crosstool"
}
extern module "//:hal_part_A" "../../../bazel-out/k8-fastbuild/bin/hal_part_A.cppmap"
extern module "crosstool" "../../../external/local_config_cc/module.modulemap"

Which would not make LLVM happy when main.c grabs stuff from what is here would be another hop "//:hal_part_A" without (generated) //:hal asking for re-export.

This may be relatively young feature and could still see more development (incl. new API to define such use cases)? ATM I suspect this would me diving into it one-self and extending it (trying to upstream the changes).


One more update... and of course another very obvious (that I've missed so far) but possibly viable in this instance workaround, use alias to select() instead of a wrapping cc_library:

alias(
    name = "hal",
    actual = select({
        "//part:part_A": ":hal_part_A",
        "//part:part_B": ":hal_part_B",
    }),
)

This used to be a bit more problematic when dealing with constraint_value, but seems to be OK now... I suspect at least since bzl 5 (or 6?).