Add a 'local_defines' attribute to cc_* rules

Currently, the only way to specify non-transitive defines is through the 'copts' attribute, which is not platform independent. the cc_*.local_defines has the task to pass define values to the compile command line to the given cc_* rule, but not to its dependents.

Fixes #7939

RELNOTES: cc_* rules support non-transitive defines through a 'local_defines' attribute.
PiperOrigin-RevId: 262315252
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcModule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcModule.java
index 289690f..0f0a5ff 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcModule.java
@@ -76,6 +76,7 @@
       SkylarkList<String> systemIncludes,
       SkylarkList<String> frameworkIncludes,
       SkylarkList<String> defines,
+      SkylarkList<String> localDefines,
       SkylarkList<String> userCompileFlags,
       SkylarkList<CcCompilationContext> ccCompilationContexts,
       String name,
@@ -96,6 +97,7 @@
         systemIncludes,
         frameworkIncludes,
         defines,
+        localDefines,
         userCompileFlags,
         ccCompilationContexts,
         name,
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppRuleClasses.java
index c0e7ead..80fa950 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppRuleClasses.java
@@ -168,14 +168,21 @@
           Subject to <a href="${link make-variables}">"Make" variable</a> substitution and
           <a href="${link common-definitions#sh-tokenization}">Bourne shell tokenization</a>.
           Each string, which must consist of a single Bourne shell token,
-          is prepended with <code>-D</code> (or <code>/D</code> on Windows) and added to
-          <code>COPTS</code>.
-          Unlike <a href="#cc_binary.copts"><code>copts</code></a>, these flags are added for the
-          target and every rule that depends on it!  Be very careful, since this may have
-          far-reaching effects.  When in doubt, add "-D" (or <code>/D</code> on Windows) flags to
-          <a href="#cc_binary.copts"><code>copts</code></a> instead.
+          is prepended with <code>-D</code> and added to the compile command line to this target,
+          as well as to every rule that depends on it. Be very careful, since this may have
+          far-reaching effects.  When in doubt, add define values to
+          <a href="#cc_binary.local_defines"><code>local_defines</code></a> instead.
           <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
           .add(attr("defines", STRING_LIST))
+          /*<!-- #BLAZE_RULE($cc_decl_rule).ATTRIBUTE(local_defines) -->
+          List of defines to add to the compile line.
+          Subject to <a href="${link make-variables}">"Make" variable</a> substitution and
+          <a href="${link common-definitions#sh-tokenization}">Bourne shell tokenization</a>.
+          Each string, which must consist of a single Bourne shell token,
+          is prepended with <code>-D</code> and added to the compile command line for this target,
+          but not to its dependents.
+          <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+          .add(attr("local_defines", STRING_LIST))
           /*<!-- #BLAZE_RULE($cc_decl_rule).ATTRIBUTE(includes) -->
           List of include dirs to be added to the compile line.
           <p>
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
index a507574..795aae3 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
@@ -533,6 +533,7 @@
   }
 
   private static final String DEFINES_ATTRIBUTE = "defines";
+  private static final String LOCAL_DEFINES_ATTRIBUTE = "local_defines";
 
   /**
    * Returns a list of define tokens from "defines" attribute.
@@ -543,22 +544,40 @@
    * <p>But we require that the "defines" attribute consists of a single token.
    */
   public List<String> getDefines() {
+    return getDefinesFromAttribute(DEFINES_ATTRIBUTE);
+  }
+
+  /**
+   * Returns a list of define tokens from "local_defines" attribute.
+   *
+   * <p>We tokenize the "local_defines" attribute, to ensure that the handling of quotes and
+   * backslash escapes is consistent Bazel's treatment of the "copts" attribute.
+   *
+   * <p>But we require that the "local_defines" attribute consists of a single token.
+   */
+  public List<String> getNonTransitiveDefines() {
+    return getDefinesFromAttribute(LOCAL_DEFINES_ATTRIBUTE);
+  }
+
+  private List<String> getDefinesFromAttribute(String attr) {
     List<String> defines = new ArrayList<>();
-    for (String define : ruleContext.getExpander().list(DEFINES_ATTRIBUTE)) {
+    for (String define : ruleContext.getExpander().list(attr)) {
       List<String> tokens = new ArrayList<>();
       try {
         ShellUtils.tokenize(tokens, define);
         if (tokens.size() == 1) {
           defines.add(tokens.get(0));
         } else if (tokens.isEmpty()) {
-          ruleContext.attributeError(DEFINES_ATTRIBUTE, "empty definition not allowed");
+          ruleContext.attributeError(attr, "empty definition not allowed");
         } else {
-          ruleContext.attributeError(DEFINES_ATTRIBUTE,
-              "definition contains too many tokens (found " + tokens.size()
-              + ", expecting exactly one)");
+          ruleContext.attributeError(
+              attr,
+              String.format(
+                  "definition contains too many tokens (found %d, expecting exactly one)",
+                  tokens.size()));
         }
       } catch (ShellUtils.TokenizationException e) {
-        ruleContext.attributeError(DEFINES_ATTRIBUTE, e.getMessage());
+        ruleContext.attributeError(attr, e.getMessage());
       }
     }
     return defines;
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationContext.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationContext.java
index 0e84756..c22e183 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationContext.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationContext.java
@@ -138,6 +138,12 @@
   }
 
   @Override
+  public SkylarkNestedSet getSkylarkNonTransitiveDefines() {
+    return SkylarkNestedSet.of(
+        String.class, NestedSetBuilder.wrap(Order.STABLE_ORDER, getNonTransitiveDefines()));
+  }
+
+  @Override
   public SkylarkNestedSet getSkylarkHeaders() {
     return SkylarkNestedSet.of(Artifact.class, getDeclaredIncludeSrcs());
   }
@@ -400,15 +406,22 @@
   }
 
   /**
-   * Returns the set of defines needed to compile this target (possibly empty
-   * but never null). This includes definitions from the transitive deps closure
-   * for the target. The order of the returned collection is deterministic.
+   * Returns the set of defines needed to compile this target. This includes definitions from the
+   * transitive deps closure for the target. The order of the returned collection is deterministic.
    */
   public ImmutableList<String> getDefines() {
     return commandLineCcCompilationContext.defines;
   }
 
   /**
+   * Returns the set of defines needed to compile this target. This doesn't include definitions from
+   * the transitive deps closure for the target.
+   */
+  ImmutableList<String> getNonTransitiveDefines() {
+    return commandLineCcCompilationContext.localDefines;
+  }
+
+  /**
    * Returns a {@code CcCompilationContext} that is based on a given {@code CcCompilationContext}
    * but returns empty sets for {@link #getDeclaredIncludeDirs()}.
    */
