// Copyright 2016 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.

package com.google.devtools.build.lib.rules.objc;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Streams.stream;

import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.PlatformOptions;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.BuildOptionsView;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.packages.BuiltinProvider;
import com.google.devtools.build.lib.packages.Info;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.packages.StructImpl;
import com.google.devtools.build.lib.rules.apple.AppleCommandLineOptions;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration.ConfigurationDistinguisher;
import com.google.devtools.build.lib.rules.apple.ApplePlatform;
import com.google.devtools.build.lib.rules.apple.ApplePlatform.PlatformType;
import com.google.devtools.build.lib.rules.apple.DottedVersion;
import com.google.devtools.build.lib.rules.cpp.CcInfo;
import com.google.devtools.build.lib.rules.cpp.CcLinkingContext;
import com.google.devtools.build.lib.rules.cpp.CcToolchainProvider;
import com.google.devtools.build.lib.rules.cpp.CppSemantics;
import com.google.devtools.build.lib.rules.objc.AppleLinkingOutputs.TargetTriplet;
import com.google.devtools.build.lib.rules.objc.CompilationSupport.ExtraLinkArgs;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import net.starlark.java.eval.Dict;

/** Support utility for creating multi-arch Apple binaries. */
public class MultiArchBinarySupport {
  private final RuleContext ruleContext;
  private final CppSemantics cppSemantics;

  /** A tuple of values about dependency trees in a specific child configuration. */
  @AutoValue
  abstract static class DependencySpecificConfiguration {
    static DependencySpecificConfiguration create(
        BuildConfigurationValue config,
        CcToolchainProvider toolchain,
        ObjcProvider objcLinkProvider,
        ObjcProvider objcProviderWithAvoidDepsSymbols,
        CcInfo ccInfoWithAvoidDepsSymbols) {
      return new AutoValue_MultiArchBinarySupport_DependencySpecificConfiguration(
          config,
          toolchain,
          objcLinkProvider,
          objcProviderWithAvoidDepsSymbols,
          ccInfoWithAvoidDepsSymbols);
    }

    /** Returns the child configuration for this tuple. */
    abstract BuildConfigurationValue config();

    /** Returns the cc toolchain for this configuration. */
    abstract CcToolchainProvider toolchain();

    /**
     * Returns the {@link ObjcProvider} to use as input to the support controlling link actions;
     * avoid deps symbols should be subtracted from this provider.
     */
    abstract ObjcProvider objcLinkProvider();

    /**
     * Returns the {@link ObjcProvider} to propagate up to dependers; this will not have avoid deps
     * symbols subtracted, thus signaling that this target is still responsible for those symbols.
     */
    abstract ObjcProvider objcProviderWithAvoidDepsSymbols();

    /**
     * Returns the {@link CcInfo} to propagate up to dependers; this will not have avoid deps
     * symbols subtracted, thus signaling that this target is still responsible for those symbols.
     */
    abstract CcInfo ccInfoWithAvoidDepsSymbols();
  }

  /** @param ruleContext the current rule context */
  public MultiArchBinarySupport(RuleContext ruleContext, CppSemantics cppSemantics) {
    this.ruleContext = ruleContext;
    this.cppSemantics = cppSemantics;
  }

