// Copyright 2014 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.cpp;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
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.actions.Root;
import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
import com.google.devtools.build.lib.skyframe.FileValue;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.lib.vfs.ZipFileSystem;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.LipoMode;
import com.google.devtools.build.skyframe.SkyFunction;
import java.io.IOException;
import java.util.Collection;
import java.util.zip.ZipException;

/**
 * Support class for FDO (feedback directed optimization) and LIPO (lightweight inter-procedural
 * optimization).
 *
 * <p>Here follows a quick run-down of how FDO/LIPO builds work (for non-FDO/LIPO builds, none
 * of this applies):
 *
 * <p>{@link FdoSupport#create} is called from {@link FdoSupportFunction} (a {@link SkyFunction}),
 * which is requested from Skyframe by the {@code cc_toolchain} rule. It extracts the FDO .zip (in
 * case we work with an explicitly generated FDO profile file) or analyzes the .afdo.imports file
 * next to the .afdo file (if AutoFDO is in effect).
 *
 * <p>.afdo.imports files contain one import a line. A line is two paths separated by a colon,
 * with functions in the second path being referenced by functions in the first path. These are
 * then put into the imports map. If we do AutoFDO, we don't handle individual .gcda files, so
 * gcdaFiles will be empty.
 *
 * <p>Regular .fdo zip files contain .gcda files (which are added to gcdaFiles) and
 * .gcda.imports files. There is one .gcda.imports file for every source file and it contains one
 * path in every line, which can either be a path to a source file that contains a function
 * referenced by the original source file or the .gcda file for such a referenced file. They
 * both are added to the imports map.
 *
 * <p>If we do LIPO, we create an extra configuration that is called the "LIPO context collector",
 * whose job it is to collect information that every configured target compiled with LIPO needs.
 * The top-level target of this configuration is the LIPO context (always a cc_binary) and is an
 * implicit dependency of every cc_* rule through their :lipo_context_collector attribute. The
 * collected information is encapsulated in {@link LipoContextProvider}.
 *
 * <p>For each C++ compile action in the target configuration, {@link #configureCompilation} is
 * called, which adds command line options and input files required for the build. There are
 * three cases:
 *
 * <ul>
 * <li>If we do AutoFDO, the .afdo file and the source files containing the functions imported
 * by the original source file (as determined from the inputs map) are added.
 * <li>If we do FDO, the .gcda file corresponding to the source file is added.
 * <li>If we do LIPO, in addition to the .gcda file corresponding to the source file
 * (like for FDO) the source files that contain the functions referenced by the source file and
 * their .gcda files are added, too.
 * </ul>
 *
 * <p>If we do LIPO, the actual C++ compilation context for LIPO compilation actions is pieced
 * together from the CppCompileContext in LipoContextProvider and that of the rule being compiled.
 * (see {@link CppCompilationContext#mergeForLipo}) This is so that the include files for the
 * extra LIPO sources are found and is, strictly speaking, incorrect, since it also changes the
 * declared include directories of the main source file, which in theory can result in the
 * compilation passing even though it should fail with undeclared inclusion errors.
 *
 * <p>During the actual execution of the C++ compile action, the extra sources also need to be
 * include scanned, which is the reason why they are {@link IncludeScannable} objects and not
 * simple artifacts. We currently create these {@link IncludeScannable} objects by creating actual
 * C++ compile actions in the LIPO context collector configuration which are then never executed.
 * In fact, these C++ compile actions are never even registered with Skyframe. For this we
 * propagate a bit from {@code BuildConfiguration.isActionsEnabled} to
 * {@code CachingAnalysisEnvironment.allowRegisteringActions}, which causes actions to be silently
 * discarded after configured targets are created.
 */
@Immutable
public class FdoSupport {

  /**
   * The FDO mode we are operating in.
   *
   * LIPO can only be active if this is not <code>OFF</code>, but all of the modes below can work
   * with LIPO either off or on.
   */
  private enum FdoMode {
    /** FDO is turned off. */
    OFF,

    /** Profiling-based FDO using an explicitly recorded profile. */
    VANILLA,

    /** FDO based on automatically collected data. */
    AUTO_FDO,

    /** Instrumentation-based FDO implemented on LLVM. */
    LLVM_FDO,
  }

