// Copyright 2015 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.bazel.rules.android;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.RuleDefinition;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.AndroidNdkCrosstools;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.AndroidNdkCrosstools.NdkCrosstoolsException;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.ApiLevel;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.NdkMajorRevision;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.NdkPaths;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.NdkRelease;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.StlImpl;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.StlImpls;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.StlImpls.GnuLibStdCppStlImpl;
import com.google.devtools.build.lib.bazel.rules.android.ndkcrosstools.StlImpls.LibCppStlImpl;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.rules.repository.RepositoryDirectoryValue;
import com.google.devtools.build.lib.rules.repository.WorkspaceAttributeMapper;
import com.google.devtools.build.lib.skyframe.DirectoryListingValue;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.util.ResourceFileLoader;
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.Root;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.CToolchain;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.CompilationMode;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.CompilationModeFlags;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.CrosstoolRelease;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.ToolPath;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/** Implementation of the {@code android_ndk_repository} rule. */
public class AndroidNdkRepositoryFunction extends AndroidRepositoryFunction {

  private static final String TOOLCHAIN_NAME_PREFIX = "toolchain-";
  private static final String PATH_ENV_VAR = "ANDROID_NDK_HOME";
  private static final PathFragment PLATFORMS_DIR = PathFragment.create("platforms");

  private static final ImmutableList<String> PATH_ENV_VAR_AS_LIST = ImmutableList.of(PATH_ENV_VAR);

  private static String getDefaultCrosstool(Integer majorRevision) {
    // From NDK 17, libc++ replaces gnu-libstdc++ as the default STL.
    return majorRevision <= 16 ? GnuLibStdCppStlImpl.NAME : LibCppStlImpl.NAME;
  }

  private static PathFragment getAndroidNdkHomeEnvironmentVar(
      Path workspace, Map<String, String> env) {
    return workspace.getRelative(PathFragment.create(env.get(PATH_ENV_VAR))).asFragment();
  }

  private static String createBuildFile(
      String ruleName, String defaultCrosstool, List<CrosstoolStlPair> crosstools) {

    String buildFileTemplate = getTemplate("android_ndk_build_file_template.txt");
    String ccToolchainSuiteTemplate = getTemplate("android_ndk_cc_toolchain_suite_template.txt");
    String ccToolchainTemplate = getTemplate("android_ndk_cc_toolchain_template.txt");
    String stlFilegroupTemplate = getTemplate("android_ndk_stl_filegroup_template.txt");
    String vulkanValidationLayersTemplate =
        getTemplate("android_ndk_vulkan_validation_layers_template.txt");
    String miscLibrariesTemplate = getTemplate("android_ndk_misc_libraries_template.txt");

    StringBuilder ccToolchainSuites = new StringBuilder();
    StringBuilder ccToolchainRules = new StringBuilder();
    StringBuilder stlFilegroups = new StringBuilder();
    StringBuilder vulkanValidationLayers = new StringBuilder();
    for (CrosstoolStlPair crosstoolStlPair : crosstools) {

      // Create the cc_toolchain_suite rule
      CrosstoolRelease crosstool = crosstoolStlPair.crosstoolRelease;

      StringBuilder toolchainMap = new StringBuilder();
      for (CToolchain toolchain : crosstool.getToolchainList()) {
        toolchainMap.append(
            String.format(
                "      '%s': ':%s',\n      '%s|%s': ':%s',\n",
                toolchain.getTargetCpu(),
                toolchain.getToolchainIdentifier(),
                toolchain.getTargetCpu(),
                toolchain.getCompiler(),
                toolchain.getToolchainIdentifier()));
      }

      String toolchainName = createToolchainName(crosstoolStlPair.stlImpl.getName());

      ccToolchainSuites.append(
          ccToolchainSuiteTemplate
              .replace("%toolchainName%", toolchainName)
              .replace("%toolchainMap%", toolchainMap.toString().trim())
              .replace("%crosstoolReleaseProto%", crosstool.toString()));

      // Create the cc_toolchain rules
      for (CToolchain toolchain : crosstool.getToolchainList()) {
        ccToolchainRules.append(
            createCcToolchainRule(
                ccToolchainTemplate, crosstoolStlPair.stlImpl.getName(), toolchain));
      }

      // Create the STL file group rules
      for (Map.Entry<String, String> entry :
          crosstoolStlPair.stlImpl.getFilegroupNamesAndFilegroupFileGlobPatterns().entrySet()) {

        stlFilegroups.append(
            stlFilegroupTemplate
                .replace("%name%", entry.getKey())
                .replace("%fileGlobPattern%", entry.getValue()));
      }

      // Create the Vulkan validation layers libraries
      for (CToolchain toolchain : crosstool.getToolchainList()) {
        vulkanValidationLayers.append(
            vulkanValidationLayersTemplate
                .replace("%toolchainName%", toolchain.getToolchainIdentifier())
                .replace("%cpu%", toolchain.getTargetCpu()));
      }
    }

    return buildFileTemplate
        .replace("%ruleName%", ruleName)
        .replace("%defaultCrosstool%", "//:toolchain-" + defaultCrosstool)
        .replace("%ccToolchainSuites%", ccToolchainSuites)
        .replace("%ccToolchainRules%", ccToolchainRules)
        .replace("%stlFilegroups%", stlFilegroups)
        .replace("%vulkanValidationLayers%", vulkanValidationLayers)
        .replace("%miscLibraries%", miscLibrariesTemplate);
  }