  /**
   * Registers actions to link a single-platform/architecture Apple binary in a specific
   * configuration.
   *
   * @param dependencySpecificConfiguration a single {@link DependencySpecificConfiguration} that
   *     corresponds to the child configuration to link for this target. Can be obtained via {@link
   *     #getDependencySpecificConfigurations}
   * @param extraLinkArgs the extra linker args to add to link actions linking single-architecture
   *     binaries together
   * @param extraLinkInputs the extra linker inputs to be made available during link actions
   * @param isStampingEnabled whether linkstamping is enabled
   * @param infoCollections a list of provider collections which are propagated from the
   *     dependencies in the requested configuration
   * @param outputMapCollector a map to which output groups created by compile action generation are
   *     added
   * @return an {@link Artifact} representing the single-architecture binary linked from this call
   * @throws RuleErrorException if there are attribute errors in the current rule context
   */
  public Artifact registerConfigurationSpecificLinkActions(
      DependencySpecificConfiguration dependencySpecificConfiguration,
      ExtraLinkArgs extraLinkArgs,
      Iterable<Artifact> extraLinkInputs,
      boolean isStampingEnabled,
      Iterable<? extends TransitiveInfoCollection> infoCollections,
      Map<String, NestedSet<Artifact>> outputMapCollector)
      throws RuleErrorException, InterruptedException {
    IntermediateArtifacts intermediateArtifacts =
        ObjcRuleClasses.intermediateArtifacts(
            ruleContext, dependencySpecificConfiguration.config());
    J2ObjcMappingFileProvider j2ObjcMappingFileProvider =
        J2ObjcMappingFileProvider.union(
            getTypedProviders(infoCollections, J2ObjcMappingFileProvider.PROVIDER));
    J2ObjcEntryClassProvider j2ObjcEntryClassProvider =
        new J2ObjcEntryClassProvider.Builder()
            .addTransitive(getTypedProviders(infoCollections, J2ObjcEntryClassProvider.PROVIDER))
            .build();
    ImmutableList<CcLinkingContext> ccLinkingContexts =
        getTypedProviders(infoCollections, CcInfo.PROVIDER).stream()
            .map(CcInfo::getCcLinkingContext)
            .collect(toImmutableList());

    ObjcProvider objcProvider = dependencySpecificConfiguration.objcLinkProvider();

    CompilationSupport compilationSupport =
        new CompilationSupport.Builder(ruleContext, cppSemantics)
            .setConfig(dependencySpecificConfiguration.config())
            .setToolchainProvider(dependencySpecificConfiguration.toolchain())
            .build();

    compilationSupport
        .registerLinkActions(
            objcProvider,
            ccLinkingContexts,
            j2ObjcMappingFileProvider,
            j2ObjcEntryClassProvider,
            extraLinkArgs,
            extraLinkInputs,
            isStampingEnabled)
        .validateAttributes();
    ruleContext.assertNoErrors();

    return intermediateArtifacts.strippedSingleArchitectureBinary();
  }

