// 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.python;

import static net.starlark.java.eval.Starlark.NONE;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.FailAction;
import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
import com.google.devtools.build.lib.actions.extra.PythonInfo;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.OutputGroupInfo;
import com.google.devtools.build.lib.analysis.PseudoAction;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.Util;
import com.google.devtools.build.lib.analysis.platform.ToolchainInfo;
import com.google.devtools.build.lib.analysis.test.InstrumentedFilesCollector;
import com.google.devtools.build.lib.analysis.test.InstrumentedFilesCollector.InstrumentationSpec;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.rules.cpp.CcInfo;
import com.google.devtools.build.lib.rules.cpp.CppFileTypes;
import com.google.devtools.build.lib.server.FailureDetails.FailAction.Code;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
import com.google.devtools.build.lib.util.FileType;
import com.google.devtools.build.lib.util.FileTypeSet;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.protobuf.GeneratedMessage.GeneratedExtension;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Starlark;

/** A helper class for analyzing a Python configured target. */
public final class PyCommon {

  private static final InstrumentationSpec INSTRUMENTATION_SPEC =
      new InstrumentationSpec(FileTypeSet.of(PyRuleClasses.PYTHON_SOURCE))
          .withSourceAttributes("srcs")
          .withDependencyAttributes("deps", "data");

  /** Name of the version attribute. */
  public static final String PYTHON_VERSION_ATTRIBUTE = "python_version";

  /**
   * Returns the Python version based on the {@code python_version} attribute of the given {@code
   * AttributeMap}.
   *
   * <p>It is expected that the attribute is defined, string-typed, and defaults to {@link
   * PythonVersion#_INTERNAL_SENTINEL}. The returned version is the value of {@code python_version}
   * if it is not the sentinel (in which case it is either {@code PY2} or {@code PY3}), or null if
   * it is the sentinel.
   *
   * @throws IllegalArgumentException if the attribute is not present, not string-typed, or not
   *     parsable as a target {@link PythonVersion} value or the sentinel value
   */
  @Nullable
  public static PythonVersion readPythonVersionFromAttribute(AttributeMap attrs) {
    PythonVersion pythonVersionAttr =
        PythonVersion.parseTargetOrSentinelValue(attrs.get(PYTHON_VERSION_ATTRIBUTE, Type.STRING));
    return pythonVersionAttr != PythonVersion._INTERNAL_SENTINEL ? pythonVersionAttr : null;
  }

  /** The context for the target this {@code PyCommon} is helping to analyze. */
  private final RuleContext ruleContext;

  /** The pluggable semantics object with hooks that customizes how analysis is done. */
  private final PythonSemantics semantics;

  /**
   * The Python major version for which this target is being built, as per the {@code
   * python_version} attribute or the configuration.
   *
   * <p>This is always either {@code PY2} or {@code PY3}.
   */
  private final PythonVersion version;

  /**
   * The level of compatibility with Python major versions, as per the {@code srcs_version}
   * attribute.
   */
  private final PythonVersion sourcesVersion;

  /**
   * The Python sources belonging to this target's transitive {@code deps}, not including this
   * target's own {@code srcs}.
   */
  private final NestedSet<Artifact> dependencyTransitivePythonSources;

  /**
   * The Python sources belonging to this target's transitive {@code deps}, including the Python
   * sources in this target's {@code srcs}.
   */
  private final NestedSet<Artifact> transitivePythonSources;

  /**
   * The Python sources from this target's {@code srcs}.
   *
   * <p>This is computed slightly differently than the difference between {@link
   * #dependencyTransitivePythonSources} and {@link transitivePythonSources}; it includes the
   * filesToBuild rather than the prerequisite artifacts.
   */
  // TODO(bazel-team): Can this be simplified to instead just be (transitivePythonSources -
  // dependencyTransitivePythonSources)?
  private final List<Artifact> directPythonSources;

  /** Whether this target or any of its {@code deps} or {@code data} deps has a shared library. */
  private final boolean usesSharedLibraries;

  /** Extra Python module import paths propagated or used by this target. */
  private final NestedSet<String> imports;

  /**
   * Whether any of this target's transitive {@code deps} have PY2-only source files, including this
   * target itself.
   */
  private final boolean hasPy2OnlySources;

  /**
   * Whether any of this target's transitive {@code deps} have PY3-only source files, including this
   * target itself.
   */
  private final boolean hasPy3OnlySources;