  /**
   * Path within profile data .zip files that is considered the root of the
   * profile information directory tree.
   */
  private static final PathFragment ZIP_ROOT = new PathFragment("/");

  /**
   * Returns true if the given fdoFile represents an AutoFdo profile.
   */
  public static final boolean isAutoFdo(String fdoFile) {
    return CppFileTypes.GCC_AUTO_PROFILE.matches(fdoFile);
  }

  /**
   * Returns true if the given fdoFile represents an LLVM profile.
   */
  public static final boolean isLLVMFdo(String fdoFile) {
    return CppFileTypes.LLVM_PROFILE.matches(fdoFile);
  }

  /**
   * Coverage information output directory passed to {@code --fdo_instrument},
   * or {@code null} if FDO instrumentation is disabled.
   */
  private final PathFragment fdoInstrument;

  /**
   * Path of the profile file passed to {@code --fdo_optimize}, or
   * {@code null} if FDO optimization is disabled.  The profile file
   * can be a coverage ZIP or an AutoFDO feedback file.
   */
  private final Path fdoProfile;

  /**
   * Temporary directory to which the coverage ZIP file is extracted to
   * (relative to the exec root), or {@code null} if FDO optimization is
   * disabled. This is used to create artifacts for the extracted files.
   *
   * <p>Note that this root is intentionally not registered with the artifact
   * factory.
   */
  private final Root fdoRoot;

  /**
   * The relative path of the FDO root to the exec root.
   */
  private final PathFragment fdoRootExecPath;

  /**
   * Path of FDO files under the FDO root.
   */
  private final PathFragment fdoPath;

  /**
   * LIPO mode passed to {@code --lipo}. This is only used if
   * {@code fdoProfile != null}.
   */
  private final LipoMode lipoMode;

  /**
   * FDO mode.
   */
  private final FdoMode fdoMode;

  /**
   * The {@code .gcda} files that have been extracted from the ZIP file,
   * relative to the root of the ZIP file.
   */
  private final ImmutableSet<PathFragment> gcdaFiles;

  /**
   * Multimap from .gcda file base names to auxiliary input files.
   *
   * <p>The keys of the multimap are the exec root relative paths of .gcda files
   * with the extension removed. The values are the lines from the accompanying
   * .gcda.imports file.
   *
   * <p>The contents of the multimap are copied verbatim from the .gcda.imports
   * files and not yet checked for validity.
   */
  private final ImmutableMultimap<PathFragment, PathFragment> imports;

  /**
   * Creates an FDO support object.
   *
   * @param fdoInstrument value of the --fdo_instrument option
   * @param fdoProfile path to the profile file passed to --fdo_optimize option
   * @param lipoMode value of the --lipo_mode option
   */
  private FdoSupport(FdoMode fdoMode, LipoMode lipoMode, Root fdoRoot, PathFragment fdoRootExecPath,
      PathFragment fdoInstrument, Path fdoProfile, FdoZipContents fdoZipContents) {
    this.fdoInstrument = fdoInstrument;
    this.fdoProfile = fdoProfile;
    this.fdoRoot = fdoRoot;
    this.fdoRootExecPath = fdoRootExecPath;
    this.fdoPath = fdoProfile == null
        ? null
        : FileSystemUtils.removeExtension(new PathFragment("_fdo").getChild(
            fdoProfile.getBaseName()));
    this.lipoMode = lipoMode;
    this.fdoMode = fdoMode;
    this.gcdaFiles = fdoZipContents.gcdaFiles;
    this.imports = fdoZipContents.imports;
  }

  public Root getFdoRoot() {
    return fdoRoot;
  }