  /**
   * Returns a map of {@link DependencySpecificConfiguration} instances keyed by their split
   * transition key. Each dependency specific configuration comprise all information about the
   * dependencies for each child configuration. This can be used both to register actions in {@link
   * #registerConfigurationSpecificLinkActions} and collect provider information to be propagated
   * upstream.
   *
   * @param splitToolchains a map from split toolchains for which dependencies of the current rule
   *     are built
   * @param splitDeps a map from split "deps" of the current rule.
   * @param avoidDepsProviders {@link TransitiveInfoCollection}s that dependencies of the current
   *     rule have propagated which should not be linked into the binary
   * @throws RuleErrorException if there are attribute errors in the current rule context
   */
  public ImmutableMap<Optional<String>, DependencySpecificConfiguration>
      getDependencySpecificConfigurations(
          Map<Optional<String>, List<ConfiguredTargetAndData>> splitToolchains,
          Map<Optional<String>, List<ConfiguredTargetAndData>> splitDeps,
          ImmutableList<TransitiveInfoCollection> avoidDepsProviders)
          throws RuleErrorException, InterruptedException {
    Iterable<ObjcProvider> avoidDepsObjcProviders = getAvoidDepsObjcProviders(avoidDepsProviders);
    ImmutableList<CcLinkingContext> avoidDepsCcLinkingContexts =
        getTypedProviders(avoidDepsProviders, CcInfo.PROVIDER).stream()
            .map(CcInfo::getCcLinkingContext)
            .collect(toImmutableList());

    ImmutableMap.Builder<Optional<String>, DependencySpecificConfiguration> childInfoBuilder =
        ImmutableMap.builder();
    for (Optional<String> splitTransitionKey : splitToolchains.keySet()) {
      ConfiguredTargetAndData ctad =
          Iterables.getOnlyElement(splitToolchains.get(splitTransitionKey));
      BuildConfigurationValue childToolchainConfig = ctad.getConfiguration();
      IntermediateArtifacts intermediateArtifacts =
          ObjcRuleClasses.intermediateArtifacts(ruleContext, childToolchainConfig);

      List<? extends TransitiveInfoCollection> propagatedDeps =
          getProvidersFromCtads(splitDeps.get(splitTransitionKey));

      ObjcCommon common =
          common(
              ruleContext,
              childToolchainConfig,
              intermediateArtifacts,
              propagatedDeps,
              avoidDepsObjcProviders);
      ObjcProvider objcProviderWithAvoidDepsSymbols = common.getObjcProvider();
      ObjcProvider objcProvider =
          objcProviderWithAvoidDepsSymbols.subtractSubtrees(
              avoidDepsObjcProviders, avoidDepsCcLinkingContexts);

      CcInfo ccInfoWithAvoidDepsSymbols = common.createCcInfo();

      CcToolchainProvider toolchainProvider =
          ctad.getConfiguredTarget().get(CcToolchainProvider.PROVIDER);

      childInfoBuilder.put(
          splitTransitionKey,
          DependencySpecificConfiguration.create(
              childToolchainConfig,
              toolchainProvider,
              objcProvider,
              objcProviderWithAvoidDepsSymbols,
              ccInfoWithAvoidDepsSymbols));
    }

    return childInfoBuilder.buildOrThrow();
  }

  /**
   * Returns the ConfigurationDistinguisher that maps directly to the given PlatformType.
   *
   * @throws IllegalArgumentException if the platform type attribute is an unsupported value
   */
  private static ConfigurationDistinguisher configurationDistinguisher(PlatformType platformType) {
    switch (platformType) {
      case IOS:
        return ConfigurationDistinguisher.APPLEBIN_IOS;
      case CATALYST:
        return ConfigurationDistinguisher.APPLEBIN_CATALYST;
      case VISIONOS:
        return ConfigurationDistinguisher.APPLEBIN_VISIONOS;
      case WATCHOS:
        return ConfigurationDistinguisher.APPLEBIN_WATCHOS;
      case TVOS:
        return ConfigurationDistinguisher.APPLEBIN_TVOS;
      case MACOS:
        return ConfigurationDistinguisher.APPLEBIN_MACOS;
    }
    throw new IllegalArgumentException("Unsupported platform type " + platformType);
  }

  /**
   * Returns the preferred minimum OS version based on information found from inputs.
   *
   * @param buildOptions the build's top-level options
   * @param platformType the platform type attribute found from the given rule being built
   * @param minimumOsVersion a minimum OS version represented to override command line options, if
   *     one has been found
   * @return an {@link DottedVersion.Option} representing the preferred minimum OS version if found,
   *     or null
   * @throws IllegalArgumentException if the platform type attribute is an unsupported value and the
   *     optional minimumOsVersion is not present
   */
  private static DottedVersion.Option minimumOsVersionOption(
      BuildOptionsView buildOptions,
      PlatformType platformType,
      Optional<DottedVersion> minimumOsVersion) {
    if (minimumOsVersion.isPresent()) {
      return DottedVersion.option(minimumOsVersion.get());
    }
    DottedVersion.Option option;
    switch (platformType) {
      case IOS:
      case CATALYST:
        option = buildOptions.get(AppleCommandLineOptions.class).iosMinimumOs;
        break;
      case VISIONOS:
        // TODO: Replace with CppOptions.minimumOsVersion
        option = DottedVersion.option(DottedVersion.fromStringUnchecked("1.0"));
        break;
      case WATCHOS:
        option = buildOptions.get(AppleCommandLineOptions.class).watchosMinimumOs;
        break;
      case TVOS:
        option = buildOptions.get(AppleCommandLineOptions.class).tvosMinimumOs;
        break;
      case MACOS:
        option = buildOptions.get(AppleCommandLineOptions.class).macosMinimumOs;
        break;
      default:
        throw new IllegalArgumentException("Unsupported platform type " + platformType);
    }
    return option;
  }

