// Copyright 2020 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.analysis.constraints;

import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironmentsProvider.RemovedEnvironmentCulprit;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.EnvironmentGroup;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.server.FailureDetails.Analysis;
import com.google.devtools.build.lib.server.FailureDetails.Analysis.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.skyframe.DetailedException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Implementation of the semantics of Bazel's constraint specification and enforcement system.
 *
 * <p>This is how the system works:
 *
 * <p>All build rules can declare which "static environments" they can be built for, where a "static
 * environment" is a label instance of an {@link EnvironmentRule} rule declared in a BUILD file.
 * There are various ways to do this:
 *
 * <ul>
 *   <li>Through a "restricted to" attribute setting ({@link
 *       com.google.devtools.build.lib.packages.RuleClass#RESTRICTED_ENVIRONMENT_ATTR}). This is the
 *       most direct form of specification - it declares the exact set of environments the rule
 *       supports (for its group - see precise details below).
 *   <li>Through a "compatible with" attribute setting ({@link
 *       com.google.devtools.build.lib.packages.RuleClass#COMPATIBLE_ENVIRONMENT_ATTR}. This
 *       declares <b>additional</b> environments a rule supports in addition to "standard"
 *       environments that are supported by default (see below).
 *   <li>Through "default" specifications in {@link EnvironmentGroup} rules. Every environment
 *       belongs to a group of thematically related peers (e.g. "target architectures", "JDK
 *       versions", or "mobile devices"). An environment group's definition includes which of these
 *       environments should be supported "by default" if not otherwise specified by one of the
 *       above mechanisms. In particular, a rule with no environment-related attributes
 *       automatically inherits all defaults.
 *   <li>Through a rule class default ({@link
 *       com.google.devtools.build.lib.packages.RuleClass.Builder#restrictedTo} and {@link
 *       com.google.devtools.build.lib.packages.RuleClass.Builder#compatibleWith}). This overrides
 *       global defaults for all instances of the given rule class. This can be used, for example,
 *       to make all *_test rules "testable" without each instance having to explicitly declare this
 *       capability.
 * </ul>
 *
 * <p>Groups exist to model the idea that some environments are related while others have nothing to
 * do with each other. Say, for example, we want to say a rule works for PowerPC platforms but not
 * x86. We can do so by setting its "restricted to" attribute to {@code ['//sample/path:powerpc']}.
 * Because both PowerPC and x86 are in the same "target architectures" group, this setting removes
 * x86 from the set of supported environments. But since JDK support belongs to its own group ("JDK
 * versions") it says nothing about which JDK the rule supports.
 *
 * <p>More precisely, if a rule has a "restricted to" value of [A, B, C], this removes support for
 * all default environments D such that group(D) is in [group(A), group(B), group(C)] AND D is not
 * in [A, B, C] (in other words, D isn't explicitly opted back in). The rule's full set of supported
 * environments thus becomes [A, B, C] + all defaults that belong to unrelated groups.
 *
 * <p>If the rule has a "compatible with" value of [E, F, G], these are unconditionally added to its
 * set of supported environments (in addition to the results from above).
 *
 * <p>An environment may not appear in both a rule's "restricted to" and "compatible with" values.
 * If two environments belong to the same group, they must either both be in "restricted to", both
 * be in "compatible with", or not explicitly specified.
 *
 * <p>Given all the above, constraint enforcement is this: rule A can depend on rule B if, for every
 * static environment A supports, B also supports that environment.
 *
 * <p>Configurable attributes introduce the additional concept of "refined environments". Given:
 *
 * <pre>
 *   java_library(
 *       name = "lib",
 *       restricted_to = [":A", ":B"],
 *       deps = select({
 *           ":config_a": [":depA"],
 *           ":config_b": [":depB"],
 *       }))
 *   java_library(
 *       name = "depA",
 *       restricted_to = [":A"])
 *   java_library(
 *       name = "depB",
 *       restricted_to = [":B"])
 * </pre>
 *
 * "lib"'s static environments are what are declared via restricted_to: {@code [":A", ":B"]}. But
 * normal constraint checking doesn't work well here: neither "depA" or "depB" supports both
 * environments, so each is technically invalid. But the two of them together <i>do</i> support both
 * environments. So constraint checking with selects checks that "lib"'s environments are supported
 * by the <i>union</i> of its selectable dependencies, then <i>refines</i> its environments to
 * whichever deps get chosen. In other words:
 *
 * <ol>
 *   <li>The above example is considered constraint-valid.
 *   <li>When building with "config_a", "lib"'s refined environment set is {@code [":A"]}.
 *   <li>When building with "config_b", "lib"'s refined environment set is {@code [":B"]}.
 *   <li>Any rule depending on "lib" has its environments refined by the intersection with "lib". So
 *       if "depender" has {@code restricted_to = [":A", ":B"]} and {@code deps = [":lib"]}, then
 *       when building with "config_a", "depender"'s refined environment set is {@code [":A"]}.
 *   <li>For each environment group, every rule's refined environment set must be non-empty. This
 *       ensures the "chosen" dep in a select matches all rules up the dependency chain. So if
 *       "depender" had {@code restricted_to = [":B"]}, it wouldn't be allowed in a "config_a"
 *       build.
 * </ol>
 *
 * </code>.
 *
 * @param <T> The type of object to check for constraints.
 */
public interface ConstraintSemantics<T> {

  /**
   * Returns the environment group that owns the given environment. Both must belong to the same
   * package.
   *
   * @throws EnvironmentLookupException if the input is not an {@link EnvironmentRule} or no
   *     matching group is found
   */
  static EnvironmentGroup getEnvironmentGroup(Target envTarget) throws EnvironmentLookupException {
    if (!(envTarget instanceof Rule)
        || !((Rule) envTarget).getRuleClass().equals(ConstraintConstants.ENVIRONMENT_RULE)) {
      throw createEnvironmentLookupException(
          envTarget.getLabel() + " is not a valid environment definition",
          Code.INVALID_ENVIRONMENT);
    }
    for (EnvironmentGroup group : envTarget.getPackage().getTargets(EnvironmentGroup.class)) {
      if (group.getEnvironments().contains(envTarget.getLabel())) {
        return group;
      }
    }
    throw createEnvironmentLookupException(
        "cannot find the group for environment " + envTarget.getLabel(),
        Code.ENVIRONMENT_MISSING_FROM_GROUPS);
  }

  /**
   * Returns the set of environments this rule supports.
   *
   * <p>Note this set is <b>not complete</b> - it doesn't include environments from groups we don't
   * "know about". Environments and groups can be declared in any package. If the rule includes no
   * references to that package, then it simply doesn't know anything about them. But the constraint
   * semantics say the rule should support the defaults for that group. We encode this implicitly:
   * given the returned set, for any group that's not in the set the rule is also considered to
   * support that group's defaults.
   *
   * @param context analysis context for the rule. A rule error is triggered here if invalid
   *     constraint settings are discovered.
   * @return the environments this rule supports, not counting defaults "unknown" to this rule as
   *     described above. Returns null if any errors are encountered.
   */
  @Nullable
  EnvironmentCollection getSupportedEnvironments(T context);

  /**
   * Performs constraint checking on the given rule's dependencies and reports any errors. This
   * includes:
   *
   * <ul>
   *   <li>Static environment checking: if this rule supports environment E, all deps outside
   *       selects must also support E
   *   <li>Refined environment computation: this rule's refined environments are its static
   *       environments intersected with the refined environments of all dependencies (including
   *       chosen deps in selects)
   *   <li>Refined environment checking: no environment groups can be "emptied" due to refinement
   * </ul>
   *
   * @param context the rule to analyze
   * @param staticEnvironments the rule's supported environments, as defined by the return value of
   *     {@link #getSupportedEnvironments}. In particular, for any environment group that's not in
   *     this collection, the rule is assumed to support the defaults for that group.
   * @param refinedEnvironments a builder for populating this rule's refined environments
   * @param removedEnvironmentCulprits a builder for populating the core dependencies that trigger
   *     pruning away environments through refinement. If multiple dependencies qualify (e.g. two
   *     direct deps under the current rule), one is arbitrarily chosen.
   */
  void checkConstraints(
      T context,
      EnvironmentCollection staticEnvironments,
      EnvironmentCollection.Builder refinedEnvironments,
      Map<Label, RemovedEnvironmentCulprit> removedEnvironmentCulprits);

  /**
   * Returns an {@link EnvironmentLookupException} with the specified message and detailed failure
   * code.
   */
  static EnvironmentLookupException createEnvironmentLookupException(String message, Code code) {
    return new EnvironmentLookupException(
        DetailedExitCode.of(
            FailureDetail.newBuilder()
                .setMessage(message)
                .setAnalysis(Analysis.newBuilder().setCode(code))
                .build()));
  }

  /** Exception indicating errors finding/parsing environments or their containing groups. */
  class EnvironmentLookupException extends Exception implements DetailedException {
    private final DetailedExitCode detailedExitCode;

    private EnvironmentLookupException(DetailedExitCode detailedExitCode) {
      super(detailedExitCode.getFailureDetail().getMessage());
      this.detailedExitCode = detailedExitCode;
    }

    @Override
    public DetailedExitCode getDetailedExitCode() {
      return detailedExitCode;
    }
  }
}
