| // Copyright 2023 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.producers; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; |
| import com.google.devtools.build.lib.packages.Globber.Operation; |
| import com.google.devtools.build.lib.packages.producers.GlobComputationProducer.GlobDetail; |
| import com.google.devtools.build.lib.skyframe.PackageLookupValue; |
| import com.google.devtools.build.lib.util.Pair; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.skyframe.SkyValue; |
| import com.google.devtools.build.skyframe.state.StateMachine; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Handles package (sub-)directory entry which is also a directory and matches part or all of the |
| * glob pattern fragments. |
| * |
| * <p>Created by {@link PatternWithWildcardProducer} or {@link PatternWithoutWildcardProducer} after |
| * they confirm that {@linkplain #direntPath the directory dirent} matches part or all of the glob |
| * pattern fragments. File dirents are handled inline within the two upstream producers. |
| * |
| * <p>Checks whether {@link #direntPath} is a qualified glob matching result, and add to result if |
| * so. Before creating the next {@link FragmentProducer} also checks whether (1) there are pattern |
| * fragment(s) left to match and (2) subpackage crossing exists . |
| */ |
| final class DirectoryDirentProducer implements StateMachine, Consumer<SkyValue> { |
| |
| // -------------------- Input -------------------- |
| private final GlobDetail globDetail; |
| |
| /** The {@link PathFragment} of the dirent containing the package fragments. */ |
| private final PathFragment direntPath; |
| |
| private final int fragmentIndex; |
| |
| // -------------------- Internal State -------------------- |
| private PackageLookupValue packageLookupValue = null; |
| @Nullable private final Set<Pair<PathFragment, Integer>> visitedGlobSubTasks; |
| |
| // -------------------- Output -------------------- |
| private final FragmentProducer.ResultSink resultSink; |
| |
| DirectoryDirentProducer( |
| GlobDetail globDetail, |
| PathFragment direntPath, |
| int fragmentIndex, |
| FragmentProducer.ResultSink resultSink, |
| @Nullable Set<Pair<PathFragment, Integer>> visitedGlobSubTasks) { |
| // Upstream logic should already have appended some dirent to the package fragment when |
| // constructing this `direntPath`. |
| Preconditions.checkArgument( |
| !direntPath.equals(globDetail.packageIdentifier().getPackageFragment())); |
| this.direntPath = direntPath; |
| this.globDetail = globDetail; |
| this.fragmentIndex = fragmentIndex; |
| this.resultSink = resultSink; |
| this.visitedGlobSubTasks = visitedGlobSubTasks; |
| } |
| |
| @Override |
| public StateMachine step(Tasks tasks) { |
| // Check whether the next directory path matches any `IgnoredPackagePrefix`. |
| for (PathFragment ignoredPrefix : globDetail.ignoredPackagePrefixesPatterns()) { |
| if (direntPath.startsWith(ignoredPrefix)) { |
| return DONE; |
| } |
| } |
| |
| tasks.lookUp( |
| PackageLookupValue.key( |
| PackageIdentifier.create(globDetail.packageIdentifier().getRepository(), direntPath)), |
| (Consumer<SkyValue>) this); |
| return this::checkSubpackageExistence; |
| } |
| |
| @Override |
| public void accept(SkyValue skyValue) { |
| packageLookupValue = (PackageLookupValue) skyValue; |
| } |
| |
| private StateMachine checkSubpackageExistence(Tasks tasks) { |
| Preconditions.checkNotNull(packageLookupValue); |
| if (packageLookupValue |
| instanceof PackageLookupValue.IncorrectRepositoryReferencePackageLookupValue) { |
| // Cross repository boundary, so glob expansion should not descend into that subdir. |
| return DONE; |
| } |
| |
| if (packageLookupValue.packageExists()) { |
| // Cross the package boundary. The subdir contains a BUILD file and thus defines another |
| // package, so glob expansion should not descend into that subdir. |
| if (globDetail.globOperation().equals(Operation.SUBPACKAGES)) { |
| // If this is a subpackages() call, we need to check whether this subpackage is a glob |
| // matching before returning. |
| if (shouldAddResult(/* isSubpackage= */ true)) { |
| resultSink.acceptPathFragmentWithPackageFragment(direntPath); |
| } |
| } |
| return DONE; |
| } |
| |
| return addResultsOrCreateNextFragmentProducer(tasks); |
| } |
| |
| private StateMachine addResultsOrCreateNextFragmentProducer(Tasks tasks) { |
| // Even for directory dirent, we need to check whether this path is a matching result. |
| if (shouldAddResult(/* isSubpackage= */ false)) { |
| resultSink.acceptPathFragmentWithPackageFragment(direntPath); |
| } |
| |
| int nextFragmentIndex = |
| globDetail.patternFragments().get(fragmentIndex).equals("**") |
| ? fragmentIndex |
| : fragmentIndex + 1; |
| if (nextFragmentIndex == globDetail.patternFragments().size()) { |
| // When the last glob pattern is not double star, we have already processed all pattern |
| // fragments, so execution enters this block and immediately returns. |
| return DONE; |
| } |
| |
| if (visitedGlobSubTasks == null |
| || visitedGlobSubTasks.add(Pair.of(direntPath, nextFragmentIndex))) { |
| // Create the next unvisited `FragmentProducer` if it has not been processed. |
| tasks.enqueue( |
| new FragmentProducer( |
| globDetail, direntPath, nextFragmentIndex, visitedGlobSubTasks, resultSink)); |
| } |
| return DONE; |
| } |
| |
| /** Returns {@code true} if {@code path} can be added as a glob matching result. */ |
| private boolean shouldAddResult(boolean isSubpackage) { |
| switch (globDetail.globOperation()) { |
| case FILES: |
| // The dirent is always a directory, so it can never match FILE operation. |
| return false; |
| case SUBPACKAGES: |
| return isSubpackage |
| && allRemainPatternFragmentsDoubleStar(globDetail.patternFragments(), fragmentIndex); |
| case FILES_AND_DIRS: |
| return fragmentIndex == globDetail.patternFragments().size() - 1 && !isSubpackage; |
| } |
| throw new IllegalStateException( |
| "Unexpected globber globberOperation = [" + globDetail.globOperation() + "]"); |
| } |
| |
| private static boolean allRemainPatternFragmentsDoubleStar( |
| ImmutableList<String> patternFragments, int index) { |
| if (index == patternFragments.size() - 1) { |
| // Already covered all pattern fragments at this point, so we don't need to check additional |
| // pattern fragments. |
| return true; |
| } |
| return patternFragments.subList(index + 1, patternFragments.size()).stream() |
| .allMatch("**"::equals); |
| } |
| } |