Damien Martin-Guillerez | f88f4d8 | 2015-09-25 13:56:55 +0000 | [diff] [blame] | 1 | // Copyright 2014 The Bazel Authors. All rights reserved. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 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. |
| 14 | package com.google.devtools.build.lib.exec; |
| 15 | |
ulfjack | 1a80bb8 | 2019-11-26 07:52:35 -0800 | [diff] [blame] | 16 | |
Ulf Adams | 015aad9 | 2016-07-13 16:49:40 +0000 | [diff] [blame] | 17 | import com.google.common.annotations.VisibleForTesting; |
Damien Martin-Guillerez | e5b7c59 | 2016-01-18 11:03:59 +0000 | [diff] [blame] | 18 | import com.google.common.base.Preconditions; |
felly | a0e7b67 | 2020-01-29 11:31:07 -0800 | [diff] [blame] | 19 | import com.google.common.collect.ImmutableList; |
| 20 | import com.google.common.collect.ImmutableMap; |
cparsons | 8358148 | 2018-04-16 11:49:24 -0700 | [diff] [blame] | 21 | import com.google.common.collect.Lists; |
ulfjack | 112d41f | 2019-11-28 00:33:14 -0800 | [diff] [blame] | 22 | import com.google.devtools.build.lib.actions.Artifact; |
ulfjack | 34328d5 | 2019-11-19 06:13:48 -0800 | [diff] [blame] | 23 | import com.google.devtools.build.lib.actions.EnvironmentalExecException; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 24 | import com.google.devtools.build.lib.actions.ExecException; |
felly | a0e7b67 | 2020-01-29 11:31:07 -0800 | [diff] [blame] | 25 | import com.google.devtools.build.lib.actions.FilesetOutputSymlink; |
mschaller | 1eabf52 | 2020-06-10 22:03:13 -0700 | [diff] [blame] | 26 | import com.google.devtools.build.lib.server.FailureDetails.Execution; |
mschaller | 4557667 | 2020-06-10 19:15:07 -0700 | [diff] [blame] | 27 | import com.google.devtools.build.lib.server.FailureDetails.Execution.Code; |
mschaller | 1eabf52 | 2020-06-10 22:03:13 -0700 | [diff] [blame] | 28 | import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; |
Benjamin Peterson | c63e4fe | 2018-10-11 10:18:04 -0700 | [diff] [blame] | 29 | import com.google.devtools.build.lib.shell.Command; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 30 | import com.google.devtools.build.lib.shell.CommandException; |
| 31 | import com.google.devtools.build.lib.util.CommandBuilder; |
pcloudy | 0ec9d78 | 2018-08-28 08:56:16 -0700 | [diff] [blame] | 32 | import com.google.devtools.build.lib.util.CommandUtils; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 33 | import com.google.devtools.build.lib.util.OsUtils; |
Benjamin Peterson | c63e4fe | 2018-10-11 10:18:04 -0700 | [diff] [blame] | 34 | import com.google.devtools.build.lib.util.io.OutErr; |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 35 | import com.google.devtools.build.lib.vfs.Dirent; |
| 36 | import com.google.devtools.build.lib.vfs.FileStatus; |
Lukacs Berki | 31b059f | 2016-08-04 11:55:20 +0000 | [diff] [blame] | 37 | import com.google.devtools.build.lib.vfs.FileSystemUtils; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 38 | import com.google.devtools.build.lib.vfs.Path; |
ulfjack | 1a80bb8 | 2019-11-26 07:52:35 -0800 | [diff] [blame] | 39 | import com.google.devtools.build.lib.vfs.PathFragment; |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 40 | import com.google.devtools.build.lib.vfs.Symlinks; |
Lukacs Berki | 31b059f | 2016-08-04 11:55:20 +0000 | [diff] [blame] | 41 | import java.io.IOException; |
ulfjack | 1a80bb8 | 2019-11-26 07:52:35 -0800 | [diff] [blame] | 42 | import java.util.HashMap; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 43 | import java.util.List; |
ulfjack | 06760b7 | 2019-11-18 05:08:20 -0800 | [diff] [blame] | 44 | import java.util.Map; |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 45 | import javax.annotation.Nullable; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 46 | |
| 47 | /** |
ulfjack | 4d7f8f7 | 2017-11-29 03:37:04 -0800 | [diff] [blame] | 48 | * Helper class responsible for the symlink tree creation. Used to generate runfiles and fileset |
| 49 | * symlink farms. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 50 | */ |
| 51 | public final class SymlinkTreeHelper { |
Ulf Adams | 015aad9 | 2016-07-13 16:49:40 +0000 | [diff] [blame] | 52 | @VisibleForTesting |
| 53 | public static final String BUILD_RUNFILES = "build-runfiles" + OsUtils.executableExtension(); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 54 | |
Lukacs Berki | 31b059f | 2016-08-04 11:55:20 +0000 | [diff] [blame] | 55 | private final Path inputManifest; |
| 56 | private final Path symlinkTreeRoot; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 57 | private final boolean filesetTree; |
| 58 | |
| 59 | /** |
ulfjack | 4d7f8f7 | 2017-11-29 03:37:04 -0800 | [diff] [blame] | 60 | * Creates SymlinkTreeHelper instance. Can be used independently of SymlinkTreeAction. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 61 | * |
| 62 | * @param inputManifest exec path to the input runfiles manifest |
Lukacs Berki | 31b059f | 2016-08-04 11:55:20 +0000 | [diff] [blame] | 63 | * @param symlinkTreeRoot the root of the symlink tree to be created |
Googler | e854c86 | 2018-05-18 05:15:04 -0700 | [diff] [blame] | 64 | * @param filesetTree true if this is fileset symlink tree, false if this is a runfiles symlink |
| 65 | * tree. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 66 | */ |
ulfjack | 4d7f8f7 | 2017-11-29 03:37:04 -0800 | [diff] [blame] | 67 | public SymlinkTreeHelper(Path inputManifest, Path symlinkTreeRoot, boolean filesetTree) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 68 | this.inputManifest = inputManifest; |
| 69 | this.symlinkTreeRoot = symlinkTreeRoot; |
| 70 | this.filesetTree = filesetTree; |
| 71 | } |
| 72 | |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 73 | private Path getOutputManifest() { |
| 74 | return symlinkTreeRoot.getChild("MANIFEST"); |
Lukacs Berki | 31b059f | 2016-08-04 11:55:20 +0000 | [diff] [blame] | 75 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 76 | |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 77 | /** Creates a symlink tree by making VFS calls. */ |
ulfjack | 112d41f | 2019-11-28 00:33:14 -0800 | [diff] [blame] | 78 | public void createSymlinksDirectly(Path symlinkTreeRoot, Map<PathFragment, Artifact> symlinks) |
| 79 | throws IOException { |
| 80 | Preconditions.checkState(!filesetTree); |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 81 | // Our strategy is to minimize mutating file system operations as much as possible. Ideally, if |
| 82 | // there is an existing symlink tree with the expected contents, we don't make any changes. Our |
| 83 | // algorithm goes as follows: |
| 84 | // |
| 85 | // 1. Create a tree structure that represents the entire set of paths that we want to exist. The |
| 86 | // tree structure contains a node for every intermediate directory. For example, this is the |
| 87 | // tree structure corresponding to the symlinks {"a/b/c": "foobar", "a/d/e": null}: |
| 88 | // |
| 89 | // / b - c (symlink to "foobar") |
| 90 | // a |
| 91 | // \ d - e (empty file) |
| 92 | // |
| 93 | // Note that we need to distinguish directories, symlinks, and empty files. In the Directory |
| 94 | // class below, we use two maps for that purpose: one for directories, and one for symlinks |
| 95 | // and empty files. This avoids having to create additional classes / objects to distinguish |
| 96 | // them. |
| 97 | // |
| 98 | // 2. Perform a depth-first traversal over the on-disk file system tree and make each directory |
| 99 | // match our expected directory layout. To that end, call readdir, and compare the result |
| 100 | // with the contents of the corresponding node in the in-memory tree. |
| 101 | // |
| 102 | // For each Dirent entry in the readdir result: |
| 103 | // - If the entry is not in the current node, if the entry has an incompatible type, or if it |
| 104 | // is a symlink that points to the wrong location, delete the entry on disk (recursively). |
| 105 | // - Otherwise: |
| 106 | // - If the entry is a directory, recurse into that directory |
| 107 | // - In all cases, delete the entry in the current in-memory node. |
| 108 | // |
| 109 | // 3. For every remaining entry in the node, create the corresponding file, symlink, or |
| 110 | // directory on disk. If it is a directory, recurse into that directory. |
| 111 | Directory root = new Directory(); |
| 112 | for (Map.Entry<PathFragment, Artifact> entry : symlinks.entrySet()) { |
| 113 | // This creates intermediate directory nodes as a side effect. |
| 114 | Directory parentDir = root.walk(entry.getKey().getParentDirectory()); |
| 115 | parentDir.addSymlink(entry.getKey().getBaseName(), entry.getValue()); |
ulfjack | 112d41f | 2019-11-28 00:33:14 -0800 | [diff] [blame] | 116 | } |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 117 | root.syncTreeRecursively(symlinkTreeRoot); |
ulfjack | 112d41f | 2019-11-28 00:33:14 -0800 | [diff] [blame] | 118 | } |
| 119 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 120 | /** |
buchgr | 1c85fa0 | 2019-08-29 02:31:38 -0700 | [diff] [blame] | 121 | * Creates symlink tree and output manifest using the {@code build-runfiles.cc} tool. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 122 | * |
buchgr | 1c85fa0 | 2019-08-29 02:31:38 -0700 | [diff] [blame] | 123 | * @param enableRunfiles If {@code false} only the output manifest is created. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 124 | */ |
Googler | e854c86 | 2018-05-18 05:15:04 -0700 | [diff] [blame] | 125 | public void createSymlinks( |
buchgr | 1c85fa0 | 2019-08-29 02:31:38 -0700 | [diff] [blame] | 126 | Path execRoot, |
| 127 | OutErr outErr, |
Dmitry Lomov | dfe2f10 | 2016-02-12 14:41:05 +0000 | [diff] [blame] | 128 | BinTools binTools, |
ulfjack | 06760b7 | 2019-11-18 05:08:20 -0800 | [diff] [blame] | 129 | Map<String, String> shellEnvironment, |
Dmitry Lomov | a148b4c | 2016-06-21 12:04:34 +0000 | [diff] [blame] | 130 | boolean enableRunfiles) |
janakr | ca6d7ac | 2020-08-18 07:42:05 -0700 | [diff] [blame] | 131 | throws ExecException, InterruptedException { |
Lukacs Berki | 31b059f | 2016-08-04 11:55:20 +0000 | [diff] [blame] | 132 | if (enableRunfiles) { |
ulfjack | 34328d5 | 2019-11-19 06:13:48 -0800 | [diff] [blame] | 133 | createSymlinksUsingCommand(execRoot, binTools, shellEnvironment, outErr); |
Lukacs Berki | 31b059f | 2016-08-04 11:55:20 +0000 | [diff] [blame] | 134 | } else { |
ulfjack | 34328d5 | 2019-11-19 06:13:48 -0800 | [diff] [blame] | 135 | copyManifest(); |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | /** Copies the input manifest to the output manifest. */ |
| 140 | public void copyManifest() throws ExecException { |
| 141 | // Pretend we created the runfiles tree by copying the manifest |
| 142 | try { |
| 143 | symlinkTreeRoot.createDirectoryAndParents(); |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 144 | FileSystemUtils.copyFile(inputManifest, getOutputManifest()); |
ulfjack | 34328d5 | 2019-11-19 06:13:48 -0800 | [diff] [blame] | 145 | } catch (IOException e) { |
mschaller | 4557667 | 2020-06-10 19:15:07 -0700 | [diff] [blame] | 146 | throw new EnvironmentalExecException(e, Code.SYMLINK_TREE_MANIFEST_COPY_IO_EXCEPTION); |
ulfjack | 34328d5 | 2019-11-19 06:13:48 -0800 | [diff] [blame] | 147 | } |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Creates a symlink tree using a CommandBuilder. This means that the symlink tree will always be |
| 152 | * present on the developer's workstation. Useful when running commands locally. |
| 153 | * |
| 154 | * <p>Warning: this method REALLY executes the command on the box Bazel is running on, without any |
| 155 | * kind of synchronization, locking, or anything else. |
| 156 | */ |
| 157 | public void createSymlinksUsingCommand( |
| 158 | Path execRoot, BinTools binTools, Map<String, String> shellEnvironment, OutErr outErr) |
janakr | ca6d7ac | 2020-08-18 07:42:05 -0700 | [diff] [blame] | 159 | throws EnvironmentalExecException, InterruptedException { |
ulfjack | 34328d5 | 2019-11-19 06:13:48 -0800 | [diff] [blame] | 160 | Command command = createCommand(execRoot, binTools, shellEnvironment); |
| 161 | try { |
| 162 | if (outErr != null) { |
| 163 | command.execute(outErr.getOutputStream(), outErr.getErrorStream()); |
| 164 | } else { |
| 165 | command.execute(); |
Lukacs Berki | 31b059f | 2016-08-04 11:55:20 +0000 | [diff] [blame] | 166 | } |
ulfjack | 34328d5 | 2019-11-19 06:13:48 -0800 | [diff] [blame] | 167 | } catch (CommandException e) { |
mschaller | 1eabf52 | 2020-06-10 22:03:13 -0700 | [diff] [blame] | 168 | throw new EnvironmentalExecException( |
| 169 | e, |
| 170 | FailureDetail.newBuilder() |
| 171 | .setMessage(CommandUtils.describeCommandFailure(true, e)) |
| 172 | .setExecution( |
| 173 | Execution.newBuilder().setCode(Code.SYMLINK_TREE_CREATION_COMMAND_EXCEPTION)) |
| 174 | .build()); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 175 | } |
| 176 | } |
| 177 | |
ulfjack | 4d7f8f7 | 2017-11-29 03:37:04 -0800 | [diff] [blame] | 178 | @VisibleForTesting |
ulfjack | 06760b7 | 2019-11-18 05:08:20 -0800 | [diff] [blame] | 179 | Command createCommand(Path execRoot, BinTools binTools, Map<String, String> shellEnvironment) { |
Benjamin Peterson | 693e96e | 2019-04-04 02:42:14 -0700 | [diff] [blame] | 180 | Preconditions.checkNotNull(shellEnvironment); |
cparsons | 8358148 | 2018-04-16 11:49:24 -0700 | [diff] [blame] | 181 | List<String> args = Lists.newArrayList(); |
Benjamin Peterson | 693e96e | 2019-04-04 02:42:14 -0700 | [diff] [blame] | 182 | args.add(binTools.getEmbeddedPath(BUILD_RUNFILES).asFragment().getPathString()); |
Fabian Meumertzheim | d834905 | 2022-09-16 04:29:33 -0700 | [diff] [blame] | 183 | args.add("--allow_relative"); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 184 | if (filesetTree) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 185 | args.add("--use_metadata"); |
| 186 | } |
Dmitry Lomov | e36a66c | 2017-02-17 14:48:48 +0000 | [diff] [blame] | 187 | args.add(inputManifest.relativeTo(execRoot).getPathString()); |
| 188 | args.add(symlinkTreeRoot.relativeTo(execRoot).getPathString()); |
Benjamin Peterson | 693e96e | 2019-04-04 02:42:14 -0700 | [diff] [blame] | 189 | return new CommandBuilder() |
| 190 | .addArgs(args) |
| 191 | .setWorkingDir(execRoot) |
| 192 | .setEnv(shellEnvironment) |
| 193 | .build(); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 194 | } |
ulfjack | 1a80bb8 | 2019-11-26 07:52:35 -0800 | [diff] [blame] | 195 | |
felly | a0e7b67 | 2020-01-29 11:31:07 -0800 | [diff] [blame] | 196 | static ImmutableMap<PathFragment, PathFragment> processFilesetLinks( |
| 197 | ImmutableList<FilesetOutputSymlink> links, PathFragment root, PathFragment execRoot) { |
| 198 | Map<PathFragment, PathFragment> symlinks = new HashMap<>(); |
| 199 | for (FilesetOutputSymlink symlink : links) { |
| 200 | symlinks.put(root.getRelative(symlink.getName()), symlink.reconstituteTargetPath(execRoot)); |
ulfjack | 1a80bb8 | 2019-11-26 07:52:35 -0800 | [diff] [blame] | 201 | } |
felly | a0e7b67 | 2020-01-29 11:31:07 -0800 | [diff] [blame] | 202 | return ImmutableMap.copyOf(symlinks); |
ulfjack | 1a80bb8 | 2019-11-26 07:52:35 -0800 | [diff] [blame] | 203 | } |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 204 | |
| 205 | private static final class Directory { |
| 206 | private final Map<String, Artifact> symlinks = new HashMap<>(); |
| 207 | private final Map<String, Directory> directories = new HashMap<>(); |
| 208 | |
| 209 | void addSymlink(String basename, @Nullable Artifact artifact) { |
| 210 | symlinks.put(basename, artifact); |
| 211 | } |
| 212 | |
| 213 | Directory walk(PathFragment dir) { |
| 214 | Directory result = this; |
jhorvitz | 6034795 | 2021-02-18 10:05:05 -0800 | [diff] [blame] | 215 | for (String segment : dir.segments()) { |
| 216 | result = result.directories.computeIfAbsent(segment, unused -> new Directory()); |
ulfjack | 80e703a | 2020-01-22 06:51:30 -0800 | [diff] [blame] | 217 | } |
| 218 | return result; |
| 219 | } |
| 220 | |
| 221 | void syncTreeRecursively(Path at) throws IOException { |
| 222 | // This is a reimplementation of the C++ code in build-runfiles.cc. This avoids having to ship |
| 223 | // a separate native tool to create a few runfiles. |
| 224 | // TODO(ulfjack): Measure performance. |
| 225 | FileStatus stat = at.statNullable(Symlinks.FOLLOW); |
| 226 | if (stat == null) { |
| 227 | at.createDirectoryAndParents(); |
| 228 | } else if (!stat.isDirectory()) { |
| 229 | at.deleteTree(); |
| 230 | at.createDirectoryAndParents(); |
| 231 | } |
| 232 | // TODO(ulfjack): provide the mode bits from FileStatus and use that to construct the correct |
| 233 | // chmod call here. Note that we do not have any tests for this right now. Something like |
| 234 | // this: |
| 235 | // if (!stat.isExecutable() || !stat.isReadable()) { |
| 236 | // at.chmod(stat.getMods() | 0700); |
| 237 | // } |
| 238 | for (Dirent dirent : at.readdir(Symlinks.FOLLOW)) { |
| 239 | String basename = dirent.getName(); |
| 240 | Path next = at.getChild(basename); |
| 241 | if (symlinks.containsKey(basename)) { |
| 242 | Artifact value = symlinks.remove(basename); |
| 243 | if (value == null) { |
| 244 | if (dirent.getType() != Dirent.Type.FILE) { |
| 245 | next.deleteTree(); |
| 246 | FileSystemUtils.createEmptyFile(next); |
| 247 | } |
| 248 | // For consistency with build-runfiles.cc, we don't truncate the file if one exists. |
| 249 | } else { |
| 250 | // TODO(ulfjack): On Windows, this call makes a copy rather than creating a symlink. |
| 251 | FileSystemUtils.ensureSymbolicLink(next, value.getPath().asFragment()); |
| 252 | } |
| 253 | } else if (directories.containsKey(basename)) { |
| 254 | Directory nextDir = directories.remove(basename); |
| 255 | if (dirent.getType() != Dirent.Type.DIRECTORY) { |
| 256 | next.deleteTree(); |
| 257 | } |
| 258 | nextDir.syncTreeRecursively(at.getChild(basename)); |
| 259 | } else { |
| 260 | at.getChild(basename).deleteTree(); |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | for (Map.Entry<String, Artifact> entry : symlinks.entrySet()) { |
| 265 | Path next = at.getChild(entry.getKey()); |
| 266 | if (entry.getValue() == null) { |
| 267 | FileSystemUtils.createEmptyFile(next); |
| 268 | } else { |
| 269 | FileSystemUtils.ensureSymbolicLink(next, entry.getValue().getPath().asFragment()); |
| 270 | } |
| 271 | } |
| 272 | for (Map.Entry<String, Directory> entry : directories.entrySet()) { |
| 273 | entry.getValue().syncTreeRecursively(at.getChild(entry.getKey())); |
| 274 | } |
| 275 | } |
| 276 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 277 | } |