@@ -481,18 +494,21 @@
     private final ImmutableList<PathFragment> systemIncludeDirs;
     private final ImmutableList<PathFragment> frameworkIncludeDirs;
     private final ImmutableList<String> defines;
+    private final ImmutableList<String> localDefines;
 
     CommandLineCcCompilationContext(
         ImmutableList<PathFragment> includeDirs,
         ImmutableList<PathFragment> quoteIncludeDirs,
         ImmutableList<PathFragment> systemIncludeDirs,
         ImmutableList<PathFragment> frameworkIncludeDirs,
-        ImmutableList<String> defines) {
+        ImmutableList<String> defines,
+        ImmutableList<String> localDefines) {
       this.includeDirs = includeDirs;
       this.quoteIncludeDirs = quoteIncludeDirs;
       this.systemIncludeDirs = systemIncludeDirs;
       this.frameworkIncludeDirs = frameworkIncludeDirs;
       this.defines = defines;
+      this.localDefines = localDefines;
     }
   }
 
@@ -524,6 +540,7 @@
     private final NestedSetBuilder<Artifact> transitivePicModules = NestedSetBuilder.stableOrder();
     private final Set<Artifact> directModuleMaps = new LinkedHashSet<>();
     private final Set<String> defines = new LinkedHashSet<>();
+    private final Set<String> localDefines = new LinkedHashSet<>();
     private CppModuleMap cppModuleMap;
     private CppModuleMap verificationModuleMap;
     private boolean propagateModuleMapAsActionInput = true;
@@ -733,6 +750,12 @@
       return this;
     }
 
