blob: 2799fcc21b4d25ec4c8db8561a801a699a348f2a [file] [log] [blame]
// Copyright 2019 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.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.actions.MissingInputFileException;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration;
import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.syntax.Location;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
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.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
/**
* Function that reads the contents of a mapping file specified in {@code --platform_mappings} and
* parses them for use in a {@link PlatformMappingValue}.
*
* <p>Note that this class only parses the mapping-file specific format, parsing (and validation) of
* flags contained therein is left to the invocation of {@link
* PlatformMappingValue#map(BuildConfigurationValue.Key, BuildOptions)}.
*/
public class PlatformMappingFunction implements SkyFunction {
@Nullable
@Override
public PlatformMappingValue compute(SkyKey skyKey, Environment env)
throws PlatformMappingException, InterruptedException {
PlatformMappingValue.Key platformMappingKey = (PlatformMappingValue.Key) skyKey.argument();
PathFragment workspaceRelativeMappingPath =
platformMappingKey.getWorkspaceRelativeMappingPath();
PathPackageLocator pkgLocator = PrecomputedValue.PATH_PACKAGE_LOCATOR.get(env);
if (pkgLocator == null) {
return null;
}
ImmutableList<Root> pathEntries = pkgLocator.getPathEntries();
for (Root root : pathEntries) {
RootedPath rootedMappingPath = RootedPath.toRootedPath(root, workspaceRelativeMappingPath);
FileValue fileValue = (FileValue) env.getValue(FileValue.key(rootedMappingPath));
if (fileValue == null) {
return null;
}
if (!fileValue.exists()) {
continue;
}
if (fileValue.isDirectory()) {
throw new PlatformMappingException(
new MissingInputFileException(
createFailureDetail(
String.format(
"--platform_mappings was set to '%s' relative to the top-level workspace"
+ " '%s' but that path refers to a directory, not a file",
workspaceRelativeMappingPath, root),
Code.PLATFORM_MAPPINGS_FILE_IS_DIRECTORY),
Location.BUILTIN),
SkyFunctionException.Transience.PERSISTENT);
}
Iterable<String> lines;
try {
lines =
FileSystemUtils.readLines(fileValue.realRootedPath().asPath(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new PlatformMappingException(e, SkyFunctionException.Transience.TRANSIENT);
}
return new Parser(lines.iterator()).parse().toPlatformMappingValue();
}
if (!platformMappingKey.wasExplicitlySetByUser()) {
// If no flag was passed and the default mapping file does not exist treat this as if the
// mapping file was empty rather than an error.
return PlatformMappingValue.EMPTY;
}
throw new PlatformMappingException(
new MissingInputFileException(
createFailureDetail(
String.format(
"--platform_mappings was set to '%s' but no such file exists relative to the "
+ "package path roots, '%s'",
workspaceRelativeMappingPath, pathEntries),
Code.PLATFORM_MAPPINGS_FILE_NOT_FOUND),
Location.BUILTIN),
SkyFunctionException.Transience.PERSISTENT);
}
private static FailureDetail createFailureDetail(String message, Code detailedCode) {
return FailureDetail.newBuilder()
.setMessage(message)
.setBuildConfiguration(BuildConfiguration.newBuilder().setCode(detailedCode))
.build();
}
@Nullable
@Override
public String extractTag(SkyKey skyKey) {
return null;
}
@VisibleForTesting
static class PlatformMappingException extends SkyFunctionException {
public PlatformMappingException(Exception cause, Transience transience) {
super(cause, transience);
}
}
@VisibleForTesting
static class Parser {
private final Iterator<String> lines;
/**
* Using an optional to represent the next line with contents, {@link Optional#empty()} if we
* reached end of file.
*
* <p>Stores the current non-comment, non-empty, non-whitespace line. Don't access the field
* directly, it can either be "used up" by calling {@link #consume()} or retrieved without
* moving on by calling {@link #peek()}.
*/
private Optional<String> line;
Parser(Iterator<String> lines) {
this.lines = lines;
}
Mappings parse() throws PlatformMappingException {
goToNextContentLine();
if (!line.isPresent()) {
return new Mappings(ImmutableMap.of(), ImmutableMap.of());
}
Map<Label, Collection<String>> platformsToFlags = ImmutableMap.of();
Map<Collection<String>, Label> flagsToPlatforms = ImmutableMap.of();
if (!peek().equalsIgnoreCase("platforms:") && !peek().equalsIgnoreCase("flags:")) {
throwParsingException("Expected 'platforms:' or 'flags:' but got " + peek());
}
if (peek().equalsIgnoreCase("platforms:")) {
consume();
platformsToFlags = platformsToFlags();
}
if (line.isPresent()) {
if (!peek().equalsIgnoreCase("flags:")) {
throwParsingException("Expected 'flags:' but got " + peek());
}
consume();
flagsToPlatforms = flagsToPlatforms();
}
if (line.isPresent()) {
throwParsingException("Expected end of file but got " + peek());
}
return new Mappings(platformsToFlags, flagsToPlatforms);
}
private Map<Label, Collection<String>> platformsToFlags() throws PlatformMappingException {
ImmutableMap.Builder<Label, Collection<String>> platformsToFlags = ImmutableMap.builder();
while (line.isPresent() && !peek().equalsIgnoreCase("flags:")) {
Label platform = platform();
Collection<String> flags = flags();
platformsToFlags.put(platform, flags);
}
try {
return platformsToFlags.build();
} catch (IllegalArgumentException e) {
throw throwParsingException(
e, "Got duplicate platform entries but each platform key must be unique");
}
}
private Label platform() throws PlatformMappingException {
if (!line.isPresent()) {
throwParsingException("Expected platform label but got end of file");
}
String label = consume();
Label platform;
try {
ImmutableMap<RepositoryName, RepositoryName> emptyRepositoryMapping = ImmutableMap.of();
// It is ok for us to use an empty repository mapping in this instance because all platform
// labels used in the mapping file should be relative to the root repository. Repository
// mappings however only apply within a repository imported by the root repository.
platform = Label.parseAbsolute(label, emptyRepositoryMapping);
} catch (LabelSyntaxException e) {
throw throwParsingException(e, "Expected platform label but got " + label);
}
return platform;
}
private Collection<String> flags() throws PlatformMappingException {
ImmutableSet.Builder<String> flags = ImmutableSet.builder();
// Note: Short form flags are not supported.
while (lineIsFlag()) {
flags.add(consume());
}
ImmutableSet<String> parsedFlags = flags.build();
if (parsedFlags.isEmpty()) {
if (!line.isPresent()) {
throwParsingException("Expected a flag but got end of file");
}
throwParsingException(
"Expected a standard format flag (starting with --) but got " + peek());
}
return parsedFlags;
}
private Map<Collection<String>, Label> flagsToPlatforms() throws PlatformMappingException {
ImmutableMap.Builder<Collection<String>, Label> flagsToPlatforms = ImmutableMap.builder();
while (lineIsFlag()) {
Collection<String> flags = flags();
Label platform = platform();
flagsToPlatforms.put(flags, platform);
}
try {
return flagsToPlatforms.build();
} catch (IllegalArgumentException e) {
throw throwParsingException(
e, "Got duplicate flags entries but each flags key must be unique");
}
}
private String consume() {
Preconditions.checkState(
line.isPresent(), "Must make sure that a line exists before consuming.");
String value = line.get();
goToNextContentLine();
return value;
}
private String peek() {
Preconditions.checkState(
line.isPresent(), "Must make sure that a line exists before peeking.");
return line.get();
}
private AssertionError throwParsingException(Exception e, String message)
throws PlatformMappingException {
throw new PlatformMappingException(
new PlatformMappingParsingException(message, e),
SkyFunctionException.Transience.PERSISTENT);
}
private void throwParsingException(String message) throws PlatformMappingException {
throw new PlatformMappingException(
new PlatformMappingParsingException(message), SkyFunctionException.Transience.PERSISTENT);
}
private boolean lineIsFlag() {
return line.isPresent() && peek().startsWith("--");
}
private void goToNextContentLine() {
while (lines.hasNext()) {
String line = lines.next().trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
this.line = Optional.of(line);
return;
}
line = Optional.empty();
}
}
/**
* Simple data holder to make testing easier. Only for use internal to this file/tests thereof.
*/
@VisibleForTesting
static class Mappings {
final Map<Label, Collection<String>> platformsToFlags;
final Map<Collection<String>, Label> flagsToPlatforms;
Mappings(
Map<Label, Collection<String>> platformsToFlags,
Map<Collection<String>, Label> flagsToPlatforms) {
this.platformsToFlags = platformsToFlags;
this.flagsToPlatforms = flagsToPlatforms;
}
PlatformMappingValue toPlatformMappingValue() {
return new PlatformMappingValue(platformsToFlags, flagsToPlatforms);
}
}
/**
* Inner wrapper exception to work around the fact that {@link SkyFunctionException} cannot carry
* a message of its own.
*/
private static class PlatformMappingParsingException extends Exception {
public PlatformMappingParsingException(String message) {
super(message);
}
public PlatformMappingParsingException(String message, Throwable cause) {
super(message, cause);
}
}
}