| // 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.common.collect.ImmutableMap; |
| import com.google.devtools.build.lib.actions.FileValue; |
| 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.packages.Type; |
| import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException; |
| import com.google.devtools.build.lib.skyframe.PackageLookupValue; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.lib.vfs.Root; |
| 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; |
| import java.util.Map; |
| import javax.annotation.Nullable; |
| import net.starlark.java.eval.EvalException; |
| import net.starlark.java.eval.Starlark; |
| |
| /** |
| * Encapsulates the 2-step behavior of creating workspace and build files for the new_*_repository |
| * rules. |
| */ |
| public class NewRepositoryFileHandler { |
| |
| private NewRepositoryWorkspaceFileHandler workspaceFileHandler; |
| private NewRepositoryBuildFileHandler buildFileHandler; |
| |
| public NewRepositoryFileHandler(Path workspacePath) { |
| this.workspaceFileHandler = new NewRepositoryWorkspaceFileHandler(workspacePath); |
| this.buildFileHandler = new NewRepositoryBuildFileHandler(workspacePath); |
| } |
| |
| public boolean prepareFile(Rule rule, Environment env) |
| throws RepositoryFunctionException, InterruptedException { |
| if (!this.workspaceFileHandler.prepareFile(rule, env)) { |
| return false; |
| } |
| if (!this.buildFileHandler.prepareFile(rule, env)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| public void finishFile(Rule rule, Path outputDirectory, Map<String, String> markerData) |
| throws RepositoryFunctionException { |
| this.workspaceFileHandler.finishFile(rule, outputDirectory, markerData); |
| this.buildFileHandler.finishFile(rule, outputDirectory, markerData); |
| } |
| |
| /** |
| * Encapsulates the 2-step behavior of creating files for the new_*_repository rules, based on a |
| * pair of attributes defined in {@link #getFileAttrName()} and {@link #getFileContentAttrName()}. |
| */ |
| private abstract static class BaseFileHandler { |
| |
| private final Path workspacePath; |
| private final String filename; |
| private FileValue fileValue; |
| private String fileContent; |
| |
| private BaseFileHandler(Path workspacePath, String filename) { |
| this.workspacePath = workspacePath; |
| this.filename = filename; |
| } |
| |
| protected abstract String getFileAttrName(); |
| |
| protected abstract String getFileContentAttrName(); |
| |
| protected abstract String getDefaultContent(Rule rule) throws RepositoryFunctionException; |
| |
| /** |
| * Prepares for writing a file by validating the FOO_file and FOO_file_content attributes of the |
| * rule. |
| * |
| * @return true if the 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 defines both the FOO_file and |
| * FOO_file_content attributes, or if the workspace file could not be retrieved, written, or |
| * symlinked. |
| */ |
| public boolean prepareFile(Rule rule, Environment env) |
| throws RepositoryFunctionException, InterruptedException { |
| |
| WorkspaceAttributeMapper mapper = WorkspaceAttributeMapper.of(rule); |
| boolean hasFile = mapper.isAttributeValueExplicitlySpecified(getFileAttrName()); |
| boolean hasFileContent = mapper.isAttributeValueExplicitlySpecified(getFileContentAttrName()); |
| |
| if (hasFile && hasFileContent) { |
| throw new RepositoryFunctionException( |
| Starlark.errorf( |
| "Rule cannot have both a '%s' and '%s' attribute", |
| getFileAttrName(), getFileContentAttrName()), |
| Transience.PERSISTENT); |
| } else if (hasFile) { |
| |
| fileValue = getFileValue(rule, env); |
| if (env.valuesMissing()) { |
| return false; |
| } |
| |
| } else if (hasFileContent) { |
| |
| try { |
| fileContent = mapper.get(getFileContentAttrName(), Type.STRING); |
| } catch (EvalException e) { |
| throw new RepositoryFunctionException(e, Transience.PERSISTENT); |
| } |
| |
| } else { |
| fileContent = getDefaultContent(rule); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Writes the file, based on the state set by prepareFile(). |
| * |
| * @param outputDirectory the directory to write the file. |
| * @throws RepositoryFunctionException if the file could not be written or symlinked |
| * @throws IllegalStateException if {@link #prepareFile} was not called before this, or if |
| * {@link #prepareFile} failed and this was called. |
| */ |
| public void finishFile(Rule rule, Path outputDirectory, Map<String, String> markerData) |
| throws RepositoryFunctionException { |
| if (fileValue != null) { |
| // Link x/FILENAME to <build_root>/x.FILENAME. |
| symlinkFile(fileValue, filename, outputDirectory); |
| String fileAttribute = getFileAttributeValue(rule); |
| String fileKey; |
| if (LabelValidator.isAbsolute(fileAttribute)) { |
| fileKey = getFileAttributeAsLabel(rule).toString(); |
| } else { |
| // TODO(pcloudy): Don't add absolute path into markerData once it's not supported |
| fileKey = fileValue.realRootedPath().asPath().getPathString(); |
| } |
| try { |
| markerData.put("FILE:" + fileKey, RepositoryFunction.fileValueToMarkerValue(fileValue)); |
| } catch (IOException e) { |
| throw new RepositoryFunctionException(e, Transience.TRANSIENT); |
| } |
| } else if (fileContent != null) { |
| RepositoryFunction.writeFile(outputDirectory, filename, fileContent); |
| } else { |
| throw new IllegalStateException("prepareFile() must be called before finishFile()"); |
| } |
| } |
| |
| private String getFileAttributeValue(Rule rule) throws RepositoryFunctionException { |
| WorkspaceAttributeMapper mapper = WorkspaceAttributeMapper.of(rule); |
| String fileAttribute; |
| try { |
| fileAttribute = mapper.get(getFileAttrName(), Type.STRING); |
| } catch (EvalException e) { |
| throw new RepositoryFunctionException(e, Transience.PERSISTENT); |
| } |
| return fileAttribute; |
| } |
| |
| private Label getFileAttributeAsLabel(Rule rule) throws RepositoryFunctionException { |
| Label label; |
| try { |
| // Parse a label |
| label = Label.parseAbsolute(getFileAttributeValue(rule), ImmutableMap.of()); |
| } catch (LabelSyntaxException ex) { |
| throw new RepositoryFunctionException( |
| Starlark.errorf( |
| "the '%s' attribute does not specify a valid label: %s", |
| getFileAttrName(), ex.getMessage()), |
| Transience.PERSISTENT); |
| } |
| return label; |
| } |
| |
| @Nullable |
| private FileValue getFileValue(Rule rule, Environment env) |
| throws RepositoryFunctionException, InterruptedException { |
| String fileAttribute = getFileAttributeValue(rule); |
| RootedPath rootedFile; |
| |
| if (LabelValidator.isAbsolute(fileAttribute)) { |
| Label label = getFileAttributeAsLabel(rule); |
| SkyKey pkgSkyKey = PackageLookupValue.key(label.getPackageIdentifier()); |
| PackageLookupValue pkgLookupValue = (PackageLookupValue) env.getValue(pkgSkyKey); |
| if (pkgLookupValue == null) { |
| return null; |
| } |
| if (!pkgLookupValue.packageExists()) { |
| throw new RepositoryFunctionException( |
| Starlark.errorf("Unable to load package for %s: not found.", fileAttribute), |
| Transience.PERSISTENT); |
| } |
| |
| // And now for the file |
| Root packageRoot = pkgLookupValue.getRoot(); |
| rootedFile = RootedPath.toRootedPath(packageRoot, label.toPathFragment()); |
| } else { |
| // TODO(dmarting): deprecate using a path for the workspace_file attribute. |
| PathFragment file = PathFragment.create(fileAttribute); |
| Path fileTarget = workspacePath.getRelative(file); |
| if (!fileTarget.exists()) { |
| throw new RepositoryFunctionException( |
| Starlark.errorf( |
| "the '%s' attribute does not specify an existing file (%s does not exist)", |
| getFileAttrName(), fileTarget), |
| Transience.PERSISTENT); |
| } |
| |
| if (file.isAbsolute()) { |
| rootedFile = |
| RootedPath.toRootedPath( |
| Root.fromPath(fileTarget.getParentDirectory()), |
| PathFragment.create(fileTarget.getBaseName())); |
| } else { |
| rootedFile = RootedPath.toRootedPath(Root.fromPath(workspacePath), file); |
| } |
| } |
| SkyKey fileKey = FileValue.key(rootedFile); |
| FileValue fileValue; |
| 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 workspace_file is a symlink to somewhere under the |
| // external/ directory, but if you do that, you are really asking for trouble. |
| fileValue = (FileValue) env.getValueOrThrow(fileKey, IOException.class); |
| if (fileValue == null) { |
| return null; |
| } |
| } catch (IOException e) { |
| throw new RepositoryFunctionException( |
| new IOException("Cannot lookup " + fileAttribute + ": " + e.getMessage()), |
| Transience.TRANSIENT); |
| } |
| |
| if (!fileValue.isFile() || fileValue.isSpecialFile()) { |
| throw new RepositoryFunctionException( |
| Starlark.errorf("%s is not a regular file", rootedFile.asPath()), |
| Transience.PERSISTENT); |
| } |
| |
| return fileValue; |
| } |
| |
| /** |
| * Symlinks a file from the local filesystem into the external repository's root. |
| * |
| * @param fileValue {@link FileValue} representing the file to be linked in |
| * @param outputDirectory the directory of the remote repository |
| * @throws RepositoryFunctionException if the file specified does not exist or cannot be linked. |
| */ |
| private static void symlinkFile(FileValue fileValue, String filename, Path outputDirectory) |
| throws RepositoryFunctionException { |
| Path filePath = outputDirectory.getRelative(filename); |
| RepositoryFunction.createSymbolicLink(filePath, fileValue.realRootedPath().asPath()); |
| } |
| } |
| |
| /** |
| * Encapsulates the 2-step behavior of creating workspace files for the new_*_repository rules. |
| */ |
| public static class NewRepositoryWorkspaceFileHandler extends BaseFileHandler { |
| |
| public NewRepositoryWorkspaceFileHandler(Path workspacePath) { |
| super(workspacePath, "WORKSPACE"); |
| } |
| |
| @Override |
| protected String getFileAttrName() { |
| return "workspace_file"; |
| } |
| |
| @Override |
| protected String getFileContentAttrName() { |
| return "workspace_file_content"; |
| } |
| |
| @Override |
| protected String getDefaultContent(Rule rule) { |
| return String.format( |
| "# DO NOT EDIT: automatically generated WORKSPACE file for %s\n" |
| + "workspace(name = \"%s\")\n", |
| rule.getTargetKind(), rule.getName()); |
| } |
| } |
| |
| /** Encapsulates the 2-step behavior of creating build files for the new_*_repository rules. */ |
| public static class NewRepositoryBuildFileHandler extends BaseFileHandler { |
| |
| public NewRepositoryBuildFileHandler(Path workspacePath) { |
| super(workspacePath, "BUILD.bazel"); |
| } |
| |
| @Override |
| protected String getFileAttrName() { |
| return "build_file"; |
| } |
| |
| @Override |
| protected String getFileContentAttrName() { |
| return "build_file_content"; |
| } |
| |
| @Override |
| protected String getDefaultContent(Rule rule) throws RepositoryFunctionException { |
| throw new RepositoryFunctionException( |
| Starlark.errorf("Rule requires a 'build_file' or 'build_file_content' attribute"), |
| Transience.PERSISTENT); |
| } |
| } |
| } |