  static String createToolchainName(String stlName) {
    return TOOLCHAIN_NAME_PREFIX + stlName;
  }

  private static String createCcToolchainRule(
      String ccToolchainTemplate, String version, CToolchain toolchain) {

    // TODO(bazel-team): It's unfortunate to have to extract data from a CToolchain proto like this.
    // It would be better to have a higher-level construction (like an AndroidToolchain class)
    // from which the CToolchain proto and rule information needed here can be created.
    // Alternatively it would be nicer to just be able to glob the entire NDK and add that one glob
    // to each cc_toolchain rule, and then the complexities in the method and the templates can
    // go away, but globbing the entire NDK takes ~60 seconds, mostly because of MD5ing all the
    // binary files in the NDK (eg the .so / .a / .o files).

    // This also includes the files captured with cxx_builtin_include_directory.
    // Use gcc specifically because clang toolchains will have both gcc and llvm toolchain paths,
    // but the gcc tool will actually be clang.
    ToolPath gcc = null;
    for (ToolPath toolPath : toolchain.getToolPathList()) {
      if ("gcc".equals(toolPath.getName())) {
        gcc = toolPath;
      }
    }
    checkNotNull(gcc, "gcc not found in crosstool toolpaths");
    String toolchainDirectory = NdkPaths.getToolchainDirectoryFromToolPath(gcc.getPath());

    // Create file glob patterns for the various files that the toolchain references.

    String androidPlatformIncludes =
        NdkPaths.stripRepositoryPrefix(toolchain.getBuiltinSysroot()) + "/**/*";

    List<String> toolchainFileGlobPatterns = new ArrayList<>();
    toolchainFileGlobPatterns.add(androidPlatformIncludes);

    for (String cxxFlag : toolchain.getUnfilteredCxxFlagList()) {
      if (!cxxFlag.startsWith("-")) { // Skip flag names
        toolchainFileGlobPatterns.add(NdkPaths.stripRepositoryPrefix(cxxFlag) + "/**/*");
      }
    }

    // For NDK 15 and up. Unfortunately, the toolchain does not encode the NDK revision number.
    toolchainFileGlobPatterns.add("ndk/sysroot/**/*");

    // If this is a clang toolchain, also add the corresponding gcc toolchain to the globs.
    int gccToolchainIndex = toolchain.getCompilerFlagList().indexOf("-gcc-toolchain");
    if (gccToolchainIndex > -1) {
      String gccToolchain = toolchain.getCompilerFlagList().get(gccToolchainIndex + 1);
      toolchainFileGlobPatterns.add(NdkPaths.stripRepositoryPrefix(gccToolchain) + "/**/*");
    }

    StringBuilder toolchainFileGlobs = new StringBuilder();
    for (String toolchainFileGlobPattern : toolchainFileGlobPatterns) {
      toolchainFileGlobs.append(String.format("        \"%s\",\n", toolchainFileGlobPattern));
    }

    return ccToolchainTemplate
        .replace("%toolchainName%", toolchain.getToolchainIdentifier())
        .replace("%cpu%", toolchain.getTargetCpu())
        .replace("%platform_cpu%", getPlatformCpuLabel(toolchain.getTargetCpu()))
        .replace("%compiler%", toolchain.getCompiler())
        .replace("%version%", version)
        .replace("%dynamicRuntimeLibs%", toolchain.getDynamicRuntimesFilegroup())
        .replace("%staticRuntimeLibs%", toolchain.getStaticRuntimesFilegroup())
        .replace("%toolchainDirectory%", toolchainDirectory)
        .replace("%toolchainFileGlobs%", toolchainFileGlobs.toString().trim());
  }