+    /** Adds multiple non-transitive defines. */
+    public Builder addNonTransitiveDefines(Iterable<String> defines) {
+      Iterables.addAll(this.localDefines, defines);
+      return this;
+    }
+
     /** Sets the C++ module map. */
     public Builder setCppModuleMap(CppModuleMap cppModuleMap) {
       this.cppModuleMap = cppModuleMap;
@@ -806,7 +829,8 @@
               ImmutableList.copyOf(quoteIncludeDirs),
               ImmutableList.copyOf(systemIncludeDirs),
               ImmutableList.copyOf(frameworkIncludeDirs),
-              ImmutableList.copyOf(defines)),
+              ImmutableList.copyOf(defines),
+              ImmutableList.copyOf(localDefines)),
           // TODO(b/110873917): We don't have the middle man compilation prerequisite, therefore, we
           // use the compilation prerequisites as they were passed to the builder, i.e. we use every
           // header instead of a middle man.
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
index 14a2fb5..c281f6e 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
@@ -237,6 +237,7 @@
   private ImmutableList<String> copts = ImmutableList.of();
   private CoptsFilter coptsFilter = CoptsFilter.alwaysPasses();
   private final Set<String> defines = new LinkedHashSet<>();
+  private final Set<String> localDefines = new LinkedHashSet<>();
   private final List<CcCompilationContext> ccCompilationContexts = new ArrayList<>();
   private Set<PathFragment> looseIncludeDirs = ImmutableSet.of();
   private final List<PathFragment> systemIncludeDirs = new ArrayList<>();
@@ -334,6 +335,7 @@
 
     setCopts(Iterables.concat(common.getCopts(), additionalCopts));
     addDefines(common.getDefines());
+    addNonTransitiveDefines(common.getNonTransitiveDefines());
     setLooseIncludeDirs(common.getLooseIncludeDirs());
     addSystemIncludeDirs(common.getSystemIncludeDirs());
     setCoptsFilter(common.getCoptsFilter());
@@ -535,12 +537,24 @@
     this.coptsFilter = Preconditions.checkNotNull(coptsFilter);
   }
 
-  /** Adds the given defines to the compiler command line. */
+  /**
+   * Adds the given defines to the compiler command line of this target as well as its dependent
+   * targets.
+   */
   public CcCompilationHelper addDefines(Iterable<String> defines) {
     Iterables.addAll(this.defines, defines);
     return this;
   }
 
+  /**
+   * Adds the given defines to the compiler command line. These defines are not propagated
+   * transitively to the dependent targets.
+   */
+  public CcCompilationHelper addNonTransitiveDefines(Iterable<String> defines) {
+    Iterables.addAll(this.localDefines, defines);
+    return this;
+  }
+
   /** For adding CC compilation infos that affect compilation, e.g: from dependencies. */
   public CcCompilationHelper addCcCompilationContexts(
       Iterable<CcCompilationContext> ccCompilationContexts) {
@@ -918,6 +932,8 @@
     // But defines come after those inherited from deps.
     ccCompilationContextBuilder.addDefines(defines);
 
+    ccCompilationContextBuilder.addNonTransitiveDefines(localDefines);
+
     // There are no ordering constraints for declared include dirs/srcs.
     ccCompilationContextBuilder.addDeclaredIncludeSrcs(publicHeaders.getHeaders());
     ccCompilationContextBuilder.addDeclaredIncludeSrcs(publicTextualHeaders);
@@ -1477,7 +1493,8 @@
           ccCompilationContext.getQuoteIncludeDirs(),
           ccCompilationContext.getSystemIncludeDirs(),
           ccCompilationContext.getFrameworkIncludeDirs(),
-          ccCompilationContext.getDefines());
+          ccCompilationContext.getDefines(),
+          ccCompilationContext.getNonTransitiveDefines());
 
       if (usePrebuiltParent) {
         parent = buildVariables.build();
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java
index da7cb04..fb8a16f 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java
@@ -250,7 +250,8 @@
         asStringNestedSet(quoteIncludeDirs),
         asStringNestedSet(systemIncludeDirs),
         asStringNestedSet(frameworkIncludeDirs),
-        asStringNestedSet(defines));
+        asStringNestedSet(defines),
+        NestedSetBuilder.emptySet(Order.STABLE_ORDER));
   }
 
   @Override
@@ -508,7 +509,8 @@
       Object includes,
       Object quoteIncludes,
       Object frameworkIncludes,
-      Object defines)
+      Object defines,
+      Object localDefines)
       throws EvalException {
     CcCompilationContext.Builder ccCompilationContext =
         CcCompilationContext.builder(
@@ -536,6 +538,8 @@
             .map(x -> PathFragment.create(x))
             .collect(ImmutableList.toImmutableList()));
     ccCompilationContext.addDefines(toNestedSetOfStrings(defines, "defines").getSet(String.class));
+    ccCompilationContext.addNonTransitiveDefines(
+        toNestedSetOfArtifacts(localDefines, "local_defines").getSet(String.class));
     return ccCompilationContext.build();
   }
 