  /** Creates an initialized {@link FdoSupport} instance. */
  static FdoSupport create(
      SkyFunction.Environment env,
      PathFragment fdoInstrument,
      Path fdoProfile,
      LipoMode lipoMode,
      Path execRoot)
      throws IOException, FdoException, InterruptedException {
    FdoMode fdoMode;
    if (fdoProfile != null && isAutoFdo(fdoProfile.getBaseName())) {
      fdoMode = FdoMode.AUTO_FDO;
    } else if (fdoProfile != null && isLLVMFdo(fdoProfile.getBaseName())) {
      fdoMode = FdoMode.LLVM_FDO;
    } else if (fdoProfile != null) {
      fdoMode = FdoMode.VANILLA;
    } else {
      fdoMode = FdoMode.OFF;
    }

    if (fdoProfile == null) {
      lipoMode = LipoMode.OFF;
    }

    Root fdoRoot =
        (fdoProfile == null)
            ? null
            : Root.asDerivedRoot(execRoot, execRoot.getRelative(
                PrecomputedValue.PRODUCT_NAME.get(env) + "-fdo"), true);

    PathFragment fdoRootExecPath = fdoProfile == null
        ? null
        : fdoRoot.getExecPath().getRelative(FileSystemUtils.removeExtension(
            new PathFragment("_fdo").getChild(fdoProfile.getBaseName())));

    if (fdoProfile != null) {
      if (lipoMode != LipoMode.OFF) {
        // Incrementality is not supported for LIPO builds, see FdoSupport#scannables.
        // Ensure that the Skyframe value containing the configuration will not be reused to avoid
        // incrementality issues.
        PrecomputedValue.dependOnBuildId(env);
      } else {
        Path path = fdoMode == FdoMode.AUTO_FDO ? getAutoFdoImportsPath(fdoProfile) : fdoProfile;
        env.getValue(FileValue.key(RootedPath.toRootedPathMaybeUnderRoot(path,
            ImmutableList.of(execRoot))));
      }
    }

    if (env.valuesMissing()) {
      return null;
    }

    FdoZipContents fdoZipContents = extractFdoZip(
        fdoMode, lipoMode, execRoot, fdoProfile, fdoRootExecPath,
        PrecomputedValue.PRODUCT_NAME.get(env));
    return new FdoSupport(
        fdoMode, lipoMode, fdoRoot, fdoRootExecPath, fdoInstrument, fdoProfile, fdoZipContents);
  }

  @Immutable
  private static class FdoZipContents {
    private final ImmutableSet<PathFragment> gcdaFiles;
    private final ImmutableMultimap<PathFragment, PathFragment> imports;

    private FdoZipContents(ImmutableSet<PathFragment> gcdaFiles,
        ImmutableMultimap<PathFragment, PathFragment> imports) {
      this.gcdaFiles = gcdaFiles;
      this.imports = imports;
    }
  }

  /**
   * Extracts the FDO zip file and collects data from it that's needed during analysis.
   *
   * <p>When an {@code --fdo_optimize} compile is requested, unpacks the given
   * FDO gcda zip file into a clean working directory under execRoot.
   *
   * @throws FdoException if the FDO ZIP contains a file of unknown type
   */
  private static FdoZipContents extractFdoZip(FdoMode fdoMode, LipoMode lipoMode, Path execRoot,
      Path fdoProfile, PathFragment fdoRootExecPath, String productName)
      throws IOException, FdoException {
    // The execRoot != null case is only there for testing. We cannot provide a real ZIP file in
    // tests because ZipFileSystem does not work with a ZIP on an in-memory file system.
    // IMPORTANT: Keep in sync with #declareSkyframeDependencies to avoid incrementality issues.
    ImmutableSet<PathFragment> gcdaFiles = ImmutableSet.of();
    ImmutableMultimap<PathFragment, PathFragment> imports = ImmutableMultimap.of();

    if (fdoProfile != null && execRoot != null) {
      Path fdoDirPath = execRoot.getRelative(fdoRootExecPath);

      FileSystemUtils.deleteTreesBelow(fdoDirPath);
      FileSystemUtils.createDirectoryAndParents(fdoDirPath);

      if (fdoMode == FdoMode.AUTO_FDO) {
        if (lipoMode != LipoMode.OFF) {
          imports = readAutoFdoImports(getAutoFdoImportsPath(fdoProfile));
        }
        FileSystemUtils.ensureSymbolicLink(
            execRoot.getRelative(getAutoProfilePath(fdoProfile, fdoRootExecPath)), fdoProfile);
      } else if (fdoMode == FdoMode.LLVM_FDO) {
        FileSystemUtils.ensureSymbolicLink(
            execRoot.getRelative(getLLVMProfilePath(fdoProfile, fdoRootExecPath)), fdoProfile);
      } else {
        Path zipFilePath = new ZipFileSystem(fdoProfile).getRootDirectory();
        String outputSymlinkName = productName + "-out";
        if (!zipFilePath.getRelative(outputSymlinkName).isDirectory()) {
          throw new ZipException(
              "FDO zip files must be zipped directly above '"
                  + outputSymlinkName
                  + "' for the compiler to find the profile");
        }
        ImmutableSet.Builder<PathFragment> gcdaFilesBuilder = ImmutableSet.builder();
        ImmutableMultimap.Builder<PathFragment, PathFragment> importsBuilder =
            ImmutableMultimap.builder();
        extractFdoZipDirectory(zipFilePath, fdoDirPath, gcdaFilesBuilder, importsBuilder);
        gcdaFiles = gcdaFilesBuilder.build();
        imports = importsBuilder.build();
      }
    }

    return new FdoZipContents(gcdaFiles, imports);
  }