  private static String getPlatformCpuLabel(String targetCpu) {
    // Create a mapping of CcToolchain CPU values to platform arch constraint values
    // in @bazel_tools//platforms
    switch (targetCpu) {
      case "x86":
        return "x86_32";
      case "armeabi-v7a":
        return "arm";
      case "arm64-v8a":
        return "aarch64";
      default:
        return "x86_64";
    }
  }

  private static String getTemplate(String templateFile) {
    try {
      return ResourceFileLoader.loadResource(AndroidNdkRepositoryFunction.class, templateFile);
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }
  }

  @Override
  public boolean isLocal(Rule rule) {
    return true;
  }

  @Override
  public boolean verifyMarkerData(Rule rule, Map<String, String> markerData, Environment env)
      throws InterruptedException {
    WorkspaceAttributeMapper attributes = WorkspaceAttributeMapper.of(rule);
    if (attributes.isAttributeValueExplicitlySpecified("path")) {
      return true;
    }
    return super.verifyEnvironMarkerData(markerData, env, PATH_ENV_VAR_AS_LIST);
  }

  @Override
  public RepositoryDirectoryValue.Builder fetch(
      Rule rule,
      Path outputDirectory,
      BlazeDirectories directories,
      Environment env,
      Map<String, String> markerData,
      SkyKey key)
      throws InterruptedException, RepositoryFunctionException {
    Map<String, String> environ =
        declareEnvironmentDependencies(markerData, env, PATH_ENV_VAR_AS_LIST);
    if (environ == null) {
      return null;
    }
    prepareLocalRepositorySymlinkTree(rule, outputDirectory);
    WorkspaceAttributeMapper attributes = WorkspaceAttributeMapper.of(rule);
    PathFragment pathFragment;
    String userDefinedPath = null;
    if (attributes.isAttributeValueExplicitlySpecified("path")) {
      userDefinedPath = getPathAttr(rule);
      pathFragment = getTargetPath(userDefinedPath, directories.getWorkspace());
    } else if (environ.get(PATH_ENV_VAR) != null) {
      userDefinedPath = environ.get(PATH_ENV_VAR);
      pathFragment = getAndroidNdkHomeEnvironmentVar(directories.getWorkspace(), environ);
    } else {
      throw new RepositoryFunctionException(
          new EvalException(
              rule.getLocation(),
              "Either the path attribute of android_ndk_repository or the ANDROID_NDK_HOME "
                  + " environment variable must be set."),
          Transience.PERSISTENT);
    }

    Path ndkSymlinkTreeDirectory = outputDirectory.getRelative("ndk");
    try {
      ndkSymlinkTreeDirectory.createDirectory();
    } catch (IOException e) {
      throw new RepositoryFunctionException(e, Transience.TRANSIENT);
    }

    Path ndkHome = directories.getOutputBase().getFileSystem().getPath(pathFragment);
    if (!symlinkLocalRepositoryContents(ndkSymlinkTreeDirectory, ndkHome, userDefinedPath)) {
      return null;
    }

    String ruleName = rule.getName();

    // We need to fetch the NDK release info from the actual home to avoid cycle in the
    // dependency graph (the path relative to the repository root depends on the
    // repository being fetched).
    NdkRelease ndkRelease = getNdkRelease(ndkHome, env);
    if (env.valuesMissing()) {
      return null;
    }

    String apiLevelString;
    if (attributes.isAttributeValueExplicitlySpecified("api_level")) {
      try {
        apiLevelString = attributes.get("api_level", Type.INTEGER).toString();
      } catch (EvalException e) {
        throw new RepositoryFunctionException(e, Transience.PERSISTENT);
      }
    } else {
      DirectoryListingValue platformsDirectoryValue =
          getDirectoryListing(ndkHome, PLATFORMS_DIR, env);
      if (platformsDirectoryValue == null) {
        return null;
      }

      ImmutableSortedSet<Integer> apiLevels = getApiLevels(platformsDirectoryValue.getDirents());
      if (apiLevels.isEmpty()) {
        // Every Android NDK to date ships with multiple api levels, so the only reason that this
        // would be empty is if the user is not pointing to a standard NDK or has tinkered with it
        // themselves.
        throwInvalidPathException(
            ndkHome,
            new EvalException(
                rule.getLocation(),
                "android_ndk_repository requires that at least one Android platform is present in "
                    + "the Android NDK platforms directory."));
      }
      apiLevelString = apiLevels.first().toString();
    }

    // NDK minor revisions should be backwards compatible within a major revision, the crosstools
    // we generate don't care about the minor revision.
    NdkMajorRevision ndkMajorRevision;
    if (!ndkRelease.isValid) {
      String warningMessage =
          String.format(
              "The revision of the Android NDK referenced by android_ndk_repository rule '%s' "
                  + "could not be determined (the revision string found is '%s'). "
                  + "Bazel will attempt to treat the NDK as if it was r%s. This may cause "
                  + "compilation and linkage problems. Please download a supported NDK version.\n",
              ruleName,
              ndkRelease.rawRelease,
              AndroidNdkCrosstools.LATEST_KNOWN_REVISION.getKey());
      env.getListener().handle(Event.warn(warningMessage));
      ndkMajorRevision = AndroidNdkCrosstools.LATEST_KNOWN_REVISION.getValue();
    } else if (!AndroidNdkCrosstools.isKnownNDKRevision(ndkRelease)) {
      String warningMessage =
          String.format(
              "The major revision of the Android NDK referenced by android_ndk_repository rule "
                  + "'%s' is %s. The major revisions supported by Bazel are %s. Bazel will attempt "
                  + "to treat the NDK as if it was r%s. This may cause compilation and linkage "
                  + "problems. Please download a supported NDK version.\n",
              ruleName,
              ndkRelease.majorRevision,
              AndroidNdkCrosstools.KNOWN_NDK_MAJOR_REVISIONS.keySet(),
              AndroidNdkCrosstools.LATEST_KNOWN_REVISION.getKey());
      env.getListener().handle(Event.warn(warningMessage));
      ndkMajorRevision = AndroidNdkCrosstools.LATEST_KNOWN_REVISION.getValue();
    } else {
      ndkMajorRevision =
          AndroidNdkCrosstools.KNOWN_NDK_MAJOR_REVISIONS.get(ndkRelease.majorRevision);
    }

    ApiLevel apiLevel = ndkMajorRevision.apiLevel(env.getListener(), ruleName, apiLevelString);

    ImmutableList.Builder<CrosstoolStlPair> crosstoolsAndStls = ImmutableList.builder();
    try {

      String hostPlatform = AndroidNdkCrosstools.getHostPlatform(ndkRelease);
      NdkPaths ndkPaths = new NdkPaths(ruleName, hostPlatform, apiLevel, ndkRelease.majorRevision);

      for (StlImpl stlImpl : StlImpls.get(ndkPaths, ndkRelease.majorRevision)) {
        CrosstoolRelease crosstoolRelease =
            ndkMajorRevision.crosstoolRelease(ndkPaths, stlImpl, hostPlatform);
        crosstoolsAndStls.add(new CrosstoolStlPair(crosstoolRelease, stlImpl));
      }

    } catch (NdkCrosstoolsException e) {
      throw new RepositoryFunctionException(new IOException(e), Transience.PERSISTENT);
    }

    String defaultCrosstool = getDefaultCrosstool(ndkRelease.majorRevision);

    ImmutableList<CrosstoolStlPair> crosstoolStlPairs = crosstoolsAndStls.build();
    String buildFile = createBuildFile(ruleName, defaultCrosstool, crosstoolStlPairs);
    writeBuildFile(outputDirectory, buildFile);
    ImmutableList.Builder<String> bigConditional = ImmutableList.builder();
    for (CrosstoolStlPair pair : crosstoolStlPairs) {
      for (CToolchain toolchain : pair.crosstoolRelease.getToolchainList()) {
        bigConditional.addAll(generateBzlConfigFor(pair.stlImpl.getName(), toolchain));
      }
    }
    writeFile(
        outputDirectory,
        "cc_toolchain_config.bzl",
        getTemplate("android_ndk_cc_toolchain_config_template.txt")
            .replaceAll(
                "%big_conditional_populating_variables%",
                Joiner.on("\n" + "    ").join(bigConditional.build())));
    return RepositoryDirectoryValue.builder().setPath(outputDirectory);
  }

