diff --git a/cc/common/cc_helper.bzl b/cc/common/cc_helper.bzl
index 51f2309b..1afb0105 100644
--- a/cc/common/cc_helper.bzl
+++ b/cc/common/cc_helper.bzl
@@ -814,13 +814,13 @@ def _get_cc_flags_make_variable(_ctx, feature_configuration, cc_toolchain):
def _package_exec_path(ctx, package, sibling_repository_layout):
return get_relative_path(_repository_exec_path(ctx.label.workspace_name, sibling_repository_layout), package)
-def _include_dirs(ctx, additional_make_variable_substitutions):
+def _include_dirs(ctx, additional_make_variable_substitutions, attr = "includes"):
result = []
sibling_repository_layout = ctx.configuration.is_sibling_repository_layout()
package = ctx.label.package
package_exec_path = _package_exec_path(ctx, package, sibling_repository_layout)
package_source_root = _package_source_root(ctx.label.workspace_name, package, sibling_repository_layout)
- for include in ctx.attr.includes:
+ for include in getattr(ctx.attr, attr):
includes_attr = _expand(ctx, include, additional_make_variable_substitutions)
if is_path_absolute(includes_attr):
continue
diff --git a/cc/private/cc_common.bzl b/cc/private/cc_common.bzl
index 9d496ecb..f37f643e 100644
--- a/cc/private/cc_common.bzl
+++ b/cc/private/cc_common.bzl
@@ -461,6 +461,7 @@ def _compile(
textual_hdrs = [],
additional_exported_hdrs = _UNBOUND, # TODO(ilist@): remove, there are no uses
includes = [],
+ local_includes = [],
quote_includes = [],
system_includes = [],
framework_includes = [],
@@ -546,6 +547,7 @@ def _compile(
textual_hdrs = textual_hdrs,
additional_exported_hdrs = additional_exported_hdrs,
includes = includes,
+ local_includes = local_includes,
quote_includes = quote_includes,
system_includes = system_includes,
framework_includes = framework_includes,
diff --git a/cc/private/cc_info.bzl b/cc/private/cc_info.bzl
index 5ab8d735..a014ce42 100644
--- a/cc/private/cc_info.bzl
+++ b/cc/private/cc_info.bzl
@@ -27,6 +27,9 @@ CcCompilationContextInfo = provider(
# CommandLineCcCompilationContext fields:
"includes": "Returns the set of search paths (as strings) for header files referenced " +
"both by angle bracket and quotes. Usually passed with -I.",
+ "local_includes": "Returns the set of search paths (as strings) for header files referenced " +
+ "both by angle bracket and quotes. Usually passed with -I. These values " +
+ "are not propagated to the target's transitive dependents.",
"quote_includes": "Returns the set of search paths (as strings) for header files " +
"referenced by quotes, e.g. #include \"foo/bar/header.h\". They can be " +
"either relative to the exec root or absolute. Usually passed with -iquote.",
@@ -114,6 +117,7 @@ EMPTY_COMPILATION_CONTEXT = CcCompilationContextInfo(
direct_private_headers = [],
direct_textual_headers = [],
includes = depset(),
+ local_includes = depset(),
quote_includes = depset(),
system_includes = depset(),
framework_includes = depset(),
@@ -276,6 +280,7 @@ def create_compilation_context(
*,
headers = None,
includes = None,
+ local_includes = None,
quote_includes = None,
system_includes = None,
framework_includes = None,
@@ -302,6 +307,7 @@ def create_compilation_context(
Args:
headers: A depset of headers to compile.
includes: A depset of include directories.
+ local_includes: A depset of local include directories.
quote_includes: A depset of quoted include directories.
system_includes: A depset of system include directories.
framework_includes: A depset of framework include directories.
@@ -361,6 +367,7 @@ def create_compilation_context(
direct_private_headers = header_info.modular_private_headers,
direct_textual_headers = header_info.textual_headers,
includes = includes,
+ local_includes = local_includes,
quote_includes = quote_includes,
system_includes = system_includes,
framework_includes = framework_includes,
@@ -466,6 +473,7 @@ def _merge_compilation_contexts(*, compilation_context = EMPTY_COMPILATION_CONTE
transitive = [dep.defines for dep in all_deps] + [compilation_context.defines],
),
local_defines = compilation_context.local_defines,
+ local_includes = compilation_context.local_includes,
headers = depset(
direct = compilation_context.headers.to_list(),
transitive = [dep.headers for dep in all_deps],
diff --git a/cc/private/compile/cc_compilation_helper.bzl b/cc/private/compile/cc_compilation_helper.bzl
index 01aa11fc..2520bfe3 100644
--- a/cc/private/compile/cc_compilation_helper.bzl
+++ b/cc/private/compile/cc_compilation_helper.bzl
@@ -356,6 +356,7 @@ def _init_cc_compilation_context(
framework_include_dirs,
system_include_dirs,
include_dirs,
+ local_includes,
feature_configuration,
public_headers_artifacts,
include_prefix,
@@ -559,6 +560,7 @@ def _init_cc_compilation_context(
external_includes = depset(external_include_dirs),
system_includes = depset(system_include_dirs_for_context),
includes = depset(include_dirs_for_context),
+ local_includes = depset(local_includes),
virtual_to_original_headers = virtual_to_original_headers,
dependent_cc_compilation_contexts = dependent_cc_compilation_contexts,
non_code_inputs = additional_inputs,
diff --git a/cc/private/compile/compile.bzl b/cc/private/compile/compile.bzl
index d958cc90..47229428 100644
--- a/cc/private/compile/compile.bzl
+++ b/cc/private/compile/compile.bzl
@@ -103,6 +103,7 @@ def compile(
textual_hdrs = [],
additional_exported_hdrs = [],
includes = [],
+ local_includes = [],
# TODO(b/396122076): seems unused; double-check and remove
loose_includes = None, # buildifier: disable=unused-variable
quote_includes = [],
@@ -292,6 +293,7 @@ def compile(
framework_include_dirs = framework_includes,
system_include_dirs = system_includes,
include_dirs = includes,
+ local_includes = local_includes,
feature_configuration = feature_configuration,
public_headers_artifacts = public_hdrs_artifacts,
include_prefix = include_prefix,
diff --git a/cc/private/compile/compile_build_variables.bzl b/cc/private/compile/compile_build_variables.bzl
index 4867fcad..98b78f21 100644
--- a/cc/private/compile/compile_build_variables.bzl
+++ b/cc/private/compile/compile_build_variables.bzl
@@ -93,6 +93,7 @@ def create_compile_variables(
output_file = None,
user_compile_flags = None,
includes = None,
+ local_includes = None,
include_directories = None,
quote_include_directories = None,
system_include_directories = None,
@@ -123,6 +124,7 @@ def create_compile_variables(
includes: paths to headers that should be included using -include
user_compile_flags: List of additional compilation flags (copts).
include_directories: Depset of include directories.
+ local_includes: Depset of local include directories.
quote_include_directories: Depset of quote include directories.
system_include_directories: Depset of system include directories.
framework_include_directories: Depset of framework include directories.
@@ -158,6 +160,7 @@ def create_compile_variables(
fdo_build_stamp = _get_fdo_build_stamp(cpp_configuration, fdo_context, feature_configuration),
variables_extension = variables_extension,
includes = includes or [],
+ local_includes = local_includes or depset(),
include_dirs = include_directories or depset(),
quote_include_dirs = quote_include_directories or depset(),
system_include_dirs = system_include_directories or depset(),
@@ -198,6 +201,7 @@ def setup_common_compile_build_variables(
fdo_build_stamp = _get_fdo_build_stamp(cpp_configuration, fdo_context, feature_configuration),
variables_extension = variables_extension,
include_dirs = cc_compilation_context.includes,
+ local_includes = cc_compilation_context.local_includes,
quote_include_dirs = cc_compilation_context.quote_includes,
system_include_dirs = cc_compilation_context.system_includes,
framework_include_dirs = cc_compilation_context.framework_includes,
@@ -216,6 +220,7 @@ def _setup_common_compile_build_variables_internal(
variables_extension = [], # [dict{str,object}]
additional_build_variables = {}, # dict{str,str}
include_dirs = depset(),
+ local_includes = depset(),
quote_include_dirs = depset(),
system_include_dirs = depset(),
framework_include_dirs = depset(),
@@ -226,7 +231,7 @@ def _setup_common_compile_build_variables_internal(
if feature_configuration.is_enabled("use_header_modules"):
result[_VARS.MODULE_FILES] = []
- result[_VARS.INCLUDE_PATHS] = include_dirs
+ result[_VARS.INCLUDE_PATHS] = depset(transitive = [include_dirs, local_includes])
result[_VARS.QUOTE_INCLUDE_PATHS] = quote_include_dirs
result[_VARS.SYSTEM_INCLUDE_PATHS] = system_include_dirs
if includes:
diff --git a/cc/private/rules_impl/attrs.bzl b/cc/private/rules_impl/attrs.bzl
index 876074cd..cff6d972 100644
--- a/cc/private/rules_impl/attrs.bzl
+++ b/cc/private/rules_impl/attrs.bzl
@@ -144,6 +144,20 @@ very careful, since this may have far-reaching effects. When in doubt, add
The added include paths will include generated files as well as
files in the source tree.
-I path_to_package/include_entry.
+
+Unlike INCLUDES, these flags are added for
+this target and not added to every target that depends on it.
+
+The added include paths will include generated files as well as
+files in the source tree.
"""),
"defines": attr.string_list(doc = """
List of defines to add to the compile line of this and all dependent targets.
diff --git a/cc/private/rules_impl/cc_binary.bzl b/cc/private/rules_impl/cc_binary.bzl
index 5dccd2c9..8957971b 100644
--- a/cc/private/rules_impl/cc_binary.bzl
+++ b/cc/private/rules_impl/cc_binary.bzl
@@ -540,6 +540,7 @@ def cc_binary_impl(ctx, additional_linkopts, force_linkstatic = False):
defines = cc_helper.defines(ctx, additional_make_variable_substitutions),
local_defines = cc_helper.local_defines(ctx, additional_make_variable_substitutions) + cc_helper.get_local_defines_for_runfiles_lookup(ctx, ctx.attr.deps),
includes = cc_helper.include_dirs(ctx, additional_make_variable_substitutions),
+ local_includes = cc_helper.include_dirs(ctx, additional_make_variable_substitutions, attr = "local_includes"),
private_hdrs = cc_helper.get_private_hdrs(ctx),
public_hdrs = cc_helper.get_public_hdrs(ctx),
copts_filter = cc_helper.copts_filter(ctx, additional_make_variable_substitutions),
diff --git a/cc/private/rules_impl/cc_library.bzl b/cc/private/rules_impl/cc_library.bzl
index a6699594..2f849bff 100755
--- a/cc/private/rules_impl/cc_library.bzl
+++ b/cc/private/rules_impl/cc_library.bzl
@@ -64,6 +64,7 @@ def _cc_library_impl(ctx):
defines = cc_helper.defines(ctx, additional_make_variable_substitutions),
local_defines = cc_helper.local_defines(ctx, additional_make_variable_substitutions) + cc_helper.get_local_defines_for_runfiles_lookup(ctx, ctx.attr.deps + ctx.attr.implementation_deps),
includes = cc_helper.include_dirs(ctx, additional_make_variable_substitutions),
+ local_includes = cc_helper.include_dirs(ctx, additional_make_variable_substitutions, attr = "local_includes"),
copts_filter = cc_helper.copts_filter(ctx, additional_make_variable_substitutions),
purpose = "cc_library-compile",
srcs = cc_helper.get_srcs(ctx),
diff --git a/tests/local_includes/BUILD b/tests/local_includes/BUILD
new file mode 100644
index 00000000..90f7db33
--- /dev/null
+++ b/tests/local_includes/BUILD
@@ -0,0 +1,47 @@
+# Copyright 2025 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("//cc:cc_binary.bzl", "cc_binary")
+load("//cc:cc_library.bzl", "cc_library")
+
+licenses(["notice"])
+
+cc_library(
+ name = "lib",
+ srcs = [
+ "lib/lib.c",
+ "lib/private/private.c",
+ "lib/private/private.h",
+ ],
+ hdrs = ["lib/include/public.h"],
+ includes = [
+ "lib/include",
+ ],
+ local_includes = [
+ "lib/private",
+ ],
+)
+
+cc_binary(
+ name = "bin",
+ srcs = [
+ "binary.c",
+ "binary_helper.c",
+ "private/binary_helper.h",
+ ],
+ local_includes = [
+ "private",
+ ],
+ deps = [":lib"],
+)
diff --git a/tests/local_includes/binary.c b/tests/local_includes/binary.c
new file mode 100644
index 00000000..a78687b6
--- /dev/null
+++ b/tests/local_includes/binary.c
@@ -0,0 +1,11 @@
+#include "binary_helper.h"
+#include "public.h"
+
+#if __has_include("private.h")
+#error "private.h should not be on the include path"
+#endif
+
+
+int main() {
+ return foo() + helper();
+}
diff --git a/tests/local_includes/binary_helper.c b/tests/local_includes/binary_helper.c
new file mode 100644
index 00000000..97f18b58
--- /dev/null
+++ b/tests/local_includes/binary_helper.c
@@ -0,0 +1,3 @@
+int helper() {
+ return 42;
+}
diff --git a/tests/local_includes/lib/include/public.h b/tests/local_includes/lib/include/public.h
new file mode 100644
index 00000000..5d5f8f0c
--- /dev/null
+++ b/tests/local_includes/lib/include/public.h
@@ -0,0 +1 @@
+int foo();
diff --git a/tests/local_includes/lib/lib.c b/tests/local_includes/lib/lib.c
new file mode 100644
index 00000000..602f9fff
--- /dev/null
+++ b/tests/local_includes/lib/lib.c
@@ -0,0 +1,5 @@
+#include "private.h"
+
+int foo() {
+ return bar();
+}
diff --git a/tests/local_includes/lib/private/private.c b/tests/local_includes/lib/private/private.c
new file mode 100644
index 00000000..a4957daa
--- /dev/null
+++ b/tests/local_includes/lib/private/private.c
@@ -0,0 +1,3 @@
+int bar() {
+ return 0;
+}
diff --git a/tests/local_includes/lib/private/private.h b/tests/local_includes/lib/private/private.h
new file mode 100644
index 00000000..c362dd03
--- /dev/null
+++ b/tests/local_includes/lib/private/private.h
@@ -0,0 +1 @@
+int bar();
diff --git a/tests/local_includes/private/binary_helper.h b/tests/local_includes/private/binary_helper.h
new file mode 100644
index 00000000..7b89ca58
--- /dev/null
+++ b/tests/local_includes/private/binary_helper.h
@@ -0,0 +1 @@
+int helper();