@@ -1513,6 +1517,7 @@
       SkylarkList<String> systemIncludes,
       SkylarkList<String> frameworkIncludes,
       SkylarkList<String> defines,
+      SkylarkList<String> localDefines,
       SkylarkList<String> userCompileFlags,
       SkylarkList<CcCompilationContext> ccCompilationContexts,
       String name,
@@ -1585,6 +1590,7 @@
                     .map(PathFragment::create)
                     .collect(ImmutableList.toImmutableList()))
             .addDefines(defines)
+            .addNonTransitiveDefines(localDefines)
             .setCopts(userCompileFlags)
             .addAdditionalCompilationInputs(headersForClifDoNotUseThisParam)
             .addAditionalIncludeScanningRoots(headersForClifDoNotUseThisParam);
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CompileBuildVariables.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CompileBuildVariables.java
index bd4b054..9307bd3 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CompileBuildVariables.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CompileBuildVariables.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.events.Location;
@@ -135,7 +136,8 @@
       Iterable<PathFragment> quoteIncludeDirs,
       Iterable<PathFragment> systemIncludeDirs,
       Iterable<PathFragment> frameworkIncludeDirs,
-      Iterable<String> defines) {
+      Iterable<String> defines,
+      Iterable<String> localDefines) {
     try {
       if (usePic
           && !featureConfiguration.isEnabled(CppRuleClasses.PIC)
@@ -165,7 +167,8 @@
           getSafePathStrings(quoteIncludeDirs),
           getSafePathStrings(systemIncludeDirs),
           getSafePathStrings(frameworkIncludeDirs),
-          defines);
+          defines,
+          localDefines);
     } catch (EvalException e) {
       ruleErrorConsumer.ruleError(e.getMessage());
       return CcToolchainVariables.EMPTY;
@@ -197,7 +200,8 @@
       Iterable<String> quoteIncludeDirs,
       Iterable<String> systemIncludeDirs,
       Iterable<String> frameworkIncludeDirs,
-      Iterable<String> defines)
+      Iterable<String> defines,
+      Iterable<String> localDefines)
       throws EvalException {
     if (usePic
         && !featureConfiguration.isEnabled(CppRuleClasses.PIC)
@@ -227,7 +231,8 @@
         quoteIncludeDirs,
         systemIncludeDirs,
         frameworkIncludeDirs,
-        defines);
+        defines,
+        localDefines);
   }
 
   private static CcToolchainVariables setupVariables(
@@ -253,7 +258,8 @@
       Iterable<String> quoteIncludeDirs,
       Iterable<String> systemIncludeDirs,
       Iterable<String> frameworkIncludeDirs,
-      Iterable<String> defines) {
+      Iterable<String> defines,
+      Iterable<String> localDefines) {
     CcToolchainVariables.Builder buildVariables = CcToolchainVariables.builder(parent);
     setupCommonVariablesInternal(
         buildVariables,
@@ -268,7 +274,8 @@
         quoteIncludeDirs,
         systemIncludeDirs,
         frameworkIncludeDirs,
-        defines);
+        defines,
+        localDefines);
     setupSpecificVariables(
         buildVariables,
         sourceFile,
@@ -354,7 +361,8 @@
       Iterable<PathFragment> quoteIncludeDirs,
       Iterable<PathFragment> systemIncludeDirs,
       Iterable<PathFragment> frameworkIncludeDirs,
-      Iterable<String> defines) {
+      Iterable<String> defines,
+      Iterable<String> localDefines) {
     setupCommonVariablesInternal(
         buildVariables,
         featureConfiguration,
@@ -368,7 +376,8 @@
         getSafePathStrings(quoteIncludeDirs),
         getSafePathStrings(systemIncludeDirs),
         getSafePathStrings(frameworkIncludeDirs),
-        defines);
+        defines,
+        localDefines);
   }
 
   private static void setupCommonVariablesInternal(
@@ -384,13 +393,15 @@
       Iterable<String> quoteIncludeDirs,
       Iterable<String> systemIncludeDirs,
       Iterable<String> frameworkIncludeDirs,
-      Iterable<String> defines) {
+      Iterable<String> defines,
+      Iterable<String> localDefines) {
     Preconditions.checkNotNull(directModuleMaps);
     Preconditions.checkNotNull(includeDirs);
     Preconditions.checkNotNull(quoteIncludeDirs);
     Preconditions.checkNotNull(systemIncludeDirs);
     Preconditions.checkNotNull(frameworkIncludeDirs);
     Preconditions.checkNotNull(defines);
+    Preconditions.checkNotNull(localDefines);
 
     if (featureConfiguration.isEnabled(CppRuleClasses.MODULE_MAPS) && cppModuleMap != null) {
       // If the feature is enabled and cppModuleMap is null, we are about to fail during analysis
@@ -427,12 +438,14 @@
       allDefines =
           ImmutableList.<String>builder()
               .addAll(defines)
+              .addAll(localDefines)
               .add(CppConfiguration.FDO_STAMP_MACRO + "=\"" + fdoStamp + "\"")
               .build();
     } else {
-      allDefines = defines;
+      allDefines = Iterables.concat(defines, localDefines);
     }
 
+
     buildVariables.addStringSequenceVariable(PREPROCESSOR_DEFINES.getVariableName(), allDefines);
 
     buildVariables.addAllStringVariables(additionalBuildVariables);
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkstampCompileHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkstampCompileHelper.java
index 86c6808..f9b5eec 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkstampCompileHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkstampCompileHelper.java
@@ -185,6 +185,7 @@
             additionalLinkstampDefines,
             ccToolchainProvider,
             fdoBuildStamp,
-            codeCoverageEnabled));
+            codeCoverageEnabled),
+        /* localDefines= */ ImmutableList.of());
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/BazelCcModuleApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/BazelCcModuleApi.java
index 6ccac29..33bebdb 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/BazelCcModuleApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/BazelCcModuleApi.java
@@ -156,7 +156,18 @@
             type = SkylarkList.class),
         @Param(
             name = "defines",
-            doc = "Set of defines needed to compile this target. Each define is a string.",
+            doc =
+                "Set of defines needed to compile this target. Each define is a string. Propagated"
+                    + " to dependents transitively.",
+            positional = false,
+            named = true,
+            defaultValue = "[]",
+            type = SkylarkList.class),
+        @Param(
+            name = "local_defines",
+            doc =
+                "Set of defines needed to compile this target. Each define is a string. Not"
+                    + " propagated to dependents transitively.",
             positional = false,
             named = true,
             defaultValue = "[]",
@@ -211,6 +222,7 @@
       SkylarkList<String> systemIncludes,
       SkylarkList<String> frameworkIncludes,
       SkylarkList<String> defines,
+      SkylarkList<String> localDefines,
       SkylarkList<String> userCompileFlags,
       SkylarkList<CompilationContextT> ccCompilationContexts,
       String name,
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/CcCompilationContextApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/CcCompilationContextApi.java
index 80115c8..fb0df89 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/CcCompilationContextApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/CcCompilationContextApi.java
@@ -31,11 +31,21 @@
 public interface CcCompilationContextApi {
   @SkylarkCallable(
       name = "defines",
-      doc = "Returns the set of defines needed to compile this target. Each define is a string.",
+      doc =
+          "Returns the set of defines needed to compile this target. Each define is a string."
+              + " These values are propagated to the target's transitive dependencies.",
       structField = true)
   SkylarkNestedSet getSkylarkDefines();
 
   @SkylarkCallable(
+      name = "local_defines",
+      doc =
+          "Returns the set of defines needed to compile this target. Each define is a string."
+              + " These values are not propagated to the target's transitive dependencies.",
+      structField = true)
+  SkylarkNestedSet getSkylarkNonTransitiveDefines();
+
+  @SkylarkCallable(
       name = "headers",
       doc = "Returns the set of headers needed to compile this target.",
       structField = true)
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/CcModuleApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/CcModuleApi.java
index 41bddc3..97196b1 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/CcModuleApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/cpp/CcModuleApi.java
@@ -701,11 +701,22 @@
             type = Object.class),
         @Param(
             name = "defines",
-            doc = "Set of defines needed to compile this target. Each define is a string",
+            doc =
+                "Set of defines needed to compile this target. Each define is a string. Propagated"
+                    + " transitively to dependents.",
             positional = false,
             named = true,
             defaultValue = "unbound",
-            type = Object.class)
+            type = Object.class),
+        @Param(
+            name = "local_defines",
+            doc =
+                "Set of defines needed to compile this target. Each define is a string. Not"
+                    + " propagated transitively to dependents.",
+            positional = false,
+            named = true,
+            defaultValue = "unbound",
+            type = Object.class),
       })
   CompilationContextT createCcCompilationContext(
       Object headers,
@@ -713,7 +724,8 @@
       Object includes,
       Object quoteIncludes,
       Object frameworkIncludes,
-      Object defines)
+      Object defines,
+      Object localDefines)
       throws EvalException;
 
   // TODO(b/65151735): Remove when cc_flags is entirely set from features.
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/cpp/FakeCcModule.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/cpp/FakeCcModule.java
index c37b315..d156042 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/cpp/FakeCcModule.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/cpp/FakeCcModule.java
@@ -175,7 +175,8 @@
       Object includes,
       Object quoteIncludes,
       Object frameworkIncludes,
