// 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.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, InterruptedException { WorkspaceAttributeMapper mapper = WorkspaceAttributeMapper.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) { try { buildFileContent = mapper.get("build_file_content", Type.STRING); } catch (EvalException e) { throw new RepositoryFunctionException(e, Transience.PERSISTENT); } } 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 /x.BUILD.bazel. 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, InterruptedException { WorkspaceAttributeMapper mapper = WorkspaceAttributeMapper.of(rule); String buildFileAttribute; try { buildFileAttribute = mapper.get("build_file", Type.STRING); } catch (EvalException e) { throw new RepositoryFunctionException(e, Transience.PERSISTENT); } 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.bazel"); RepositoryFunction.createSymbolicLink(buildFilePath, buildFileValue.realRootedPath().asPath()); } }