  /**
   * Creates a derivative set of build options for the given split transition with default options.
   *
   * @param buildOptions the build's top-level options
   * @param platformType the platform type attribute found from the given rule being built
   * @param minimumOsVersionOption a minimum OS version option for this given split
   * @return an {@link BuildOptionsView} to be used as a basis for a given multi arch binary split
   *     transition
   */
  private static BuildOptionsView defaultBuildOptionsForSplit(
      BuildOptionsView buildOptions,
      PlatformType platformType,
      DottedVersion.Option minimumOsVersionOption) {
    BuildOptionsView splitOptions = buildOptions.clone();

    AppleCommandLineOptions appleCommandLineOptions =
        splitOptions.get(AppleCommandLineOptions.class);
    appleCommandLineOptions.applePlatformType = platformType;
    switch (platformType) {
      case IOS:
      case CATALYST:
        appleCommandLineOptions.iosMinimumOs = minimumOsVersionOption;
        break;
      case WATCHOS:
        appleCommandLineOptions.watchosMinimumOs = minimumOsVersionOption;
        break;
      case TVOS:
        appleCommandLineOptions.tvosMinimumOs = minimumOsVersionOption;
        break;
      case MACOS:
        appleCommandLineOptions.macosMinimumOs = minimumOsVersionOption;
        break;
      case VISIONOS:
        // TODO: use CppOptions.minimumOsVersion
        break;
    }
    return splitOptions;
  }

  /**
   * Creates a split transition mapping based on --apple_platforms and --platforms.
   *
   * @param buildOptions the build's top-level options
   * @param platformType the platform type attribute found from the given rule being built
   * @param minimumOsVersion a minimum OS version represented to override command line options, if
   *     one has been found
   * @param applePlatforms the {@link List} of {@link Label}s representing Apple platforms to split
   *     on
   * @return an {@link ImmutableMap<String, BuildOptions>} representing the split transition for all
   *     platforms found
   */
  public static ImmutableMap<String, BuildOptions> handleApplePlatforms(
      BuildOptionsView buildOptions,
      PlatformType platformType,
      Optional<DottedVersion> minimumOsVersion,
      List<Label> applePlatforms) {
    ImmutableMap.Builder<String, BuildOptions> splitBuildOptions = ImmutableMap.builder();

    ConfigurationDistinguisher configurationDistinguisher =
        configurationDistinguisher(platformType);
    DottedVersion.Option minimumOsVersionOption =
        minimumOsVersionOption(buildOptions, platformType, minimumOsVersion);

    for (Label platform : ImmutableSortedSet.copyOf(applePlatforms)) {
      BuildOptionsView splitOptions =
          defaultBuildOptionsForSplit(buildOptions, platformType, minimumOsVersionOption);

      // Disable multi-platform options for child configurations.
      splitOptions.get(AppleCommandLineOptions.class).applePlatforms = ImmutableList.of();

      // The cpu flag will be set by platform mapping if a mapping exists.
      splitOptions.get(PlatformOptions.class).platforms = ImmutableList.of(platform);
      if (splitOptions.get(ObjcCommandLineOptions.class).enableCcDeps) {
        // Only set the (CC-compilation) configs for dependencies if explicitly required by the
        // user.
        // This helps users of the iOS rules who do not depend on CC rules as these config values
        // require additional flags to work (e.g. a custom crosstool) which now only need to be
        // set if this feature is explicitly requested.
        AppleCrosstoolTransition.setAppleCrosstoolTransitionPlatformConfiguration(
            buildOptions, splitOptions, platform);
      }
      AppleCommandLineOptions appleCommandLineOptions =
          splitOptions.get(AppleCommandLineOptions.class);
      // Set the configuration distinguisher last, as the method
      // setAppleCrosstoolTransitionPlatformConfiguration will set this value to the Apple CROSSTOOL
      // configuration distinguisher, and we want to make sure it's set for the right platform
      // instead in this split transition.
      appleCommandLineOptions.configurationDistinguisher = configurationDistinguisher;

      splitBuildOptions.put(platform.getCanonicalForm(), splitOptions.underlying());
    }

    return splitBuildOptions.buildOrThrow();
  }