  /**
   * Recursively extracts a directory from the GCDA ZIP file into a target
   * directory.
   *
   * <p>Imports files are not written to disk. Their content is directly added
   * to an internal data structure.
   *
   * <p>The files are written at $EXECROOT/blaze-fdo/_fdo/(base name of profile zip), and the
   * {@code _fdo} directory there is symlinked to from the exec root, so that the file are also
   * available at $EXECROOT/_fdo/..., which is their exec path. We need to jump through these
   * hoops because the FDO root 1. needs to be a source root, thus the exec path of its root is
   * ".", 2. it must not be equal to the exec root so that the artifact factory does not get
   * confused, 3. the files under it must be reachable by their exec path from the exec root.
   *
   * @throws IOException if any of the I/O operations failed
   * @throws FdoException if the FDO ZIP contains a file of unknown type
   */
  private static void extractFdoZipDirectory(Path sourceDir, Path targetDir,
      ImmutableSet.Builder<PathFragment> gcdaFilesBuilder,
      ImmutableMultimap.Builder<PathFragment, PathFragment> importsBuilder)
          throws IOException, FdoException {
    for (Path sourceFile : sourceDir.getDirectoryEntries()) {
      Path targetFile = targetDir.getRelative(sourceFile.getBaseName());
      if (sourceFile.isDirectory()) {
        targetFile.createDirectory();
        extractFdoZipDirectory(sourceFile, targetFile, gcdaFilesBuilder, importsBuilder);
      } else {
        if (CppFileTypes.COVERAGE_DATA.matches(sourceFile)) {
          FileSystemUtils.copyFile(sourceFile, targetFile);
          gcdaFilesBuilder.add(
              sourceFile.relativeTo(sourceFile.getFileSystem().getRootDirectory()));
        } else if (CppFileTypes.COVERAGE_DATA_IMPORTS.matches(sourceFile)) {
          readCoverageImports(sourceFile, importsBuilder);
        } else {
            throw new FdoException("FDO ZIP file contained a file of unknown type: " + sourceFile);
        }
      }
    }
  }


  /**
   * Reads a .gcda.imports file and stores the imports information.
   *
   * @throws FdoException if an auxiliary LIPO input was not found
   */
  private static void readCoverageImports(Path importsFile,
      ImmutableMultimap.Builder<PathFragment, PathFragment> importsBuilder)
          throws IOException, FdoException {
    PathFragment key = importsFile.asFragment().relativeTo(ZIP_ROOT);
    String baseName = key.getBaseName();
    String ext = Iterables.getOnlyElement(CppFileTypes.COVERAGE_DATA_IMPORTS.getExtensions());
    key = key.replaceName(baseName.substring(0, baseName.length() - ext.length()));

    for (String line : FileSystemUtils.iterateLinesAsLatin1(importsFile)) {
      if (!line.isEmpty()) {
        // We can't yet fully check the validity of a line. this is done later
        // when we actually parse the contained paths.
        PathFragment execPath = new PathFragment(line);
        if (execPath.isAbsolute()) {
          throw new FdoException("Absolute paths not allowed in gcda imports file " + importsFile
              + ": " + execPath);
        }
        importsBuilder.put(key, new PathFragment(line));
      }
    }
  }

