blob: 712a5fa03fca9ae717f591094413659dd2443aa9 [file] [log] [blame]
// 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.analysis.actions;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.syntax.SkylarkDict;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.util.ResourceFileLoader;
import com.google.devtools.build.lib.util.StringUtilities;
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 com.google.protobuf.ByteString;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* Action to expand a template and write the expanded content to a file.
*/
@AutoCodec
@Immutable // if all substitutions are immutable
public final class TemplateExpansionAction extends AbstractFileWriteAction {
private static final String GUID = "786c1fe0-dca8-407a-b108-e1ecd6d1bc7f";
/**
* A pair of a string to be substituted and a string to substitute it with.
* For simplicity, these are called key and value. All implementations must
* be immutable, and always return the identical key. The returned values
* must be the same, though they need not be the same object.
*
* <p>It should be assumed that the {@link #getKey} invocation is cheap, and
* that the {@link #getValue} invocation is expensive.
*/
@Immutable // if the keys and values in the passed in lists and maps are all immutable
public abstract static class Substitution {
private Substitution() {
}
public abstract String getKey();
public abstract String getValue();
@AutoCodec.VisibleForSerialization
@AutoCodec
static final class StringSubstitution extends Substitution {
private final String key;
private final String value;
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
StringSubstitution(String key, String value) {
this.key = key;
this.value = value;
}
@Override
public String getKey() {
return key;
}
@Override
public String getValue() {
return value;
}
}
@AutoCodec.VisibleForSerialization
@AutoCodec
static final class ListSubstitution extends Substitution {
private final String key;
private final ImmutableList<?> value;
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
ListSubstitution(String key, ImmutableList<?> value) {
this.key = key;
this.value = value;
}
@Override
public String getKey() {
return key;
}
@Override
public String getValue() {
return Joiner.on(" ").join(value);
}
}
/**
* Returns an immutable Substitution instance for the given key and value.
*/
public static Substitution of(final String key, final String value) {
return new StringSubstitution(key, value);
}
/**
* Returns an immutable Substitution instance for the key and list of values. The
* values will be joined by spaces before substitution.
*/
public static Substitution ofSpaceSeparatedList(
final String key, final ImmutableList<?> value) {
return new ListSubstitution(key, value);
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object instanceof Substitution) {
Substitution substitution = (Substitution) object;
return substitution.getKey().equals(this.getKey())
&& substitution.getValue().equals(this.getValue());
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(getKey(), getValue());
}
@Override
public String toString() {
return "Substitution(" + getKey() + " -> " + getValue() + ")";
}
}
/**
* A substitution with a fixed key, and a computed value. The computed value
* must not change over the lifetime of an instance, though the {@link
* #getValue} method may return different String objects.
*
* <p>It should be assumed that the {@link #getKey} invocation is cheap, and
* that the {@link #getValue} invocation is expensive.
*/
public abstract static class ComputedSubstitution extends Substitution {
private final String key;
public ComputedSubstitution(String key) {
this.key = key;
}
@Override
public String getKey() {
return key;
}
}
/**
* Expands a fragment value.
*
* <p>This is slighly more memory efficient since it defers the expansion of the path fragment's
* string until requested. Often a template action is never executed, meaning the string is never
* needed.
*/
@AutoCodec
public static final class PathFragmentSubstitution extends ComputedSubstitution {
private final PathFragment pathFragment;
public PathFragmentSubstitution(String key, PathFragment pathFragment) {
super(key);
this.pathFragment = pathFragment;
}
@Override
public String getValue() {
return pathFragment.getPathString();
}
}
/**
* Expands a label value to its canonical string value.
*
* <p>This is more memory efficient than directly using the {@Label#toString}, since that method
* constructs a new string every time it's called.
*/
@AutoCodec
public static final class LabelSubstitution extends ComputedSubstitution {
private final Label label;
public LabelSubstitution(String key, Label label) {
super(key);
this.label = label;
}
@Override
public String getValue() {
return label.getCanonicalForm();
}
}
/**
* Expands a collection of artifacts to their short (root relative paths).
*
* <p>This is much more memory efficient than eagerly joining them into a string.
*/
@AutoCodec
public static final class JoinedArtifactShortPathSubstitution extends ComputedSubstitution {
private final Iterable<Artifact> artifacts;
private final String joinStr;
public JoinedArtifactShortPathSubstitution(
String key, ImmutableList<Artifact> artifacts, String joinStr) {
this(key, (Iterable<Artifact>) artifacts, joinStr);
}
public JoinedArtifactShortPathSubstitution(
String key, NestedSet<Artifact> artifacts, String joinStr) {
this(key, (Iterable<Artifact>) artifacts, joinStr);
}
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
JoinedArtifactShortPathSubstitution(String key, Iterable<Artifact> artifacts, String joinStr) {
super(key);
this.artifacts = artifacts;
this.joinStr = joinStr;
}
@Override
public String getValue() {
return StreamSupport.stream(artifacts.spliterator(), false)
.map(artifact -> artifact.getRootRelativePath().getPathString())
.collect(Collectors.joining(joinStr));
}
}
/**
* A template that contains text content, or alternatively throws an {@link
* IOException}.
*/
@Immutable // all subclasses are immutable
public abstract static class Template {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
/**
* We only allow subclasses in this file.
*/
private Template() {
}
/**
* Returns the text content of the template.
*/
protected abstract String getContent() throws IOException;
/**
* Returns a string that is used for the action key. This must change if
* the getContent method returns something different, but is not allowed to
* throw an exception.
*/
protected abstract String getKey();
@AutoCodec.VisibleForSerialization
@AutoCodec
static final class ErrorTemplate extends Template {
private final IOException e;
private final String templateName;
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
ErrorTemplate(IOException e, String templateName) {
this.e = e;
this.templateName = templateName;
}
@Override
protected String getContent() throws IOException {
throw new IOException(
"failed to load resource file '"
+ templateName
+ "' due to I/O error: "
+ e.getMessage(),
e);
}
@Override
protected String getKey() {
return "ERROR: " + e.getMessage();
}
}
@AutoCodec.VisibleForSerialization
@AutoCodec
static final class StringTemplate extends Template {
private final String templateText;
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
StringTemplate(String templateText) {
this.templateText = templateText;
}
@Override
protected String getContent() {
return templateText;
}
@Override
protected String getKey() {
return templateText;
}
}
@AutoCodec.VisibleForSerialization
@AutoCodec
static final class ArtifactTemplate extends Template {
private final Artifact templateArtifact;
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
ArtifactTemplate(Artifact templateArtifact) {
this.templateArtifact = templateArtifact;
}
@Override
protected String getContent() throws IOException {
Path templatePath = templateArtifact.getPath();
try {
return FileSystemUtils.readContent(templatePath, DEFAULT_CHARSET);
} catch (IOException e) {
throw new IOException(
"failed to load template file '"
+ templatePath.getPathString()
+ "' due to I/O error: "
+ e.getMessage(),
e);
}
}
@Override
protected String getKey() {
// This isn't strictly necessary, because the action inputs are automatically considered.
return "ARTIFACT: " + templateArtifact.getExecPathString();
}
}
/**
* Loads a template from the given resource. The resource is looked up
* relative to the given class. If the resource cannot be loaded, the returned
* template throws an {@link IOException} when {@link #getContent} is
* called. This makes it safe to use this method in a constant initializer.
*/
public static Template forResource(final Class<?> relativeToClass, final String templateName) {
try {
String content = ResourceFileLoader.loadResource(relativeToClass, templateName);
return forString(content);
} catch (final IOException e) {
return new ErrorTemplate(e, templateName);
}
}
/**
* Returns a template for the given text string.
*/
public static Template forString(final String templateText) {
return new StringTemplate(templateText);
}
/**
* Returns a template that loads the given artifact. It is important that
* the artifact is also an input for the action, or this won't work.
* Therefore this method is private, and you should use the corresponding
* {@link TemplateExpansionAction} constructor.
*/
private static Template forArtifact(final Artifact templateArtifact) {
return new ArtifactTemplate(templateArtifact);
}
}
private final Template template;
private final ImmutableList<Substitution> substitutions;
/**
* Creates a new TemplateExpansionAction instance.
*
* @param owner the action owner.
* @param inputs the Artifacts that this Action depends on
* @param primaryOutput the Artifact that will be created by executing this Action.
* @param template the template that will be expanded by this Action.
* @param substitutions the substitutions that will be applied to the template. All substitutions
* will be applied in order.
* @param makeExecutable iff true will change the output file to be executable.
*/
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
TemplateExpansionAction(
ActionOwner owner,
Collection<Artifact> inputs,
Artifact primaryOutput,
Template template,
List<Substitution> substitutions,
boolean makeExecutable) {
super(owner, inputs, primaryOutput, makeExecutable);
this.template = template;
this.substitutions = ImmutableList.copyOf(substitutions);
}
/**
* Creates a new TemplateExpansionAction instance for an artifact template.
*
* @param owner the action owner.
* @param templateArtifact the Artifact that will be read as the text template
* file
* @param output the Artifact that will be created by executing this Action.
* @param substitutions the substitutions that will be applied to the
* template. All substitutions will be applied in order.
* @param makeExecutable iff true will change the output file to be
* executable.
*/
public TemplateExpansionAction(ActionOwner owner,
Artifact templateArtifact,
Artifact output,
List<Substitution> substitutions,
boolean makeExecutable) {
this(owner, ImmutableList.of(templateArtifact), output, Template.forArtifact(templateArtifact),
substitutions, makeExecutable);
}
/**
* Creates a new TemplateExpansionAction instance without inputs.
*
* @param owner the action owner.
* @param output the Artifact that will be created by executing this Action.
* @param template the template
* @param substitutions the substitutions that will be applied to the
* template. All substitutions will be applied in order.
* @param makeExecutable iff true will change the output file to be
* executable.
*/
public TemplateExpansionAction(ActionOwner owner,
Artifact output,
Template template,
List<Substitution> substitutions,
boolean makeExecutable) {
this(owner, Artifact.NO_ARTIFACTS, output, template, substitutions, makeExecutable);
}
/**
* Expands the template by applying all substitutions.
* @param template
* @return the expanded text.
*/
private String expandTemplate(String template) {
for (Substitution entry : substitutions) {
template = StringUtilities.replaceAllLiteral(template, entry.getKey(), entry.getValue());
}
return template;
}
@VisibleForTesting
public String getFileContents() throws IOException {
return expandTemplate(template.getContent());
}
@Override
public String getSkylarkContent() throws IOException {
return getFileContents();
}
@Override
public DeterministicWriter newDeterministicWriter(ActionExecutionContext ctx) throws IOException {
return new ByteStringDeterministicWriter(
ByteString.copyFrom(getFileContents().getBytes(Template.DEFAULT_CHARSET)));
}
@Override
protected void computeKey(ActionKeyContext actionKeyContext, Fingerprint fp) {
fp.addString(GUID);
fp.addString(String.valueOf(makeExecutable));
fp.addString(template.getKey());
fp.addInt(substitutions.size());
for (Substitution entry : substitutions) {
fp.addString(entry.getKey());
fp.addString(entry.getValue());
}
}
@Override
public String getMnemonic() {
return "TemplateExpand";
}
@Override
protected String getRawProgressMessage() {
return "Expanding template " + Iterables.getOnlyElement(getOutputs()).prettyPrint();
}
public List<Substitution> getSubstitutions() {
return substitutions;
}
@Override
public SkylarkDict<String, String> getSkylarkSubstitutions() {
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
for (Substitution entry : substitutions) {
builder.put(entry.getKey(), entry.getValue());
}
return SkylarkDict.copyOf(null, builder.build());
}
}