  /**
   * Returns a list of supported Apple CPUs given the minimum OS for the target platform.
   *
   * @param minimumOsVersionOption a minimum OS version represented to override command line
   *     options, if one has been found
   * @param cpus list of Apple CPUs requested by the build invocation
   * @param platformType the platform type attribute found from the given rule being built
   * @return a {@link List<String>} representing the supported list of Apple CPUs
   */
  private static List<String> supportedAppleCpusFromMinimumOs(
      DottedVersion.Option minimumOsVersionOption, List<String> cpus, PlatformType platformType) {
    List<String> supportedCpus = cpus;
    DottedVersion actualMinimumOsVersion = DottedVersion.maybeUnwrap(minimumOsVersionOption);
    DottedVersion unsupported32BitMinimumOs;
    switch (platformType) {
      case IOS:
        unsupported32BitMinimumOs = DottedVersion.fromStringUnchecked("11.0");
        break;
      case WATCHOS:
        unsupported32BitMinimumOs = DottedVersion.fromStringUnchecked("9.0");
        break;
      default:
        return supportedCpus;
    }

    if (actualMinimumOsVersion != null
        && actualMinimumOsVersion.compareTo(unsupported32BitMinimumOs) >= 0) {
      ImmutableList<String> non32BitCpus =
          cpus.stream()
              .filter(cpu -> !ApplePlatform.is32Bit(platformType, cpu))
              .collect(toImmutableList());
      if (!non32BitCpus.isEmpty()) {
        supportedCpus = non32BitCpus;
      }
    }
    return supportedCpus;
  }