  /**
   * Reads a .afdo.imports file and stores the imports information.
   */
  private static ImmutableMultimap<PathFragment, PathFragment> readAutoFdoImports(Path importsFile)
          throws IOException, FdoException {
    ImmutableMultimap.Builder<PathFragment, PathFragment> importBuilder =
        ImmutableMultimap.builder();
    for (String line : FileSystemUtils.iterateLinesAsLatin1(importsFile)) {
      if (!line.isEmpty()) {
        PathFragment key = new PathFragment(line.substring(0, line.indexOf(':')));
        if (key.isAbsolute()) {
          throw new FdoException("Absolute paths not allowed in afdo imports file " + importsFile
              + ": " + key);
        }
        for (String auxFile : line.substring(line.indexOf(':') + 1).split(" ")) {
          if (auxFile.length() == 0) {
            continue;
          }

          importBuilder.put(key, new PathFragment(auxFile));
        }
      }
    }
    return importBuilder.build();
  }

  private static Path getAutoFdoImportsPath(Path fdoProfile) {
     return fdoProfile.getParentDirectory().getRelative(fdoProfile.getBaseName() + ".imports");
  }

  /**
   * Returns the imports from the .afdo.imports file of a source file.
   *
   * @param sourceExecPath the source file
   */
  private Collection<Artifact> getAutoFdoImports(RuleContext ruleContext,
      PathFragment sourceExecPath, LipoContextProvider lipoContextProvider) {
    Preconditions.checkState(lipoMode != LipoMode.OFF);
    ImmutableCollection<PathFragment> afdoImports = imports.get(sourceExecPath);
    Preconditions.checkState(afdoImports != null,
        "AutoFDO import data missing for %s", sourceExecPath);
    ImmutableList.Builder<Artifact> result = ImmutableList.builder();
    for (PathFragment afdoImport : afdoImports) {
      Artifact afdoArtifact = lipoContextProvider.getSourceArtifactMap().get(afdoImport);
      if (afdoArtifact != null) {
        result.add(afdoArtifact);
      } else {
        ruleContext.ruleError(String.format(
            "cannot find source file '%s' referenced from '%s'", afdoImport, sourceExecPath));
      }
    }
    return result.build();
  }

  /**
   * Returns the imports from the .gcda.imports file of an object file.
   *
   * @param objDirectory the object directory of the object file's target
   * @param objectName the object file
   */
  private Iterable<PathFragment> getImports(PathFragment objDirectory, PathFragment objectName) {
    Preconditions.checkState(lipoMode != LipoMode.OFF);
    Preconditions.checkState(imports != null,
        "Tried to look up imports of uninitialized FDOSupport");
    PathFragment key = objDirectory.getRelative(FileSystemUtils.removeExtension(objectName));
    ImmutableCollection<PathFragment> importsForObject = imports.get(key);
    Preconditions.checkState(importsForObject != null, "Import data missing for %s", key);
    return importsForObject;
  }

  /**
   * Configures a compile action builder by setting up command line options and
   * auxiliary inputs according to the FDO configuration. This method does
   * nothing If FDO is disabled.
   */
  @ThreadSafe
  public void configureCompilation(CppCompileActionBuilder builder,
      CcToolchainFeatures.Variables.Builder buildVariables, RuleContext ruleContext,
      PathFragment sourceName, PathFragment sourceExecPath, boolean usePic,
      FeatureConfiguration featureConfiguration) {
    // It is a bug if this method is called with useLipo if lipo is disabled. However, it is legal
    // if is is called with !useLipo, even though lipo is enabled.
    LipoContextProvider lipoInputProvider = CppHelper.getLipoContextProvider(ruleContext);
    Preconditions.checkArgument(lipoInputProvider == null || lipoMode != LipoMode.OFF);

    // FDO is disabled -> do nothing.
    if ((fdoInstrument == null) && (fdoRoot == null)) {
      return;
    }

    if (featureConfiguration.isEnabled(CppRuleClasses.FDO_INSTRUMENT)) {
      buildVariables.addStringVariable("fdo_instrument_path", fdoInstrument.getPathString());
    }

    // Optimization phase
    if (fdoRoot != null) {
      AnalysisEnvironment env = ruleContext.getAnalysisEnvironment();
      // Declare dependency on contents of zip file.
      if (env.getSkyframeEnv().valuesMissing()) {
        return;
      }
      Iterable<Artifact> auxiliaryInputs = getAuxiliaryInputs(
          ruleContext, env, sourceName, sourceExecPath, usePic, lipoInputProvider);
      builder.addMandatoryInputs(auxiliaryInputs);
      if (!Iterables.isEmpty(auxiliaryInputs)) {
        if (featureConfiguration.isEnabled(CppRuleClasses.AUTOFDO)) {
          buildVariables.addStringVariable(
              "fdo_profile_path", getAutoProfilePath(fdoProfile, fdoRootExecPath).getPathString());
        }
        if (featureConfiguration.isEnabled(CppRuleClasses.FDO_OPTIMIZE)) {
          if (fdoMode == FdoMode.LLVM_FDO) {
            buildVariables.addStringVariable(
                "fdo_profile_path",
                getLLVMProfilePath(fdoProfile, fdoRootExecPath).getPathString());
          } else {
            buildVariables.addStringVariable("fdo_profile_path", fdoRootExecPath.getPathString());
          }
        }
      }
    }
  }

