Add `parse_headers` support to the default Unix toolchain

Also remove Ubuntu 16.04 workarounds from layering_check tests.

RELNOTES: The default Unix C++ toolchain now supports the `parse_headers` feature to validate header files with `--process_headers_in_dependencies`.

Closes #21560.

PiperOrigin-RevId: 633657012
Change-Id: Iaaa2623bcc86b219b02567eca1c7bf9e1a77ae7d
diff --git a/site/en/docs/bazel-and-cpp.md b/site/en/docs/bazel-and-cpp.md
index aab47cd..866f0f7 100644
--- a/site/en/docs/bazel-and-cpp.md
+++ b/site/en/docs/bazel-and-cpp.md
@@ -82,3 +82,22 @@
    use the [`include_prefix`](/reference/be/c-cpp#cc_library.include_prefix) and
    [`strip_include_prefix`](/reference/be/c-cpp#cc_library.strip_include_prefix)
    arguments on the `cc_library` rule target.
+
+### Toolchain features {:#toolchain-features}
+
+The following optional [features](/docs/cc-toolchain-config-reference#features)
+can improve the hygiene of a C++ project. They can be enabled using the
+`--features` command-line flag or the `features` attribute of
+[`repo`](/external/overview#repo.bazel),
+[`package`](/reference/be/functions#package) or `cc_*` rules:
+
+* The `parse_headers` feature makes it so that the C++ compiler is used to parse
+  (but not compile) all header files in the built targets and their dependencies
+  when using the
+  [`--process_headers_in_dependencies`](/reference/command-line-reference#flag--process_headers_in_dependencies)
+  flag. This can help catch issues in header-only libraries and ensure that
+  headers are self-contained and independent of the order in which they are
+  included.
+* The `layering_check` feature enforces that targets only include headers
+  provided by their direct dependencies. The default toolchain supports this
+  feature on Linux with `clang` as the compiler.
diff --git a/src/test/shell/bazel/bazel_layering_check_test.sh b/src/test/shell/bazel/bazel_layering_check_test.sh
index bbbd680..4ccf75d 100755
--- a/src/test/shell/bazel/bazel_layering_check_test.sh
+++ b/src/test/shell/bazel/bazel_layering_check_test.sh
@@ -31,6 +31,12 @@
 )
 
 cc_library(
+  name = 'hello_header_only',
+  hdrs = ['hello_header_only.h'],
+  deps = [":hello_lib"],
+)
+
+cc_library(
   name = "hello_lib",
   srcs = ["hello_private.h", "hellolib.cc"],
   hdrs = ["hello.h"],
@@ -116,10 +122,27 @@
 }
 #endif
 EOF
+
+  cat >hello/hello_header_only.h <<EOF
+#ifdef private_header
+#include "hello_private.h"
+int func() {
+  return helloPrivate() - 42;
+}
+#elif defined layering_violation
+#include "base.h"
+int func() {
+  return base() - 42;
+}
+#else
+#include "hello.h"
+int func() {
+  return hello() - 42;
+}
+#endif
+EOF
 }
 
-# TODO(hlopko): Add a test for a "toplevel" header-only library
-#   once we have parse_headers support in cc_configure.
 function test_bazel_layering_check() {
   if is_darwin; then
     echo "This test doesn't run on Darwin. Skipping."
@@ -135,7 +158,7 @@
   write_files
 
   CC="${clang_tool}" bazel build \
-    //hello:hello --linkopt=-fuse-ld=gold --features=layering_check \
+    //hello:hello --features=layering_check \
     &> "${TEST_log}" || fail "Build with layering_check failed"
 
   bazel-bin/hello/hello || fail "the built binary failed to run"
@@ -148,23 +171,65 @@
     fail "module map files were not generated"
   fi
 
-  # Specifying -fuse-ld=gold explicitly to override -fuse-ld=/usr/bin/ld.gold
-  # passed in by cc_configure because Ubuntu-16.04 ships with an old
-  # clang version that doesn't accept that.
   CC="${clang_tool}" bazel build \
     --copt=-D=private_header \
-    //hello:hello --linkopt=-fuse-ld=gold --features=layering_check \
+    //hello:hello --features=layering_check \
     &> "${TEST_log}" && fail "Build of private header violation with "\
     "layering_check should have failed"
   expect_log "use of private header from outside its module: 'hello_private.h'"
 
   CC="${clang_tool}" bazel build \
     --copt=-D=layering_violation \
-    //hello:hello --linkopt=-fuse-ld=gold --features=layering_check \
+    //hello:hello --features=layering_check \
     &> "${TEST_log}" && fail "Build of private header violation with "\
     "layering_check should have failed"
   expect_log "module //hello:hello does not depend on a module exporting "\
     "'base.h'"
 }
 
+function test_bazel_layering_check_header_only() {
+  if is_darwin; then
+    echo "This test doesn't run on Darwin. Skipping."
+    return
+  fi
+
+  local -r clang_tool=$(which clang)
+  if [[ ! -x ${clang_tool:-/usr/bin/clang_tool} ]]; then
+    echo "clang not installed. Skipping test."
+    return
+  fi
+
+  write_files
+
+  CC="${clang_tool}" bazel build \
+    //hello:hello_header_only --features=layering_check --features=parse_headers \
+    -s --process_headers_in_dependencies \
+    &> "${TEST_log}" || fail "Build with layering_check + parse_headers failed"
+
+  if [[ ! -e bazel-bin/hello/hello_header_only.cppmap ]]; then
+    fail "module map file for hello_header_only was not generated"
+  fi
+
+  if [[ ! -e bazel-bin/hello/hello_lib.cppmap ]]; then
+    fail "module map file for hello_lib was not generated"
+  fi
+
+  CC="${clang_tool}" bazel build \
+    --copt=-D=private_header \
+    //hello:hello_header_only --features=layering_check --features=parse_headers \
+    --process_headers_in_dependencies \
+    &> "${TEST_log}" && fail "Build of private header violation with "\
+    "layering_check + parse_headers should have failed"
+  expect_log "use of private header from outside its module: 'hello_private.h'"
+
+  CC="${clang_tool}" bazel build \
+    --copt=-D=layering_violation \
+    //hello:hello_header_only --features=layering_check --features=parse_headers \
+    --process_headers_in_dependencies \
+    &> "${TEST_log}" && fail "Build of private header violation with "\
+    "layering_check + parse_headers should have failed"
+  expect_log "module //hello:hello_header_only does not depend on a module exporting "\
+    "'base.h'"
+}
+
 run_suite "test layering_check"
diff --git a/src/test/shell/bazel/cc_integration_test.sh b/src/test/shell/bazel/cc_integration_test.sh
index e5e1887..19e39f4 100755
--- a/src/test/shell/bazel/cc_integration_test.sh
+++ b/src/test/shell/bazel/cc_integration_test.sh
@@ -1909,4 +1909,39 @@
   fi
 }
 
+function test_parse_headers_unclean() {
+  mkdir pkg
+  cat > pkg/BUILD <<'EOF'
+cc_library(name = "lib", hdrs = ["lib.h"])
+EOF
+  cat > pkg/lib.h <<'EOF'
+// Missing include of cstdint, which defines uint8_t.
+uint8_t foo();
+EOF
+
+  bazel build -s --process_headers_in_dependencies --features parse_headers \
+    //pkg:lib &> "$TEST_log" && fail "Build should have failed due to unclean headers"
+  expect_log "Compiling pkg/lib.h"
+  expect_log "error:.*'uint8_t'"
+
+  bazel build -s --process_headers_in_dependencies \
+    //pkg:lib &> "$TEST_log" || fail "Build should have passed"
+}
+
+function test_parse_headers_clean() {
+  mkdir pkg
+  cat > pkg/BUILD <<'EOF'
+package(features = ["parse_headers"])
+cc_library(name = "lib", hdrs = ["lib.h"])
+EOF
+  cat > pkg/lib.h <<'EOF'
+#include <cstdint>
+uint8_t foo();
+EOF
+
+  bazel build -s --process_headers_in_dependencies \
+    //pkg:lib &> "$TEST_log" || fail "Build should have passed"
+  expect_log "Compiling pkg/lib.h"
+}
+
 run_suite "cc_integration_test"
diff --git a/tools/cpp/BUILD.tpl b/tools/cpp/BUILD.tpl
index 384b63a..f99995d 100644
--- a/tools/cpp/BUILD.tpl
+++ b/tools/cpp/BUILD.tpl
@@ -84,6 +84,7 @@
     linker_files = ":compiler_deps",
     objcopy_files = ":empty",
     strip_files = ":empty",
+    supports_header_parsing = 1,
     supports_param_files = 1,
     module_map = %{modulemap},
 )
diff --git a/tools/cpp/linux_cc_wrapper.sh.tpl b/tools/cpp/linux_cc_wrapper.sh.tpl
index a83be50..629741e 100644
--- a/tools/cpp/linux_cc_wrapper.sh.tpl
+++ b/tools/cpp/linux_cc_wrapper.sh.tpl
@@ -18,8 +18,37 @@
 #
 set -eu
 
+OUTPUT=
+
+function parse_option() {
+    local -r opt="$1"
+    if [[ "${OUTPUT}" = "1" ]]; then
+        OUTPUT=$opt
+    elif [[ "$opt" = "-o" ]]; then
+        # output is coming
+        OUTPUT=1
+    fi
+}
+
+# let parse the option list
+for i in "$@"; do
+    if [[ "$i" = @* && -r "${i:1}" ]]; then
+        while IFS= read -r opt
+        do
+            parse_option "$opt"
+        done < "${i:1}" || exit 1
+    else
+        parse_option "$i"
+    fi
+done
+
 # Set-up the environment
 %{env}
 
 # Call the C++ compiler
 %{cc} "$@"
+
+# Generate an empty file if header processing succeeded.
+if [[ "${OUTPUT}" == *.h.processed ]]; then
+  echo -n > "${OUTPUT}"
+fi
diff --git a/tools/cpp/osx_cc_wrapper.sh.tpl b/tools/cpp/osx_cc_wrapper.sh.tpl
index fa60f33..e40a98b 100644
--- a/tools/cpp/osx_cc_wrapper.sh.tpl
+++ b/tools/cpp/osx_cc_wrapper.sh.tpl
@@ -71,6 +71,11 @@
 # Call the C++ compiler
 %{cc} "$@"
 
+# Generate an empty file if header processing succeeded.
+if [[ "${OUTPUT}" == *.h.processed ]]; then
+  echo -n > "${OUTPUT}"
+fi
+
 function get_library_path() {
     for libdir in ${LIB_DIRS}; do
         if [ -f ${libdir}/lib$1.so ]; then
diff --git a/tools/cpp/unix_cc_configure.bzl b/tools/cpp/unix_cc_configure.bzl
index 365e78c..3f162bd 100644
--- a/tools/cpp/unix_cc_configure.bzl
+++ b/tools/cpp/unix_cc_configure.bzl
@@ -387,6 +387,10 @@
         overriden_tools["ar"] = _find_generic(repository_ctx, "libtool", "LIBTOOL", overriden_tools)
     auto_configure_warning_maybe(repository_ctx, "CC used: " + str(cc))
     tool_paths = _get_tool_paths(repository_ctx, overriden_tools)
+
+    # The parse_header tool needs to be a wrapper around the compiler as it has
+    # to touch the output file.
+    tool_paths["parse_headers"] = "cc_wrapper.sh"
     cc_toolchain_identifier = escape_string(get_env_var(
         repository_ctx,
         "CC_TOOLCHAIN_NAME",
@@ -546,9 +550,10 @@
             "%{cc_toolchain_identifier}": cc_toolchain_identifier,
             "%{name}": cpu_value,
             "%{modulemap}": ("\":module.modulemap\"" if generate_modulemap else "None"),
-            "%{cc_compiler_deps}": get_starlark_list([":builtin_include_directory_paths"] + (
-                [":cc_wrapper"] if darwin else []
-            )),
+            "%{cc_compiler_deps}": get_starlark_list([
+                ":builtin_include_directory_paths",
+                ":cc_wrapper",
+            ]),
             "%{compiler}": escape_string(get_env_var(
                 repository_ctx,
                 "BAZEL_COMPILER",
diff --git a/tools/cpp/unix_cc_toolchain_config.bzl b/tools/cpp/unix_cc_toolchain_config.bzl
index 35e2c64..3f077a9 100644
--- a/tools/cpp/unix_cc_toolchain_config.bzl
+++ b/tools/cpp/unix_cc_toolchain_config.bzl
@@ -96,6 +96,47 @@
         ),
     ]
 
+def parse_headers_support(parse_headers_tool_path):
+    if not parse_headers_tool_path:
+        return [], []
+    action_configs = [
+        action_config(
+            action_name = ACTION_NAMES.cpp_header_parsing,
+            tools = [
+                tool(path = parse_headers_tool_path),
+            ],
+            flag_sets = [
+                flag_set(
+                    flag_groups = [
+                        flag_group(
+                            flags = [
+                                # Note: This treats all headers as C++ headers, which may lead to
+                                # parsing failures for C headers that are not valid C++.
+                                # For such headers, use features = ["-parse_headers"] to selectively
+                                # disable parsing.
+                                "-xc++-header",
+                                "-fsyntax-only",
+                            ],
+                        ),
+                    ],
+                ),
+            ],
+            implies = [
+                # Copied from the legacy feature definition in CppActionConfigs.java.
+                "legacy_compile_flags",
+                "user_compile_flags",
+                "sysroot",
+                "unfiltered_compile_flags",
+                "compiler_input_flags",
+                "compiler_output_flags",
+            ],
+        ),
+    ]
+    features = [
+        feature(name = "parse_headers"),
+    ]
+    return action_configs, features
+
 all_compile_actions = [
     ACTION_NAMES.c_compile,
     ACTION_NAMES.cpp_compile,
@@ -1488,6 +1529,12 @@
             generate_linkmap_feature,
         ]
 
+    parse_headers_action_configs, parse_headers_features = parse_headers_support(
+        parse_headers_tool_path = ctx.attr.tool_paths.get("parse_headers"),
+    )
+    action_configs += parse_headers_action_configs
+    features += parse_headers_features
+
     return cc_common.create_cc_toolchain_config_info(
         ctx = ctx,
         features = features,