  /**
   * Creates a split transition mapping based on Apple cpu options.
   *
   * @param buildOptions the build's top-level options
   * @param platformType the platform type attribute found from the given rule being built
   * @param minimumOsVersion a minimum OS version represented to override command line options, if
   *     one has been found
   * @return an {@link ImmutableMap<String, BuildOptions>} representing the split transition for all
   *     architectures found from cpu flags
   */
  public static ImmutableMap<String, BuildOptions> handleAppleCpus(
      BuildOptionsView buildOptions,
      PlatformType platformType,
      Optional<DottedVersion> minimumOsVersion) {
    List<String> cpus;
    ConfigurationDistinguisher configurationDistinguisher =
        configurationDistinguisher(platformType);
    DottedVersion.Option minimumOsVersionOption =
        minimumOsVersionOption(buildOptions, platformType, minimumOsVersion);

    switch (platformType) {
      case IOS:
        cpus = buildOptions.get(AppleCommandLineOptions.class).iosMultiCpus;
        if (cpus.isEmpty()) {
          cpus =
              ImmutableList.of(
                  AppleConfiguration.iosCpuFromCpu(buildOptions.get(CoreOptions.class).cpu));
        }
        cpus = supportedAppleCpusFromMinimumOs(minimumOsVersionOption, cpus, platformType);
        break;
      case VISIONOS:
        cpus = buildOptions.get(AppleCommandLineOptions.class).visionosCpus;
        if (cpus.isEmpty()) {
          cpus = ImmutableList.of(AppleCommandLineOptions.DEFAULT_VISIONOS_CPU);
        }
        cpus = supportedAppleCpusFromMinimumOs(minimumOsVersionOption, cpus, platformType);
        break;
      case WATCHOS:
        cpus = buildOptions.get(AppleCommandLineOptions.class).watchosCpus;
        if (cpus.isEmpty()) {
          cpus = ImmutableList.of(AppleCommandLineOptions.DEFAULT_WATCHOS_CPU);
        }
        cpus = supportedAppleCpusFromMinimumOs(minimumOsVersionOption, cpus, platformType);
        break;
      case TVOS:
        cpus = buildOptions.get(AppleCommandLineOptions.class).tvosCpus;
        if (cpus.isEmpty()) {
          cpus = ImmutableList.of(AppleCommandLineOptions.DEFAULT_TVOS_CPU);
        }
        break;
      case MACOS:
        cpus = buildOptions.get(AppleCommandLineOptions.class).macosCpus;
        if (cpus.isEmpty()) {
          cpus = ImmutableList.of(AppleCommandLineOptions.DEFAULT_MACOS_CPU);
        }
        break;
      case CATALYST:
        cpus = buildOptions.get(AppleCommandLineOptions.class).catalystCpus;
        if (cpus.isEmpty()) {
          cpus = ImmutableList.of(AppleCommandLineOptions.DEFAULT_CATALYST_CPU);
        }
        break;
      default:
        throw new IllegalArgumentException("Unsupported platform type " + platformType);
    }

    // There may be some duplicate flag values.
    cpus = ImmutableSortedSet.copyOf(cpus).asList();
    ImmutableMap.Builder<String, BuildOptions> splitBuildOptions = ImmutableMap.builder();
    for (String cpu : cpus) {
      BuildOptionsView splitOptions =
          defaultBuildOptionsForSplit(buildOptions, platformType, minimumOsVersionOption);

      AppleCommandLineOptions appleCommandLineOptions =
          splitOptions.get(AppleCommandLineOptions.class);

      appleCommandLineOptions.appleSplitCpu = cpu;

      String platformCpu = ApplePlatform.cpuStringForTarget(platformType, cpu);
      if (splitOptions.get(ObjcCommandLineOptions.class).enableCcDeps) {
        // Only set the (CC-compilation) CPU for dependencies if explicitly required by the user.
        // This helps users of the iOS rules who do not depend on CC rules as these CPU values
        // require additional flags to work (e.g. a custom crosstool) which now only need to be
        // set if this feature is explicitly requested.
        AppleCrosstoolTransition.setAppleCrosstoolTransitionCpuConfiguration(
            buildOptions, splitOptions, platformCpu);
      }
      // Set the configuration distinguisher last, as setAppleCrosstoolTransitionCpuConfiguration
      // will set this value to the Apple CROSSTOOL configuration distinguisher, and we want to make
      // sure it's set for the right platform instead in this split transition.
      appleCommandLineOptions.configurationDistinguisher = configurationDistinguisher;

      splitBuildOptions.put(platformCpu, splitOptions.underlying());
    }
    return splitBuildOptions.buildOrThrow();
  }

  private static Iterable<ObjcProvider> getAvoidDepsObjcProviders(
      ImmutableList<TransitiveInfoCollection> transitiveInfoCollections) {
    // Dylibs.
    ImmutableList<ObjcProvider> frameworkObjcProviders =
        getTypedProviders(transitiveInfoCollections, AppleDynamicFrameworkInfo.STARLARK_CONSTRUCTOR)
            .stream()
            .map(frameworkProvider -> frameworkProvider.getDepsObjcProvider())
            .collect(toImmutableList());
    // Bundle Loaders.
    ImmutableList<ObjcProvider> executableObjcProviders =
        getTypedProviders(transitiveInfoCollections, AppleExecutableBinaryInfo.STARLARK_CONSTRUCTOR)
            .stream()
            .map(frameworkProvider -> frameworkProvider.getDepsObjcProvider())
            .collect(toImmutableList());

    return Iterables.concat(
        frameworkObjcProviders,
        executableObjcProviders,
        getTypedProviders(transitiveInfoCollections, ObjcProvider.STARLARK_CONSTRUCTOR));
  }