  /**
   * Returns the auxiliary files that need to be added to the {@link CppCompileAction}.
   */
  private Iterable<Artifact> getAuxiliaryInputs(
      RuleContext ruleContext, AnalysisEnvironment env, PathFragment sourceName,
      PathFragment sourceExecPath, boolean usePic, LipoContextProvider lipoContextProvider) {
    // If --fdo_optimize was not specified, we don't have any additional inputs.
    if (fdoProfile == null) {
      return ImmutableSet.of();
    } else if (fdoMode == FdoMode.AUTO_FDO || fdoMode == FdoMode.LLVM_FDO) {
      ImmutableSet.Builder<Artifact> auxiliaryInputs = ImmutableSet.builder();

      PathFragment profileRootRelativePath = fdoMode == FdoMode.LLVM_FDO
          ? getLLVMProfileRootRelativePath(fdoProfile)
          : getAutoProfileRootRelativePath(fdoProfile);
      Artifact artifact = env.getDerivedArtifact(
          fdoPath.getRelative(profileRootRelativePath), fdoRoot);
      env.registerAction(new FdoStubAction(ruleContext.getActionOwner(), artifact));
      auxiliaryInputs.add(artifact);
      if (lipoContextProvider != null) {
        auxiliaryInputs.addAll(getAutoFdoImports(ruleContext, sourceExecPath, lipoContextProvider));
      }
      return auxiliaryInputs.build();
    } else {
      ImmutableSet.Builder<Artifact> auxiliaryInputs = ImmutableSet.builder();

      PathFragment objectName =
          FileSystemUtils.replaceExtension(sourceName, usePic ? ".pic.o" : ".o");

      Label lipoLabel = ruleContext.getLabel();
      auxiliaryInputs.addAll(
          getGcdaArtifactsForObjectFileName(ruleContext, env, objectName, lipoLabel));

      if (lipoContextProvider != null) {
        for (PathFragment importedFile : getImports(
            getNonLipoObjDir(ruleContext, lipoLabel), objectName)) {
          if (CppFileTypes.COVERAGE_DATA.matches(importedFile.getBaseName())) {
            Artifact gcdaArtifact = getGcdaArtifactsForGcdaPath(ruleContext, env, importedFile);
            if (gcdaArtifact == null) {
              ruleContext.ruleError(String.format(
                  ".gcda file %s is not in the FDO zip (referenced by source file %s)",
                  importedFile, sourceName));
            } else {
              auxiliaryInputs.add(gcdaArtifact);
            }
          } else {
            Artifact importedArtifact = lipoContextProvider.getSourceArtifactMap()
                .get(importedFile);
            if (importedArtifact != null) {
              auxiliaryInputs.add(importedArtifact);
            } else {
              ruleContext.ruleError(String.format(
                  "cannot find source file '%s' referenced from '%s'", importedFile, objectName));
            }
          }
        }
      }

      return auxiliaryInputs.build();
    }
  }

  /**
   * Returns the .gcda file artifacts for a .gcda path from the .gcda.imports file or null if the
   * referenced .gcda file is not in the FDO zip.
   */
  private Artifact getGcdaArtifactsForGcdaPath(RuleContext ruleContext,
      AnalysisEnvironment env, PathFragment gcdaPath) {
    if (!gcdaFiles.contains(gcdaPath)) {
      return null;
    }

    Artifact artifact = env.getDerivedArtifact(fdoPath.getRelative(gcdaPath), fdoRoot);
    env.registerAction(new FdoStubAction(ruleContext.getActionOwner(), artifact));
    return artifact;
  }

