Support link stamping in `apple_binary` rules

Add a `stamp` attribute to the `apple_binary` rule, and give it the same
semantics as `stamp` in `cc_binary`: When building a binary with
`stamp = 1`, Bazel will build all code from all `linkstamp` attributes
on all the binary’s transitive dependencies and include that code in the
final link. This lets you embed information about the build into the
final link without busting cache.

RELNOTES:
`apple_binary` rules now accept the `stamp` attribute with the same
semantics that it has in `cc_binary` rules.
PiperOrigin-RevId: 316519143
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkAction.java
index a8c4e603..f788cab 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkAction.java
@@ -18,6 +18,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -54,6 +55,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
 import com.google.devtools.build.lib.exec.SpawnStrategyResolver;
+import com.google.devtools.build.lib.rules.cpp.CcLinkingContext.Linkstamp;
 import com.google.devtools.build.lib.rules.cpp.Link.LinkingMode;
 import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
 import com.google.devtools.build.lib.server.FailureDetails.CppLink;
@@ -125,7 +127,7 @@
   private final LibraryToLink interfaceOutputLibrary;
   private final ImmutableMap<String, String> toolchainEnv;
   private final ImmutableMap<String, String> executionRequirements;
-  private final ImmutableList<Artifact> linkstampObjects;
+  private final ImmutableMap<Linkstamp, Artifact> linkstamps;
 
   private final LinkCommandLine linkCommandLine;
 
@@ -172,7 +174,7 @@
       boolean fake,
       Iterable<Artifact> fakeLinkerInputArtifacts,
       boolean isLtoIndexing,
-      ImmutableList<Artifact> linkstampObjects,
+      ImmutableMap<Linkstamp, Artifact> linkstamps,
       LinkCommandLine linkCommandLine,
       ActionEnvironment env,
       ImmutableMap<String, String> toolchainEnv,
@@ -188,7 +190,7 @@
     this.fake = fake;
     this.fakeLinkerInputArtifacts = CollectionUtils.makeImmutable(fakeLinkerInputArtifacts);
     this.isLtoIndexing = isLtoIndexing;
-    this.linkstampObjects = linkstampObjects;
+    this.linkstamps = linkstamps;
     this.linkCommandLine = linkCommandLine;
     this.toolchainEnv = toolchainEnv;
     this.executionRequirements = executionRequirements;
@@ -306,7 +308,13 @@
    * provenance.
    */
   public ImmutableList<Artifact> getLinkstampObjects() {
-    return linkstampObjects;
+    return linkstamps.keySet().stream()
+        .map(CcLinkingContext.Linkstamp::getArtifact)
+        .collect(ImmutableList.toImmutableList());
+  }
+
+  public ImmutableCollection<Artifact> getLinkstampObjectFileInputs() {
+    return linkstamps.values();
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionBuilder.java
index 96dbc10..4f67bf8 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionBuilder.java
@@ -1079,9 +1079,7 @@
         fake,
         fakeLinkerInputArtifacts,
         isLtoIndexing,
-        linkstampMap.keySet().stream()
-            .map(CcLinkingContext.Linkstamp::getArtifact)
-            .collect(ImmutableList.toImmutableList()),
+        linkstampMap,
         linkCommandLine,
         configuration.getActionEnvironment(),
         toolchainEnv,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/AppleBinaryRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/AppleBinaryRule.java
index ffd8b21..c4c5aa0 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/AppleBinaryRule.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/AppleBinaryRule.java
@@ -17,6 +17,7 @@
 import static com.google.devtools.build.lib.packages.Attribute.attr;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL_KEYED_STRING_DICT;
+import static com.google.devtools.build.lib.packages.BuildType.TRISTATE;
 import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
 import static com.google.devtools.build.lib.packages.Type.STRING;
 
@@ -29,6 +30,7 @@
 import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
 import com.google.devtools.build.lib.packages.RuleClass;
 import com.google.devtools.build.lib.packages.StarlarkProviderIdentifier;
+import com.google.devtools.build.lib.packages.TriState;
 import com.google.devtools.build.lib.rules.apple.AppleConfiguration;
 import com.google.devtools.build.lib.rules.config.ConfigFeatureFlagProvider;
 import com.google.devtools.build.lib.rules.config.ConfigFeatureFlagTransitionFactory;
@@ -133,6 +135,21 @@
                 .allowedFileTypes()
                 .singleArtifact()
                 .aspect(objcProtoAspect))