  private ImmutableList<String> generateBzlConfigFor(String version, CToolchain toolchain) {
    ImmutableList.Builder<String> bigConditional = ImmutableList.builder();
    String cpu = toolchain.getTargetCpu();
    String compiler = toolchain.getCompiler();

    Preconditions.checkArgument(
        toolchain.getLinkingModeFlagsCount() == 0, "linking_mode_flags not supported.");
    Preconditions.checkArgument(
        toolchain.getActionConfigCount() == 0, "action_configs not supported.");
    Preconditions.checkArgument(toolchain.getFeatureCount() == 0, "features not supported.");
    Preconditions.checkArgument(toolchain.getArFlagCount() == 0, "ar_flags not supported.");
    Preconditions.checkArgument(
        toolchain.getArtifactNamePatternCount() == 0, "artifact_name_patterns not supported.");
    Preconditions.checkArgument(toolchain.getCxxFlagCount() == 0, "cxx_flags not supported.");
    Preconditions.checkArgument(
        toolchain.getDynamicLibraryLinkerFlagCount() == 0,
        "dynamic_library_linker_flags not supported.");
    Preconditions.checkArgument(
        toolchain.getLdEmbedFlagCount() == 0, "ld_embed_flags not supported.");
    Preconditions.checkArgument(
        toolchain.getObjcopyEmbedFlagCount() == 0, "objcopy_embed_flags not supported.");
    Preconditions.checkArgument(
        toolchain.getMakeVariableCount() == 0, "make_variables not supported.");
    Preconditions.checkArgument(
        toolchain.getTestOnlyLinkerFlagCount() == 0, "test_only_linker_flags not supported.");

    CompilationModeFlags fastbuild = null;
    CompilationModeFlags dbg = null;
    CompilationModeFlags opt = null;
    for (CompilationModeFlags flags : toolchain.getCompilationModeFlagsList()) {
      Preconditions.checkArgument(
          flags.getCxxFlagCount() == 0, "compilation_mode_flags.cxx_flags not supported.");
      Preconditions.checkArgument(
          flags.getLinkerFlagCount() == 0, "compilation_mode_flags.linker_flags not supported.");
      if (flags.getMode().equals(CompilationMode.FASTBUILD)) {
        fastbuild = flags;
      } else if (flags.getMode().equals(CompilationMode.DBG)) {
        dbg = flags;
      } else if (flags.getMode().equals(CompilationMode.OPT)) {
        opt = flags;
      }
    }

    bigConditional.add(
        String.format(
            "if cpu == '%s' and compiler == '%s' and version == '%s':", cpu, compiler, version),
        String.format(
            "  default_compile_flags = [%s]",
            toSequenceOfStarlarkStrings(toolchain.getCompilerFlagList())),
        String.format(
            "  unfiltered_compile_flags = [%s]",
            toSequenceOfStarlarkStrings(toolchain.getUnfilteredCxxFlagList())),
        String.format(
            "  default_link_flags = [%s]",
            toSequenceOfStarlarkStrings(toolchain.getLinkerFlagList())),
        String.format(
            "  default_fastbuild_flags = [%s]",
            toSequenceOfStarlarkStrings(
                fastbuild != null ? fastbuild.getCompilerFlagList() : ImmutableList.of())),
        String.format(
            "  default_dbg_flags = [%s]",
            toSequenceOfStarlarkStrings(
                dbg != null ? dbg.getCompilerFlagList() : ImmutableList.of())),
        String.format(
            "  default_opt_flags = [%s]",
            toSequenceOfStarlarkStrings(
                opt != null ? opt.getCompilerFlagList() : ImmutableList.of())),
        String.format(
            "  cxx_builtin_include_directories = [%s]",
            toSequenceOfStarlarkStrings(toolchain.getCxxBuiltinIncludeDirectoryList())),
        String.format("  target_cpu = '%s'", toolchain.getTargetCpu()),
        String.format("  toolchain_identifier = '%s'", toolchain.getToolchainIdentifier()),
        String.format("  host_system_name = '%s'", toolchain.getHostSystemName()),
        String.format("  target_system_name = '%s'", toolchain.getTargetSystemName()),
        String.format("  target_libc = '%s'", toolchain.getTargetLibc()),
        String.format("  target_compiler = '%s'", toolchain.getCompiler()),
        String.format("  abi_version = '%s'", toolchain.getAbiVersion()),
        String.format("  abi_libc_version = '%s'", toolchain.getAbiLibcVersion()),
        String.format("  builtin_sysroot = '%s'", toolchain.getBuiltinSysroot()));
    bigConditional.addAll(
        toolchain.getToolPathList().stream()
            .map(
                tp ->
                    String.format(
                        "  %s_path = '%s'",
                        tp.getName().toLowerCase().replaceAll("-", "_"), tp.getPath()))
            .collect(ImmutableList.toImmutableList()));
    return bigConditional.add("").build();
  }

