Add support for implicit include directories to rule-based toolchains

BEGIN_PUBLIC

Add support for implicit include directories to rule-based toolchains

Reorients the `cc_toolchain.cxx_builtin_include_directories` attribute so it is expressed as an attribute on `cc_args` and `cc_tool` to signify that the tools or arguments imply include directories that Bazel's include path checker should allowlist. This moves the allowlist to the source of truth that implies these directories.

END_PUBLIC

PiperOrigin-RevId: 671393376
Change-Id: Ide8cae548783726835168adcd3f705028a1f4308
diff --git a/cc/toolchains/args.bzl b/cc/toolchains/args.bzl
index 2f624f3..282fa04 100644
--- a/cc/toolchains/args.bzl
+++ b/cc/toolchains/args.bzl
@@ -13,6 +13,7 @@
 # limitations under the License.
 """All providers for rule-based bazel toolchain config."""
 
+load("@bazel_skylib//rules/directory:providers.bzl", "DirectoryInfo")
 load("//cc/toolchains/impl:args_utils.bzl", "validate_nested_args")
 load(
     "//cc/toolchains/impl:collect.bzl",
@@ -50,7 +51,7 @@
         )
         files = nested.files
     else:
-        files = collect_files(ctx.attr.data)
+        files = collect_files(ctx.attr.data + ctx.attr.allowlist_include_directories)
 
     requires = collect_provider(ctx.attr.requires_any_of, FeatureConstraintInfo)
 
@@ -61,6 +62,9 @@
         nested = nested,
         env = ctx.attr.env,
         files = files,
+        allowlist_include_directories = depset(
+            direct = [d[DirectoryInfo] for d in ctx.attr.allowlist_include_directories],
+        ),
     )
     return [
         args,
@@ -72,6 +76,7 @@
                 struct(action = action, args = tuple([args]), files = files)
                 for action in actions.to_list()
             ]),
+            allowlist_include_directories = args.allowlist_include_directories,
         ),
     ]
 
@@ -89,6 +94,16 @@
         "env": attr.string_dict(
             doc = "Environment variables to be added to the command-line.",
         ),
