blob: 34abb77434ce091f0c2cae340f52fb4d13835118 [file] [log] [blame]
// Copyright 2024 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.skyframe;
import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.lib.actions.FileValue;
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.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunction.Environment.SkyKeyComputeState;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.io.IOException;
import java.util.ArrayList;
import javax.annotation.Nullable;
/** {@link SkyFunction} for {@link ProjectFilesLookupValue}. */
public class ProjectFilesLookupFunction implements SkyFunction {
/** Name of project metadata files. See {@link com.google.devtools.build.lib.analysis.Project}. */
@VisibleForTesting public static final String PROJECT_FILE_NAME = "PROJECT.scl";
private static class State implements SkyKeyComputeState {
/** Which directory up the package path are we currently examining? */
private PackageIdentifier currentDir = null;
/** Which project files have we discovered so far? */
private final ArrayList<Label> projectFiles = new ArrayList<>();
}
@Override
@Nullable
public SkyValue compute(SkyKey skyKey, Environment env)
throws InterruptedException, ProjectFilesLookupException {
State state = env.getState(State::new);
// Tracks the current directory we're checking for BUILD and PROJECT.scl files. According to the
// ProjectFilesLookupValue API contract, the original value from the skykey should be a package
// (i.e. has a BUILD file). But this code doesn't require that: if the directory doesn't have a
// BUILD file the code still gracefully finds the nearest enclosing package. This is especially
// important when walking up the directory path, since the parent directory of a package isn't
// necessarily another package.
if (state.currentDir == null) {
state.currentDir = (PackageIdentifier) skyKey.argument();
}
while (true) {
ContainingPackageLookupValue innermostPkgLookupValue =
(ContainingPackageLookupValue)
env.getValue(ContainingPackageLookupValue.key(state.currentDir));
if (innermostPkgLookupValue == null) {
return null;
}
if (!innermostPkgLookupValue.hasContainingPackage()) {
// We've reached the root directory: nothing left. This list may be empty but that's okay.
// That just means the input package has no associated project files.
return ProjectFilesLookupValue.of(state.projectFiles);
}
// Now that we've found a BUILD file, determine the project file path to look for.
PackageIdentifier innermostPkgId = innermostPkgLookupValue.getContainingPackageName();
Label projectFileLabel;
try {
projectFileLabel = Label.create(innermostPkgId, PROJECT_FILE_NAME);
} catch (LabelSyntaxException e) {
throw new IllegalStateException("Unexpected failure parsing " + PROJECT_FILE_NAME, e);
}
// Lookup the project file.
PathFragment projectFilePath =
innermostPkgId.getPackageFragment().getRelative(PROJECT_FILE_NAME);
SkyKey fileSkyKey =
FileValue.key(
RootedPath.toRootedPath(
innermostPkgLookupValue.getContainingPackageRoot(), projectFilePath));
FileValue fileValue;
try {
fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class);
} catch (IOException e) {
throw new ProjectFilesLookupException(e);
}
if (fileValue == null) {
return null;
}
if (fileValue.isFile()) {
state.projectFiles.add(projectFileLabel);
} else if (fileValue.exists()) {
throw new ProjectFilesLookupException(
new UnexpectedProjectFileTypeException(
projectFilePath.getPathString() + " isn't a file"));
}
PathFragment parentDir = innermostPkgId.getPackageFragment().getParentDirectory();
if (parentDir == null) {
// Hit the root directory. Returns the results we've collected.
return ProjectFilesLookupValue.of(state.projectFiles);
}
state.currentDir = PackageIdentifier.create(state.currentDir.getRepository(), parentDir);
}
}
/** A project file exists but isn't a file. */
private static final class UnexpectedProjectFileTypeException extends Exception {
UnexpectedProjectFileTypeException(String msg) {
super(msg);
}
}
private static final class ProjectFilesLookupException extends SkyFunctionException {
ProjectFilesLookupException(IOException e) {
super(e, Transience.PERSISTENT);
}
ProjectFilesLookupException(UnexpectedProjectFileTypeException e) {
super(e, Transience.PERSISTENT);
}
}
}