  private String toSequenceOfStarlarkStrings(Iterable<String> flags) {
    return "'" + Joiner.on("', '").join(flags) + "'";
  }

  @Override
  public Class<? extends RuleDefinition> getRuleDefinition() {
    return AndroidNdkRepositoryRule.class;
  }

  private NdkRelease getNdkRelease(Path directory, Environment env)
      throws RepositoryFunctionException, InterruptedException {

    // For NDK r11+
    Path releaseFilePath = directory.getRelative("source.properties");
    if (!releaseFilePath.exists()) {
      // For NDK r10e
      releaseFilePath = directory.getRelative("RELEASE.TXT");
    }

    SkyKey releaseFileKey =
        FileValue.key(RootedPath.toRootedPath(Root.fromPath(directory), releaseFilePath));

    String releaseFileContent = "";
    try {
      env.getValueOrThrow(releaseFileKey, IOException.class);

      releaseFileContent =
          new String(FileSystemUtils.readContent(releaseFilePath), StandardCharsets.UTF_8);
    } catch (IOException e) {
      throwInvalidPathException(
          directory,
          new IOException(
              "Could not read "
                  + releaseFilePath.getBaseName()
                  + " in Android NDK: "
                  + e.getMessage()));
    }

    return NdkRelease.create(releaseFileContent.trim());
  }

  @Override
  protected void throwInvalidPathException(Path path, Exception e)
      throws RepositoryFunctionException {
    throw new RepositoryFunctionException(
        new IOException(
            String.format(
                "%s Unable to read the Android NDK at %s, the path may be invalid. Is "
                    + "the path in android_ndk_repository() or %s set correctly? If the path is "
                    + "correct, the contents in the Android NDK directory may have been modified.",
                e.getMessage(), path, PATH_ENV_VAR),
            e),
        Transience.PERSISTENT);
  }

  private static final class CrosstoolStlPair {

    private final CrosstoolRelease crosstoolRelease;
    private final StlImpl stlImpl;

    private CrosstoolStlPair(CrosstoolRelease crosstoolRelease, StlImpl stlImpl) {
      this.crosstoolRelease = crosstoolRelease;
      this.stlImpl = stlImpl;
    }
  }
}
