| // Copyright 2022 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.runtime; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Splitter; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.devtools.build.lib.analysis.BlazeDirectories; |
| import com.google.devtools.build.lib.vfs.FileSystem; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.lib.vfs.Symlinks; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Factory for creating {@link PathFragment}s from command-line options. |
| * |
| * <p>The difference between this and using {@link PathFragment#create(String)} directly is that |
| * this factory replaces values starting with {@code %<name>%} with the corresponding (named) roots |
| * (e.g., {@code %workspace%/foo} becomes {@code </path/to/workspace>/foo}). |
| */ |
| public final class CommandLinePathFactory { |
| /** An exception thrown while attempting to resolve a path. */ |
| public static class CommandLinePathFactoryException extends IOException { |
| public CommandLinePathFactoryException(String message) { |
| super(message); |
| } |
| } |
| |
| private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("^(%([a-z_]+)%/+)?([^%].*)$"); |
| |
| private static final Splitter PATH_SPLITTER = Splitter.on(File.pathSeparator); |
| |
| private final FileSystem fileSystem; |
| private final ImmutableMap<String, Path> roots; |
| |
| @VisibleForTesting |
| public CommandLinePathFactory(FileSystem fileSystem, ImmutableMap<String, Path> roots) { |
| this.fileSystem = Preconditions.checkNotNull(fileSystem); |
| this.roots = Preconditions.checkNotNull(roots); |
| } |
| |
| static CommandLinePathFactory create(FileSystem fileSystem, BlazeDirectories directories) { |
| Preconditions.checkNotNull(fileSystem); |
| Preconditions.checkNotNull(directories); |
| |
| ImmutableMap.Builder<String, Path> wellKnownRoots = ImmutableMap.builder(); |
| |
| // This is necessary because some tests don't have a workspace set. |
| Path workspace = directories.getWorkspace(); |
| if (workspace != null) { |
| wellKnownRoots.put("workspace", workspace); |
| } |
| |
| return new CommandLinePathFactory(fileSystem, wellKnownRoots.buildOrThrow()); |
| } |
| |
| /** Creates a {@link Path}. */ |
| public Path create(Map<String, String> env, String value) throws IOException { |
| Preconditions.checkNotNull(env); |
| Preconditions.checkNotNull(value); |
| |
| Matcher matcher = REPLACEMENT_PATTERN.matcher(value); |
| Preconditions.checkArgument(matcher.matches()); |
| |
| String rootName = matcher.group(2); |
| PathFragment path = PathFragment.create(matcher.group(3)); |
| if (path.containsUplevelReferences()) { |
| throw new CommandLinePathFactoryException( |
| String.format( |
| Locale.US, "Path '%s' must not contain any uplevel references ('..')", value)); |
| } |
| |
| // Case 1: `path` is relative to a well-known root. |
| if (!Strings.isNullOrEmpty(rootName)) { |
| Path root = roots.get(rootName); |
| if (root == null) { |
| throw new CommandLinePathFactoryException( |
| String.format(Locale.US, "Unknown root %s", rootName)); |
| } |
| return root.getRelative(path); |
| } |
| |
| // Case 2: `value` is an absolute path. |
| if (path.isAbsolute()) { |
| return fileSystem.getPath(path); |
| } |
| |
| // Case 3: `value` is a relative path. |
| // |
| // Since relative paths from the command-line are ambiguous to where they are relative to (i.e., |
| // relative to the workspace?, the directory Bazel is running in? relative to the `.bazelrc` the |
| // flag is from?), we only allow relative paths with a single segment (i.e., no `/`) and treat |
| // it as relative to the user's `PATH`. |
| if (path.segmentCount() > 1) { |
| throw new CommandLinePathFactoryException( |
| String.format( |
| Locale.US, |
| "Path '%s' must either be absolute or not contain any path separators", |
| value)); |
| } |
| |
| String pathVariable = env.getOrDefault("PATH", ""); |
| if (!Strings.isNullOrEmpty(pathVariable)) { |
| for (String lookupPath : PATH_SPLITTER.split(pathVariable)) { |
| Path maybePath = fileSystem.getPath(lookupPath).getRelative(path); |
| if (maybePath.exists(Symlinks.FOLLOW) |
| && maybePath.isFile(Symlinks.FOLLOW) |
| && maybePath.isExecutable()) { |
| return maybePath; |
| } |
| } |
| } |
| |
| throw new FileNotFoundException( |
| String.format( |
| Locale.US, "Could not find file with name '%s' on PATH '%s'", path, pathVariable)); |
| } |
| } |