-      Object defines)
+      Object defines,
+      Object localDefines)
       throws EvalException {
     return null;
   }
@@ -201,6 +202,7 @@
       SkylarkList<String> includes,
       SkylarkList<String> quoteIncludes,
       SkylarkList<String> defines,
+      SkylarkList<String> localDefines,
       SkylarkList<String> systemIncludes,
       SkylarkList<String> frameworkIncludes,
       SkylarkList<String> userCompileFlags,
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
index a0cf9bd..67693b9 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
@@ -150,6 +150,18 @@
   }
 
   @Test
+  public void testLocalDefinesAndMakeVariables() throws Exception {
+    ConfiguredTarget l =
+        scratchConfiguredTarget(
+            "a",
+            "l",
+            "cc_library(name='l', srcs=['l.cc'], local_defines=['V=$(FOO)'], toolchains=[':v'])",
+            "make_variable_tester(name='v', variables={'FOO': 'BAR'})");
+    assertThat(l.get(CcInfo.PROVIDER).getCcCompilationContext().getNonTransitiveDefines())
+        .contains("V=BAR");
+  }
+
+  @Test
   public void testMisconfiguredCrosstoolRaisesErrorWhenLinking() throws Exception {
     AnalysisMock.get()
         .ccSupport()
@@ -1179,6 +1191,30 @@
     assertContainsSublist(action.getCompilerOptions(), ImmutableList.of("-DBAR", "-DFOO"));
   }
 