  /**
   * Information about the runtime, as obtained from the toolchain.
   *
   * <p>This is non-null only if
   *
   * <ol>
   *   <li>the configuration says to pull the runtime from the toolchain (rather than from the
   *       legacy flags),
   *   <li>the target defines the attribute "$py_toolchain_type" (in which case it MUST also declare
   *       that it requires the Python toolchain type), and
   *   <li>we can successfully read the runtime info from the toolchain provider.
   * </ol>
   */
  @Nullable private final PyRuntimeInfo runtimeFromToolchain;

  // Null if requiresMainFile is false, or the main artifact couldn't be determined.
  @Nullable private Artifact mainArtifact = null;

  private Artifact executable = null;

  private NestedSet<Artifact> filesToBuild = null;

  private static String getOrderErrorMessage(String fieldName, Order expected, Order actual) {
    return String.format(
        "Incompatible order for %s: expected 'default' or '%s', got '%s'",
        fieldName, expected.getStarlarkName(), actual.getStarlarkName());
  }

  // TODO(bazel-team): validateSources is the result of refactoring while preserving
  // legacy behavior across some (but not all) Google-internal uses of PyCommon. Ideally all call
  // sites should be updated to expect the same validation steps.
  public PyCommon(
      RuleContext ruleContext,
      PythonSemantics semantics,
      boolean validateSources,
      boolean requiresMainFile) {
    this.ruleContext = ruleContext;
    this.semantics = semantics;
    this.version = ruleContext.getFragment(PythonConfiguration.class).getPythonVersion();
    this.sourcesVersion = initSrcsVersionAttr(ruleContext);
    this.dependencyTransitivePythonSources = initDependencyTransitivePythonSources(ruleContext);
    this.transitivePythonSources = initTransitivePythonSources(ruleContext);
    this.mainArtifact = requiresMainFile ? initMainArtifact(ruleContext) : null;
    this.directPythonSources =
        initAndMaybeValidateDirectPythonSources(
            ruleContext, semantics, /*validate=*/ validateSources, this.mainArtifact);
    this.usesSharedLibraries = initUsesSharedLibraries(ruleContext);
    this.imports = initImports(ruleContext, semantics);
    this.hasPy2OnlySources = initHasPy2OnlySources(ruleContext, this.sourcesVersion);
    this.hasPy3OnlySources = initHasPy3OnlySources(ruleContext, this.sourcesVersion);
    this.runtimeFromToolchain = initRuntimeFromToolchain(ruleContext, this.version);
    validatePythonVersionAttr();
  }

  /** Returns the parsed value of the "srcs_version" attribute. */
  private static PythonVersion initSrcsVersionAttr(RuleContext ruleContext) {
    String attrValue = ruleContext.attributes().get("srcs_version", Type.STRING);
    try {
      return PythonVersion.parseSrcsValue(attrValue);
    } catch (IllegalArgumentException ex) {
      // Should already have been disallowed in the rule.
      ruleContext.attributeError(
          "srcs_version",
          String.format(
              "'%s' is not a valid value. Expected one of: %s",
              attrValue, Joiner.on(", ").join(PythonVersion.SRCS_STRINGS)));
      return PythonVersion.DEFAULT_SRCS_VALUE;
    }
  }

  private static NestedSet<Artifact> initDependencyTransitivePythonSources(
      RuleContext ruleContext) {
    NestedSetBuilder<Artifact> builder = NestedSetBuilder.compileOrder();
    collectTransitivePythonSourcesFromDeps(ruleContext, builder);
    return builder.build();
  }

  private static NestedSet<Artifact> initTransitivePythonSources(RuleContext ruleContext) {
    NestedSetBuilder<Artifact> builder = NestedSetBuilder.compileOrder();
    collectTransitivePythonSourcesFromDeps(ruleContext, builder);
    builder.addAll(
        ruleContext.getPrerequisiteArtifacts("srcs").filter(PyRuleClasses.PYTHON_SOURCE).list());
    return builder.build();
  }

