aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
blob: 1b35631108830c797b6d9a50ba9d994fb881bc61 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Copyright 2017 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.exec;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.io.LineProcessor;
import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
import com.google.devtools.build.lib.analysis.AnalysisUtils;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Representation of a Fileset manifest.
 */
public final class FilesetManifest {
  /**
   * Mode that determines how to handle relative target paths.
   */
  public enum RelativeSymlinkBehavior {
    /** Ignore any relative target paths. */
    IGNORE,

    /** Give an error if a relative target path is encountered. */
    ERROR,

    /**
     * Attempt to locally resolve the relative target path. Consider a manifest with two entries,
     * foo points to bar, and bar points to the absolute path /foobar. In that case, we can
     * determine that foo actually points at /foobar. Throw an exception if the local resolution
     * fails, e.g., if the target is not in the current manifest, or if it points at another
     * symlink (we could theoretically resolve recursively, but that's more complexity).
     */
    RESOLVE;
  }

  public static FilesetManifest parseManifestFile(
      PathFragment manifest,
      Path execRoot,
      String workspaceName,
      RelativeSymlinkBehavior relSymlinkBehavior)
          throws IOException {
    Path file = execRoot.getRelative(AnalysisUtils.getManifestPathFromFilesetPath(manifest));
    try {
      return FileSystemUtils.asByteSource(file).asCharSource(UTF_8)
          .readLines(
              new ManifestLineProcessor(workspaceName, manifest, relSymlinkBehavior));
    } catch (IllegalStateException e) {
      // We can't throw IOException from getResult below, so we instead use an unchecked exception,
      // and convert it to an IOException here.
      throw new IOException(e.getMessage(), e);
    }
  }

  public static FilesetManifest constructFilesetManifest(
      List<FilesetOutputSymlink> outputSymlinks,
      PathFragment targetPrefix,
      RelativeSymlinkBehavior relSymlinkbehavior)
      throws IOException {
    LinkedHashMap<PathFragment, String> entries = new LinkedHashMap<>();
    Map<PathFragment, String> relativeLinks = new HashMap<>();
    for (FilesetOutputSymlink outputSymlink : outputSymlinks) {
      PathFragment fullLocation = targetPrefix.getRelative(outputSymlink.getName());
      String artifact = outputSymlink.getTargetPath().getPathString();
      artifact = artifact.isEmpty() ? null : artifact;
      addSymlinkEntry(artifact, fullLocation, relSymlinkbehavior, entries, relativeLinks);
    }
    return constructFilesetManifest(entries, relativeLinks);
  }

  private static final class ManifestLineProcessor implements LineProcessor<FilesetManifest> {
    private final String workspaceName;
    private final PathFragment targetPrefix;
    private final RelativeSymlinkBehavior relSymlinkBehavior;

    private int lineNum;
    private final LinkedHashMap<PathFragment, String> entries = new LinkedHashMap<>();
    // Resolution order of relative links can affect the outcome of the resolution. In particular,
    // if there's a symlink to a symlink, then resolution fails if the first symlink is resolved
    // first, but works if the second symlink is resolved first.
    private final LinkedHashMap<PathFragment, String> relativeLinks = new LinkedHashMap<>();

    ManifestLineProcessor(
        String workspaceName,
        PathFragment targetPrefix,
        RelativeSymlinkBehavior relSymlinkBehavior) {
      this.workspaceName = workspaceName;
      this.targetPrefix = targetPrefix;
      this.relSymlinkBehavior = relSymlinkBehavior;
    }

    @Override
    public boolean processLine(String line) throws IOException {
      if (++lineNum % 2 == 0) {
        // Digest line, skip.
        return true;
      }
      if (line.isEmpty()) {
        return true;
      }

      String artifact;
      PathFragment location;
      int pos = line.indexOf(' ');
      if (pos == -1) {
        location = PathFragment.create(line);
        artifact = null;
      } else {
        location = PathFragment.create(line.substring(0, pos));
        String targetPath = line.substring(pos + 1);
        artifact = targetPath.isEmpty() ? null : targetPath;

        if (!workspaceName.isEmpty()) {
          if (!location.getSegment(0).equals(workspaceName)) {
            throw new IOException(
                String.format(
                    "fileset manifest line must start with '%s': '%s'", workspaceName, location));
          } else {
            // Erase "<workspaceName>/" prefix.
            location = location.subFragment(1);
          }
        }
      }

      PathFragment fullLocation = targetPrefix.getRelative(location);
      addSymlinkEntry(artifact, fullLocation, relSymlinkBehavior, entries, relativeLinks);
      return true;
    }

    @Override
    public FilesetManifest getResult() {
      return constructFilesetManifest(entries, relativeLinks);
    }
  }

  private static void addSymlinkEntry(
      String artifact,
      PathFragment fullLocation,
      RelativeSymlinkBehavior relSymlinkBehavior,
      LinkedHashMap<PathFragment, String> entries,
      Map<PathFragment, String> relativeLinks)
      throws IOException {
    if (!entries.containsKey(fullLocation)) {
      boolean isRelativeSymlink = artifact != null && !artifact.startsWith("/");
      if (isRelativeSymlink && relSymlinkBehavior.equals(RelativeSymlinkBehavior.ERROR)) {
        throw new IOException(String.format("runfiles target is not absolute: %s", artifact));
      }
      if (!isRelativeSymlink || relSymlinkBehavior.equals(RelativeSymlinkBehavior.RESOLVE)) {
        entries.put(fullLocation, artifact);
        if (artifact != null && !artifact.startsWith("/")) {
          relativeLinks.put(fullLocation, artifact);
        }
      }
    }
  }

  private static FilesetManifest constructFilesetManifest(
      Map<PathFragment, String> entries, Map<PathFragment, String> relativeLinks) {
    // Resolve relative symlinks if possible. Note that relativeLinks only contains entries in
    // RESOLVE mode.
    for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) {
      PathFragment location = e.getKey();
      String value = e.getValue();
      PathFragment actualLocation = location.getParentDirectory().getRelative(value);
      String actual = entries.get(actualLocation);
      if (actual == null) {
        throw new IllegalStateException(
            String.format(
                "runfiles target '%s' is a relative symlink, and could not be resolved within the "
                    + "same Fileset",
                value));
      }
      if (!actual.startsWith("/")) {
        throw new IllegalStateException(
            String.format(
                "runfiles target '%s' is a relative symlink, and points to another symlink",
                value));
      }
      entries.put(location, actual);
    }
    return new FilesetManifest(entries);
  }

  private final Map<PathFragment, String> entries;

  private FilesetManifest(Map<PathFragment, String> entries) {
    this.entries = Collections.unmodifiableMap(entries);
  }

  public Map<PathFragment, String> getEntries() {
    return entries;
  }
}