  private PathFragment getNonLipoObjDir(RuleContext ruleContext, Label label) {
    return ruleContext.getConfiguration().getBinFragment()
        .getRelative(CppHelper.getObjDirectory(label));
  }

  /**
   * Returns a list of .gcda file artifacts for an object file path.
   *
   * <p>The resulting set is either empty (because no .gcda file exists for the
   * given object file) or contains one or two artifacts (the file itself and a
   * symlink to it).
   */
  private ImmutableList<Artifact> getGcdaArtifactsForObjectFileName(RuleContext ruleContext,
      AnalysisEnvironment env, PathFragment objectFileName, Label lipoLabel) {
    // We put the .gcda files relative to the location of the .o file in the instrumentation run.
    String gcdaExt = Iterables.getOnlyElement(CppFileTypes.COVERAGE_DATA.getExtensions());
    PathFragment baseName = FileSystemUtils.replaceExtension(objectFileName, gcdaExt);
    PathFragment gcdaFile = getNonLipoObjDir(ruleContext, lipoLabel).getRelative(baseName);

    if (!gcdaFiles.contains(gcdaFile)) {
      // If the object is a .pic.o file and .pic.gcda is not found, we should try finding .gcda too
      String picoExt = Iterables.getOnlyElement(CppFileTypes.PIC_OBJECT_FILE.getExtensions());
      baseName = FileSystemUtils.replaceExtension(objectFileName, gcdaExt, picoExt);
      if (baseName == null) {
        // Object file is not .pic.o
        return ImmutableList.of();
      }
      gcdaFile = getNonLipoObjDir(ruleContext, lipoLabel).getRelative(baseName);
      if (!gcdaFiles.contains(gcdaFile)) {
        // .gcda file not found
        return ImmutableList.of();
      }
    }

    final Artifact artifact = env.getDerivedArtifact(fdoPath.getRelative(gcdaFile), fdoRoot);
    env.registerAction(new FdoStubAction(ruleContext.getActionOwner(), artifact));

    return ImmutableList.of(artifact);
  }


  private static PathFragment getAutoProfilePath(Path fdoProfile, PathFragment fdoRootExecPath) {
    return fdoRootExecPath.getRelative(getAutoProfileRootRelativePath(fdoProfile));
  }

  private static PathFragment getAutoProfileRootRelativePath(Path fdoProfile) {
    return new PathFragment(fdoProfile.getBaseName());
  }


  private static PathFragment getLLVMProfilePath(Path fdoProfile, PathFragment fdoRootExecPath) {
    return fdoRootExecPath.getRelative(getLLVMProfileRootRelativePath(fdoProfile));
  }

  private static PathFragment getLLVMProfileRootRelativePath(Path fdoProfile) {
    return new PathFragment(fdoProfile.getBaseName());
  }

  /**
   * Returns whether AutoFDO is enabled.
   */
  @ThreadSafe
  public boolean isAutoFdoEnabled() {
    return fdoMode == FdoMode.AUTO_FDO;
  }

  /**
   * Adds the FDO profile output path to the variable builder.
   * If FDO is disabled, no build variable is added.
   */
  @ThreadSafe
  public void getLinkOptions(FeatureConfiguration featureConfiguration,
      CcToolchainFeatures.Variables.Builder buildVariables
      ) {
    if (featureConfiguration.isEnabled(CppRuleClasses.FDO_INSTRUMENT)) {
      buildVariables.addStringVariable("fdo_instrument_path", fdoInstrument.getPathString());
    }
  }

  /**
   * Returns the path of the FDO output tree (relative to the execution root)
   * containing the .gcda profile files, or null if FDO is not enabled.
   */
  @VisibleForTesting
  public PathFragment getFdoOptimizeDir() {
    return fdoRootExecPath;
  }

  /**
   * An exception indicating an issue with FDO coverage files.
   */
  public static final class FdoException extends Exception {
    FdoException(String message) {
      super(message);
    }
  }
}
