| // 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.packages; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| |
| import com.google.auto.value.AutoBuilder; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedMap; |
| import com.google.common.collect.Iterables; |
| import com.google.devtools.build.lib.cmdline.BazelModuleContext; |
| import com.google.devtools.build.lib.cmdline.BazelModuleContext.LoadGraphVisitor; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.cmdline.LabelSyntaxException; |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; |
| import com.google.devtools.build.lib.cmdline.RepositoryMapping; |
| import com.google.devtools.build.lib.cmdline.StarlarkThreadContext; |
| import com.google.devtools.build.lib.collect.CollectionUtils; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.EventHandler; |
| import com.google.devtools.build.lib.packages.Package.Builder.PackageLimits; |
| import com.google.devtools.build.lib.packages.Package.Builder.PackageSettings; |
| import com.google.devtools.build.lib.packages.TargetRecorder.MacroNamespaceViolationException; |
| import com.google.devtools.build.lib.packages.TargetRecorder.NameConflictException; |
| import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; |
| import com.google.devtools.build.lib.server.FailureDetails.PackageLoading; |
| import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code; |
| import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext; |
| import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec; |
| import com.google.devtools.build.lib.skyframe.serialization.SerializationContext; |
| import com.google.devtools.build.lib.skyframe.serialization.SerializationException; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; |
| import com.google.devtools.build.lib.util.DetailedExitCode; |
| import com.google.devtools.build.lib.util.HashCodes; |
| 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.errorprone.annotations.CanIgnoreReturnValue; |
| import com.google.protobuf.CodedInputStream; |
| import com.google.protobuf.CodedOutputStream; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.Semaphore; |
| import javax.annotation.Nullable; |
| import net.starlark.java.eval.EvalException; |
| import net.starlark.java.eval.Module; |
| import net.starlark.java.eval.StarlarkSemantics; |
| import net.starlark.java.eval.StarlarkThread; |
| import net.starlark.java.eval.SymbolGenerator; |
| import net.starlark.java.syntax.Location; |
| |
| /** |
| * A package, which is a container of {@link Rule}s, each of which contains a dictionary of named |
| * attributes. |
| * |
| * <p>Package instances are intended to be immutable and for all practical purposes can be treated |
| * as such. Note, however, that some member variables exposed via the public interface are not |
| * strictly immutable, so until their types are guaranteed immutable we're not applying the |
| * {@code @Immutable} annotation here. |
| * |
| * <p>This class should not be extended - it's only non-final for mocking! |
| * |
| * <p>When changing this class, make sure to make corresponding changes to serialization! |
| */ |
| @SuppressWarnings("JavaLangClash") |
| public class Package extends Packageoid { |
| |
| // TODO(bazel-team): This class and its builder are ginormous. Future refactoring work might |
| // attempt to separate the concerns of: |
| // - instantiating targets/macros, adding them to the package, and accessing/indexing them |
| // afterwards |
| // - utility logical like validating names, checking for conflicts, etc. |
| // - tracking and enforcement of limits |
| |
| // ==== Static fields and enums ==== |
| |
| /** |
| * How to enforce config_setting visibility settings. |
| * |
| * <p>This is a temporary setting in service of https://github.com/bazelbuild/bazel/issues/12669. |
| * After enough depot cleanup, config_setting will have the same visibility enforcement as all |
| * other rules. |
| */ |
| public enum ConfigSettingVisibilityPolicy { |
| /** Don't enforce visibility for any config_setting. */ |
| LEGACY_OFF, |
| /** Honor explicit visibility settings on config_setting, else use //visibility:public. */ |
| DEFAULT_PUBLIC, |
| /** Enforce config_setting visibility exactly the same as all other rules. */ |
| DEFAULT_STANDARD |
| } |
| |
| // ==== Target and macro fields ==== |
| |
| // Can be changed during BUILD file evaluation due to exports_files() modifying its visibility. |
| // Cannot be in Declarations because, since it's a Target, it holds a back reference to this |
| // Package object. |
| private InputFile buildFile; |
| |
| /** |
| * The collection of all symbolic macro instances defined in this package, indexed by their {@link |
| * MacroInstance#getId id} (not name). Null until the package is fully initialized by its |
| * builder's {@code finishBuild()}. |
| */ |
| // TODO(bazel-team): Consider enforcing that macro namespaces are "exclusive", meaning that target |
| // names may only suffix a macro name when the target is created (transitively) within the macro. |
| // This would be a major change that would break the (common) use case where a BUILD file |
| // declares both "foo" and "foo_test". |
| @Nullable private ImmutableSortedMap<String, MacroInstance> macros; |
| |
| /** |
| * A map from names of targets declared in a symbolic macro which violate macro naming rules, such |
| * as "lib%{name}-src.jar" implicit outputs in java rules, to the name of the macro instance where |
| * they were declared. |
| * |
| * <p>Initialized by the builder in {@link Builder#finishBuild}. |
| */ |
| @Nullable private ImmutableMap<String, String> macroNamespaceViolatingTargets; |
| |
| /** |
| * A map from names of targets declared in a symbolic macro to the (innermost) macro instance |
| * where they were declared. Omits targets not declared in symbolic macros. |
| * |
| * <p>Null for packages produced by deserialization. |
| */ |
| // TODO: #19922 - If this field were made serializable (currently it's not), it would subsume |
| // macroNamespaceViolatingTargets, since we can just map the target to its macro and then check |
| // whether it is in the macro's namespace. |
| // |
| // TODO: #19922 - Don't maintain this extra map of all macro-instantiated targets. We have a |
| // couple options: |
| // 1) Have Target store a reference to its declaring MacroInstance directly. To avoid adding a |
| // field to that class (a not insignificant cost), we can merge it with the reference to its |
| // package: If we're not in a macro, we point to the package, and if we are, we point to the |
| // innermost macro, and hop to the MacroInstance to get a reference to the Package (or parent |
| // macro). |
| // 2) To support lazy macro evaluation, we'll probably need a prefix trie in Package to find the |
| // macros whose namespaces contain the requested target name. For targets that respect their |
| // macro's namespace, we could just look them up in the trie. This assumes we already know |
| // whether the target is well-named, which we wouldn't if we got rid of |
| // macroNamespaceViolatingTargets. |
| @Nullable private ImmutableMap<String, MacroInstance> targetsToDeclaringMacro; |
| |
| /** |
| * A map from names of targets declared in a symbolic macro to the package where the macro that |
| * declared it was defined, as per {@link MacroInstance#getDefinitionPackage}. Omits targets not |
| * declared in symbolic macros. |
| * |
| * <p>Null for packages not produced by deserialization. |
| */ |
| @Nullable private ImmutableMap<String, PackageIdentifier> targetsToDeclaringPackage; |
| |
| // ==== Constructor ==== |
| |
| /** |
| * Constructs a new (incomplete) Package instance. Intended only for use by {@link |
| * Package.Builder}. |
| * |
| * <p>Packages and Targets refer to one another. Therefore, the builder needs to have a Package |
| * instance on-hand before it can associate any targets with the package. The {@link |
| * Package.Metadata} fields like the package's name must be known before that point, while other |
| * fields are filled in only when the builder calls {@link Builder#finishBuild}. |
| */ |
| // TODO(#19922): Better separate fields that must be known a priori from those determined through |
| // BUILD evaluation. |
| private Package(Metadata metadata, Declarations declarations) { |
| super(metadata, declarations); |
| } |
| |
| // ==== General package metadata accessors ==== |
| |
| /** |
| * Returns the name of this package. If this build is using external repositories then this name |
| * may not be unique! |
| */ |
| public String getName() { |
| return metadata.getName(); |
| } |
| |
| /** Like {@link #getName}, but has type {@code PathFragment}. */ |
| public PathFragment getNameFragment() { |
| return getPackageIdentifier().getPackageFragment(); |
| } |
| |
| /** |
| * Returns the filename of the BUILD file which defines this package. The parent directory of the |
| * BUILD file is the package directory. |
| */ |
| public RootedPath getFilename() { |
| return metadata.buildFilename(); |
| } |
| |
| /** Returns the directory containing the package's BUILD file. */ |
| public Path getPackageDirectory() { |
| return metadata.getPackageDirectory(); |
| } |
| |
| /** |
| * How to enforce visibility on <code>config_setting</code> See {@link |
| * ConfigSettingVisibilityPolicy} for details. |
| */ |
| @Nullable |
| public ConfigSettingVisibilityPolicy getConfigSettingVisibilityPolicy() { |
| return metadata.configSettingVisibilityPolicy(); |
| } |
| |
| /** Convenience wrapper for {@link Metadata#workspaceName} */ |
| public String getWorkspaceName() { |
| return getMetadata().workspaceName(); |
| } |
| |
| /** Returns the InputFile target for this package's BUILD file. */ |
| public InputFile getBuildFile() { |
| return buildFile; |
| } |
| |
| /** Convenience wrapper for {@link Declarations#getPackageArgs} */ |
| public PackageArgs getPackageArgs() { |
| return getDeclarations().getPackageArgs(); |
| } |
| |
| /** Convenience wrapper for {@link Declarations#getMakeEnvironment} */ |
| public ImmutableMap<String, String> getMakeEnvironment() { |
| return getDeclarations().getMakeEnvironment(); |
| } |
| |
| /** |
| * Returns the root of the source tree beneath which this package's BUILD file was found. |
| * |
| * <p>Assumes invariant: {@code |
| * getSourceRoot().getRelative(packageId.getSourceRoot()).equals(getPackageDirectory())} |
| */ |
| public Root getSourceRoot() { |
| return metadata.sourceRoot(); |
| } |
| |
| private static ImmutableList<Label> computeTransitiveLoads(Iterable<Module> directLoads) { |
| Set<Label> loads = new LinkedHashSet<>(); |
| BazelModuleContext.visitLoadGraphRecursively(directLoads, loads::add); |
| return ImmutableList.copyOf(loads); |
| } |
| |
| // ==== Target and macro accessors ==== |
| |
| /** |
| * Returns a (read-only, ordered) iterable of all the targets belonging to this package which are |
| * instances of the specified class. |
| */ |
| public <T extends Target> Iterable<T> getTargets(Class<T> targetClass) { |
| return Iterables.filter(targets.values(), targetClass); |
| } |
| |
| /** |
| * Returns the rule that corresponds to a particular BUILD target name. Useful for walking through |
| * the dependency graph of a target. Fails if the target is not a Rule. |
| */ |
| public Rule getRule(String targetName) { |
| return (Rule) targets.get(targetName); |
| } |
| |
| /** |
| * Returns a map from names of targets declared in a symbolic macro which violate macro naming |
| * rules, such as "lib%{name}-src.jar" implicit outputs in java rules, to the name of the macro |
| * instance where they were declared. |
| */ |
| ImmutableMap<String, String> getMacroNamespaceViolatingTargets() { |
| Preconditions.checkNotNull( |
| macroNamespaceViolatingTargets, |
| "This method is only available after the package has been loaded."); |
| return macroNamespaceViolatingTargets; |
| } |
| |
| /** |
| * Returns a map from names of targets declared in a symbolic macro to the package containing said |
| * macro's .bzl code. |
| */ |
| ImmutableMap<String, PackageIdentifier> getTargetsToDeclaringPackage() { |
| if (targetsToDeclaringPackage != null) { |
| return targetsToDeclaringPackage; |
| } else { |
| ImmutableMap.Builder<String, PackageIdentifier> result = ImmutableMap.builder(); |
| for (Map.Entry<String, MacroInstance> entry : targetsToDeclaringMacro.entrySet()) { |
| result.put(entry.getKey(), entry.getValue().getDefinitionPackage()); |
| } |
| return result.buildOrThrow(); |
| } |
| } |
| |
| @Override |
| public void checkMacroNamespaceCompliance(Target target) throws MacroNamespaceViolationException { |
| Preconditions.checkArgument( |
| this.equals(target.getPackage()), "Target must belong to this package"); |
| @Nullable |
| String macroNamespaceViolated = getMacroNamespaceViolatingTargets().get(target.getName()); |
| if (macroNamespaceViolated != null) { |
| throw new MacroNamespaceViolationException( |
| String.format( |
| "Target %s declared in symbolic macro '%s' violates macro naming rules and cannot be" |
| + " built. %s", |
| target.getLabel(), macroNamespaceViolated, TargetRecorder.MACRO_NAMING_RULES), |
| target); |
| } |
| } |
| |
| @Override |
| public Target getTarget(String targetName) throws NoSuchTargetException { |
| Target target = targets.get(targetName); |
| if (target != null) { |
| return target; |
| } |
| |
| Label label; |
| try { |
| label = Label.create(metadata.packageIdentifier(), targetName); |
| } catch (LabelSyntaxException e) { |
| throw new IllegalArgumentException(targetName, e); |
| } |
| |
| if (metadata.succinctTargetNotFoundErrors()) { |
| throw new NoSuchTargetException( |
| label, String.format("target '%s' not declared in package '%s'", targetName, getName())); |
| } else { |
| String alternateTargetSuggestion = |
| getAlternateTargetSuggestion(metadata, targetName, targets.keySet()); |
| throw new NoSuchTargetException( |
| label, |
| String.format( |
| "target '%s' not declared in package '%s' defined by %s%s", |
| targetName, |
| getName(), |
| metadata.buildFilename().asPath().getPathString(), |
| alternateTargetSuggestion)); |
| } |
| } |
| |
| static String getAlternateTargetSuggestion( |
| Metadata metadata, String targetName, ImmutableSet<String> otherTargets) { |
| // If there's a file on the disk that's not mentioned in the BUILD file, |
| // produce a more informative error. NOTE! this code path is only executed |
| // on failure, which is (relatively) very rare. In the common case no |
| // stat(2) is executed. |
| Path filename = metadata.getPackageDirectory().getRelative(targetName); |
| if (!PathFragment.isNormalized(targetName) || "*".equals(targetName)) { |
| // Don't check for file existence if the target name is not normalized |
| // because the error message would be confusing and wrong. If the |
| // targetName is "foo/bar/.", and there is a directory "foo/bar", it |
| // doesn't mean that "//pkg:foo/bar/." is a valid label. |
| // Also don't check if the target name is a single * character since |
| // it's invalid on Windows. |
| return ""; |
| } else if (filename.isDirectory()) { |
| return "; however, a source directory of this name exists. (Perhaps add " |
| + "'exports_files([\"" |
| + targetName |
| + "\"])' to " |
| + getRepoRelativeBuildFilePathString(metadata) |
| + ", or define a " |
| + "filegroup?)"; |
| } else if (filename.exists()) { |
| return "; however, a source file of this name exists. (Perhaps add " |
| + "'exports_files([\"" |
| + targetName |
| + "\"])' to " |
| + getRepoRelativeBuildFilePathString(metadata) |
| + "?)"; |
| } else { |
| return TargetSuggester.suggestTargets(targetName, otherTargets); |
| } |
| } |
| |
| private static String getRepoRelativeBuildFilePathString(Metadata metadata) { |
| return metadata |
| .packageIdentifier() |
| .getPackageFragment() |
| .getRelative(metadata.buildFilename().asPath().getBaseName()) |
| .getPathString(); |
| } |
| |
| /** |
| * Returns all symbolic macros defined in the package, indexed by {@link MacroInstance#getId id}. |
| * |
| * <p>Note that {@code MacroInstance}s hold just the information known at the time a macro was |
| * declared, even though by the time the {@code Package} is fully constructed we already have |
| * fully evaluated these macros. |
| */ |
| public ImmutableMap<String, MacroInstance> getMacrosById() { |
| return macros; |
| } |
| |
| /** |
| * Returns the (innermost) symbolic macro instance that declared the given target, or null if the |
| * target was not created in a symbolic macro. |
| * |
| * <p>Throws {@link IllegalArgumentException} if the given name is not a target in this package. |
| * |
| * <p>For packages produced by deserialization, this information is not available and {@code |
| * IllegalStateException} is thrown. |
| */ |
| @Nullable |
| public MacroInstance getDeclaringMacroForTarget(String target) { |
| Preconditions.checkState( |
| targetsToDeclaringMacro != null, |
| "Cannot retrieve MacroInstance information from deserialized packages"); |
| Preconditions.checkArgument(targets.containsKey(target), "unknown target '%s'", target); |
| return targetsToDeclaringMacro.get(target); |
| } |
| |
| /** |
| * Returns the id of the package where the (innermost) macro that declared the given target was |
| * defined (as per {@link MacroInstance#getDefinitionLocation}), or null if the target was not |
| * created in a symbolic macro. |
| * |
| * <p>The caller should interpret a null result to mean that the declaration location of the |
| * target is this package. |
| * |
| * <p>Throws {@link IllegalArgumentException} if the given name is not a target in this package. |
| */ |
| @Nullable |
| public PackageIdentifier getDeclaringPackageForTargetIfInMacro(String target) { |
| Preconditions.checkArgument(targets.containsKey(target), "unknown target '%s'", target); |
| // Exactly one of targetsToDeclaringMacro and targetsToDeclaringPackage is non-null, depending |
| // on whether this package was produced by deserialization. |
| if (targetsToDeclaringMacro != null) { |
| MacroInstance macro = targetsToDeclaringMacro.get(target); |
| return macro != null ? macro.getDefinitionPackage() : null; |
| } else { |
| return targetsToDeclaringPackage.get(target); |
| } |
| } |
| |
| // ==== Stringification / debugging ==== |
| |
| @Override |
| public String toString() { |
| return "Package(" |
| + getName() |
| + ")=" |
| + (targets != null ? getTargets(Rule.class) : "initializing..."); |
| } |
| |
| @Override |
| public String getShortDescription() { |
| return "package " + getPackageIdentifier().getCanonicalForm(); |
| } |
| |
| /** |
| * Dumps the package for debugging. Do not depend on the exact format/contents of this debugging |
| * output. |
| */ |
| public void dump(PrintStream out) { |
| out.println(" Package " + getName() + " (" + metadata.buildFilename().asPath() + ")"); |
| |
| // Rules: |
| out.println(" Rules"); |
| for (Rule rule : getTargets(Rule.class)) { |
| out.println(" " + rule.getTargetKind() + " " + rule.getLabel()); |
| for (Attribute attr : rule.getAttributes()) { |
| for (Object possibleValue : |
| AggregatingAttributeMapper.of(rule).visitAttribute(attr.getName(), attr.getType())) { |
| out.println(" " + attr.getName() + " = " + possibleValue); |
| } |
| } |
| } |
| |
| // Files: |
| out.println(" Files"); |
| for (FileTarget file : getTargets(FileTarget.class)) { |
| out.print(" " + file.getTargetKind() + " " + file.getLabel()); |
| if (file instanceof OutputFile) { |
| out.println(" (generated by " + ((OutputFile) file).getGeneratingRule().getLabel() + ")"); |
| } else { |
| out.println(); |
| } |
| } |
| } |
| |
| // ==== Error reporting ==== |
| |
| /** |
| * Returns an error {@link Event} with {@link Location} and {@link DetailedExitCode} properties. |
| */ |
| public static Event error(Location location, String message, Code code) { |
| return errorWithDetailedExitCode( |
| location, |
| message, |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage(message) |
| .setPackageLoading(PackageLoading.newBuilder().setCode(code)) |
| .build())); |
| } |
| |
| /** Similar to {@link #error} but with a custom {@link DetailedExitCode}. */ |
| public static Event errorWithDetailedExitCode( |
| Location location, String message, DetailedExitCode detailedExitCode) { |
| Event error = Event.error(location, message); |
| return error.withProperty(DetailedExitCode.class, detailedExitCode); |
| } |
| |
| /** |
| * If {@code pkg.containsErrors()}, sends an errorful "package contains errors" {@link Event} |
| * (augmented with {@code pkg.getFailureDetail()}, if present) to the given {@link EventHandler}. |
| */ |
| public static void maybeAddPackageContainsErrorsEventToHandler( |
| Package pkg, EventHandler eventHandler) { |
| if (pkg.containsErrors()) { |
| eventHandler.handle( |
| Event.error( |
| String.format( |
| "package contains errors: %s%s", |
| pkg.getNameFragment(), |
| pkg.getFailureDetail() != null |
| ? ": " + pkg.getFailureDetail().getMessage() |
| : ""))); |
| } |
| } |
| |
| /** |
| * Given a {@link FailureDetail} and target, returns a modified {@code FailureDetail} that |
| * attributes its error to the target. |
| * |
| * <p>If the given detail is null, then a generic {@link Code#TARGET_MISSING} detail identifying |
| * the target is returned. |
| */ |
| public static FailureDetail contextualizeFailureDetailForTarget( |
| @Nullable FailureDetail failureDetail, Target target) { |
| String prefix = |
| "Target '" + target.getLabel() + "' contains an error and its package is in error"; |
| if (failureDetail == null) { |
| return FailureDetail.newBuilder() |
| .setMessage(prefix) |
| .setPackageLoading(PackageLoading.newBuilder().setCode(Code.TARGET_MISSING)) |
| .build(); |
| } |
| return failureDetail.toBuilder().setMessage(prefix + ": " + failureDetail.getMessage()).build(); |
| } |
| |
| // ==== Builders ==== |
| |
| /** Returns a new {@link Builder} suitable for constructing an ordinary package. */ |
| public static Builder newPackageBuilder( |
| PackageSettings packageSettings, |
| PackageIdentifier id, |
| RootedPath filename, |
| String workspaceName, |
| Optional<String> associatedModuleName, |
| Optional<String> associatedModuleVersion, |
| boolean noImplicitFileExport, |
| boolean simplifyUnconditionalSelectsInRuleAttrs, |
| RepositoryMapping repositoryMapping, |
| RepositoryMapping mainRepositoryMapping, |
| @Nullable Semaphore cpuBoundSemaphore, |
| PackageOverheadEstimator packageOverheadEstimator, |
| @Nullable ImmutableMap<Location, String> generatorMap, |
| // TODO(bazel-team): See Builder() constructor comment about use of null for this param. |
| @Nullable ConfigSettingVisibilityPolicy configSettingVisibilityPolicy, |
| @Nullable Globber globber, |
| boolean enableNameConflictChecking, |
| boolean trackFullMacroInformation, |
| PackageLimits packageLimits) { |
| return new Builder( |
| Metadata.builder() |
| .packageIdentifier(id) |
| .buildFilename(filename) |
| .workspaceName(workspaceName) |
| .repositoryMapping(repositoryMapping) |
| .associatedModuleName(associatedModuleName) |
| .associatedModuleVersion(associatedModuleVersion) |
| .configSettingVisibilityPolicy(configSettingVisibilityPolicy) |
| .succinctTargetNotFoundErrors(packageSettings.succinctTargetNotFoundErrors()) |
| .build(), |
| new Declarations.Builder(), |
| SymbolGenerator.create(id), |
| packageSettings.precomputeTransitiveLoads(), |
| noImplicitFileExport, |
| simplifyUnconditionalSelectsInRuleAttrs, |
| mainRepositoryMapping, |
| cpuBoundSemaphore, |
| packageOverheadEstimator, |
| generatorMap, |
| globber, |
| enableNameConflictChecking, |
| trackFullMacroInformation, |
| packageLimits); |
| } |
| |
| /** Returns a new {@link Builder} suitable for constructing a package from package pieces. */ |
| public static Builder newPackageFromPackagePiecesBuilder( |
| PackageSettings packageSettings, |
| Metadata metadata, |
| Declarations declarations, |
| boolean noImplicitFileExport, |
| boolean simplifyUnconditionalSelectsInRuleAttrs, |
| RepositoryMapping mainRepositoryMapping, |
| @Nullable Semaphore cpuBoundSemaphore, |
| PackageOverheadEstimator packageOverheadEstimator, |
| @Nullable ImmutableMap<Location, String> generatorMap, |
| @Nullable Globber globber, |
| boolean enableNameConflictChecking, |
| boolean trackFullMacroInformation, |
| PackageLimits packageLimits, |
| InputFile buildFile) { |
| Builder builder = |
| new Builder( |
| metadata, |
| declarations.checkImmutable(), |
| SymbolGenerator.create(metadata.packageIdentifier()), |
| packageSettings.precomputeTransitiveLoads(), |
| noImplicitFileExport, |
| simplifyUnconditionalSelectsInRuleAttrs, |
| mainRepositoryMapping, |
| cpuBoundSemaphore, |
| packageOverheadEstimator, |
| generatorMap, |
| globber, |
| enableNameConflictChecking, |
| trackFullMacroInformation, |
| packageLimits); |
| checkArgument( |
| buildFile.getPackageMetadata().packageIdentifier().equals(metadata.packageIdentifier())); |
| builder.recorder.replaceInputFileUnchecked(buildFile); |
| builder.setBuildFile(buildFile); |
| return builder; |
| } |
| |
| // ==== Non-trivial nested classes ==== |
| |
| /** |
| * Common base class for builders for {@link Package} and {@link PackagePiece.ForBuildFile} |
| * objects, containing the shared logic for processing top-level BUILD file declarations, for |
| * example the "package" callable. |
| */ |
| // TODO(https://github.com/bazelbuild/bazel/issues/23852): this class should be moved elsewhere - |
| // probably to an inner clas of Packageoid - but that would require also moving Declarations and |
| // PackageArgs, so that their private fields can be mutated only by the builder. |
| public abstract static class AbstractBuilder extends TargetDefinitionContext { |
| private final boolean precomputeTransitiveLoads; |
| |
| /** True iff the "package" function has already been called in this BUILD file. */ |
| private boolean packageFunctionUsed; |
| |
| protected final boolean noImplicitFileExport; |
| |
| /** Retrieves this object from a Starlark thread. Returns null if not present. */ |
| @Nullable |
| public static AbstractBuilder fromOrNull(StarlarkThread thread) { |
| StarlarkThreadContext ctx = thread.getThreadLocal(StarlarkThreadContext.class); |
| return ctx instanceof AbstractBuilder builder ? builder : null; |
| } |
| |
| /** |
| * Retrieves this object from a Starlark thread. If not present, throws an {@link EvalException} |
| * with an error message indicating that {@code what} can only be used in a BUILD file or a |
| * legacy macro. |
| */ |
| @CanIgnoreReturnValue |
| public static AbstractBuilder fromOrFailAllowBuildOnly( |
| StarlarkThread thread, String what, String participle) throws EvalException { |
| @Nullable StarlarkThreadContext ctx = thread.getThreadLocal(StarlarkThreadContext.class); |
| if (ctx instanceof AbstractBuilder builder |
| && builder.recorder.getCurrentMacroFrame() == null) { |
| return builder; |
| } |
| throw newFromOrFailException( |
| what, participle, thread.getSemantics(), EnumSet.of(FromOrFailMode.NO_MACROS)); |
| } |
| |
| /** |
| * Retrieves this object from a Starlark thread. If not present, throws an {@link EvalException} |
| * with an error message indicating that {@code what} can only be used in a BUILD file or a |
| * legacy macro. |
| */ |
| @CanIgnoreReturnValue |
| public static AbstractBuilder fromOrFailAllowBuildOnly(StarlarkThread thread, String what) |
| throws EvalException { |
| return fromOrFailAllowBuildOnly(thread, what, "used"); |
| } |
| |
| // TODO(#19922): Require this to be set before BUILD evaluation. |
| @CanIgnoreReturnValue |
| public AbstractBuilder setLoads(Iterable<Module> directLoads) { |
| Declarations.Builder declarationsBuilder = |
| pkg.getDeclarations().checkMutable().checkLoadsNotSet(); |
| if (precomputeTransitiveLoads) { |
| declarationsBuilder.setTransitiveLoads(computeTransitiveLoads(directLoads)); |
| } else { |
| declarationsBuilder.setDirectLoads(ImmutableList.copyOf(directLoads)); |
| } |
| return this; |
| } |
| |
| static ImmutableList<Label> computeTransitiveLoads(Iterable<Module> directLoads) { |
| Set<Label> loads = new LinkedHashSet<>(); |
| BazelModuleContext.visitLoadGraphRecursively(directLoads, loads::add); |
| return ImmutableList.copyOf(loads); |
| } |
| |
| @CanIgnoreReturnValue |
| AbstractBuilder setTransitiveLoadsForDeserialization(ImmutableList<Label> transitiveLoads) { |
| pkg.getDeclarations().checkMutable().checkLoadsNotSet().setTransitiveLoads(transitiveLoads); |
| return this; |
| } |
| |
| public void mergePackageArgsFrom(PackageArgs packageArgs) { |
| pkg.getDeclarations().checkMutable().mergePackageArgsFrom(packageArgs); |
| } |
| |
| public void mergePackageArgsFrom(PackageArgs.Builder builder) { |
| mergePackageArgsFrom(builder.build()); |
| } |
| |
| public void setMakeVariable(String name, String value) { |
| makeEnv.put(name, value); |
| } |
| |
| /** Returns whether the "package" function has been called yet */ |
| boolean isPackageFunctionUsed() { |
| return packageFunctionUsed; |
| } |
| |
| void setPackageFunctionUsed() { |
| packageFunctionUsed = true; |
| } |
| |
| public Set<Target> getTargets() { |
| return recorder.getTargets(); |
| } |
| |
| /** Adds an environment group to the package. Not valid within symbolic macros. */ |
| void addEnvironmentGroup( |
| String name, |
| List<Label> environments, |
| List<Label> defaults, |
| EventHandler eventHandler, |
| Location location) |
| throws NameConflictException, LabelSyntaxException { |
| Preconditions.checkState(currentMacro() == null); |
| |
| if (hasDuplicateLabels(environments, name, "environments", location, eventHandler) |
| || hasDuplicateLabels(defaults, name, "defaults", location, eventHandler)) { |
| setContainsErrors(); |
| return; |
| } |
| |
| EnvironmentGroup group = |
| new EnvironmentGroup(createLabel(name), pkg, environments, defaults, location); |
| recorder.addTarget(group); |
| |
| // Invariant: once group is inserted into targets, it must also: |
| // (a) be inserted into environmentGroups, or |
| // (b) have its group.processMemberEnvironments called. |
| // Otherwise it will remain uninitialized, |
| // causing crashes when it is later toString-ed. |
| |
| for (Event error : group.validateMembership()) { |
| eventHandler.handle(error); |
| setContainsErrors(); |
| } |
| |
| // For each declared environment, make sure it doesn't also belong to some other group. |
| for (Label environment : group.getEnvironments()) { |
| EnvironmentGroup otherGroup = environmentGroups.get(environment); |
| if (otherGroup != null) { |
| eventHandler.handle( |
| Package.error( |
| location, |
| String.format( |
| "environment %s belongs to both %s and %s", |
| environment, group.getLabel(), otherGroup.getLabel()), |
| Code.ENVIRONMENT_IN_MULTIPLE_GROUPS)); |
| setContainsErrors(); |
| // Ensure the orphan gets (trivially) initialized. |
| group.processMemberEnvironments(ImmutableMap.of()); |
| } else { |
| environmentGroups.put(environment, group); |
| } |
| } |
| } |
| |
| /** |
| * Returns true if any labels in the given list appear multiple times, reporting an appropriate |
| * error message if so. |
| * |
| * <p>TODO(bazel-team): apply this to all build functions (maybe automatically?), possibly |
| * integrate with RuleClass.checkForDuplicateLabels. |
| */ |
| private static boolean hasDuplicateLabels( |
| List<Label> labels, |
| String owner, |
| String attrName, |
| Location location, |
| EventHandler eventHandler) { |
| Set<Label> dupes = CollectionUtils.duplicatedElementsOf(labels); |
| for (Label dupe : dupes) { |
| eventHandler.handle( |
| Package.error( |
| location, |
| String.format( |
| "label '%s' is duplicated in the '%s' list of '%s'", dupe, attrName, owner), |
| Code.DUPLICATE_LABEL)); |
| } |
| return !dupes.isEmpty(); |
| } |
| |
| protected void beforeBuildWithoutDiscoveringAssumedInputFiles() throws NoSuchPackageException { |
| // We create an InputFile corresponding to the BUILD file in Builder's constructor. However, |
| // the visibility of this target may be overridden with an exports_files directive, so we wait |
| // until now to obtain the current instance from the targets map. |
| setBuildFile((InputFile) recorder.getTargetMap().get(metadata.buildFileLabel().getName())); |
| |
| super.beforeBuild(); |
| } |
| |
| protected abstract void setBuildFile(InputFile buildFile); |
| |
| @CanIgnoreReturnValue |
| @Override |
| protected AbstractBuilder beforeBuild() throws NoSuchPackageException { |
| beforeBuildWithoutDiscoveringAssumedInputFiles(); |
| Map<String, InputFile> newInputFiles = |
| createAssumedInputFiles(pkg, recorder, noImplicitFileExport); |
| for (InputFile file : newInputFiles.values()) { |
| recorder.addInputFileUnchecked(file); |
| } |
| return this; |
| } |
| |
| /** |
| * Creates and returns input files for targets that have been referenced but not explicitly |
| * declared in this package. |
| * |
| * <p>Precisely: For each label L appearing in one or more label-typed attributes of one or more |
| * declarations D (either of a target or a symbolic macro), we create an {@code InputFile} for L |
| * and return it in the map (keyed by its name) if all of the following are true: |
| * |
| * <ol> |
| * <li>L points to within the current package. |
| * <li>The package does not otherwise declare a target or macro named L. |
| * <li>D is not itself declared inside a symbolic macro. |
| * </ol> |
| * |
| * <p>The third condition ensures that we can know all *possible* implicitly created input files |
| * without evaluating any symbolic macros. However, if the label lies within one or more |
| * symbolic macro's namespaces, then we do still need to evaluate those macros to determine |
| * whether or not the second condition is true, i.e. whether the label points to a target the |
| * macro declares (or a submacro it clashes with), or defaults to an implicitly created input |
| * file. |
| */ |
| private static Map<String, InputFile> createAssumedInputFiles( |
| Packageoid pkg, TargetRecorder recorder, boolean noImplicitFileExport) { |
| Map<String, InputFile> implicitlyCreatedInputFiles = new HashMap<>(); |
| |
| for (Rule rule : recorder.getRules()) { |
| if (!recorder.isRuleCreatedInMacro(rule)) { |
| for (Label label : recorder.getRuleLabels(rule)) { |
| maybeCreateAssumedInputFile( |
| implicitlyCreatedInputFiles, |
| pkg, |
| recorder, |
| noImplicitFileExport, |
| label, |
| rule.getLocation()); |
| } |
| } |
| } |
| |
| for (MacroInstance macro : recorder.getMacroMap().values()) { |
| if (macro.getParent() == null) { |
| macro.visitExplicitAttributeLabels( |
| label -> |
| maybeCreateAssumedInputFile( |
| implicitlyCreatedInputFiles, |
| pkg, |
| recorder, |
| noImplicitFileExport, |
| label, |
| // TODO(bazel-team): We don't save a MacroInstance's location information yet, |
| // but when we do, use that here. |
| Location.BUILTIN)); |
| } |
| } |
| |
| return implicitlyCreatedInputFiles; |
| } |
| |
| /** |
| * Adds an implicitly created input file to the given map if the label points within the current |
| * package and there is no existing target or macro for that label. |
| */ |
| private static void maybeCreateAssumedInputFile( |
| Map<String, InputFile> implicitlyCreatedInputFiles, |
| Packageoid pkg, |
| TargetRecorder recorder, |
| boolean noImplicitFileExport, |
| Label label, |
| Location loc) { |
| String name = label.getName(); |
| if (!label.getPackageIdentifier().equals(pkg.getPackageIdentifier())) { |
| return; |
| } |
| if (recorder.getTargetMap().containsKey(name) |
| || recorder.hasMacroWithName(name) |
| || implicitlyCreatedInputFiles.containsKey(name)) { |
| return; |
| } |
| |
| implicitlyCreatedInputFiles.put( |
| name, |
| noImplicitFileExport |
| ? new PrivateVisibilityInputFile(pkg, label, loc) |
| : new InputFile(pkg, label, loc)); |
| } |
| |
| @Override |
| protected void finalBuilderValidationHook() { |
| // Now all targets have been loaded, so we validate the group's member environments. |
| for (EnvironmentGroup envGroup : ImmutableSet.copyOf(environmentGroups.values())) { |
| List<Event> errors = envGroup.processMemberEnvironments(recorder.getTargetMap()); |
| if (!errors.isEmpty()) { |
| Event.replayEventsOn(localEventHandler, errors); |
| // TODO(bazel-team): Can't we automatically infer containsError from the presence of |
| // ERRORs on our handler? |
| setContainsErrors(); |
| } |
| } |
| } |
| |
| @Override |
| protected void packageoidInitializationHook() { |
| // Finish Package.Declarations construction. |
| if (pkg.getDeclarations() instanceof Declarations.Builder declarationsBuilder) { |
| if (declarationsBuilder.directLoads == null |
| && declarationsBuilder.transitiveLoads == null) { |
| checkState(pkg.containsErrors(), "Loads not set for error-free package"); |
| setLoads(ImmutableList.of()); |
| } |
| pkg.declarations = declarationsBuilder.setMakeEnvironment(makeEnv).build(); |
| } |
| } |
| |
| AbstractBuilder( |
| Package.Metadata metadata, |
| Packageoid pkg, |
| SymbolGenerator<?> symbolGenerator, |
| boolean precomputeTransitiveLoads, |
| boolean noImplicitFileExport, |
| boolean simplifyUnconditionalSelectsInRuleAttrs, |
| RepositoryMapping mainRepositoryMapping, |
| @Nullable Semaphore cpuBoundSemaphore, |
| PackageOverheadEstimator packageOverheadEstimator, |
| @Nullable ImmutableMap<Location, String> generatorMap, |
| @Nullable Globber globber, |
| boolean enableNameConflictChecking, |
| boolean trackFullMacroInformation, |
| boolean enableTargetMapSnapshotting, |
| PackageLimits packageLimits) { |
| super( |
| metadata, |
| pkg, |
| symbolGenerator, |
| simplifyUnconditionalSelectsInRuleAttrs, |
| mainRepositoryMapping, |
| cpuBoundSemaphore, |
| packageOverheadEstimator, |
| generatorMap, |
| globber, |
| enableNameConflictChecking, |
| trackFullMacroInformation, |
| enableTargetMapSnapshotting, |
| packageLimits); |
| this.precomputeTransitiveLoads = precomputeTransitiveLoads; |
| this.noImplicitFileExport = noImplicitFileExport; |
| if (metadata.getName().startsWith("javatests/")) { |
| mergePackageArgsFrom(PackageArgs.builder().setDefaultTestOnly(true)); |
| } |
| // Add target for the BUILD file itself. |
| // (This may be overridden by an exports_file declaration; or, for a package from package |
| // pieces, by the PackagePiece.ForBuildFile's BUILD file target set in |
| // newPackageFromPackagePiecesBuilder().) |
| recorder.addInputFileUnchecked( |
| new InputFile(pkg, metadata.buildFileLabel(), metadata.getBuildFileLocation())); |
| } |
| } |
| |
| /** |
| * A builder for {@link Package} objects. Only intended to be used by {@link PackageFactory} and |
| * {@link com.google.devtools.build.lib.skyframe.PackageFunction}. |
| */ |
| public static class Builder extends AbstractBuilder { |
| |
| /** |
| * A bundle of statically-defined options affecting package construction, that is not specific |
| * to any particular package and does not change for the lifetime of the server. |
| */ |
| public interface PackageSettings { |
| /** |
| * Returns whether or not extra detail should be added to {@link NoSuchTargetException}s |
| * thrown from {@link #getTarget}. Useful for toning down verbosity in situations where it can |
| * be less helpful. |
| */ |
| // TODO(bazel-team): Arguably, this could be replaced by a boolean param to getTarget(), or |
| // some separate action taken by the caller. But there's a lot of call sites that would need |
| // updating. |
| default boolean succinctTargetNotFoundErrors() { |
| return false; |
| } |
| |
| /** |
| * Determines whether to precompute a list of transitively loaded starlark files while |
| * building packages. |
| * |
| * <p>Typically, direct loads are stored as a {@code ImmutableList<Module>}. This is |
| * sufficient to reconstruct the full load graph by recursively traversing {@link |
| * BazelModuleContext#loads}. If the package is going to be serialized, however, it may make |
| * more sense to precompute a flat list containing the labels of all transitively loaded bzl |
| * files since {@link Module} is costly to serialize. |
| * |
| * <p>If this returns {@code true}, transitive loads are stored as an {@code |
| * ImmutableList<Label>} and direct loads are not stored. |
| */ |
| default boolean precomputeTransitiveLoads() { |
| return false; |
| } |
| |
| PackageSettings DEFAULTS = new PackageSettings() {}; |
| } |
| |
| /** A bundle of options affecting resource limits on package construction. */ |
| public interface PackageLimits { |
| /** |
| * The maximum number of Starlark computation steps that are allowed to be executed while |
| * building a package (or, transitively, any package piece). If this limit is exceeded, the |
| * package or package piece immediately stops building. |
| * |
| * <p>Confusingly, for historical Google-specific reasons, this limit is <em>not</em> the same |
| * as {@code --max_computation_steps}. |
| * |
| * <ul> |
| * <li>This limit (maxStarlarkComputationStepsPerPackage) is only set by Google-specific |
| * logic, is currently not used in open-source Bazel, and exceeding the limit causes the |
| * package builder to immediately stop and print a stack trace. The intent is to harden |
| * infrastructure against runaway Starlark computations. |
| * <li>By contrast, {@code --max_computation_steps} is enforced by {@link PackageFactory} |
| * post-factum, after the package has been built. The intent is to enforce code health |
| * by limiting the complexity of packages in a repo. |
| * </ul> |
| * |
| * <p>If lazy symbolic macro expansion is enabled, unless a complete {@link Package} is |
| * loaded, the limit is enforced only per package piece. |
| */ |
| // TODO(b/417468797): merge with --max_computation_steps enforcement. |
| default long maxStarlarkComputationStepsPerPackage() { |
| return Long.MAX_VALUE; |
| } |
| |
| public static final PackageLimits DEFAULTS = new PackageLimits() {}; |
| } |
| |
| // The snapshot of {@link #targets} for use in rule finalizer macros. Contains all |
| // non-finalizer-instantiated rule targets (i.e. all rule targets except for those instantiated |
| // in a finalizer or in a macro called from a finalizer). |
| // |
| // Initialized by expandAllRemainingMacros() and reset to null by beforeBuild(). |
| @Nullable private Map<String, Rule> rulesSnapshotViewForFinalizers; |
| |
| /** |
| * Ids of all symbolic macros that have been declared but not yet evaluated. |
| * |
| * <p>These are listed in the order they were declared. (This probably doesn't matter, but let's |
| * be protective against possible non-determinism.) |
| * |
| * <p>Generally, ordinary symbolic macros are evaluated eagerly and not added to this set, while |
| * finalizers, as well as any macros called by finalizers, always use deferred evaluation and |
| * end up in here. |
| */ |
| private final Set<String> unexpandedMacros = new LinkedHashSet<>(); |
| |
| private Builder( |
| Metadata metadata, |
| Declarations declarations, |
| SymbolGenerator<?> symbolGenerator, |
| boolean precomputeTransitiveLoads, |
| boolean noImplicitFileExport, |
| boolean simplifyUnconditionalSelectsInRuleAttrs, |
| RepositoryMapping mainRepositoryMapping, |
| @Nullable Semaphore cpuBoundSemaphore, |
| PackageOverheadEstimator packageOverheadEstimator, |
| @Nullable ImmutableMap<Location, String> generatorMap, |
| @Nullable Globber globber, |
| boolean enableNameConflictChecking, |
| boolean trackFullMacroInformation, |
| PackageLimits packageLimits) { |
| super( |
| metadata, |
| new Package(metadata, declarations), |
| symbolGenerator, |
| precomputeTransitiveLoads, |
| noImplicitFileExport, |
| simplifyUnconditionalSelectsInRuleAttrs, |
| mainRepositoryMapping, |
| cpuBoundSemaphore, |
| packageOverheadEstimator, |
| generatorMap, |
| globber, |
| enableNameConflictChecking, |
| trackFullMacroInformation, |
| /* enableTargetMapSnapshotting= */ true, |
| packageLimits); |
| } |
| |
| /** Retrieves this object from a Starlark thread. Returns null if not present. */ |
| @Nullable |
| public static Builder fromOrNull(StarlarkThread thread) { |
| StarlarkThreadContext ctx = thread.getThreadLocal(StarlarkThreadContext.class); |
| return ctx instanceof Builder builder ? builder : null; |
| } |
| |
| Package getPackage() { |
| return (Package) pkg; |
| } |
| |
| @Override |
| @CanIgnoreReturnValue |
| public Builder setLoads(Iterable<Module> directLoads) { |
| return (Builder) super.setLoads(directLoads); |
| } |
| |
| @Override |
| Map<String, Rule> getRulesSnapshotView() { |
| if (rulesSnapshotViewForFinalizers != null) { |
| return rulesSnapshotViewForFinalizers; |
| } else { |
| return super.getRulesSnapshotView(); |
| } |
| } |
| |
| @Override |
| @Nullable |
| Rule getNonFinalizerInstantiatedRule(String name) { |
| if (rulesSnapshotViewForFinalizers != null) { |
| return rulesSnapshotViewForFinalizers.get(name); |
| } else { |
| return super.getNonFinalizerInstantiatedRule(name); |
| } |
| } |
| |
| public void addRuleUnchecked(Rule rule) { |
| Preconditions.checkArgument(rule.getPackage() == pkg); |
| recorder.addRuleUnchecked(rule); |
| } |
| |
| /** Adds all targets, macros, and Starlark computation steps from a given package piece. */ |
| public void addAllFromPackagePiece(PackagePiece packagePiece) throws NameConflictException { |
| // We add the BUILD file in newPackageFromPackagePiecesBuilder(), not here. (We want to ensure |
| // that the package always has a BUILD file target, even if addAllFromPackagePiece would throw |
| // a name conflict.) |
| this.recorder.addAllFromPackagePiece(packagePiece, /* skipBuildFile= */ true); |
| this.computationSteps += packagePiece.getComputationSteps(); |
| } |
| |
| @Override |
| public boolean eagerlyExpandMacros() { |
| return true; |
| } |
| |
| @Override |
| public void addMacro(MacroInstance macro) throws NameConflictException { |
| super.addMacro(macro); |
| unexpandedMacros.add(macro.getId()); |
| } |
| |
| // For Package deserialization. |
| void putAllMacroNamespaceViolatingTargets(Map<String, String> macroNamespaceViolatingTargets) { |
| recorder.putAllMacroNamespaceViolatingTargets(macroNamespaceViolatingTargets); |
| } |
| |
| void putAllTargetsToDeclaringPackage(Map<String, PackageIdentifier> targetsToDeclaringPackage) { |
| recorder.putAllTargetsToDeclaringPackage(targetsToDeclaringPackage); |
| } |
| |
| /** |
| * Marks a symbolic macro as having finished evaluating. |
| * |
| * <p>This will prevent the macro from being run by {@link #expandAllRemainingMacros}. |
| * |
| * <p>The macro must not have previously been marked complete. |
| */ |
| public void markMacroComplete(MacroInstance macro) { |
| String id = macro.getId(); |
| if (!unexpandedMacros.remove(id)) { |
| throw new IllegalArgumentException( |
| String.format("Macro id '%s' unknown or already marked complete", id)); |
| } |
| } |
| |
| /** |
| * Ensures that all symbolic macros in an error-free package have expanded. No-op if the package |
| * already {@link #containsErrors}. |
| * |
| * <p>This does not run any macro that has already been evaluated. It *does* run macros that are |
| * newly discovered during the operation of this method. |
| */ |
| public void expandAllRemainingMacros(StarlarkSemantics semantics) |
| throws EvalException, InterruptedException { |
| // TODO: #19922 - Protect against unreasonable macro stack depth and large numbers of symbolic |
| // macros overall, for both the eager and deferred evaluation strategies. |
| |
| // Note that this operation is idempotent for symmetry with build()/buildPartial(). Though |
| // it's not entirely clear that this is necessary. |
| |
| // TODO: #19922 - Once compatibility with native.existing_rules() in legacy macros is no |
| // longer a concern, we will want to support delayed expansion of non-finalizer macros before |
| // the finalizer expansion step. |
| |
| // Finalizer expansion step. Requires that the package not be in error (no point in finalizing |
| // a package that already threw an EvalException). |
| if (!containsErrors() && !unexpandedMacros.isEmpty()) { |
| Preconditions.checkState( |
| unexpandedMacros.stream() |
| .allMatch(id -> recorder.getMacroMap().get(id).getMacroClass().isFinalizer()), |
| "At the beginning of finalizer expansion, unexpandedMacros must contain only" |
| + " finalizers"); |
| |
| // Save a snapshot of rule targets for use by native.existing_rules() inside all finalizers. |
| // We must take this snapshot before calling any finalizer because the snapshot must not |
| // include any rule instantiated by a finalizer or macro called from a finalizer. |
| if (rulesSnapshotViewForFinalizers == null) { |
| Preconditions.checkState( |
| recorder.getTargetMap() instanceof SnapshottableBiMap<?, ?>, |
| "Cannot call expandAllRemainingMacros() after beforeBuild() has been called"); |
| rulesSnapshotViewForFinalizers = getRulesSnapshotView(); |
| } |
| |
| while (!unexpandedMacros.isEmpty()) { // NB: collection mutated by body |
| String id = unexpandedMacros.iterator().next(); |
| MacroInstance macro = recorder.getMacroMap().get(id); |
| MacroClass.executeMacroImplementation(macro, this, semantics); |
| } |
| } |
| } |
| |
| @Override |
| @CanIgnoreReturnValue |
| protected Builder beforeBuild() throws NoSuchPackageException { |
| // For correct semantics, we refuse to build a package that hasn't thrown any EvalExceptions |
| // but has declared symbolic macros that have not yet been expanded. (Currently finalizers are |
| // the only use case where this happens, but the Package logic is agnostic to that detail.) |
| // |
| // Production code should be calling expandAllRemainingMacros() to guarantee that nothing is |
| // left unexpanded. Tests that do not declare any symbolic macros need not make the call. |
| // Package deserialization doesn't have to do it either, since we shouldn't be evaluating |
| // symbolic macros on the deserialized result of an already evaluated package. |
| Preconditions.checkState( |
| unexpandedMacros.isEmpty() || containsErrors(), |
| "Cannot build a package with unexpanded symbolic macros; call" |
| + " expandAllRemainingMacros()"); |
| |
| // SnapshottableBiMap does not allow removing targets (in order to efficiently track rule |
| // insertion order). However, we *do* need to support removal of targets in |
| // PackageFunction.handleLabelsCrossingSubpackagesAndPropagateInconsistentFilesystemExceptions |
| // which is called *between* calls to beforeBuild and finishBuild. We thus repoint the targets |
| // map to the SnapshottableBiMap's underlying bimap and thus stop tracking insertion order. |
| // After this point, snapshots of targets should no longer be used, and any further |
| // getRulesSnapshotView calls will throw. |
| if (recorder.getTargetMap() instanceof SnapshottableBiMap<?, ?>) { |
| recorder.unwrapSnapshottableBiMap(); |
| rulesSnapshotViewForFinalizers = null; |
| } |
| |
| super.beforeBuild(); |
| |
| return this; |
| } |
| |
| @Override |
| @CanIgnoreReturnValue |
| public Builder buildPartial() throws NoSuchPackageException { |
| return (Builder) super.buildPartial(); |
| } |
| |
| @Override |
| protected void setBuildFile(InputFile buildFile) { |
| ((Package) pkg).buildFile = checkNotNull(buildFile); |
| } |
| |
| @Override |
| public Package finishBuild() { |
| return (Package) super.finishBuild(); |
| } |
| |
| @Override |
| protected void packageoidInitializationHook() { |
| super.packageoidInitializationHook(); |
| Package pkg = getPackage(); |
| pkg.computationSteps = getComputationSteps(); |
| pkg.macros = ImmutableSortedMap.copyOf(recorder.getMacroMap()); |
| pkg.macroNamespaceViolatingTargets = |
| ImmutableMap.copyOf(recorder.getMacroNamespaceViolatingTargets()); |
| pkg.targetsToDeclaringMacro = |
| recorder.getTargetsToDeclaringMacro() != null |
| ? ImmutableSortedMap.copyOf(recorder.getTargetsToDeclaringMacro()) |
| : null; |
| pkg.targetsToDeclaringPackage = |
| recorder.getTargetsToDeclaringPackage() != null |
| ? ImmutableSortedMap.copyOf(recorder.getTargetsToDeclaringPackage()) |
| : null; |
| } |
| |
| /** Completes package construction. Idempotent. */ |
| // TODO(brandjon): Do we actually care about idempotence? |
| public Package build() throws NoSuchPackageException { |
| return build(/* discoverAssumedInputFiles= */ true); |
| } |
| |
| /** |
| * Constructs the package (or does nothing if it's already built) and returns it. |
| * |
| * @param discoverAssumedInputFiles whether to automatically add input file targets to this |
| * package for "dangling labels", i.e. labels mentioned in this package that point to an |
| * up-until-now non-existent target in this package |
| */ |
| Package build(boolean discoverAssumedInputFiles) throws NoSuchPackageException { |
| if (alreadyBuilt) { |
| return getPackage(); |
| } |
| if (discoverAssumedInputFiles) { |
| beforeBuild(); |
| } else { |
| beforeBuildWithoutDiscoveringAssumedInputFiles(); |
| } |
| return finishBuild(); |
| } |
| } |
| |
| /** A collection of data that is known before BUILD file evaluation even begins. */ |
| // TODO(bazel-team): move to Packageoid.java or to its own file to reduce size of Package.java? |
| @AutoCodec |
| public record Metadata( |
| PackageIdentifier packageIdentifier, |
| RootedPath buildFilename, |
| Label buildFileLabel, |
| /** |
| * The name of the workspace this package is in. Used as a prefix for the runfiles directory. |
| */ |
| String workspaceName, |
| RepositoryMapping repositoryMapping, |
| Optional<String> associatedModuleName, |
| Optional<String> associatedModuleVersion, |
| @Nullable ConfigSettingVisibilityPolicy configSettingVisibilityPolicy, |
| boolean succinctTargetNotFoundErrors, |
| Root sourceRoot) { |
| |
| public static Builder builder() { |
| return new AutoBuilder_Package_Metadata_Builder(); |
| } |
| |
| /** Builder for {@link Metadata}. */ |
| @AutoBuilder(callMethod = "of") |
| public interface Builder { |
| Builder packageIdentifier(PackageIdentifier packageIdentifier); |
| |
| Builder buildFilename(RootedPath buildFilename); |
| |
| Builder workspaceName(String workspaceName); |
| |
| Builder repositoryMapping(RepositoryMapping repositoryMapping); |
| |
| Builder associatedModuleName(Optional<String> associatedModuleName); |
| |
| Builder associatedModuleVersion(Optional<String> associatedModuleVersion); |
| |
| Builder configSettingVisibilityPolicy( |
| @Nullable ConfigSettingVisibilityPolicy configSettingVisibilityPolicy); |
| |
| Builder succinctTargetNotFoundErrors(boolean succinctTargetNotFoundErrors); |
| |
| Metadata build(); |
| } |
| |
| static Metadata of( |
| PackageIdentifier packageIdentifier, |
| RootedPath buildFilename, |
| String workspaceName, |
| RepositoryMapping repositoryMapping, |
| Optional<String> associatedModuleName, |
| Optional<String> associatedModuleVersion, |
| @Nullable ConfigSettingVisibilityPolicy configSettingVisibilityPolicy, |
| boolean succinctTargetNotFoundErrors) { |
| Label buildFileLabel; |
| try { |
| buildFileLabel = |
| Label.create(packageIdentifier, buildFilename.getRootRelativePath().getBaseName()); |
| } catch (LabelSyntaxException e) { |
| // This can't actually happen. |
| throw new AssertionError("Package BUILD file has an illegal name: " + buildFilename, e); |
| } |
| return new Metadata( |
| packageIdentifier, |
| buildFilename, |
| buildFileLabel, |
| workspaceName, |
| repositoryMapping, |
| associatedModuleName, |
| associatedModuleVersion, |
| configSettingVisibilityPolicy, |
| succinctTargetNotFoundErrors, |
| computeSourceRoot(packageIdentifier, buildFilename)); |
| } |
| |
| /** |
| * @deprecated Use {@link #builder()} instead. |
| */ |
| @Deprecated |
| public Metadata { |
| Preconditions.checkNotNull(packageIdentifier); |
| Preconditions.checkNotNull(buildFilename); |
| Preconditions.checkNotNull(workspaceName); |
| Preconditions.checkNotNull(repositoryMapping); |
| Preconditions.checkNotNull(associatedModuleName); |
| Preconditions.checkNotNull(associatedModuleVersion); |
| Preconditions.checkNotNull(sourceRoot); |
| } |
| |
| /** Returns the name of this package (sans repository), e.g. "foo/bar". */ |
| public String getName() { |
| return packageIdentifier.getPackageFragment().getPathString(); |
| } |
| |
| /** |
| * Returns the directory in which this package's BUILD file resides. |
| * |
| * <p>All InputFile members of the packages are located relative to this directory. |
| */ |
| public Path getPackageDirectory() { |
| return getPackageDirectory(buildFilename); |
| } |
| |
| /** Returns the {@link Location} of the package's BUILD file. */ |
| public Location getBuildFileLocation() { |
| return Location.fromFile(buildFilename.asPath().toString()); |
| } |
| |
| private static Path getPackageDirectory(RootedPath buildFilename) { |
| return buildFilename.asPath().getParentDirectory(); |
| } |
| |
| private static Root computeSourceRoot( |
| PackageIdentifier packageIdentifier, RootedPath buildFilename) { |
| Preconditions.checkNotNull(packageIdentifier); |
| Preconditions.checkNotNull(buildFilename); |
| |
| RootedPath buildFileRootedPath = buildFilename; |
| Root buildFileRoot = buildFileRootedPath.getRoot(); |
| PathFragment pkgIdFragment = packageIdentifier.getSourceRoot(); |
| PathFragment pkgDirFragment = buildFileRootedPath.getRootRelativePath().getParentDirectory(); |
| |
| Root sourceRoot; |
| if (pkgIdFragment.equals(pkgDirFragment)) { |
| // Fast path: BUILD file path and package name are the same, don't create an extra root. |
| sourceRoot = buildFileRoot; |
| } else { |
| // TODO(bazel-team): Can this expr be simplified to just pkgDirFragment? |
| PathFragment current = buildFileRootedPath.asPath().asFragment().getParentDirectory(); |
| for (int i = 0, len = pkgIdFragment.segmentCount(); i < len && current != null; i++) { |
| current = current.getParentDirectory(); |
| } |
| if (current == null || current.isEmpty()) { |
| // This is never really expected to work. The below check should fail. |
| sourceRoot = buildFileRoot; |
| } else { |
| // Note that current is an absolute path. |
| sourceRoot = Root.fromPath(buildFileRoot.getRelative(current)); |
| } |
| } |
| |
| Preconditions.checkArgument( |
| sourceRoot.asPath() != null |
| && sourceRoot.getRelative(pkgIdFragment).equals(getPackageDirectory(buildFilename)), |
| "Invalid BUILD file name for package '%s': %s (in source %s with packageDirectory %s and" |
| + " package identifier source root %s)", |
| packageIdentifier, |
| buildFilename, |
| sourceRoot, |
| getPackageDirectory(buildFilename), |
| packageIdentifier.getSourceRoot()); |
| |
| return sourceRoot; |
| } |
| } |
| |
| /** |
| * A collection of data about a package that is known after BUILD file evaluation has completed, |
| * which doesn't require expanding any symbolic macros, and which transitively doesn't hold |
| * references to {@link Package} or {@link PackagePiece} objects. Treated as immutable after BUILD |
| * file evaluation has completed. |
| * |
| * <p>Instances of the base {@link Declarations} class are immutable; for a mutable builder, see |
| * {@link Declarations.Builder}. |
| */ |
| public static sealed class Declarations permits Declarations.Builder { |
| // All fields are non-final only to allow builder subclass to mutate them. |
| |
| // These two fields are mutated only during BUILD file evaluation (not during symbolic macro |
| // evaluation). |
| protected PackageArgs packageArgs = PackageArgs.DEFAULT; |
| protected ImmutableMap<String, String> makeEnv; |
| |
| // These two fields are mutually exclusive. Which one is set depends on |
| // PackageSettings#precomputeTransitiveLoads. See Package.Builder#setLoads. |
| @Nullable protected ImmutableList<Module> directLoads; |
| @Nullable protected ImmutableList<Label> transitiveLoads; |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| return obj instanceof Declarations other |
| && Objects.equals(packageArgs, other.packageArgs) |
| && Objects.equals(makeEnv, other.makeEnv) |
| // Serializers use getOrComputeTransitivelyLoadedStarlarkFiles() and don't distinguish |
| // between directLoads and transitiveLoads. |
| && Objects.equals( |
| getOrComputeTransitivelyLoadedStarlarkFilesInternal(), |
| other.getOrComputeTransitivelyLoadedStarlarkFilesInternal()); |
| } |
| |
| @Override |
| public int hashCode() { |
| return HashCodes.hashObjects( |
| packageArgs, |
| makeEnv, |
| // Serializers use getOrComputeTransitivelyLoadedStarlarkFiles() and don't distinguish |
| // between directLoads and transitiveLoads. |
| getOrComputeTransitivelyLoadedStarlarkFilesInternal()); |
| } |
| |
| /** |
| * Returns the collection of package-level attributes set by the {@code package()} callable and |
| * similar methods. |
| */ |
| public PackageArgs getPackageArgs() { |
| return packageArgs; |
| } |
| |
| /** |
| * Returns the "Make" environment of this package, containing package-local definitions of |
| * "Make" variables. |
| */ |
| public ImmutableMap<String, String> getMakeEnvironment() { |
| return makeEnv; |
| } |
| |
| /** |
| * Returns a list of Starlark files transitively loaded by this package. |
| * |
| * <p>If transitive loads are not {@linkplain PackageSettings#precomputeTransitiveLoads |
| * precomputed}, performs a traversal over the load graph to compute them. |
| * |
| * <p>If only the count of transitively loaded files is needed, use {@link |
| * #countTransitivelyLoadedStarlarkFiles}. For a customized online visitation, use {@link |
| * #visitLoadGraph}. |
| * |
| * <p>This method can only be used after the Package or PackagePiece has been fully initialized |
| * (i.e. after {@link TargetDefinitionContext#finishBuild} has been called). |
| */ |
| public ImmutableList<Label> getOrComputeTransitivelyLoadedStarlarkFiles() { |
| return checkNotNull(getOrComputeTransitivelyLoadedStarlarkFilesInternal()); |
| } |
| |
| @Nullable |
| private ImmutableList<Label> getOrComputeTransitivelyLoadedStarlarkFilesInternal() { |
| if (transitiveLoads != null) { |
| return transitiveLoads; |
| } else if (directLoads != null) { |
| return computeTransitiveLoads(directLoads); |
| } else { |
| // Declarations not fully initialized. |
| return null; |
| } |
| } |
| |
| /** |
| * Counts the number Starlark files transitively loaded by this package. |
| * |
| * <p>If transitive loads are not {@linkplain PackageSettings#precomputeTransitiveLoads |
| * precomputed}, performs a traversal over the load graph to count them. |
| * |
| * <p>This method can only be used after the Package or PackagePiece has been fully initialized |
| * (i.e. after {@link TargetDefinitionContext#finishBuild} has been called). |
| */ |
| public int countTransitivelyLoadedStarlarkFiles() { |
| if (transitiveLoads != null) { |
| return transitiveLoads.size(); |
| } |
| Set<Label> loads = new HashSet<>(); |
| visitLoadGraph(loads::add); |
| return loads.size(); |
| } |
| |
| /** |
| * Performs an online visitation of the load graph rooted at this package. |
| * |
| * <p>If transitive loads were {@linkplain PackageSettings#precomputeTransitiveLoads |
| * precomputed}, each file is passed to {@link LoadGraphVisitor#visit} once regardless of its |
| * return value. |
| * |
| * <p>This method can only be used after the Package or PackagePiece has been fully initialized |
| * (i.e. after {@link TargetDefinitionContext#finishBuild} has been called). |
| */ |
| public <E1 extends Exception, E2 extends Exception> void visitLoadGraph( |
| LoadGraphVisitor<E1, E2> visitor) throws E1, E2 { |
| if (transitiveLoads != null) { |
| for (Label load : transitiveLoads) { |
| visitor.visit(load); |
| } |
| } else { |
| BazelModuleContext.visitLoadGraphRecursively(directLoads, visitor); |
| } |
| } |
| |
| @CanIgnoreReturnValue |
| public Declarations.Builder checkMutable() { |
| if (this instanceof Declarations.Builder builder) { |
| return builder; |
| } |
| throw new IllegalStateException("Package declarations has been finalized and is immutable."); |
| } |
| |
| @CanIgnoreReturnValue |
| public Declarations checkImmutable() { |
| if (this instanceof Builder) { |
| throw new IllegalStateException("Package declarations is in mutable state."); |
| } |
| return this; |
| } |
| |
| private Declarations( |
| PackageArgs packageArgs, |
| ImmutableMap<String, String> makeEnv, |
| @Nullable ImmutableList<Module> directLoads, |
| @Nullable ImmutableList<Label> transitiveLoads) { |
| this.packageArgs = checkNotNull(packageArgs); |
| this.makeEnv = checkNotNull(makeEnv); |
| this.directLoads = directLoads; |
| this.transitiveLoads = transitiveLoads; |
| checkArgument( |
| directLoads == null ^ transitiveLoads == null, |
| "Exactly one of directLoads and transitiveLoads must be set"); |
| } |
| |
| /** Default constructor for use only by {@link Builder}. */ |
| private Declarations() {} |
| |
| /** Builder for {@link Declarations}. */ |
| public static final class Builder extends Declarations { |
| |
| @CanIgnoreReturnValue |
| public Builder setPackageArgs(PackageArgs packageArgs) { |
| this.packageArgs = checkNotNull(packageArgs); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public Builder mergePackageArgsFrom(PackageArgs packageArgs) { |
| this.packageArgs = this.packageArgs.mergeWith(checkNotNull(packageArgs)); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public Builder setMakeEnvironment(Map<String, String> makeEnv) { |
| this.makeEnv = ImmutableMap.copyOf(checkNotNull(makeEnv)); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public Builder setDirectLoads(List<Module> directLoads) { |
| this.directLoads = ImmutableList.copyOf(checkNotNull(directLoads)); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public Builder setTransitiveLoads(List<Label> transitiveLoads) { |
| this.transitiveLoads = ImmutableList.copyOf(checkNotNull(transitiveLoads)); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| private Builder checkLoadsNotSet() { |
| checkState(directLoads == null, "Direct loads already set: %s", directLoads); |
| checkState(transitiveLoads == null, "Transitive loads already set: %s", transitiveLoads); |
| return this; |
| } |
| |
| public Declarations build() { |
| return new Declarations(packageArgs, makeEnv, directLoads, transitiveLoads); |
| } |
| |
| public Builder() { |
| packageArgs = PackageArgs.DEFAULT; |
| makeEnv = ImmutableMap.of(); |
| } |
| } |
| } |
| |
| /** Package codec implementation. */ |
| @VisibleForTesting |
| static final class PackageCodec implements ObjectCodec<Package> { |
| @Override |
| public Class<Package> getEncodedClass() { |
| return Package.class; |
| } |
| |
| @Override |
| public void serialize(SerializationContext context, Package input, CodedOutputStream codedOut) |
| throws IOException, SerializationException { |
| context.checkClassExplicitlyAllowed(Package.class, input); |
| PackageCodecDependencies codecDeps = context.getDependency(PackageCodecDependencies.class); |
| codecDeps.getPackageSerializer().serialize(context, input, codedOut); |
| } |
| |
| @Override |
| public Package deserialize(DeserializationContext context, CodedInputStream codedIn) |
| throws SerializationException, IOException { |
| PackageCodecDependencies codecDeps = context.getDependency(PackageCodecDependencies.class); |
| return codecDeps.getPackageSerializer().deserialize(context, codedIn); |
| } |
| } |
| } |