aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/rules/repository/NewRepositoryBuildFileHandler.java
blob: 456d80842fb533a25d97cdd8cf5d6b28ba2846bb (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
// Copyright 2014 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.rules.repository;

import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.LabelValidator;
import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException;
import com.google.devtools.build.lib.skyframe.FileSymlinkException;
import com.google.devtools.build.lib.skyframe.FileValue;
import com.google.devtools.build.lib.skyframe.InconsistentFilesystemException;
import com.google.devtools.build.lib.skyframe.PackageLookupValue;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;

import java.io.IOException;

/**
 * Encapsulates the 2-step behavior of creating build files for the new_*_repository rules.
 */
public class NewRepositoryBuildFileHandler {

  private final Path workspacePath;
  private FileValue buildFileValue;
  private String buildFileContent;

  public NewRepositoryBuildFileHandler(Path workspacePath) {
    this.workspacePath = workspacePath;
  }

  /**
   * Prepares for writing a build file by validating the build_file and build_file_content
   * attributes of the rule.
   *
   * @return true if the build file was successfully created, false if the environment is missing
   *     values (the calling fetch() function should return null in this case).
   * @throws RepositoryFunctionException if the rule does not define the build_file or
   *     build_file_content attributes, or if it defines both, or if the build file could not be
   *     retrieved, written, or symlinked.
   */
  public boolean prepareBuildFile(Rule rule, Environment env)
      throws RepositoryFunctionException {

    AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
    boolean hasBuildFile = mapper.isAttributeValueExplicitlySpecified("build_file");
    boolean hasBuildFileContent = mapper.isAttributeValueExplicitlySpecified("build_file_content");

    if (hasBuildFile && hasBuildFileContent) {

      String error = String.format(
          "Rule %s cannot have both a 'build_file' and 'build_file_content' attribute", rule);
      throw new RepositoryFunctionException(
          new EvalException(rule.getLocation(), error), Transience.PERSISTENT);

    } else if (hasBuildFile) {

      buildFileValue = getBuildFileValue(rule, env);
      if (env.valuesMissing()) {
        return false;
      }

    } else if (hasBuildFileContent) { 

      buildFileContent = mapper.get("build_file_content", Type.STRING);

    } else {

      String error = String.format(
          "Rule %s requires a 'build_file' or 'build_file_content' attribute", rule);
      throw new RepositoryFunctionException(
          new EvalException(rule.getLocation(), error), Transience.PERSISTENT);
    }

    return true;
  }

  /**
   * Writes the build file, based on the state set by prepareBuildFile().
   * 
   * @param outputDirectory the directory to write the build file.
   * @throws RepositoryFunctionException if the build file could not be written or symlinked
   * @throws IllegalStateException if prepareBuildFile() was not called before this, or if
   *     prepareBuildFile() failed and this was called.
   */
  public void finishBuildFile(Path outputDirectory) throws RepositoryFunctionException {
    if (buildFileValue != null) {
      // Link x/BUILD to <build_root>/x.BUILD.
      symlinkBuildFile(buildFileValue, outputDirectory);
    } else if (buildFileContent != null) {
      RepositoryFunction.writeBuildFile(outputDirectory, buildFileContent);
    } else {
      throw new IllegalStateException(
          "prepareBuildFile() must be called before finishBuildFile()");
    }
  }

  private FileValue getBuildFileValue(Rule rule, Environment env)
      throws RepositoryFunctionException {
    AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
    String buildFileAttribute = mapper.get("build_file", Type.STRING);
    RootedPath rootedBuild;

    if (LabelValidator.isAbsolute(buildFileAttribute)) {
      try {
        // Parse a label
        Label label = Label.parseAbsolute(buildFileAttribute);
        SkyKey pkgSkyKey = PackageLookupValue.key(label.getPackageIdentifier());
        PackageLookupValue pkgLookupValue = (PackageLookupValue) env.getValue(pkgSkyKey);
        if (pkgLookupValue == null) {
          return null;
        }
        if (!pkgLookupValue.packageExists()) {
          throw new RepositoryFunctionException(
              new EvalException(rule.getLocation(),
                  "Unable to load package for " + buildFileAttribute + ": not found."),
              Transience.PERSISTENT);
        }

        // And now for the file
        Path packageRoot = pkgLookupValue.getRoot();
        rootedBuild = RootedPath.toRootedPath(packageRoot, label.toPathFragment());
      } catch (LabelSyntaxException ex) {
        throw new RepositoryFunctionException(
            new EvalException(rule.getLocation(),
                String.format("In %s the 'build_file' attribute does not specify a valid label: %s",
                    rule, ex.getMessage())),
            Transience.PERSISTENT);
      }
    } else {
      // TODO(dmarting): deprecate using a path for the build_file attribute.
      PathFragment buildFile = new PathFragment(buildFileAttribute);
      Path buildFileTarget = workspacePath.getRelative(buildFile);
      if (!buildFileTarget.exists()) {
        throw new RepositoryFunctionException(
            new EvalException(rule.getLocation(),
                String.format("In %s the 'build_file' attribute does not specify an existing file "
                    + "(%s does not exist)", rule, buildFileTarget)),
            Transience.PERSISTENT);
      }

      if (buildFile.isAbsolute()) {
        rootedBuild = RootedPath.toRootedPath(
            buildFileTarget.getParentDirectory(), new PathFragment(buildFileTarget.getBaseName()));
      } else {
        rootedBuild = RootedPath.toRootedPath(workspacePath, buildFile);
      }
    }
    SkyKey buildFileKey = FileValue.key(rootedBuild);
    FileValue buildFileValue;
    try {
      // Note that this dependency is, strictly speaking, not necessary: the symlink could simply
      // point to this FileValue and the symlink chasing could be done while loading the package
      // but this results in a nicer error message and it's correct as long as RepositoryFunctions
      // don't write to things in the file system this FileValue depends on. In theory, the latter
      // is possible if the file referenced by build_file is a symlink to somewhere under the
      // external/ directory, but if you do that, you are really asking for trouble.
      buildFileValue = (FileValue) env.getValueOrThrow(buildFileKey, IOException.class,
          FileSymlinkException.class, InconsistentFilesystemException.class);
      if (buildFileValue == null) {
        return null;
      }
    } catch (IOException | FileSymlinkException | InconsistentFilesystemException e) {
      throw new RepositoryFunctionException(
          new IOException("Cannot lookup " + buildFileAttribute + ": " + e.getMessage()),
          Transience.TRANSIENT);
    }

    return buildFileValue;
  }

  /**
   * Symlinks a BUILD file from the local filesystem into the external repository's root.
   * @param buildFileValue {@link FileValue} representing the BUILD file to be linked in
   * @param outputDirectory the directory of the remote repository
   * @throws RepositoryFunctionException if the BUILD file specified does not exist or cannot be
   *         linked.
   */
  private void symlinkBuildFile(
      FileValue buildFileValue, Path outputDirectory) throws RepositoryFunctionException {
    Path buildFilePath = outputDirectory.getRelative("BUILD");
    RepositoryFunction.createSymbolicLink(buildFilePath, buildFileValue.realRootedPath().asPath());
  }
}