In BAZEL, Is there a way to prevent dependency C/C++ headers from being propagated to dependent library?

Viewed 999

For example: lib_a is an internal library. It exposes interface header alpha.h.

lib_b is an API library. It includes alpha.h in beta.c and exposes interface header beta.h which does not include alpha.h.

exe_c is a test app that utilizes lib_b. Since it is "external" to API library lib_b, it should not have access to internal library lib_a's headers, namely alpha.h.

Now, is there a way to prevent the include path of alpha.h from being added to exe_c 's compilation command-line?

2 Answers

Update 5/24/22: Bazel added an implementation_deps feature.

This is possible with a custom rule and macro.

Here's how you can write a macro my_cc_library so that you can do something like this

my_cc_library(
    name = "b",
    hdrs = [
        "b.h",
    ],
    srcs = [
        "b.cc",
    ]
    impl_deps = [
        "//a:a",
    ],
)

And targets that depend on b will not be able to include headers from dependency a.

Step 1

Add a custom rule that takes a base library and forms a new CcInfo provider with only the headers you want to expose.

def _my_cc_slim_library(ctx):
  cc_info = ctx.attr.base_library[CcInfo]
  compilation_ctx = cc_info.compilation_context

  compilation_ctx_p = cc_common.create_compilation_context(
      headers = depset(direct=ctx.files.hdrs),
      includes = compilation_ctx.includes,
      quote_includes = compilation_ctx.quote_includes,
  )

  dep_cc_infos = [dep[CcInfo] for dep in ctx.attr.deps]

  cc_info_p = cc_common.merge_cc_infos(
      cc_infos = [
          CcInfo(
              compilation_context = compilation_ctx_p,
              linking_context = cc_info.linking_context,
          )
      ] + dep_cc_infos,
  )
  
  return [cc_info_p]

my_cc_slim_library = rule(
    implementation = _my_cc_slim_library,
    attrs = {
        "base_library": attr.label(mandatory=True, providers = [CcInfo]),
        "hdrs": attr.label_list(allow_files=True),
        "deps": attr.label_list(providers = [CcInfo]),
    },
)

Step 2

Add the custom macro my_cc_library. This macro will create a base library and then use the rule my_cc_slim_library to strip out the unwanted header dependencies.

def my_cc_library(
    name,
    hdrs = [],
    deps = [],
    impl_deps = [],
    **kwargs):
  native.cc_library(
      name = name + "-base",
      hdrs = hdrs,
      deps = deps + impl_deps,
      **kwargs)
  my_cc_slim_library(
      name = name,
      hdrs = hdrs,
      base_library = name + "-base",
      deps = deps,
  )

Example

Now here's how to use it in the example.

bazel/my_cc_library.bzl <- contains the custom rule and macro

a/a.h

int do_a();

a/a.cc

#include "a/a.h"

int do_a() {
  return 1;
}

a/BUILD

package(default_visibility = ["//visibility:public"])

cc_library(
    name = "a",
    hdrs = [
        "a.h",
    ],
    srcs = [
        "a.cc",
    ],
)

b/b.h

int do_b();

b/b.cc

#include "a/a.h"

int do_b() {
  return do_a() + 2;
}

b/BUILD

load(
    "//bazel:my_cc_library.bzl",
    "my_cc_library",
)

package(default_visibility = ["//visibility:public"])

my_cc_library(
    name = "b",
    hdrs = [
        "b.h",
    ],
    srcs = [
        "b.cc",
    ],
    impl_deps = [
        "//a:a",
    ],
)

main.cc

#include <iostream>

// #include "a/a.h"   <- this would cause an error
#include "b/b.h"

int main() {
  std::cout << do_b() << std::endl;
  return 0;
}

BUILD

cc_binary(
    name = "exe",
    srcs = [
        "main.cc",
    ],
    deps = [
        "//b:b",
    ],
)

Sorry, it looks like there is no way to prevent this in Bazel at the moment.

From https://docs.bazel.build/versions/master/be/c-cpp.html#hdrs:

For cc_library rules, headers in hdrs comprise the public interface of the library and can be directly included both from the files in hdrs and srcs of the library itself as well as from files in hdrs and srcs of cc_* rules that list the library in their deps. Headers in srcs must only be directly included from the files in hdrs and srcs of the library itself.

and

Unfortunately Bazel currently cannot distinguish between direct and transitive inclusions, so it cannot detect error cases where a file illegally includes a header directly that is only allowed to be included transitively. For example, Bazel would not complain if in the example above foo.cc directly includes baz.h. This would be illegal, because foo does not directly depend on baz. Currently, no error is produced in that case, but such error checking may be added in the future.

Example project:

.
├── BUILD
├── c.cpp
├── lib_b
│   ├── b.cpp
│   ├── b.h
│   ├── BUILD
│   └── lib_a
│       ├── a.cpp
│       ├── a.h
│       └── BUILD
└── WORKSPACE

./WORKSPACE (empty)
./lib_b/BUILD

load("@rules_cc//cc:defs.bzl", "cc_library")
cc_library(
    name="lib_b",
    srcs=["b.cpp"],
    hdrs=["b.h"],
    deps=["//lib_b/lib_a"],
    visibility=["//visibility:public"]
)

./lib_b/lib_a/BUILD

load("@rules_cc//cc:defs.bzl", "cc_library")
cc_library(
    name="lib_a",
    srcs=["a.cpp"],
    hdrs=["a.h"],
    visibility=["//lib_b:__subpackages__"]
)

./lib_b/lib_a/a.h

#pragma once
void a();

./lib_b/lib_a/a.cpp

void a(){}

./lib_b/b.cpp

void b(){}

./lib_b/b.h

#pragma once
#include "lib_a/a.h"
void b();

./c.cpp

#include "lib_b/b.h"
#include "lib_b/lib_a/a.h" // <--- this is illegal
int main() {
    return 0;
}

./BUILD

load("@rules_cc//cc:defs.bzl", "cc_binary")
cc_binary(
    name="exc_c",
    srcs=["c.cpp"],
    deps=["//lib_b"]
)

Bazel builds and runs exc_c without warnings

> $ bazel run exc_c                                                                                                                                               
INFO: Analyzed target //:exc_c (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:exc_c up-to-date:
  bazel-bin/exc_c
INFO: Elapsed time: 0.219s, Critical Path: 0.01s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
Related