+        "allowlist_include_directories": attr.label_list(
+            providers = [DirectoryInfo],
+            doc = """Include paths implied by using this rule.
+
+Some flags (e.g. --sysroot) imply certain include paths are available despite
+not explicitly specifying a normal include path flag (`-I`, `-isystem`, etc.).
+Bazel checks that all included headers are properly provided by a dependency or
+allowlisted through this mechanism.
+""",
+        ),
         "requires_any_of": attr.label_list(
             providers = [FeatureConstraintInfo],
             doc = """This will be enabled when any of the constraints are met.
diff --git a/cc/toolchains/cc_toolchain_info.bzl b/cc/toolchains/cc_toolchain_info.bzl
index 36e922b..2325ddb 100644
--- a/cc/toolchains/cc_toolchain_info.bzl
+++ b/cc/toolchains/cc_toolchain_info.bzl
@@ -87,6 +87,7 @@
         "nested": "(Optional[NestedArgsInfo]) The args expand. Equivalent to a flag group.",
         "files": "(depset[File]) Files required for the args",
         "env": "(dict[str, str]) Environment variables to apply",
+        "allowlist_include_directories": "(depset[DirectoryInfo]) Include directories implied by these arguments that should be allowlisted in Bazel's include checker",
     },
 )
 ArgsListInfo = provider(
@@ -97,6 +98,7 @@
         "args": "(Sequence[ArgsInfo]) The flag sets contained within",
         "files": "(depset[File]) The files required for all of the arguments",
         "by_action": "(Sequence[struct(action=ActionTypeInfo, args=List[ArgsInfo], files=depset[Files])]) Relevant information about the args keyed by the action type.",
+        "allowlist_include_directories": "(depset[DirectoryInfo]) Include directories implied by these arguments that should be allowlisted in Bazel's include checker",
     },
 )
 
@@ -114,6 +116,7 @@
         "external": "(bool) Whether a feature is defined elsewhere.",
         "overridable": "(bool) Whether the feature is an overridable feature.",
         "overrides": "(Optional[FeatureInfo]) The feature that this overrides. Must be a known feature",
+        "allowlist_include_directories": "(depset[DirectoryInfo]) Include directories implied by this feature that should be allowlisted in Bazel's include checker",
     },
 )
 FeatureSetInfo = provider(
@@ -152,6 +155,7 @@
         "exe": "(File) The file corresponding to the tool",
         "runfiles": "(runfiles) The files required to run the tool",
         "execution_requirements": "(Sequence[str]) A set of execution requirements of the tool",
+        "allowlist_include_directories": "(depset[DirectoryInfo]) Built-in include directories implied by this tool that should be allowlisted in Bazel's include checker",
     },
 )
 
@@ -174,5 +178,6 @@
         "tool_map": "(ToolConfigInfo) A provider mapping toolchain action types to tools.",
         "args": "(Sequence[ArgsInfo]) A list of arguments to be unconditionally applied to the toolchain.",
         "files": "(dict[ActionTypeInfo, depset[File]]) Files required for the toolchain, keyed by the action type.",
+        "allowlist_include_directories": "(depset[DirectoryInfo]) Built-in include directories implied by this toolchain's args and tools that should be allowlisted in Bazel's include checker",
     },
 )
diff --git a/cc/toolchains/feature.bzl b/cc/toolchains/feature.bzl
index 075ff07..a282762 100644
--- a/cc/toolchains/feature.bzl
+++ b/cc/toolchains/feature.bzl
@@ -62,13 +62,14 @@
     if name.startswith("implied_by_"):
         fail("Feature names starting with 'implied_by' are reserved")
 
+    args = collect_args_lists(ctx.attr.args, ctx.label)
     feature = FeatureInfo(
         label = ctx.label,
         name = name,
         # Unused field, but leave it just in case we want to reuse it in the
         # future.
         enabled = False,
-        args = collect_args_lists(ctx.attr.args, ctx.label),
+        args = args,
         implies = collect_features(ctx.attr.implies),
         requires_any_of = tuple(collect_provider(
             ctx.attr.requires_any_of,
@@ -81,6 +82,7 @@
         external = False,
         overridable = False,
         overrides = overrides,
+        allowlist_include_directories = args.allowlist_include_directories,
     )
 
     return [
diff --git a/cc/toolchains/impl/collect.bzl b/cc/toolchains/impl/collect.bzl
index a1daa23..f41aa7d 100644
--- a/cc/toolchains/impl/collect.bzl
+++ b/cc/toolchains/impl/collect.bzl
@@ -106,6 +106,7 @@
                 exe = info.files_to_run.executable,
                 runfiles = collect_data(ctx, [target]),
                 execution_requirements = tuple(),
+                allowlist_include_directories = depset(),
             ))
         else:
             fail("Expected %s to be a cc_tool or a binary rule" % target.label)
@@ -141,6 +142,9 @@
         label = label,
         args = tuple(args),
         files = depset(transitive = transitive_files),
+        allowlist_include_directories = depset(
+            transitive = [a.allowlist_include_directories for a in args],
+        ),
         by_action = tuple([
             struct(
                 action = k,
diff --git a/cc/toolchains/impl/external_feature.bzl b/cc/toolchains/impl/external_feature.bzl
index 0853b32..1e11bc9 100644
--- a/cc/toolchains/impl/external_feature.bzl
+++ b/cc/toolchains/impl/external_feature.bzl
@@ -43,6 +43,7 @@
         external = True,
         overridable = ctx.attr.overridable,
         overrides = None,
+        allowlist_include_directories = depset(),
     )
     providers = [
         feature,
diff --git a/cc/toolchains/impl/legacy_converter.bzl b/cc/toolchains/impl/legacy_converter.bzl
index 99cc353..7197716 100644
--- a/cc/toolchains/impl/legacy_converter.bzl
+++ b/cc/toolchains/impl/legacy_converter.bzl
@@ -146,7 +146,7 @@
     """Converts a rule-based toolchain into the legacy providers.
 
     Args:
-        toolchain: CcToolchainConfigInfo: The toolchain config to convert.
+        toolchain: (ToolchainConfigInfo) The toolchain config to convert.
     Returns:
         A struct containing parameters suitable to pass to
           cc_common.create_cc_toolchain_config_info.
@@ -165,10 +165,17 @@
         requires_any_of = [],
         mutually_exclusive = [],
         external = False,
+        allowlist_include_directories = depset(),
     )))
     action_configs = _convert_tool_map(toolchain.tool_map)
 
+    cxx_builtin_include_directories = [
+        d.path
+        for d in toolchain.allowlist_include_directories.to_list()
+    ]
+
     return struct(
         features = [ft for ft in features if ft != None],
         action_configs = sorted(action_configs, key = lambda ac: ac.action_name),
+        cxx_builtin_include_directories = cxx_builtin_include_directories,
     )
diff --git a/cc/toolchains/impl/nested_args.bzl b/cc/toolchains/impl/nested_args.bzl
index 2a5fb07..17ebb77 100644
--- a/cc/toolchains/impl/nested_args.bzl
+++ b/cc/toolchains/impl/nested_args.bzl
@@ -97,7 +97,7 @@
         args = ctx.attr.args,
         format = ctx.attr.format,
         nested = collect_provider(ctx.attr.nested, NestedArgsInfo),
-        files = collect_files(ctx.attr.data),
+        files = collect_files(ctx.attr.data + getattr(ctx.attr, "allowlist_include_directories", [])),
         iterate_over = ctx.attr.iterate_over,
         requires_not_none = _var(ctx.attr.requires_not_none),
         requires_none = _var(ctx.attr.requires_none),
