| // 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.events.Location; |
| import com.google.devtools.build.lib.pkgcache.PathPackageLocator; |
| 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( |
| 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), |
| 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( |
| String.format( |
| "--platform_mappings was set to '%s' but no such file exists relative to the " |
| + "package path roots, '%s'", |
| workspaceRelativeMappingPath, pathEntries), |
| Location.BUILTIN), |
| SkyFunctionException.Transience.PERSISTENT); |
| } |
| |
| @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); |
| } |
| } |
| } |