+  @Test
+  public void testLocalDefinesNotPassedTransitively() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        "cc_library(",
+        "    name = 'bar',",
+        "    defines = ['TRANSITIVE_BAR'],",
+        "    local_defines = ['LOCAL_BAR'],",
+        ")",
+        "cc_library(",
+        "    name = 'foo',",
+        "    srcs = ['foo.cc'],",
+        "    defines = ['TRANSITIVE_FOO'],",
+        "    local_defines = ['LOCAL_FOO'],",
+        "    deps = [':bar'],",
+        ")");
+    CppCompileAction action = getCppCompileAction("//foo");
+    // Inherited defines come first.
+    assertContainsSublist(
+        action.getCompilerOptions(),
+        ImmutableList.of("-DTRANSITIVE_BAR", "-DTRANSITIVE_FOO", "-DLOCAL_FOO"));
+    assertThat(action.getCompilerOptions()).doesNotContain("-DLOCAL_BAR");
+  }
+
   // Regression test - setting "-shared" caused an exception when computing the link command.
   @Test
   public void testLinkOptsNotPassedToStaticLink() throws Exception {
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/SkylarkCcCommonTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/SkylarkCcCommonTest.java
index 0e68213..2c8d61a 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/SkylarkCcCommonTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/SkylarkCcCommonTest.java
@@ -1062,6 +1062,7 @@
         "    hdrs = ['dep1.h'],",
         "    includes = ['dep1/baz'],",
         "    defines = ['DEP1'],",
+        "    local_defines = ['LOCALDEP1'],",
         ")",
         "cc_library(",
         "    name = 'dep2',",
@@ -1108,6 +1109,7 @@
         "    '_quote_include': attr.string(default='quux/abc'),",
         "    '_framework_include': attr.string(default='fuux/fgh'),",
         "    '_define': attr.string(default='MYDEFINE'),",
+        "    '_local_define': attr.string(default='MYLOCALDEFINE'),",
         "    '_deps': attr.label_list(default=['//a:dep1', '//a:dep2'])",
         "  },",
         "  fragments = ['cpp'],",
@@ -1135,6 +1137,7 @@
     List<String> mergedDefines =
         ((SkylarkNestedSet) myInfo.getValue("merged_defines")).getSet(String.class).toList();
     assertThat(mergedDefines).containsAtLeast("MYDEFINE", "DEP1", "DEP2");
+    assertThat(mergedDefines).doesNotContain("LOCALDEP1");
 
     List<String> mergedSystemIncludes =
         ((SkylarkNestedSet) myInfo.getValue("merged_system_includes"))