  private ObjcCommon common(
      RuleContext ruleContext,
      BuildConfigurationValue buildConfiguration,
      IntermediateArtifacts intermediateArtifacts,
      List<? extends TransitiveInfoCollection> propagatedDeps,
      Iterable<ObjcProvider> additionalDepProviders)
      throws InterruptedException {

    ObjcCommon.Builder commonBuilder =
        new ObjcCommon.Builder(ObjcCommon.Purpose.LINK_ONLY, ruleContext, buildConfiguration)
            .setCompilationAttributes(
                CompilationAttributes.Builder.fromRuleContext(ruleContext).build())
            .addDeps(propagatedDeps)
            .addObjcProviders(additionalDepProviders)
            .setIntermediateArtifacts(intermediateArtifacts)
            .setAlwayslink(false);

    return commonBuilder.build();
  }

  private static <T extends Info> ImmutableList<T> getTypedProviders(
      Iterable<? extends TransitiveInfoCollection> infoCollections,
      BuiltinProvider<T> providerClass) {
    return stream(infoCollections)
        .filter(infoCollection -> infoCollection.get(providerClass) != null)
        .map(infoCollection -> infoCollection.get(providerClass))
        .collect(toImmutableList());
  }

  /** Returns providers from a list of {@link ConfiguredTargetAndData} */
  public static List<? extends TransitiveInfoCollection> getProvidersFromCtads(
      List<ConfiguredTargetAndData> ctads) {
    if (ctads == null) {
      return ImmutableList.<TransitiveInfoCollection>of();
    }
    return ctads.stream()
        .map(ConfiguredTargetAndData::getConfiguredTarget)
        .collect(Collectors.toList());
  }

  /**
   * Returns an Apple target triplet (arch, platform, environment) for a given {@link
   * BuildConfigurationValue}.
   *
   * @param config {@link BuildConfigurationValue} from rule context
   * @return {@link AppleLinkingOutputs.TargetTriplet}
   */
  public static AppleLinkingOutputs.TargetTriplet getTargetTriplet(BuildConfigurationValue config) {
    // TODO(b/177442911): Use the target platform from platform info coming from split
    // transition outputs instead of inferring this based on the target CPU.
    ApplePlatform cpuPlatform = ApplePlatform.forTargetCpu(config.getCpu());
    AppleConfiguration appleConfig = config.getFragment(AppleConfiguration.class);

    return TargetTriplet.create(
        appleConfig.getSingleArchitecture(),
        cpuPlatform.getTargetPlatform(),
        cpuPlatform.getTargetEnvironment());
  }

  /**
   * Transforms a {@link Map<Optional<String>, List<ConfiguredTargetAndData>>}, to a Starlark Dict
   * keyed by split transition keys with {@link AppleLinkingOutputs.TargetTriplet} Starlark struct
   * definition.
   *
   * @param ctads a {@link Map<Optional<String>, List<ConfiguredTargetAndData>>} from rule context
   * @return a Starlark {@link Dict<String, StructImpl>} representing split transition keys with
   *     their target triplet (architecture, platform, environment)
   */
  public static Dict<String, StructImpl> getSplitTargetTripletFromCtads(
      Map<Optional<String>, List<ConfiguredTargetAndData>> ctads) {
    Dict.Builder<String, StructImpl> result = Dict.builder();
    for (Optional<String> splitTransitionKey : ctads.keySet()) {
      TargetTriplet targetTriplet =
          getTargetTriplet(
              Iterables.getOnlyElement(ctads.get(splitTransitionKey)).getConfiguration());
      result.put(splitTransitionKey.get(), targetTriplet.toStarlarkStruct());
    }
    return result.buildImmutable();
  }
}
