blob: c9e5a7ddf95d98ac1afba6ab3bfe6fcaa21a4d8a [file] [log] [blame]
// 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.analysis.skylark;
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.Interner;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.actions.CommandLineExpansionException;
import com.google.devtools.build.lib.analysis.actions.CommandLine;
import com.google.devtools.build.lib.analysis.actions.CommandLineItem;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.concurrent.BlazeInterners;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.syntax.BaseFunction;
import com.google.devtools.build.lib.syntax.Environment;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.Printer;
import com.google.devtools.build.lib.syntax.SkylarkList;
import com.google.devtools.build.lib.syntax.SkylarkSemantics;
import java.util.ArrayList;
import java.util.IllegalFormatException;
import java.util.List;
import javax.annotation.Nullable;
/** Supports ctx.actions.args() from Skylark. */
class SkylarkCustomCommandLine extends CommandLine {
private final SkylarkSemantics skylarkSemantics;
private final EventHandler eventHandler;
private final ImmutableList<Object> arguments;
private static final Joiner LINE_JOINER = Joiner.on("\n").skipNulls();
private static final Joiner FIELD_JOINER = Joiner.on(": ").skipNulls();
static final class VectorArg {
private static Interner<VectorArg> interner = BlazeInterners.newStrongInterner();
private final boolean isNestedSet;
private final boolean hasFormat;
private final boolean hasBeforeEach;
private final boolean hasJoinWith;
private final boolean hasMapFn;
private final boolean hasLocation;
VectorArg(
boolean isNestedSet,
boolean hasFormat,
boolean hasBeforeEach,
boolean hasJoinWith,
boolean hasMapFn,
boolean hasLocation) {
this.isNestedSet = isNestedSet;
this.hasFormat = hasFormat;
this.hasBeforeEach = hasBeforeEach;
this.hasJoinWith = hasJoinWith;
this.hasMapFn = hasMapFn;
this.hasLocation = hasLocation;
}
private static void push(ImmutableList.Builder<Object> arguments, Builder arg) {
boolean wantsLocation = arg.format != null || arg.mapFn != null;
boolean hasLocation = arg.location != null && wantsLocation;
VectorArg vectorArg =
new VectorArg(
arg.nestedSet != null,
arg.format != null,
arg.beforeEach != null,
arg.joinWith != null,
arg.mapFn != null,
hasLocation);
vectorArg = interner.intern(vectorArg);
arguments.add(vectorArg);
if (vectorArg.isNestedSet) {
arguments.add(arg.nestedSet);
} else {
ImmutableList<?> list = arg.list.getImmutableList();
int count = list.size();
arguments.add(count);
for (int i = 0; i < count; ++i) {
arguments.add(list.get(i));
}
}
if (hasLocation) {
arguments.add(arg.location);
}
if (vectorArg.hasMapFn) {
arguments.add(arg.mapFn);
}
if (vectorArg.hasFormat) {
arguments.add(arg.format);
}
if (vectorArg.hasBeforeEach) {
arguments.add(arg.beforeEach);
}
if (vectorArg.hasJoinWith) {
arguments.add(arg.joinWith);
}
}
private int eval(
List<Object> arguments,
int argi,
ImmutableList.Builder<String> builder,
SkylarkSemantics skylarkSemantics,
EventHandler eventHandler)
throws CommandLineExpansionException {
final List<Object> mutatedValues;
final int count;
if (isNestedSet) {
NestedSet<?> nestedSet = (NestedSet<?>) arguments.get(argi++);
mutatedValues = Lists.newArrayList(nestedSet);
count = mutatedValues.size();
} else {
count = (Integer) arguments.get(argi++);
mutatedValues = new ArrayList<>(count);
for (int i = 0; i < count; ++i) {
mutatedValues.add(arguments.get(argi++));
}
}
final Location location = hasLocation ? (Location) arguments.get(argi++) : null;
if (hasMapFn) {
BaseFunction mapFn = (BaseFunction) arguments.get(argi++);
Object result = applyMapFn(mapFn, mutatedValues, location, skylarkSemantics, eventHandler);
if (!(result instanceof List)) {
throw new CommandLineExpansionException(
errorMessage(
"map_fn must return a list, got " + result.getClass().getSimpleName(),
location,
null));
}
List resultAsList = (List) result;
if (resultAsList.size() != count) {
throw new CommandLineExpansionException(
errorMessage(
String.format(
"map_fn must return a list of the same length as the input. "
+ "Found list of length %d, expected %d.",
resultAsList.size(), count),
location,
null));
}
mutatedValues.clear();
mutatedValues.addAll(resultAsList);
}
for (int i = 0; i < count; ++i) {
mutatedValues.set(i, CommandLineItem.expandToCommandLine(mutatedValues.get(i)));
}
if (hasFormat) {
String formatStr = (String) arguments.get(argi++);
Formatter formatter = new Formatter(formatStr, location);
try {
for (int i = 0; i < count; ++i) {
mutatedValues.set(i, formatter.format(mutatedValues.get(i)));
}
} catch (IllegalFormatException e) {
throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, null));
}
}
if (hasBeforeEach) {
String beforeEach = (String) arguments.get(argi++);
for (int i = 0; i < count; ++i) {
builder.add(beforeEach);
builder.add((String) mutatedValues.get(i));
}
} else if (hasJoinWith) {
String joinWith = (String) arguments.get(argi++);
builder.add(Joiner.on(joinWith).join(mutatedValues));
} else {
for (int i = 0; i < count; ++i) {
builder.add((String) mutatedValues.get(i));
}
}
return argi;
}
static class Builder {
@Nullable private final SkylarkList<?> list;
@Nullable private final NestedSet<?> nestedSet;
private String format;
private String beforeEach;
private String joinWith;
private Location location;
private BaseFunction mapFn;
public Builder(SkylarkList<?> list) {
this.list = list;
this.nestedSet = null;
}
public Builder(NestedSet<?> nestedSet) {
this.list = null;
this.nestedSet = nestedSet;
}
Builder setLocation(Location location) {
this.location = location;
return this;
}
Builder setFormat(String format) {
this.format = format;
return this;
}
Builder setBeforeEach(String beforeEach) {
this.beforeEach = beforeEach;
return this;
}
public Builder setJoinWith(String joinWith) {
this.joinWith = joinWith;
return this;
}
public Builder setMapFn(BaseFunction mapFn) {
this.mapFn = mapFn;
return this;
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
VectorArg vectorArg = (VectorArg) o;
return isNestedSet == vectorArg.isNestedSet
&& hasFormat == vectorArg.hasFormat
&& hasBeforeEach == vectorArg.hasBeforeEach
&& hasJoinWith == vectorArg.hasJoinWith
&& hasMapFn == vectorArg.hasMapFn
&& hasLocation == vectorArg.hasLocation;
}
@Override
public int hashCode() {
return Objects.hashCode(
isNestedSet, hasFormat, hasBeforeEach, hasJoinWith, hasMapFn, hasLocation);
}
}
static final class ScalarArg {
private static Interner<ScalarArg> interner = BlazeInterners.newStrongInterner();
private final boolean hasFormat;
private final boolean hasMapFn;
private final boolean hasLocation;
public ScalarArg(boolean hasFormat, boolean hasMapFn, boolean hasLocation) {
this.hasFormat = hasFormat;
this.hasMapFn = hasMapFn;
this.hasLocation = hasLocation;
}
private static void push(ImmutableList.Builder<Object> arguments, Builder arg) {
boolean wantsLocation = arg.format != null || arg.mapFn != null;
boolean hasLocation = arg.location != null && wantsLocation;
ScalarArg scalarArg = new ScalarArg(arg.format != null, arg.mapFn != null, hasLocation);
scalarArg = interner.intern(scalarArg);
arguments.add(scalarArg);
arguments.add(arg.object);
if (hasLocation) {
arguments.add(arg.location);
}
if (scalarArg.hasMapFn) {
arguments.add(arg.mapFn);
}
if (scalarArg.hasFormat) {
arguments.add(arg.format);
}
}
private int eval(
List<Object> arguments,
int argi,
ImmutableList.Builder<String> builder,
SkylarkSemantics skylarkSemantics,
EventHandler eventHandler)
throws CommandLineExpansionException {
Object object = arguments.get(argi++);
final Location location = hasLocation ? (Location) arguments.get(argi++) : null;
if (hasMapFn) {
BaseFunction mapFn = (BaseFunction) arguments.get(argi++);
object = applyMapFn(mapFn, object, location, skylarkSemantics, eventHandler);
}
object = CommandLineItem.expandToCommandLine(object);
if (hasFormat) {
String formatStr = (String) arguments.get(argi++);
Formatter formatter = new Formatter(formatStr, location);
object = formatter.format(object);
}
builder.add((String) object);
return argi;
}
static class Builder {
private Object object;
private String format;
private BaseFunction mapFn;
private Location location;
Builder(Object object) {
this.object = object;
}
Builder setLocation(Location location) {
this.location = location;
return this;
}
Builder setFormat(String format) {
this.format = format;
return this;
}
public Builder setMapFn(BaseFunction mapFn) {
this.mapFn = mapFn;
return this;
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ScalarArg scalarArg = (ScalarArg) o;
return hasFormat == scalarArg.hasFormat
&& hasMapFn == scalarArg.hasMapFn
&& hasLocation == scalarArg.hasLocation;
}
@Override
public int hashCode() {
return Objects.hashCode(hasFormat, hasMapFn, hasLocation);
}
}
static class Builder {
private final SkylarkSemantics skylarkSemantics;
private final ImmutableList.Builder<Object> arguments = ImmutableList.builder();
private final EventHandler eventHandler;
public Builder(SkylarkSemantics skylarkSemantics, EventHandler eventHandler) {
this.skylarkSemantics = skylarkSemantics;
this.eventHandler = eventHandler;
}
void add(Object object) {
arguments.add(object);
}
void add(VectorArg.Builder vectorArg) {
VectorArg.push(arguments, vectorArg);
}
void add(ScalarArg.Builder scalarArg) {
ScalarArg.push(arguments, scalarArg);
}
SkylarkCustomCommandLine build() {
return new SkylarkCustomCommandLine(this);
}
}
SkylarkCustomCommandLine(Builder builder) {
this.arguments = builder.arguments.build();
this.skylarkSemantics = builder.skylarkSemantics;
this.eventHandler = builder.eventHandler;
}
@Override
public Iterable<String> arguments() throws CommandLineExpansionException {
ImmutableList.Builder<String> result = ImmutableList.builder();
for (int argi = 0; argi < arguments.size(); ) {
Object arg = arguments.get(argi++);
if (arg instanceof VectorArg) {
argi = ((VectorArg) arg).eval(arguments, argi, result, skylarkSemantics, eventHandler);
} else if (arg instanceof ScalarArg) {
argi = ((ScalarArg) arg).eval(arguments, argi, result, skylarkSemantics, eventHandler);
} else {
result.add(CommandLineItem.expandToCommandLine(arg));
}
}
return result.build();
}
private static class Formatter {
private final String formatStr;
@Nullable private final Location location;
private final ArrayList<Object> args;
public Formatter(String formatStr, Location location) {
this.formatStr = formatStr;
this.location = location;
this.args = new ArrayList<>(1); // Reused arg list to reduce GC
this.args.add(null);
}
String format(Object object) throws CommandLineExpansionException {
try {
args.set(0, object);
return Printer.getPrinter().formatWithList(formatStr, args).toString();
} catch (IllegalFormatException e) {
throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, null));
}
}
}
private static Object applyMapFn(
BaseFunction mapFn,
Object arg,
Location location,
SkylarkSemantics skylarkSemantics,
EventHandler eventHandler)
throws CommandLineExpansionException {
ImmutableList<Object> args = ImmutableList.of(arg);
try (Mutability mutability = Mutability.create("map_fn")) {
Environment env =
Environment.builder(mutability)
.setSemantics(skylarkSemantics)
.setEventHandler(eventHandler)
.build();
return mapFn.call(args, ImmutableMap.of(), null, env);
} catch (EvalException e) {
throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, e.getCause()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CommandLineExpansionException(
errorMessage("Thread was interrupted", location, null));
}
}
private static String errorMessage(
String message, @Nullable Location location, @Nullable Throwable cause) {
return LINE_JOINER.join(
"\n", FIELD_JOINER.join(location, message), getCauseMessage(cause, message));
}
private static String getCauseMessage(@Nullable Throwable cause, String message) {
if (cause == null) {
return null;
}
String causeMessage = cause.getMessage();
if (causeMessage == null) {
return null;
}
if (message == null) {
return causeMessage;
}
// Skip the cause if it is redundant with the message so far.
if (message.contains(causeMessage)) {
return null;
}
return causeMessage;
}
}