+        /*<!-- #BLAZE_RULE(apple_binary).ATTRIBUTE(stamp) -->
+        Enable link stamping.
+        Whether to encode build information into the binary. Possible values:
+        <ul>
+          <li><code>stamp = 1</code>: Stamp the build information into the
+            binary. Stamped binaries are only rebuilt when their dependencies
+            change. Use this if there are tests that depend on the build
+            information.</li>
+          <li><code>stamp = 0</code>: Always replace build information by constant
+            values. This gives good build result caching.</li>
+          <li><code>stamp = -1</code>: Embedding of build information is controlled
+            by the <a href="../user-manual.html#flag--stamp">--[no]stamp</a> flag.</li>
+        </ul>
+        <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+        .add(attr("stamp", TRISTATE).value(TriState.AUTO))
         .add(
             attr("feature_flags", LABEL_KEYED_STRING_DICT)
                 .undocumented("the feature flag feature has not yet been launched")
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java
index 56bc435..4b63841 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java
@@ -82,6 +82,7 @@
 import com.google.devtools.build.lib.rules.cpp.CcCompilationHelper;
 import com.google.devtools.build.lib.rules.cpp.CcCompilationHelper.CompilationInfo;
 import com.google.devtools.build.lib.rules.cpp.CcCompilationOutputs;
+import com.google.devtools.build.lib.rules.cpp.CcLinkingContext;
 import com.google.devtools.build.lib.rules.cpp.CcLinkingHelper;
 import com.google.devtools.build.lib.rules.cpp.CcToolchain;
 import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.CollidingProvidesException;
@@ -1128,7 +1129,13 @@
             ? objcProvider.getObjcLibraries()
             : substituteJ2ObjcPrunedLibraries(objcProvider);
 
+    // Passing large numbers of inputs on the command line triggers a bug in Apple's Clang
+    // (b/29094356), so we'll create an input list manually and pass -filelist path/to/input/list.
+    // We can't populate this list yet--it needs to contain any linkstamp objects, which we won't
+    // know about until we actually create the CppLinkAction--but it needs to go into the
+    // CppLinkAction too, so create it now.
     Artifact inputFileList = intermediateArtifacts.linkerObjList();
+
     ImmutableSet<Artifact> forceLinkArtifacts = getForceLoadArtifacts(objcProvider);
 
     // Clang loads archives specified in filelists and also specified as -force_load twice,
@@ -1141,7 +1148,6 @@
                     objcProvider.get(IMPORTED_LIBRARY).toList(),
                     objcProvider.getCcLibraries()),
                 Predicates.not(Predicates.in(forceLinkArtifacts))));
-    registerObjFilelistAction(objFiles, inputFileList);
 
     LinkTargetType linkType =
         objcProvider.is(Flag.USES_CPP)
@@ -1162,7 +1168,7 @@
             .addVariableCategory(VariableCategory.EXECUTABLE_LINKING_VARIABLES);
 
     Artifact binaryToLink = getBinaryToLink();
-    CppLinkActionBuilder executableLinkAction =
+    CppLinkActionBuilder executableLinkActionBuilder =
         new CppLinkActionBuilder(
                 ruleContext,
                 ruleContext,
@@ -1199,13 +1205,13 @@
       extensionBuilder
           .setDsymSymbol(dsymSymbol)
           .addVariableCategory(VariableCategory.DSYM_VARIABLES);
-      executableLinkAction.addActionOutput(dsymSymbol);
+      executableLinkActionBuilder.addActionOutput(dsymSymbol);
     }
 
     if (objcConfiguration.generateLinkmap()) {
       Artifact linkmap = intermediateArtifacts.linkmap();
       extensionBuilder.setLinkmap(linkmap).addVariableCategory(VariableCategory.LINKMAP_VARIABLES);
-      executableLinkAction.addActionOutput(linkmap);
+      executableLinkActionBuilder.addActionOutput(linkmap);
     }
 
     if (appleConfiguration.getBitcodeMode() == AppleBitcodeMode.EMBEDDED) {
@@ -1213,11 +1219,29 @@
       extensionBuilder
           .setBitcodeSymbolMap(bitcodeSymbolMap)
           .addVariableCategory(VariableCategory.BITCODE_VARIABLES);
-      executableLinkAction.addActionOutput(bitcodeSymbolMap);
+      executableLinkActionBuilder.addActionOutput(bitcodeSymbolMap);
     }
 
-    executableLinkAction.addVariablesExtension(extensionBuilder.build());
-    ruleContext.registerAction(executableLinkAction.build());
+    executableLinkActionBuilder.addVariablesExtension(extensionBuilder.build());
+
+    for (CcLinkingContext context :
+        CppHelper.getLinkingContextsFromDeps(
+            ImmutableList.copyOf(ruleContext.getPrerequisites("deps", TransitionMode.TARGET)))) {
+      executableLinkActionBuilder.addLinkstamps(context.getLinkstamps().toList());
+    }
+
+    CppLinkAction executableLinkAction = executableLinkActionBuilder.build();
+
+    // Populate the input file list with both the compiled object files and any linkstamp object
+    // files.
+    registerObjFilelistAction(
+        ImmutableSet.<Artifact>builder()
+            .addAll(objFiles)
+            .addAll(executableLinkAction.getLinkstampObjectFileInputs())
+            .build(),
+        inputFileList);
+
+    ruleContext.registerAction(executableLinkAction);
 
     if (objcConfiguration.shouldStripBinary()) {
       registerBinaryStripAction(binaryToLink, getStrippingType(extraLinkArgs));
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/mock/osx_cc_toolchain_config.bzl b/src/test/java/com/google/devtools/build/lib/packages/util/mock/osx_cc_toolchain_config.bzl
index a11d1e4..ba74b89 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/mock/osx_cc_toolchain_config.bzl
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/mock/osx_cc_toolchain_config.bzl
@@ -3838,6 +3838,7 @@
     if (ctx.attr.cpu == "x64_windows"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch <architecture>"])],
@@ -3866,6 +3867,7 @@
     elif (ctx.attr.cpu == "ios_arm64"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch arm64"])],
@@ -3894,6 +3896,7 @@
     elif (ctx.attr.cpu == "tvos_arm64"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch arm64"])],
@@ -3922,6 +3925,7 @@
     elif (ctx.attr.cpu == "ios_armv7"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch armv7"])],
@@ -3950,6 +3954,7 @@
     elif (ctx.attr.cpu == "watchos_armv7k"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch armv7k"])],
@@ -3978,6 +3983,7 @@
     elif (ctx.attr.cpu == "ios_i386"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch i386"])],
@@ -4006,6 +4012,7 @@
     elif (ctx.attr.cpu == "watchos_i386"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch i386"])],
@@ -4034,6 +4041,7 @@
     elif (ctx.attr.cpu == "ios_x86_64"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch x86_64"])],
@@ -4062,6 +4070,7 @@
     elif (ctx.attr.cpu == "darwin_x86_64"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch x86_64"])],
@@ -4090,6 +4099,7 @@
     elif (ctx.attr.cpu == "tvos_x86_64"):
         linkstamp_compile_action = action_config(
             action_name = ACTION_NAMES.linkstamp_compile,
+            enabled = True,
             flag_sets = [
                 flag_set(
                     flag_groups = [flag_group(flags = ["-arch x86_64"])],
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
index 40ffbc6..62e166f 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
@@ -1159,4 +1159,40 @@
             "--sysroot=/usr/grte/v1")
         .inOrder();
   }
+
+  @Test
+  public void testExposesLinkstampSources() throws Exception {
+    scratch.file(
+        "x/BUILD",
+        "cc_binary(",
+        "  name = 'bin',",
+        "  deps = [':lib'],",
+        ")",
+        "cc_library(",
+        "  name = 'lib',",
+        "  linkstamp = 'linkstamp.cc',",
+        ")");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//x:bin");
+    CppLinkAction linkAction = (CppLinkAction) getGeneratingAction(configuredTarget, "x/bin");
+    assertThat(artifactsToStrings(linkAction.getLinkstampObjects()))
+        .containsExactly("src x/linkstamp.cc");
+  }
+
+  @Test
+  public void testExposesLinkstampObjects() throws Exception {
+    scratch.file(
+        "x/BUILD",
+        "cc_binary(",
+        "  name = 'bin',",
+        "  deps = [':lib'],",
+        ")",
+        "cc_library(",
+        "  name = 'lib',",
+        "  linkstamp = 'linkstamp.cc',",
+        ")");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//x:bin");
+    CppLinkAction linkAction = (CppLinkAction) getGeneratingAction(configuredTarget, "x/bin");
+    assertThat(artifactsToStrings(linkAction.getLinkstampObjectFileInputs()))
+        .containsExactly("bin x/_objs/bin/x/linkstamp.o");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/AppleBinaryTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/AppleBinaryTest.java
index 65d3e18..39a87ef 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/AppleBinaryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/AppleBinaryTest.java
@@ -1844,6 +1844,24 @@
     assertThat(objcProvider.sdkFramework().toList()).contains("TestFramework");
   }
 
+  @Test
+  public void testIncludesLinkstampFiles() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "apple_binary(",
+        "  name = 'bin',",
+        "  platform_type = 'macos',",
+        "  deps = [':lib'],",
+        ")",
+        "cc_library(",
+        "  name = 'lib',",
+        "  linkstamp = 'some_linkstamp.cc',",
+        ")");
+    CommandAction linkAction = linkAction("//test:bin");
+    assertThat(paramFileArgsForAction(linkAction))
+        .contains(execPathEndingWith(linkAction.getInputs().toList(), "some_linkstamp.o"));
+  }
+
   protected RuleType getRuleType() {
     return RULE_TYPE;
   }