  /**
   * Gathers transitive .py files from {@code deps} (not including this target's {@code srcs} and
   * adds them to {@code builder}.
   *
   * <p>If a target has the PyInfo provider, the value from that provider is used. Otherwise, we
   * fall back on collecting .py source files from the target's filesToBuild.
   */
  // TODO(bazel-team): Eliminate the fallback behavior by returning an appropriate py provider from
  // the relevant rules.
  private static void collectTransitivePythonSourcesFromDeps(
      RuleContext ruleContext, NestedSetBuilder<Artifact> builder) {
    for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps")) {
      NestedSet<Artifact> sources;
      if (dep.get(PyInfo.PROVIDER) != null) {
        sources = dep.get(PyInfo.PROVIDER).getTransitiveSourcesSet();
      } else {
        sources =
            NestedSetBuilder.<Artifact>compileOrder()
                .addAll(
                    FileType.filter(
                        dep.getProvider(FileProvider.class).getFilesToBuild().toList(),
                        PyRuleClasses.PYTHON_SOURCE))
                .build();
      }
      builder.addTransitive(sources);
    }
  }

  private static List<Artifact> initAndMaybeValidateDirectPythonSources(
      RuleContext ruleContext, PythonSemantics semantics, boolean validate, Artifact mainArtifact) {
    List<Artifact> sourceFiles = new ArrayList<>();
    // TODO(bazel-team): Need to get the transitive deps closure, not just the sources of the rule.
    for (TransitiveInfoCollection src :
        ruleContext.getPrerequisitesIf("srcs", FileProvider.class)) {
      // Make sure that none of the sources contain hyphens.
      if (validate
          && semantics.prohibitHyphensInPackagePaths()
          && Util.containsHyphen(src.getLabel().getPackageFragment())
          // It's ok to have hyphens in main file - usually no one imports it.
          && (mainArtifact == null || !src.getLabel().equals(mainArtifact.getOwnerLabel()))) {
        ruleContext.attributeError(
            "srcs",
            src.getLabel()
                + ": paths to Python sources (besides the main source) may not contain '-'");
      }
      Iterable<Artifact> pySrcs =
          FileType.filter(
              src.getProvider(FileProvider.class).getFilesToBuild().toList(),
              PyRuleClasses.PYTHON_SOURCE);
      Iterables.addAll(sourceFiles, pySrcs);
      if (validate && Iterables.isEmpty(pySrcs)) {
        ruleContext.attributeWarning(
            "srcs", "rule '" + src.getLabel() + "' does not produce any Python source files");
      }
    }
    return sourceFiles;
  }

  /**
   * Returns true if any of this target's {@code deps} or {@code data} deps has a shared library
   * file (e.g. a {@code .so}) in its transitive dependency closure.
   *
   * <p>For targets with the py provider, we consult the {@code uses_shared_libraries} field. For
   * targets without this provider, we look for {@link CppFileTypes#SHARED_LIBRARY}-type files in
   * the filesToBuild.
   */
  private static boolean initUsesSharedLibraries(RuleContext ruleContext) {
    Iterable<? extends TransitiveInfoCollection> targets;
    // The deps attribute must exist for all rule types that use PyCommon, but not necessarily the
    // data attribute.
    if (ruleContext.attributes().has("data")) {
      targets =
          Iterables.concat(
              ruleContext.getPrerequisites("deps"), ruleContext.getPrerequisites("data"));
    } else {
      targets = ruleContext.getPrerequisites("deps");
    }
    for (TransitiveInfoCollection target : targets) {
      if (target.get(PyInfo.PROVIDER) != null) {
        if (target.get(PyInfo.PROVIDER).getUsesSharedLibraries()) {
          return true;
        }
      } else if (FileType.contains(
          target.getProvider(FileProvider.class).getFilesToBuild().toList(),
          CppFileTypes.SHARED_LIBRARY)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns the transitive import paths of a target.
   *
   * <p>For targets with the PyInfo provider, the value from that provider is used. Otherwise, we
   * default to an empty set.
   */
  private static NestedSet<String> initImports(RuleContext ruleContext, PythonSemantics semantics) {
    NestedSetBuilder<String> builder = NestedSetBuilder.compileOrder();
    builder.addAll(semantics.getImports(ruleContext));
    for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps")) {
      NestedSet<String> imports;
      if (dep.get(PyInfo.PROVIDER) != null) {
        imports = dep.get(PyInfo.PROVIDER).getImportsSet();
      } else {
        imports = NestedSetBuilder.emptySet(Order.COMPILE_ORDER);
      }
      if (!builder.getOrder().isCompatible(imports.getOrder())) {
        // TODO(brandjon): We should make order an invariant of the Python provider, and move this
        // check into PyInfo.
        ruleContext.ruleError(
            getOrderErrorMessage("imports", builder.getOrder(), imports.getOrder()));
      } else {
        builder.addTransitive(imports);
      }
    }
    return builder.build();
  }

  /**
   * Returns true if any of {@code deps} has a py provider with {@code has_py2_only_sources} set, or
   * this target has a {@code srcs_version} of {@code PY2} or {@code PY2ONLY}.
   */
  private static boolean initHasPy2OnlySources(
      RuleContext ruleContext, PythonVersion sourcesVersion) {
    if (sourcesVersion == PythonVersion.PY2 || sourcesVersion == PythonVersion.PY2ONLY) {
      return true;
    }
    for (PyInfo depInfo : ruleContext.getPrerequisites("deps", PyInfo.PROVIDER)) {
      if (depInfo.getHasPy2OnlySources()) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns true if any of {@code deps} has a py provider with {@code has_py3_only_sources} set, or
   * this target has {@code srcs_version} of {@code PY3} or {@code PY3ONLY}.
   */
  private static boolean initHasPy3OnlySources(
      RuleContext ruleContext, PythonVersion sourcesVersion) {
    if (sourcesVersion == PythonVersion.PY3 || sourcesVersion == PythonVersion.PY3ONLY) {
      return true;
    }
    for (PyInfo depInfo : ruleContext.getPrerequisites("deps", PyInfo.PROVIDER)) {
      if (depInfo.getHasPy3OnlySources()) {
        return true;
      }
    }
    return false;
  }

  /**
   * Retrieves the {@link PyRuntimeInfo} object in the given field of the given {@link
   * ToolchainInfo}.
   *
   * <p>If the field holds {@code None}, null is returned instead.
   *
   * <p>If the field does not exist on the given {@code ToolchainInfo}, or is not a {@code
   * PyRuntimeInfo} and not {@code None}, an error is reported on the {@code ruleContext} and null
   * is returned.
   *
   * <p>If the {@code PyRuntimeInfo} does not have {@code expectedVersion} as its Python version, an
   * error is reported on the {@code ruleContext} (but the provider is still returned).
   */
  @Nullable
  private static PyRuntimeInfo parseRuntimeField(
      RuleContext ruleContext,
      PythonVersion expectedVersion,
      ToolchainInfo toolchainInfo,
      String field) {
    Object fieldValue;
    try {
      fieldValue = toolchainInfo.getValue(field);
    } catch (EvalException e) {
      ruleContext.ruleError(
          String.format(
              "Error parsing the Python toolchain's ToolchainInfo: Could not retrieve field "
                  + "'%s': %s",
              field, e.getMessage()));
      return null;
    }
    if (fieldValue == null) {
      ruleContext.ruleError(
          String.format(
              "Error parsing the Python toolchain's ToolchainInfo: field '%s' is missing", field));
      return null;
    }
    if (fieldValue == NONE) {
      return null;
    }
    if (!(fieldValue instanceof PyRuntimeInfo)) {
      ruleContext.ruleError(
          String.format(
              "Error parsing the Python toolchain's ToolchainInfo: Expected a PyRuntimeInfo in "
                  + "field '%s', but got '%s'",
              field, Starlark.type(fieldValue)));
      return null;
    }
    PyRuntimeInfo pyRuntimeInfo = (PyRuntimeInfo) fieldValue;
    if (pyRuntimeInfo.getPythonVersion() != expectedVersion) {
      ruleContext.ruleError(
          String.format(
              "Error retrieving the Python runtime from the toolchain: Expected field '%s' to have "
                  + "a runtime with python_version = '%s', but got python_version = '%s'",
              field, expectedVersion.name(), pyRuntimeInfo.getPythonVersion().name()));
    }
    return pyRuntimeInfo;
  }

  /**
   * Returns a {@link PyRuntimeInfo} representing the runtime to use for this target, as retrieved
   * from the resolved Python toolchain.
   *
   * <p>If the configuration says to use the legacy mechanism for obtaining the runtime rather than
   * the toolchain mechanism, OR if this target's rule class does not define the
   * "$py_toolchain_type" attribute, then null is returned. In this case no attempt is made to
   * retrieve any toolchain information, and no errors are reported.
   *
   * <p>Otherwise, the toolchain provider structure is retrieved and validated, and any errors are
   * reported on the rule context. If we're unable to determine the runtime due to an error, or if
   * the toolchain does not specify a runtime for the version of Python we need, null is returned.
   *
   * @throws IllegalArgumentException if the rule class defines the "$py_toolchain_type" attribute
   *     but does not declare a requirement on the toolchain type
   */
  @Nullable
  private static PyRuntimeInfo initRuntimeFromToolchain(
      RuleContext ruleContext, PythonVersion version) {
    if (!shouldGetRuntimeFromToolchain(ruleContext)
        || !ruleContext.attributes().has("$py_toolchain_type", BuildType.NODEP_LABEL)) {
      return null;
    }
    Label toolchainType = ruleContext.attributes().get("$py_toolchain_type", BuildType.NODEP_LABEL);
    ToolchainInfo toolchainInfo = ruleContext.getToolchainContext().forToolchainType(toolchainType);
    Preconditions.checkArgument(
        toolchainInfo != null,
        "Could not retrieve a Python toolchain for '%s' rule",
        ruleContext.getRule().getRuleClass());

    PyRuntimeInfo py2RuntimeInfo =
        parseRuntimeField(ruleContext, PythonVersion.PY2, toolchainInfo, "py2_runtime");
    PyRuntimeInfo py3RuntimeInfo =
        parseRuntimeField(ruleContext, PythonVersion.PY3, toolchainInfo, "py3_runtime");
    Preconditions.checkState(version == PythonVersion.PY2 || version == PythonVersion.PY3);
    PyRuntimeInfo result = version == PythonVersion.PY2 ? py2RuntimeInfo : py3RuntimeInfo;
    if (result == null) {
      ruleContext.ruleError(
          String.format(
              "The Python toolchain does not provide a runtime for Python version %s",
              version.name()));
    }

    // Hack around the fact that the autodetecting Python toolchain, which is automatically
    // registered, does not yet support windows. In this case, we want to return null so that
    // BazelPythonSemantics falls back on --python_path. See toolchain.bzl.
    // TODO(#7844): Remove this hack when the autodetecting toolchain has a windows implementation.
    if (py2RuntimeInfo != null
        && py2RuntimeInfo.getInterpreterPathString() != null
        && py2RuntimeInfo
            .getInterpreterPathString()
            .equals("/_magic_pyruntime_sentinel_do_not_use")) {
      return null;
    }

    return result;
  }

  /**
   * Reports an attribute error if {@code python_version} cannot be parsed as {@code PY2}, {@code
   * PY3}, or the sentinel value.
   *
   * <p>This *should* be enforced by rule attribute validation ({@link
   * Attribute.Builder.allowedValues}), but this check is here to fail-fast just in case.
   */
  private void validatePythonVersionAttr() {
    AttributeMap attrs = ruleContext.attributes();
    if (!attrs.has(PYTHON_VERSION_ATTRIBUTE, Type.STRING)) {
      return;
    }
    String attrValue = attrs.get(PYTHON_VERSION_ATTRIBUTE, Type.STRING);
    try {
      PythonVersion.parseTargetOrSentinelValue(attrValue);
    } catch (IllegalArgumentException ex) {
      ruleContext.attributeError(
          PYTHON_VERSION_ATTRIBUTE,
          String.format("'%s' is not a valid value. Expected either 'PY2' or 'PY3'", attrValue));
    }
  }

  /**
   * If the Python version (as determined by the configuration) is inconsistent with {@link
   * #hasPy2OnlySources} or {@link #hasPy3OnlySources}, emits a {@link FailAction} that "generates"
   * the executable.
   *
   * <p>We use a {@code FailAction} rather than a rule error because we want to defer the error
   * until the execution phase. This way, we still get a configured target that the user can query
   * over with an aspect to find the exact transitive dependency that introduced the offending
   * version constraint. (See {@code <tools repo>//tools/python/srcs_version.bzl%find_requirements})
   *
   * @return true if a {@link FailAction} was created
   */
  private boolean maybeCreateFailActionDueToTransitiveSourcesVersion() {
    String errorTemplate =
        ruleContext.getLabel()
            + ": "
            + "This target is being built for Python %s but (transitively) includes Python %s-only "
            + "sources. You can get diagnostic information about which dependencies introduce this "
            + "version requirement by running the `find_requirements` aspect. For more info see "
            + "the documentation for the `srcs_version` attribute: "
            + semantics.getSrcsVersionDocURL();

    String error = null;
    if (version == PythonVersion.PY2 && hasPy3OnlySources) {
      error = String.format(errorTemplate, "2", "3");
    } else if (version == PythonVersion.PY3 && hasPy2OnlySources) {
      error = String.format(errorTemplate, "3", "2");
    }
    if (error == null) {
      return false;
    } else {
      ruleContext.registerAction(
          new FailAction(
              ruleContext.getActionOwner(),
              ImmutableList.of(executable),
              error,
              Code.INCORRECT_PYTHON_VERSION));
      return true;
    }
  }

  public PythonVersion getVersion() {
    return version;
  }

  public PythonVersion getSourcesVersion() {
    return sourcesVersion;
  }

  /**
   * Returns the transitive Python sources collected from the deps attribute, not including sources
   * from the srcs attribute (unless they were separately reached via deps).
   */
  public NestedSet<Artifact> getDependencyTransitivePythonSources() {
    return dependencyTransitivePythonSources;
  }

  /** Returns the transitive Python sources collected from the deps and srcs attributes. */
  public NestedSet<Artifact> getTransitivePythonSources() {
    return transitivePythonSources;
  }

  public boolean usesSharedLibraries() {
    return usesSharedLibraries;
  }

  public NestedSet<String> getImports() {
    return imports;
  }

  public boolean hasPy2OnlySources() {
    return hasPy2OnlySources;
  }

  public boolean hasPy3OnlySources() {
    return hasPy3OnlySources;
  }

  /**
   * Returns {@code true} if the Python runtime should be obtained from the Python toolchain (as per
   * {@code --incompatible_use_python_toolchains}), as opposed to through the legacy mechanism
   * specified in the {@link PythonSemantics} (e.g., {@code --python_top}).
   */
  public boolean shouldGetRuntimeFromToolchain() {
    return shouldGetRuntimeFromToolchain(ruleContext);
  }

  private static boolean shouldGetRuntimeFromToolchain(RuleContext ruleContext) {
    return ruleContext.getFragment(PythonConfiguration.class).useToolchains();
  }

  /**
   * Returns a {@link PyRuntimeInfo} representing the runtime to use for this target, as retrieved
   * from the resolved toolchain.
   *
   * <p>This may only be called for executable Python rules (rules defining the attribute
   * "$py_toolchain_type", i.e. {@code py_binary} and {@code py_test}). In addition, it may not be
   * called if {@link #shouldGetRuntimeFromToolchain()} returns false.
   *
   * <p>If there was a problem retrieving the runtime information from the toolchain, null is
   * returned. An error would have already been reported on the rule context at {@code PyCommon}
   * initialization time.
   */
  @Nullable
  public PyRuntimeInfo getRuntimeFromToolchain() {
    Preconditions.checkArgument(
        ruleContext.attributes().has("$py_toolchain_type", BuildType.NODEP_LABEL),
        "Cannot retrieve Python toolchain information for '%s' rule",
        ruleContext.getRule().getRuleClass());
    Preconditions.checkArgument(
        shouldGetRuntimeFromToolchain(),
        "Access to the Python toolchain is disabled by --incompatible_use_python_toolchains=false");

    return runtimeFromToolchain;
  }

  public void initBinary(List<Artifact> srcs) {
    Preconditions.checkNotNull(version);

    if (OS.getCurrent() == OS.WINDOWS) {
      executable =
          ruleContext.getImplicitOutputArtifact(ruleContext.getTarget().getName() + ".exe");
    } else {
      executable = ruleContext.createOutputArtifact();
    }

    NestedSetBuilder<Artifact> filesToBuildBuilder =
        NestedSetBuilder.<Artifact>stableOrder().addAll(srcs).add(executable);

    if (ruleContext.getFragment(PythonConfiguration.class).buildPythonZip()) {
      filesToBuildBuilder.add(getPythonZipArtifact(executable));
    } else if (OS.getCurrent() == OS.WINDOWS) {
      // TODO(bazel-team): Here we should check target platform instead of using OS.getCurrent().
      // On Windows, add the python stub launcher in the set of files to build.
      filesToBuildBuilder.add(getPythonStubArtifactForWindows(executable));
    }

    filesToBuild = filesToBuildBuilder.build();

    if (ruleContext.hasErrors()) {
      return;
    }

    addPyExtraActionPseudoAction();
  }

  /**
   * @return an artifact next to the executable file with a given suffix.
   */
  private Artifact getArtifactWithExtension(Artifact executable, String extension) {
    // On Windows, the Python executable has .exe extension on Windows,
    // On Linux, the Python executable has no extension.
    // We can't use ruleContext#getRelatedArtifact because it would mangle files with dots in the
    // name on non-Windows platforms.
    PathFragment pathFragment =
        executable.getOutputDirRelativePath(
            ruleContext.getConfiguration().isSiblingRepositoryLayout());
    String fileName = executable.getFilename();
    if (OS.getCurrent() == OS.WINDOWS) {
      Preconditions.checkArgument(fileName.endsWith(".exe"));
      fileName = fileName.substring(0, fileName.length() - 4) + extension;
    } else {
      fileName = fileName + extension;
    }
    return ruleContext.getDerivedArtifact(pathFragment.replaceName(fileName), executable.getRoot());
  }

  /** Returns an artifact next to the executable file with ".zip" suffix. */
  public Artifact getPythonZipArtifact(Artifact executable) {
    return getArtifactWithExtension(executable, ".zip");
  }

  /**
   * Returns an artifact next to the executable file with ".temp" suffix. Used only if we're
   * building a zip.
   */
  public Artifact getPythonIntermediateStubArtifact(Artifact executable) {
    return getArtifactWithExtension(executable, ".temp");
  }

  /** Returns an artifact next to the executable file with no suffix. Only called for Windows. */
  public Artifact getPythonStubArtifactForWindows(Artifact executable) {
    return ruleContext.getRelatedArtifact(
        executable.getOutputDirRelativePath(
            ruleContext.getConfiguration().isSiblingRepositoryLayout()),
        "");
  }

  /**
   * Adds a PyInfo provider.
   *
   * <p>This is a public method because some rules just want a PyInfo provider without the other
   * things py_library needs.
   */
  public void addPyInfoProvider(RuleConfiguredTargetBuilder builder) {
    builder.addNativeDeclaredProvider(
        PyInfo.builder()
            .setTransitiveSources(transitivePythonSources)
            .setUsesSharedLibraries(usesSharedLibraries)
            .setImports(imports)
            .setHasPy2OnlySources(hasPy2OnlySources)
            .setHasPy3OnlySources(hasPy3OnlySources)
            .build());
  }

  public void addCommonTransitiveInfoProviders(
      RuleConfiguredTargetBuilder builder, NestedSet<Artifact> filesToBuild) {
    addPyInfoProvider(builder);

    // Add PyRuntimeInfo if this is an executable rule.
    if (runtimeFromToolchain != null) {
      builder.addNativeDeclaredProvider(runtimeFromToolchain);
    }

    builder
        .addNativeDeclaredProvider(
            InstrumentedFilesCollector.collect(ruleContext, INSTRUMENTATION_SPEC))
        // Python targets are not really compilable. The best we can do is make sure that all
        // generated source files are ready.
        .addOutputGroup(OutputGroupInfo.FILES_TO_COMPILE, transitivePythonSources)
        .addOutputGroup(OutputGroupInfo.COMPILATION_PREREQUISITES, transitivePythonSources);
  }

  /** Returns a list of the source artifacts */
  public List<Artifact> getPythonSources() {
    return directPythonSources;
  }

  /**
   * Adds a {@link PseudoAction} to the build graph that is only used for providing information to
   * the blaze extra_action feature.
   */
  void addPyExtraActionPseudoAction() {
    if (ruleContext.getConfiguration().getActionListeners().isEmpty()) {
      return;
    }
    registerPyExtraActionPseudoAction(ruleContext, dependencyTransitivePythonSources);
  }

  // Public so that Starlark bindings can access it. Should only be called by PyStarlarkBuiltins.
  // TODO(b/253059598): Remove support for this; https://github.com/bazelbuild/bazel/issues/16455
  public static void registerPyExtraActionPseudoAction(
      RuleContext ruleContext, NestedSet<Artifact> dependencyTransitivePythonSources) {
    ruleContext.registerAction(
        makePyExtraActionPseudoAction(
            ruleContext.getActionOwner(),
            // Has to be unfiltered sources as filtered will give an error for
            // unsupported file types where as certain tests only expect a warning.
            ruleContext.getPrerequisiteArtifacts("srcs").list(),
            // We must not add the files declared in the srcs of this rule.;
            dependencyTransitivePythonSources,
            PseudoAction.getDummyOutput(ruleContext)));
  }

  /**
   * Creates a {@link PseudoAction} that is only used for providing information to the blaze
   * extra_action feature.
   */
  public static Action makePyExtraActionPseudoAction(
      ActionOwner owner,
      List<Artifact> sources,
      NestedSet<Artifact> dependencies,
      Artifact output) {

    PythonInfo info =
        PythonInfo.newBuilder()
            .addAllSourceFile(Artifact.toExecPaths(sources))
            .addAllDepFile(Artifact.toExecPaths(dependencies.toList()))
            .build();

    return new PyPseudoAction(
        owner,
        NestedSetBuilder.<Artifact>stableOrder()
            .addAll(sources)
            .addTransitive(dependencies)
            .build(),
        ImmutableList.of(output),
        "Python",
        PYTHON_INFO,
        info);
  }

  @SerializationConstant @AutoCodec.VisibleForSerialization
  static final GeneratedExtension<ExtraActionInfo, PythonInfo> PYTHON_INFO = PythonInfo.pythonInfo;

  /** Returns the artifact for the main Python source file. */
  private static Artifact initMainArtifact(RuleContext ruleContext) {
    String mainSourceName;
    Rule target = ruleContext.getRule();
    boolean explicitMain = target.isAttributeValueExplicitlySpecified("main");
    if (explicitMain) {
      mainSourceName = ruleContext.attributes().get("main", BuildType.LABEL).getName();
      if (!mainSourceName.endsWith(".py")) {
        ruleContext.attributeError("main", "main must end in '.py'");
      }
    } else {
      String ruleName = target.getName();
      // TODO(blaze-team) Shouldn't the same check apply for all Python targets?
      if (ruleName.endsWith(".py")) {
        ruleContext.attributeError("name", "name must not end in '.py'");
      }
      mainSourceName = ruleName + ".py";
    }
    PathFragment mainSourcePath = PathFragment.create(mainSourceName);

    Artifact mainArtifact = null;
    for (Artifact outItem : ruleContext.getPrerequisiteArtifacts("srcs").list()) {
      if (outItem.getRootRelativePath().endsWith(mainSourcePath)) {
        if (mainArtifact == null) {
          mainArtifact = outItem;
        } else {
          ruleContext.attributeError(
              "srcs",
              buildMultipleMainMatchesErrorText(
                  explicitMain,
                  mainSourceName,
                  mainArtifact.getRunfilesPath().toString(),
                  outItem.getRunfilesPath().toString()));
        }
      }
    }

    if (mainArtifact == null) {
      ruleContext.attributeError("srcs", buildNoMainMatchesErrorText(explicitMain, mainSourceName));
    }

    return mainArtifact;
  }

  /**
   * Returns the path to the main python entry point, or null if this PyCommon was initialized with
   * {@code requiresMainFile} set to false (or there was an error in determining the main artifact).
   */
  @Nullable
  public String determineMainExecutableSource(boolean withWorkspaceName) {
    if (mainArtifact == null) {
      return null;
    }
    if (!withWorkspaceName) {
      return mainArtifact.getRunfilesPath().getPathString();
    }
    PathFragment workspaceName =
        PathFragment.create(ruleContext.getRule().getPackage().getWorkspaceName());
    return workspaceName.getRelative(mainArtifact.getRunfilesPath()).getPathString();
  }

  public String determineMainExecutableSource() {
    return determineMainExecutableSource(true);
  }

  public Artifact getExecutable() {
    return executable;
  }

  public NestedSet<Artifact> getFilesToBuild() {
    return filesToBuild;
  }

  /**
   * Creates the actual executable artifact, i.e., emits a generating action for {@link
   * #getExecutable()}.
   *
   * <p>If there is a transitive sources version conflict, may produce a {@link FailAction} to
   * trigger an execution-time failure. See {@link
   * #maybeCreateFailActionDueToTransitiveSourcesVersion}.
   */
  public void createExecutable(CcInfo ccInfo, Runfiles.Builder defaultRunfilesBuilder)
      throws InterruptedException, RuleErrorException {
    boolean failed = maybeCreateFailActionDueToTransitiveSourcesVersion();
    if (!failed) {
      semantics.createExecutable(ruleContext, this, ccInfo, defaultRunfilesBuilder);
    }
  }

  private static String buildMultipleMainMatchesErrorText(
      boolean explicit, String proposedMainName, String match1, String match2) {
    String errorText;
    if (explicit) {
      errorText =
          String.format(
              "file name '%s' specified by 'main' attribute matches multiple files: e.g., '%s' and"
                  + " '%s'",
              proposedMainName, match1, match2);
    } else {
      errorText =
          String.format(
              "default main file name '%s' matches multiple files.  Perhaps specify an explicit"
                  + " file with 'main' attribute?  Matches were: '%s' and '%s'",
              proposedMainName, match1, match2);
    }
    return errorText;
  }

  private static String buildNoMainMatchesErrorText(boolean explicit, String proposedMainName) {
    String errorText;
    if (explicit) {
      errorText = "could not find '" + proposedMainName + "' as specified by 'main' attribute";
    } else {
      errorText =
          String.format(
              "corresponding default '%s' does not appear in srcs. Add it or override default file"
                  + " name with a 'main' attribute",
              proposedMainName);
    }
    return errorText;
  }

  // Used purely to set the legacy ActionType of the ExtraActionInfo.
  @Immutable
  private static final class PyPseudoAction extends PseudoAction<PythonInfo> {
    private static final UUID ACTION_UUID = UUID.fromString("8d720129-bc1a-481f-8c4c-dbe11dcef319");

    public PyPseudoAction(
        ActionOwner owner,
        NestedSet<Artifact> inputs,
        Collection<Artifact> outputs,
        String mnemonic,
        GeneratedExtension<ExtraActionInfo, PythonInfo> infoExtension,
        PythonInfo info) {
      super(ACTION_UUID, owner, inputs, outputs, mnemonic, infoExtension, info);
    }
  }
}
