ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 1 | // Copyright 2017 The Bazel Authors. All rights reserved. |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
felly | f6408d6 | 2019-07-16 12:51:14 -0700 | [diff] [blame] | 14 | package com.google.devtools.build.lib.actions; |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 15 | |
janakr | 206c19b | 2018-08-29 09:07:34 -0700 | [diff] [blame] | 16 | import com.google.common.base.Preconditions; |
Googler | 18b0844 | 2018-10-04 09:55:06 -0700 | [diff] [blame] | 17 | import com.google.common.base.Strings; |
janakr | 91470ae | 2020-04-13 10:58:58 -0700 | [diff] [blame] | 18 | import com.google.common.flogger.GoogleLogger; |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 19 | import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| 20 | import com.google.devtools.build.lib.vfs.Path; |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 21 | import com.google.devtools.build.lib.vfs.PathFragment; |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 22 | import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 23 | import java.io.IOException; |
| 24 | import java.util.Collections; |
| 25 | import java.util.HashMap; |
| 26 | import java.util.LinkedHashMap; |
janakr | 206c19b | 2018-08-29 09:07:34 -0700 | [diff] [blame] | 27 | import java.util.LinkedHashSet; |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 28 | import java.util.List; |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 29 | import java.util.Map; |
Googler | 1ca8099 | 2018-10-11 17:47:22 -0700 | [diff] [blame] | 30 | import javax.annotation.Nullable; |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 31 | |
| 32 | /** |
| 33 | * Representation of a Fileset manifest. |
| 34 | */ |
| 35 | public final class FilesetManifest { |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 36 | private static final int MAX_SYMLINK_TRAVERSALS = 256; |
janakr | 91470ae | 2020-04-13 10:58:58 -0700 | [diff] [blame] | 37 | private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); |
janakr | 206c19b | 2018-08-29 09:07:34 -0700 | [diff] [blame] | 38 | |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 39 | /** |
| 40 | * Mode that determines how to handle relative target paths. |
| 41 | */ |
| 42 | public enum RelativeSymlinkBehavior { |
| 43 | /** Ignore any relative target paths. */ |
| 44 | IGNORE, |
| 45 | |
| 46 | /** Give an error if a relative target path is encountered. */ |
| 47 | ERROR, |
| 48 | |
janakr | 206c19b | 2018-08-29 09:07:34 -0700 | [diff] [blame] | 49 | /** Resolve all relative target paths. */ |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 50 | RESOLVE, |
| 51 | |
| 52 | /** Fully resolve all relative paths, even those pointing to internal directories. */ |
| 53 | RESOLVE_FULLY; |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 54 | } |
| 55 | |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 56 | public static FilesetManifest constructFilesetManifest( |
| 57 | List<FilesetOutputSymlink> outputSymlinks, |
| 58 | PathFragment targetPrefix, |
Googler | 9dc3e79 | 2018-10-17 05:16:47 -0700 | [diff] [blame] | 59 | RelativeSymlinkBehavior relSymlinkBehavior) |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 60 | throws IOException { |
| 61 | LinkedHashMap<PathFragment, String> entries = new LinkedHashMap<>(); |
| 62 | Map<PathFragment, String> relativeLinks = new HashMap<>(); |
felly | 943a9c7 | 2018-08-09 12:55:47 -0700 | [diff] [blame] | 63 | Map<String, FileArtifactValue> artifactValues = new HashMap<>(); |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 64 | for (FilesetOutputSymlink outputSymlink : outputSymlinks) { |
kush | 4b120e7 | 2018-07-11 16:21:27 -0700 | [diff] [blame] | 65 | PathFragment fullLocation = targetPrefix.getRelative(outputSymlink.getName()); |
Googler | 9dc3e79 | 2018-10-17 05:16:47 -0700 | [diff] [blame] | 66 | String artifact = Strings.emptyToNull(outputSymlink.getTargetPath().getPathString()); |
| 67 | if (isRelativeSymlink(outputSymlink)) { |
Googler | 1ca8099 | 2018-10-11 17:47:22 -0700 | [diff] [blame] | 68 | addRelativeSymlinkEntry(artifact, fullLocation, relSymlinkBehavior, relativeLinks); |
| 69 | } else if (!entries.containsKey(fullLocation)) { // Keep consistent behavior: no overwriting. |
| 70 | entries.put(fullLocation, artifact); |
| 71 | } |
felly | 943a9c7 | 2018-08-09 12:55:47 -0700 | [diff] [blame] | 72 | if (outputSymlink.getMetadata() instanceof FileArtifactValue) { |
| 73 | artifactValues.put(artifact, (FileArtifactValue) outputSymlink.getMetadata()); |
| 74 | } |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 75 | } |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 76 | resolveRelativeSymlinks(entries, relativeLinks, targetPrefix.isAbsolute(), relSymlinkBehavior); |
Googler | 9dc3e79 | 2018-10-17 05:16:47 -0700 | [diff] [blame] | 77 | return new FilesetManifest(entries, artifactValues); |
| 78 | } |
| 79 | |
| 80 | private static boolean isRelativeSymlink(FilesetOutputSymlink symlink) { |
| 81 | return !symlink.getTargetPath().isEmpty() |
| 82 | && !symlink.getTargetPath().isAbsolute() |
| 83 | && !symlink.isRelativeToExecRoot(); |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 84 | } |
| 85 | |
Googler | 1ca8099 | 2018-10-11 17:47:22 -0700 | [diff] [blame] | 86 | /** Potentially adds the relative symlink to the map, depending on {@code relSymlinkBehavior}. */ |
| 87 | private static void addRelativeSymlinkEntry( |
| 88 | @Nullable String artifact, |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 89 | PathFragment fullLocation, |
| 90 | RelativeSymlinkBehavior relSymlinkBehavior, |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 91 | Map<PathFragment, String> relativeLinks) |
| 92 | throws IOException { |
Googler | 1ca8099 | 2018-10-11 17:47:22 -0700 | [diff] [blame] | 93 | switch (relSymlinkBehavior) { |
| 94 | case ERROR: |
| 95 | throw new IOException("runfiles target is not absolute: " + artifact); |
| 96 | case RESOLVE: |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 97 | case RESOLVE_FULLY: |
Googler | 1ca8099 | 2018-10-11 17:47:22 -0700 | [diff] [blame] | 98 | if (!relativeLinks.containsKey(fullLocation)) { // Keep consistent behavior: no overwriting. |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 99 | relativeLinks.put(fullLocation, artifact); |
| 100 | } |
Googler | 1ca8099 | 2018-10-11 17:47:22 -0700 | [diff] [blame] | 101 | break; |
| 102 | case IGNORE: |
| 103 | break; // Do nothing. |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 104 | } |
| 105 | } |
| 106 | |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 107 | /** Fully resolve relative symlinks including internal directory symlinks. */ |
| 108 | private static void fullyResolveRelativeSymlinks( |
| 109 | Map<PathFragment, String> entries, Map<PathFragment, String> relativeLinks, boolean absolute) |
| 110 | throws IOException { |
| 111 | // Construct an in-memory Filesystem containing all the non-relative-symlink entries in the |
| 112 | // Fileset. Treat these as regular files in the filesystem whose contents are the "real" symlink |
| 113 | // pointing out of the Fileset. For relative symlinks, we encode these as symlinks in the |
| 114 | // in-memory Filesystem. This allows us to then crawl the filesystem for files. Any readable |
| 115 | // file is a valid part of the FilesetManifest. Dangling internal links or symlink cycles will |
| 116 | // be discovered by the in-memory filesystem. |
| 117 | InMemoryFileSystem fs = new InMemoryFileSystem(); |
| 118 | Path root = fs.getPath("/"); |
| 119 | for (Map.Entry<PathFragment, String> e : entries.entrySet()) { |
| 120 | PathFragment location = e.getKey(); |
| 121 | Path locationPath = root.getRelative(location); |
| 122 | locationPath.getParentDirectory().createDirectoryAndParents(); |
| 123 | FileSystemUtils.writeContentAsLatin1(locationPath, Strings.nullToEmpty(e.getValue())); |
| 124 | } |
| 125 | for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) { |
| 126 | PathFragment location = e.getKey(); |
| 127 | Path locationPath = fs.getPath("/").getRelative(location); |
| 128 | PathFragment value = PathFragment.create(Preconditions.checkNotNull(e.getValue(), e)); |
| 129 | Preconditions.checkState(!value.isAbsolute(), e); |
| 130 | |
| 131 | locationPath.getParentDirectory().createDirectoryAndParents(); |
| 132 | locationPath.createSymbolicLink(value); |
| 133 | } |
| 134 | |
| 135 | addSymlinks(root, entries, absolute); |
| 136 | } |
| 137 | |
| 138 | private static void addSymlinks(Path root, Map<PathFragment, String> entries, boolean absolute) |
| 139 | throws IOException { |
| 140 | for (Path path : root.getDirectoryEntries()) { |
| 141 | try { |
| 142 | if (path.isDirectory()) { |
| 143 | addSymlinks(path, entries, absolute); |
| 144 | } else { |
| 145 | String contents = new String(FileSystemUtils.readContentAsLatin1(path)); |
| 146 | entries.put( |
| 147 | absolute ? path.asFragment() : path.asFragment().toRelative(), |
| 148 | Strings.emptyToNull(contents)); |
| 149 | } |
| 150 | } catch (IOException e) { |
| 151 | logger.atWarning().log("Symlink %s is dangling or cyclic: %s", path, e.getMessage()); |
| 152 | } |
| 153 | } |
| 154 | } |
janakr | 206c19b | 2018-08-29 09:07:34 -0700 | [diff] [blame] | 155 | |
Googler | 9dc3e79 | 2018-10-17 05:16:47 -0700 | [diff] [blame] | 156 | /** |
| 157 | * Resolves relative symlinks and puts them in the {@code entries} map. |
| 158 | * |
| 159 | * <p>Note that {@code relativeLinks} should only contain entries in {@link |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 160 | * RelativeSymlinkBehavior#RESOLVE} or {@link RelativeSymlinkBehavior#RESOLVE_FULLY} mode. |
Googler | 9dc3e79 | 2018-10-17 05:16:47 -0700 | [diff] [blame] | 161 | */ |
| 162 | private static void resolveRelativeSymlinks( |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 163 | Map<PathFragment, String> entries, |
| 164 | Map<PathFragment, String> relativeLinks, |
| 165 | boolean absolute, |
| 166 | RelativeSymlinkBehavior relSymlinkBehavior) |
| 167 | throws IOException { |
| 168 | if (relSymlinkBehavior == RelativeSymlinkBehavior.RESOLVE_FULLY && !relativeLinks.isEmpty()) { |
| 169 | fullyResolveRelativeSymlinks(entries, relativeLinks, absolute); |
| 170 | } else if (relSymlinkBehavior == RelativeSymlinkBehavior.RESOLVE) { |
| 171 | for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) { |
| 172 | PathFragment location = e.getKey(); |
| 173 | String value = e.getValue(); |
| 174 | String actual = Preconditions.checkNotNull(value, e); |
| 175 | Preconditions.checkState(!actual.startsWith("/"), e); |
| 176 | PathFragment actualLocation = location; |
Googler | 1ca8099 | 2018-10-11 17:47:22 -0700 | [diff] [blame] | 177 | |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 178 | // Recursively resolve relative symlinks. |
| 179 | LinkedHashSet<String> seen = new LinkedHashSet<>(); |
| 180 | int traversals = 0; |
| 181 | do { |
| 182 | actualLocation = actualLocation.getParentDirectory().getRelative(actual); |
| 183 | actual = relativeLinks.get(actualLocation); |
| 184 | } while (++traversals <= MAX_SYMLINK_TRAVERSALS && actual != null && seen.add(actual)); |
Googler | 1ca8099 | 2018-10-11 17:47:22 -0700 | [diff] [blame] | 185 | |
felly | 6ace3f1 | 2020-06-05 11:05:18 -0700 | [diff] [blame^] | 186 | if (traversals >= MAX_SYMLINK_TRAVERSALS) { |
| 187 | logger.atWarning().log( |
| 188 | "Symlink %s is part of a chain of length at least %d" |
| 189 | + " which exceeds Blaze's maximum allowable symlink chain length", |
| 190 | location, traversals); |
| 191 | } else if (actual != null) { |
| 192 | // TODO(b/113128395): throw here. |
| 193 | logger.atWarning().log("Symlink %s forms a symlink cycle: %s", location, seen); |
| 194 | } else if (!entries.containsKey(actualLocation)) { |
| 195 | // We've found a relative symlink that points out of the fileset. We should really always |
| 196 | // throw here, but current behavior is that we tolerate such symlinks when they occur in |
| 197 | // runfiles, which is the only time this code is hit. |
| 198 | // TODO(b/113128395): throw here. |
| 199 | logger.atWarning().log( |
| 200 | "Symlink %s (transitively) points to %s" |
| 201 | + " that is not in this fileset (or was pruned because of a cycle)", |
| 202 | location, actualLocation); |
| 203 | } else { |
| 204 | // We have successfully resolved the symlink. |
| 205 | entries.put(location, entries.get(actualLocation)); |
| 206 | } |
ulfjack | 278a5e3 | 2018-06-04 05:27:38 -0700 | [diff] [blame] | 207 | } |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 208 | } |
kush | 2ce45a2 | 2018-05-02 14:15:37 -0700 | [diff] [blame] | 209 | } |
| 210 | |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 211 | private final Map<PathFragment, String> entries; |
felly | 943a9c7 | 2018-08-09 12:55:47 -0700 | [diff] [blame] | 212 | private final Map<String, FileArtifactValue> artifactValues; |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 213 | |
Googler | 9dc3e79 | 2018-10-17 05:16:47 -0700 | [diff] [blame] | 214 | private FilesetManifest( |
| 215 | Map<PathFragment, String> entries, Map<String, FileArtifactValue> artifactValues) { |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 216 | this.entries = Collections.unmodifiableMap(entries); |
felly | 943a9c7 | 2018-08-09 12:55:47 -0700 | [diff] [blame] | 217 | this.artifactValues = artifactValues; |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 218 | } |
| 219 | |
Googler | 9dc3e79 | 2018-10-17 05:16:47 -0700 | [diff] [blame] | 220 | /** |
| 221 | * Returns a mapping of symlink name to its target path. |
| 222 | * |
| 223 | * <p>Values in this map can be: |
| 224 | * |
| 225 | * <ul> |
| 226 | * <li>An absolute path. |
| 227 | * <li>A relative path, which should be considered relative to the exec root. |
| 228 | * <li>{@code null}, which represents an empty file. |
| 229 | * </ul> |
| 230 | */ |
ulfjack | c174cc2 | 2017-10-02 14:49:09 +0200 | [diff] [blame] | 231 | public Map<PathFragment, String> getEntries() { |
| 232 | return entries; |
| 233 | } |
felly | 943a9c7 | 2018-08-09 12:55:47 -0700 | [diff] [blame] | 234 | |
Googler | 9dc3e79 | 2018-10-17 05:16:47 -0700 | [diff] [blame] | 235 | /** |
| 236 | * Returns a mapping of target path to {@link FileArtifactValue}. |
| 237 | * |
| 238 | * <p>The keyset of this map is a subset of the values in the map returned by {@link #getEntries}. |
| 239 | */ |
felly | 943a9c7 | 2018-08-09 12:55:47 -0700 | [diff] [blame] | 240 | public Map<String, FileArtifactValue> getArtifactValues() { |
| 241 | return artifactValues; |
| 242 | } |
janakr | 206c19b | 2018-08-29 09:07:34 -0700 | [diff] [blame] | 243 | } |