diff --git a/cc/toolchains/impl/toolchain_config.bzl b/cc/toolchains/impl/toolchain_config.bzl
index 282588b..5c8d69c 100644
--- a/cc/toolchains/impl/toolchain_config.bzl
+++ b/cc/toolchains/impl/toolchain_config.bzl
@@ -14,7 +14,6 @@
 """Implementation of the cc_toolchain rule."""
 
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
-load("@bazel_skylib//rules/directory:providers.bzl", "DirectoryInfo")
 load(
     "//cc/toolchains:cc_toolchain_info.bzl",
     "ActionTypeSetInfo",
@@ -66,18 +65,13 @@
 
     legacy = convert_toolchain(toolchain_config)
 
-    cxx_builtin_include_directories = [
-        d[DirectoryInfo].path
-        for d in ctx.attr.cxx_builtin_include_directories
-    ]
-
     return [
         toolchain_config,
         cc_common.create_cc_toolchain_config_info(
             ctx = ctx,
             action_configs = legacy.action_configs,
             features = legacy.features,
-            cxx_builtin_include_directories = cxx_builtin_include_directories,
+            cxx_builtin_include_directories = legacy.cxx_builtin_include_directories,
             # toolchain_identifier is deprecated, but setting it to None results
             # in an error that it expected a string, and for safety's sake, I'd
             # prefer to provide something unique.
@@ -110,9 +104,6 @@
         "skip_experimental_flag_validation_for_test": attr.bool(default = False),
         "_builtin_features": attr.label(default = "//cc/toolchains/features:all_builtin_features"),
         "_enabled": attr.label(default = "//cc/toolchains:experimental_enable_rule_based_toolchains"),
-
-        # Attributes translated from legacy cc toolchains.
-        "cxx_builtin_include_directories": attr.label_list(providers = [DirectoryInfo]),
     },
     provides = [ToolchainConfigInfo],
 )
diff --git a/cc/toolchains/impl/toolchain_config_info.bzl b/cc/toolchains/impl/toolchain_config_info.bzl
index a2c6bf1..7e68b15 100644
--- a/cc/toolchains/impl/toolchain_config_info.bzl
+++ b/cc/toolchains/impl/toolchain_config_info.bzl
@@ -162,7 +162,12 @@
         action_type: _collect_files_for_action_type(action_type, tools, features, args)
         for action_type in tools.keys()
     }
-
+    allowlist_include_directories = depset(
+        transitive = [
+            src.allowlist_include_directories
+            for src in features + tools.values()
+        ] + [args.allowlist_include_directories],
+    )
     toolchain_config = ToolchainConfigInfo(
         label = label,
         features = features,
@@ -170,6 +175,7 @@
         tool_map = tool_map[ToolConfigInfo],
         args = args.args,
         files = files,
+        allowlist_include_directories = allowlist_include_directories,
     )
     _validate_toolchain(toolchain_config, fail = fail)
     return toolchain_config
diff --git a/cc/toolchains/tool.bzl b/cc/toolchains/tool.bzl
index f07af32..159a9d6 100644
--- a/cc/toolchains/tool.bzl
+++ b/cc/toolchains/tool.bzl
@@ -13,6 +13,7 @@
 # limitations under the License.
 """Implementation of cc_tool"""
 
+load("@bazel_skylib//rules/directory:providers.bzl", "DirectoryInfo")
 load("//cc/toolchains/impl:collect.bzl", "collect_data")
 load(
     ":cc_toolchain_info.bzl",
@@ -28,12 +29,15 @@
     else:
         fail("Expected cc_tool's src attribute to be either an executable or a single file")
 
-    runfiles = collect_data(ctx, ctx.attr.data + [ctx.attr.src])
+    runfiles = collect_data(ctx, ctx.attr.data + [ctx.attr.src] + ctx.attr.allowlist_include_directories)
     tool = ToolInfo(
         label = ctx.label,
         exe = exe,
         runfiles = runfiles,
         execution_requirements = tuple(ctx.attr.tags),
+        allowlist_include_directories = depset(
+            direct = [d[DirectoryInfo] for d in ctx.attr.allowlist_include_directories],
+        ),
     )
 
     link = ctx.actions.declare_file(ctx.label.name)
@@ -70,6 +74,16 @@
             allow_files = True,
             doc = "Additional files that are required for this tool to run.",
         ),
+        "allowlist_include_directories": attr.label_list(
+            providers = [DirectoryInfo],
+            doc = """Include paths implied by using this tool.
+
+Compilers may include a set of built-in headers that are implicitly available
+unless flags like `-nostdinc` are provided. Bazel checks that all included
+headers are properly provided by a dependency or allowlisted through this
+mechanism.
+""",
+        ),
     },
     provides = [ToolInfo],
     doc = """Declares a tool that can be bound to action configs.
diff --git a/tests/rule_based_toolchain/args/BUILD b/tests/rule_based_toolchain/args/BUILD
index 585ec91..3b8e1ab 100644
--- a/tests/rule_based_toolchain/args/BUILD
+++ b/tests/rule_based_toolchain/args/BUILD
@@ -29,6 +29,14 @@
     env = {"BAR": "bar"},
 )
 
+util.helper_target(
+    cc_args,
+    name = "with_dir",
+    actions = ["//tests/rule_based_toolchain/actions:all_compile"],
+    allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:directory"],
+    args = ["--secret-builtin-include-dir"],
+)
+
 analysis_test_suite(
     name = "test_suite",
     targets = TARGETS,
diff --git a/tests/rule_based_toolchain/args/args_test.bzl b/tests/rule_based_toolchain/args/args_test.bzl
index 226aadb..46ab7f9 100644
--- a/tests/rule_based_toolchain/args/args_test.bzl
+++ b/tests/rule_based_toolchain/args/args_test.bzl
@@ -39,6 +39,7 @@
     "tests/rule_based_toolchain/testdata/multiple1",
     "tests/rule_based_toolchain/testdata/multiple2",
 ]
+_TOOL_DIRECTORY = "tests/rule_based_toolchain/testdata"
 
 _CONVERTED_ARGS = subjects.struct(
     flag_sets = subjects.collection,
@@ -99,9 +100,20 @@
 
     converted.flag_sets().contains_exactly([])
 
+def _with_dir_test(env, targets):
+    with_dir = env.expect.that_target(targets.with_dir).provider(ArgsInfo)
+    with_dir.allowlist_include_directories().contains_exactly([_TOOL_DIRECTORY])
+    with_dir.files().contains_at_least(_SIMPLE_FILES)
+
+    c_compile = env.expect.that_target(targets.with_dir).provider(ArgsListInfo).by_action().get(
+        targets.c_compile[ActionTypeInfo],
+    )
+    c_compile.files().contains_at_least(_SIMPLE_FILES)
+
 TARGETS = [
     ":simple",
     ":env_only",
+    ":with_dir",
     "//tests/rule_based_toolchain/actions:c_compile",
     "//tests/rule_based_toolchain/actions:cpp_compile",
 ]
@@ -110,4 +122,5 @@
 TESTS = {
     "simple_test": _simple_test,
     "env_only_test": _env_only_test,
+    "with_dir_test": _with_dir_test,
 }
diff --git a/tests/rule_based_toolchain/args_list/BUILD b/tests/rule_based_toolchain/args_list/BUILD
index 9fc9f88..64f0fbb 100644
--- a/tests/rule_based_toolchain/args_list/BUILD
+++ b/tests/rule_based_toolchain/args_list/BUILD
@@ -42,6 +42,34 @@
     visibility = ["//tests/rule_based_toolchain:__subpackages__"],
 )
 
+util.helper_target(
+    cc_args,
+    name = "args_with_dir_1",
+    actions = ["//tests/rule_based_toolchain/actions:c_compile"],
+    allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:subdirectory_1"],
+    args = ["dir1"],
+    visibility = ["//tests/rule_based_toolchain:__subpackages__"],
+)
+
+util.helper_target(
+    cc_args,
+    name = "args_with_dir_2",
+    actions = ["//tests/rule_based_toolchain/actions:cpp_compile"],
+    allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:subdirectory_2"],
+    args = ["dir2"],
+    visibility = ["//tests/rule_based_toolchain:__subpackages__"],
+)
+
+util.helper_target(
+    cc_args_list,
+    name = "args_list_with_dir",
+    args = [
+        ":args_with_dir_1",
+        ":args_with_dir_2",
+    ],
+    visibility = ["//tests/rule_based_toolchain:__subpackages__"],
+)
+
 analysis_test_suite(
     name = "test_suite",
     targets = TARGETS,
diff --git a/tests/rule_based_toolchain/args_list/args_list_test.bzl b/tests/rule_based_toolchain/args_list/args_list_test.bzl
index 1d37145..c811673 100644
--- a/tests/rule_based_toolchain/args_list/args_list_test.bzl
+++ b/tests/rule_based_toolchain/args_list/args_list_test.bzl
@@ -26,6 +26,20 @@
 _CPP_COMPILE_FILE = "tests/rule_based_toolchain/testdata/file2"
 _BOTH_FILE = "tests/rule_based_toolchain/testdata/multiple1"
 
+_TEST_DIR_1 = "tests/rule_based_toolchain/testdata/subdir1"
+_TEST_DIR_2 = "tests/rule_based_toolchain/testdata/subdir2"
+_ALL_TEST_DIRS = [
+    _TEST_DIR_1,
+    _TEST_DIR_2,
+]
+_TEST_DIR_1_FILES = [
+    "tests/rule_based_toolchain/testdata/subdir1/file_foo",
+]
+_TEST_DIR_2_FILES = [
+    "tests/rule_based_toolchain/testdata/subdir2/file_bar",
+]
+_ALL_TEST_DIRS_FILES = _TEST_DIR_1_FILES + _TEST_DIR_2_FILES
+
 def _collect_args_lists_test(env, targets):
     args = env.expect.that_target(targets.args_list).provider(ArgsListInfo)
     args.args().contains_exactly([
@@ -53,15 +67,34 @@
         targets.all_compile_args[ArgsInfo],
     ])
 
+def _collect_args_list_dirs_test(env, targets):
+    args = env.expect.that_target(targets.args_list_with_dir).provider(ArgsListInfo)
+    args.allowlist_include_directories().contains_exactly(_ALL_TEST_DIRS)
+    args.files().contains_exactly(_ALL_TEST_DIRS_FILES)
+
+    c_compile = env.expect.that_target(targets.args_list_with_dir).provider(ArgsListInfo).by_action().get(
+        targets.c_compile[ActionTypeInfo],
+    )
+    c_compile.files().contains_exactly(_TEST_DIR_1_FILES)
+
+    cpp_compile = env.expect.that_target(targets.args_list_with_dir).provider(ArgsListInfo).by_action().get(
+        targets.cpp_compile[ActionTypeInfo],
+    )
+    cpp_compile.files().contains_exactly(_TEST_DIR_2_FILES)
+
 TARGETS = [
     ":c_compile_args",
     ":cpp_compile_args",
     ":all_compile_args",
     ":args_list",
+    ":args_with_dir_1",
+    ":args_with_dir_2",
+    ":args_list_with_dir",
     "//tests/rule_based_toolchain/actions:c_compile",
     "//tests/rule_based_toolchain/actions:cpp_compile",
 ]
 
 TESTS = {
+    "collect_args_list_dirs_test": _collect_args_list_dirs_test,
     "collect_args_lists_test": _collect_args_lists_test,
 }
diff --git a/tests/rule_based_toolchain/features/BUILD b/tests/rule_based_toolchain/features/BUILD
index 9a142be..c982318 100644
--- a/tests/rule_based_toolchain/features/BUILD
+++ b/tests/rule_based_toolchain/features/BUILD
@@ -10,7 +10,7 @@
 
 util.helper_target(
     cc_args,
-    name = "c_compile",
+    name = "c_compile_args",
     actions = ["//tests/rule_based_toolchain/actions:c_compile"],
     args = ["c"],
     data = ["//tests/rule_based_toolchain/testdata:file1"],
@@ -19,7 +19,7 @@
 util.helper_target(
     cc_feature,
     name = "simple",
-    args = [":c_compile"],
+    args = [":c_compile_args"],
     feature_name = "feature_name",
     visibility = ["//tests/rule_based_toolchain:__subpackages__"],
 )
@@ -27,7 +27,7 @@
 util.helper_target(
     cc_feature,
     name = "simple2",
-    args = [":c_compile"],
+    args = [":c_compile_args"],
     feature_name = "simple2",
 )
 
@@ -43,7 +43,7 @@
 util.helper_target(
     cc_feature,
     name = "requires",
-    args = [":c_compile"],
+    args = [":c_compile_args"],
     feature_name = "requires",
     requires_any_of = [":feature_set"],
 )
@@ -51,7 +51,7 @@
 util.helper_target(
     cc_feature,
     name = "implies",
-    args = [":c_compile"],
+    args = [":c_compile_args"],
     feature_name = "implies",
     implies = [":simple"],
 )
@@ -63,7 +63,7 @@
 util.helper_target(
     cc_feature,
     name = "mutual_exclusion_feature",
-    args = [":c_compile"],
+    args = [":c_compile_args"],
     feature_name = "mutual_exclusion",
     mutually_exclusive = [
         ":simple",
@@ -99,7 +99,7 @@
 util.helper_target(
     cc_feature,
     name = "overrides",
-    args = [":c_compile"],
+    args = [":c_compile_args"],
     overrides = ":builtin_feature",
 )
 
@@ -109,6 +109,21 @@
     feature_name = "sentinel_feature_name",
 )
 
+util.helper_target(
+    cc_args,
+    name = "args_with_dir",
+    actions = ["//tests/rule_based_toolchain/actions:c_compile"],
+    allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:subdirectory_1"],
+    args = ["--include-builtin-dirs"],
+)
+
+util.helper_target(
+    cc_feature,
+    name = "feature_with_dir",
+    args = [":args_with_dir"],
+    feature_name = "feature_with_dir",
+)
+
 analysis_test_suite(
     name = "test_suite",
     targets = TARGETS,
diff --git a/tests/rule_based_toolchain/features/features_test.bzl b/tests/rule_based_toolchain/features/features_test.bzl
index 332d27e..a0d479a 100644
--- a/tests/rule_based_toolchain/features/features_test.bzl
+++ b/tests/rule_based_toolchain/features/features_test.bzl
@@ -21,6 +21,7 @@
 )
 load(
     "//cc/toolchains:cc_toolchain_info.bzl",
+    "ActionTypeInfo",
     "ArgsInfo",
     "FeatureConstraintInfo",
     "FeatureInfo",
@@ -36,6 +37,8 @@
 visibility("private")
 
 _C_COMPILE_FILE = "tests/rule_based_toolchain/testdata/file1"
+_SUBDIR1 = "tests/rule_based_toolchain/testdata/subdir1"
+_SUBDIR1_FILES = ["tests/rule_based_toolchain/testdata/subdir1/file_foo"]
 
 def _sentinel_feature_test(env, targets):
     sentinel_feature = env.expect.that_target(targets.sentinel_feature).provider(FeatureInfo)
@@ -45,17 +48,17 @@
 def _simple_feature_test(env, targets):
     simple = env.expect.that_target(targets.simple).provider(FeatureInfo)
     simple.name().equals("feature_name")
-    simple.args().args().contains_exactly([targets.c_compile.label])
+    simple.args().args().contains_exactly([targets.c_compile_args.label])
     simple.enabled().equals(False)
     simple.overrides().is_none()
     simple.overridable().equals(False)
 
     simple.args().files().contains_exactly([_C_COMPILE_FILE])
     c_compile_action = simple.args().by_action().get(
-        targets.c_compile[ArgsInfo].actions.to_list()[0],
+        targets.c_compile_args[ArgsInfo].actions.to_list()[0],
     )
     c_compile_action.files().contains_exactly([_C_COMPILE_FILE])
-    c_compile_action.args().contains_exactly([targets.c_compile[ArgsInfo]])
+    c_compile_action.args().contains_exactly([targets.c_compile_args[ArgsInfo]])
 
     legacy = convert_feature(simple.actual)
     env.expect.that_str(legacy.name).equals("feature_name")
@@ -149,12 +152,23 @@
     overrides.name().equals("builtin_feature")
     overrides.overrides().some().label().equals(targets.builtin_feature.label)
 
+def _feature_with_directory_test(env, targets):
+    with_dir = env.expect.that_target(targets.feature_with_dir).provider(FeatureInfo)
+    with_dir.allowlist_include_directories().contains_exactly([_SUBDIR1])
+
+    c_compile = env.expect.that_target(targets.feature_with_dir).provider(FeatureInfo).args().by_action().get(
+        targets.c_compile[ActionTypeInfo],
+    )
+    c_compile.files().contains_at_least(_SUBDIR1_FILES)
+
 TARGETS = [
+    ":args_with_dir",
     ":builtin_feature",
-    ":c_compile",
+    ":c_compile_args",
     ":category",
     ":direct_constraint",
     ":feature_set",
+    ":feature_with_dir",
     ":implies",
     ":mutual_exclusion_feature",
     ":overrides",
@@ -163,6 +177,7 @@
     ":simple",
     ":simple2",
     ":transitive_constraint",
+    "//tests/rule_based_toolchain/actions:c_compile",
 ]
 
 # @unsorted-dict-items
@@ -177,4 +192,5 @@
     "feature_constraint_collects_transitive_features_test": _feature_constraint_collects_transitive_features_test,
     "external_feature_is_a_feature_test": _external_feature_is_a_feature_test,
     "feature_can_be_overridden_test": _feature_can_be_overridden_test,
+    "feature_with_directory_test": _feature_with_directory_test,
 }
diff --git a/tests/rule_based_toolchain/subjects.bzl b/tests/rule_based_toolchain/subjects.bzl
index 92f6fc8..e741d67 100644
--- a/tests/rule_based_toolchain/subjects.bzl
+++ b/tests/rule_based_toolchain/subjects.bzl
@@ -43,6 +43,10 @@
 # type.
 unknown_subject = _subjects.str
 
+# Directory depsets are quite complex, so just simplify them as a list of paths.
+# buildifier: disable=name-conventions
+_FakeDirectoryDepset = lambda value, *, meta: _subjects.collection([v.path for v in value.to_list()], meta = meta)
+
 # buildifier: disable=name-conventions
 _ActionTypeFactory = generate_factory(
     ActionTypeInfo,
@@ -78,6 +82,7 @@
     overridable = _subjects.bool,
     external = _subjects.bool,
     overrides = None,
+    allowlist_include_directories = _FakeDirectoryDepset,
 )
 
 # Break the dependency loop.
@@ -141,6 +146,7 @@
         # Use .factory so it's not inlined.
         nested = optional_subject(_NestedArgsFactory.factory),
         requires_any_of = ProviderSequence(_FeatureConstraintFactory),
+        allowlist_include_directories = _FakeDirectoryDepset,
     ),
 )
 
@@ -155,6 +161,7 @@
             files = _subjects.depset_file,
         ))({value.action: value for value in values}, meta = meta),
         files = _subjects.depset_file,
+        allowlist_include_directories = _FakeDirectoryDepset,
     ),
 )
 
@@ -179,6 +186,7 @@
         exe = _subjects.file,
         runfiles = runfiles_subject,
         execution_requirements = _subjects.collection,
+        allowlist_include_directories = _FakeDirectoryDepset,
     ),
 )
 
@@ -201,6 +209,7 @@
         tool_map = optional_subject(_ToolConfigFactory.factory),
         args = ProviderSequence(_ArgsFactory),
         files = dict_key_subject(_subjects.depset_file),
+        allowlist_include_directories = _FakeDirectoryDepset,
     ),
 )
 
diff --git a/tests/rule_based_toolchain/testdata/BUILD b/tests/rule_based_toolchain/testdata/BUILD
index 950751c..876834a 100644
--- a/tests/rule_based_toolchain/testdata/BUILD
+++ b/tests/rule_based_toolchain/testdata/BUILD
@@ -1,16 +1,35 @@
 load("@bazel_skylib//rules:native_binary.bzl", "native_binary")
 load("@bazel_skylib//rules/directory:directory.bzl", "directory")
+load("@bazel_skylib//rules/directory:subdirectory.bzl", "subdirectory")
 
 package(default_visibility = ["//tests/rule_based_toolchain:__subpackages__"])
 
 directory(
     name = "directory",
     srcs = glob(
-        ["*"],
+        ["**"],
         exclude = ["BUILD"],
     ),
 )
 
+subdirectory(
+    name = "subdirectory_1",
+    parent = ":directory",
+    path = "subdir1",
+)
+
+subdirectory(
+    name = "subdirectory_2",
+    parent = ":directory",
+    path = "subdir2",
+)
+
+subdirectory(
+    name = "subdirectory_3",
+    parent = ":directory",
+    path = "subdir3",
+)
+
 exports_files(
     glob(
         ["*"],
diff --git a/tests/rule_based_toolchain/testdata/subdir1/file_foo b/tests/rule_based_toolchain/testdata/subdir1/file_foo
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/rule_based_toolchain/testdata/subdir1/file_foo
diff --git a/tests/rule_based_toolchain/testdata/subdir2/file_bar b/tests/rule_based_toolchain/testdata/subdir2/file_bar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/rule_based_toolchain/testdata/subdir2/file_bar
diff --git a/tests/rule_based_toolchain/testdata/subdir3/file_baz b/tests/rule_based_toolchain/testdata/subdir3/file_baz
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/rule_based_toolchain/testdata/subdir3/file_baz
diff --git a/tests/rule_based_toolchain/tool/BUILD b/tests/rule_based_toolchain/tool/BUILD
index 2d58852..67ce625 100644
--- a/tests/rule_based_toolchain/tool/BUILD
+++ b/tests/rule_based_toolchain/tool/BUILD
@@ -16,6 +16,13 @@
     visibility = ["//tests/rule_based_toolchain:__subpackages__"],
 )
 
+cc_tool(
+    name = "tool_with_allowlist_include_directories",
+    src = "//tests/rule_based_toolchain/testdata:bin_wrapper.sh",
+    allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:directory"],
+    visibility = ["//tests/rule_based_toolchain:__subpackages__"],
+)
+
 cc_directory_tool(
     name = "directory_tool",
     data = ["bin"],
diff --git a/tests/rule_based_toolchain/tool/tool_test.bzl b/tests/rule_based_toolchain/tool/tool_test.bzl
index 81b62b6..ebc5164 100644
--- a/tests/rule_based_toolchain/tool/tool_test.bzl
+++ b/tests/rule_based_toolchain/tool/tool_test.bzl
@@ -32,6 +32,8 @@
 _BIN_WRAPPER_SYMLINK = "tests/rule_based_toolchain/testdata/bin_wrapper"
 _BIN_WRAPPER = "tests/rule_based_toolchain/testdata/bin_wrapper.sh"
 _BIN = "tests/rule_based_toolchain/testdata/bin"
+_FILE1 = "tests/rule_based_toolchain/testdata/file1"
+_TOOL_DIRECTORY = "tests/rule_based_toolchain/testdata"
 
 def _tool_test(env, target):
     tool = env.expect.that_target(target).provider(ToolInfo)
@@ -55,6 +57,14 @@
         _BIN,
     ])
 
+def _tool_with_allowlist_include_directories_test(env, targets):
+    tool = env.expect.that_target(targets.tool_with_allowlist_include_directories).provider(ToolInfo)
+    tool.allowlist_include_directories().contains_exactly([_TOOL_DIRECTORY])
+    tool.runfiles().contains_at_least([
+        _BIN,
+        _FILE1,
+    ])
+
 def _collect_tools_collects_tools_test(env, targets):
     env.expect.that_value(
         value = collect_tools(env.ctx, [targets.tool, targets.wrapped_tool]),
@@ -97,6 +107,7 @@
     "//tests/rule_based_toolchain/tool:tool",
     "//tests/rule_based_toolchain/tool:wrapped_tool",
     "//tests/rule_based_toolchain/tool:directory_tool",
+    "//tests/rule_based_toolchain/tool:tool_with_allowlist_include_directories",
     "//tests/rule_based_toolchain/testdata:bin_wrapper",
     "//tests/rule_based_toolchain/testdata:multiple",
     "//tests/rule_based_toolchain/testdata:bin_filegroup",
@@ -112,4 +123,5 @@
     "collect_tools_collects_binaries_test": _collect_tools_collects_binaries_test,
     "collect_tools_collects_single_files_test": _collect_tools_collects_single_files_test,
     "collect_tools_fails_on_non_binary_test": _collect_tools_fails_on_non_binary_test,
+    "tool_with_allowlist_include_directories_test": _tool_with_allowlist_include_directories_test,
 }
diff --git a/tests/rule_based_toolchain/toolchain_config/BUILD b/tests/rule_based_toolchain/toolchain_config/BUILD
index 6d18357..981758e 100644
--- a/tests/rule_based_toolchain/toolchain_config/BUILD
+++ b/tests/rule_based_toolchain/toolchain_config/BUILD
@@ -2,6 +2,7 @@
 load("//cc/toolchains:args.bzl", "cc_args")
 load("//cc/toolchains:feature.bzl", "cc_feature")
 load("//cc/toolchains:feature_set.bzl", "cc_feature_set")
+load("//cc/toolchains:tool.bzl", "cc_tool")
 load("//cc/toolchains:tool_map.bzl", "cc_tool_map")
 load("//cc/toolchains/args:sysroot.bzl", "cc_sysroot")
 load("//cc/toolchains/impl:external_feature.bzl", "cc_external_feature")
@@ -29,6 +30,7 @@
     cc_args,
     name = "c_compile_args",
     actions = ["//tests/rule_based_toolchain/actions:c_compile"],
+    allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:subdirectory_1"],
     args = ["c_compile_args"],
     data = ["//tests/rule_based_toolchain/testdata:file1"],
 )
@@ -41,6 +43,12 @@
     env = {"CPP_COMPILE": "1"},
 )
 
+cc_tool(
+    name = "c_compile_tool",
+    src = "//tests/rule_based_toolchain/testdata:bin_wrapper",
+    allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:subdirectory_3"],
+)
+
 cc_sysroot(
     name = "sysroot",
     sysroot = "//tests/rule_based_toolchain/testdata:directory",
@@ -53,9 +61,6 @@
         ":sysroot",
         ":c_compile_args",
     ],
-    cxx_builtin_include_directories = [
-        "//tests/rule_based_toolchain/testdata:directory",
-    ],
     enabled_features = [":simple_feature"],
     known_features = [":compile_feature"],
     skip_experimental_flag_validation_for_test = True,
@@ -80,6 +85,7 @@
     cc_args,
     name = "compile_args",
     actions = ["//tests/rule_based_toolchain/actions:all_compile"],
+    allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:subdirectory_2"],
     args = ["compile_args"],
     data = ["//tests/rule_based_toolchain/testdata:file2"],
 )
@@ -88,7 +94,8 @@
     cc_tool_map,
     name = "compile_tool_map",
     tools = {
-        "//tests/rule_based_toolchain/actions:all_compile": "//tests/rule_based_toolchain/tool:wrapped_tool",
+        "//tests/rule_based_toolchain/actions:c_compile": ":c_compile_tool",
+        "//tests/rule_based_toolchain/actions:cpp_compile": "//tests/rule_based_toolchain/tool:wrapped_tool",
     },
 )
 
diff --git a/tests/rule_based_toolchain/toolchain_config/toolchain_config_test.bzl b/tests/rule_based_toolchain/toolchain_config/toolchain_config_test.bzl
index 215d3fe..71a05cf 100644
--- a/tests/rule_based_toolchain/toolchain_config/toolchain_config_test.bzl
+++ b/tests/rule_based_toolchain/toolchain_config/toolchain_config_test.bzl
@@ -36,11 +36,17 @@
     "tests/rule_based_toolchain/testdata/bin_wrapper",
     # From :compile_feature's args
     "tests/rule_based_toolchain/testdata/file2",
+    # From :compile_feature's args' allowlist_include_directories
+    "tests/rule_based_toolchain/testdata/subdir2/file_bar",
 ]
 
 _COLLECTED_C_COMPILE_FILES = _COLLECTED_CPP_COMPILE_FILES + [
     # From :c_compile_args
     "tests/rule_based_toolchain/testdata/file1",
+    # From :c_compile_args's allowlist_include_directories
+    "tests/rule_based_toolchain/testdata/subdir1/file_foo",
+    # From :c_compile_tool's allowlist_include_directories
+    "tests/rule_based_toolchain/testdata/subdir3/file_baz",
 ]
 
 def _expect_that_toolchain(env, expr = None, **kwargs):