blob: 094d13622cb3f3cc5233f16f8746f307d9d9f1d9 [file] [log] [blame]
// Copyright 2016 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.android;
import com.android.builder.core.VariantType;
import com.android.repository.Revision;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* Builder for AAPT command lines, with support for making flags conditional on build tools version
* and variant type.
*/
public class AaptCommandBuilder {
private final ImmutableList.Builder<String> flags = new ImmutableList.Builder<>();
private Revision buildToolsVersion;
private VariantType variantType;
public AaptCommandBuilder(Path aapt) {
flags.add(aapt.toString());
}
/** Sets the build tools version to be used for {@link #whenVersionIsAtLeast}. */
public AaptCommandBuilder forBuildToolsVersion(@Nullable Revision buildToolsVersion) {
Preconditions.checkState(
this.buildToolsVersion == null, "A build tools version was already specified.");
this.buildToolsVersion = buildToolsVersion;
return this;
}
/** Sets the variant type to be used for {@link #whenVariantIs}. */
public AaptCommandBuilder forVariantType(VariantType variantType) {
Preconditions.checkNotNull(variantType);
Preconditions.checkState(this.variantType == null, "A variant type was already specified.");
this.variantType = variantType;
return this;
}
/** Adds a single flag to the builder. */
public AaptCommandBuilder add(String flag) {
flags.add(flag);
return this;
}
/**
* Adds a flag to the builder, along with a string value. The two will be added as different words
* in the final command line. If the value is {@code null}, neither the flag nor the value will be
* added.
*/
public AaptCommandBuilder add(String flag, @Nullable String value) {
Preconditions.checkNotNull(flag);
if (!Strings.isNullOrEmpty(value)) {
flags.add(flag);
flags.add(value);
}
return this;
}
/**
* Adds a flag to the builder, along with a path value. The path will be converted to a string
* using {@code toString}, then the flag and the path will be added to the final command line as
* different words. If the value is {@code null}, neither the flag nor the path will be added.
*
* @see #add(String,String)
*/
public AaptCommandBuilder add(String flag, @Nullable Path path) {
Preconditions.checkNotNull(flag);
if (path != null) {
add(flag, path.toString());
}
return this;
}
/**
* Adds a flag to the builder multiple times, once for each value in the given collection. {@code
* null} values will be skipped. If the collection is empty, nothing will be added. The values
* will be added in the source collection's iteration order.
*
* <p>ex. If {@code flag} is {@code "-0"} and {@code values} contains the values {@code "png"},
* {@code null}, and {@code "gif"}, then four words will be added to the final command line:
* {@code "-0", "png", "-0", "gif"}.
*/
public AaptCommandBuilder addRepeated(String flag, Collection<String> values) {
Preconditions.checkNotNull(flag);
for (String value : values) {
add(flag, value);
}
return this;
}
/**
* Adds a flag to the builder multiple times, once for each value in the given collection. {@code
* null} values will be skipped. If the collection is empty, nothing will be added. The values
* will be added in the source collection's iteration order. See {@link
* AaptCommandBuilder#addRepeated(String, Collection)} for more information. If the collection
* exceed 200 items, the values will be written to a file and passed as &lt;flag&gt @&lt;file&gt;.
*/
public AaptCommandBuilder addParameterableRepeated(
final String flag, Collection<String> values, Path workingDirectory) throws IOException {
Preconditions.checkNotNull(flag);
Preconditions.checkNotNull(workingDirectory);
if (values.size() > 200) {
add(
flag,
"@"
+ Files.write(
Files.createDirectories(workingDirectory).resolve("params" + flag),
ImmutableList.of(values.stream().collect(Collectors.joining(" ")))));
} else {
addRepeated(flag, values);
}
return this;
}
/** Adds the next flag to the builder only if the condition is true. */
public ConditionalAaptCommandBuilder when(boolean condition) {
if (condition) {
return new SuccessfulConditionCommandBuilder(this);
} else {
return new FailedConditionCommandBuilder(this);
}
}
/** Adds the next flag to the builder only if the variant type is the passed-in type. */
public ConditionalAaptCommandBuilder whenVariantIs(VariantType variantType) {
Preconditions.checkNotNull(variantType);
return when(this.variantType == variantType);
}
/**
* Adds the next flag to the builder only if the build tools version is unspecified or is greater
* than or equal to the given version.
*/
public ConditionalAaptCommandBuilder whenVersionIsAtLeast(Revision requiredVersion) {
Preconditions.checkNotNull(requiredVersion);
return when(buildToolsVersion == null || buildToolsVersion.compareTo(requiredVersion) >= 0);
}
/** Assembles the full command line as a list. */
public List<String> build() {
return flags.build();
}
public AaptCommandBuilder add(String flag, Optional<Path> optionalPath) {
Preconditions.checkNotNull(flag);
Preconditions.checkNotNull(optionalPath);
optionalPath.map(p -> add(flag, p));
return this;
}
/** Wrapper for potentially adding flags to an AaptCommandBuilder based on a conditional. */
public interface ConditionalAaptCommandBuilder {
/**
* Adds a single flag to the builder if the condition was true.
*
* @see AaptCommandBuilder#add(String)
*/
AaptCommandBuilder thenAdd(String flag);
/**
* Adds a single flag and associated string value to the builder if the value is non-null and
* the condition was true.
*
* @see AaptCommandBuilder#add(String,String)
*/
AaptCommandBuilder thenAdd(String flag, @Nullable String value);
/**
* Adds a single flag and associated path value to the builder if the value is non-null and the
* condition was true.
*
* @see AaptCommandBuilder#add(String,Path)
*/
AaptCommandBuilder thenAdd(String flag, @Nullable Path value);
/**
* Adds a single flag and associated path value to the builder if the value is non-null and the
* condition was true.
*
* @see AaptCommandBuilder#add(String,Optional)
*/
AaptCommandBuilder thenAdd(String flag, Optional<Path> value);
/**
* Adds the values in the collection to the builder, each preceded by the given flag, if the
* collection was non-empty and the condition was true.
*
* @see AaptCommandBuilder#addRepeated(String,Collection<String>)
*/
AaptCommandBuilder thenAddRepeated(String flag, Collection<String> values);
}
/**
* Forwarding implementation of ConditionalAaptCommandBuilder returned when a condition is true.
*/
private static class SuccessfulConditionCommandBuilder implements ConditionalAaptCommandBuilder {
private final AaptCommandBuilder originalCommandBuilder;
public SuccessfulConditionCommandBuilder(AaptCommandBuilder originalCommandBuilder) {
this.originalCommandBuilder = originalCommandBuilder;
}
@Override
public AaptCommandBuilder thenAdd(String flag) {
return originalCommandBuilder.add(flag);
}
@Override
public AaptCommandBuilder thenAdd(String flag, @Nullable String value) {
return originalCommandBuilder.add(flag, value);
}
@Override
public AaptCommandBuilder thenAdd(String flag, @Nullable Path value) {
return originalCommandBuilder.add(flag, value);
}
@Override
public AaptCommandBuilder thenAdd(String flag, @Nullable Optional<Path> value) {
return originalCommandBuilder.add(flag, value);
}
@Override
public AaptCommandBuilder thenAddRepeated(String flag, Collection<String> values) {
return originalCommandBuilder.addRepeated(flag, values);
}
}
/** Null implementation of ConditionalAaptCommandBuilder returned when a condition is false. */
private static class FailedConditionCommandBuilder implements ConditionalAaptCommandBuilder {
private final AaptCommandBuilder originalCommandBuilder;
public FailedConditionCommandBuilder(AaptCommandBuilder originalCommandBuilder) {
this.originalCommandBuilder = originalCommandBuilder;
}
@Override
public AaptCommandBuilder thenAdd(String flag) {
Preconditions.checkNotNull(flag);
return originalCommandBuilder;
}
@Override
public AaptCommandBuilder thenAdd(String flag, @Nullable String value) {
Preconditions.checkNotNull(flag);
return originalCommandBuilder;
}
@Override
public AaptCommandBuilder thenAdd(String flag, @Nullable Path value) {
Preconditions.checkNotNull(flag);
return originalCommandBuilder;
}
@Override
public AaptCommandBuilder thenAdd(String flag, Optional<Path> value) {
Preconditions.checkNotNull(flag);
Preconditions.checkNotNull(value);
return originalCommandBuilder;
}
@Override
public AaptCommandBuilder thenAddRepeated(String flag, Collection<String> values) {
Preconditions.checkNotNull(flag);
Preconditions.checkNotNull(values);
return originalCommandBuilder;
}
}
/**
* Executes command and returns log.
*
* @throws IOException when the process cannot execute.
*/
public String execute(String action) throws IOException {
final StringBuilder processLog = new StringBuilder();
List<String> command = build();
final Process process = new ProcessBuilder().command(command).redirectErrorStream(true).start();
processLog.append("Command: ");
Joiner.on("\\\n\t").appendTo(processLog, command);
processLog.append("\nOutput:\n");
final InputStreamReader stdout =
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8);
while (process.isAlive()) {
processLog.append(CharStreams.toString(stdout));
}
// Make sure the full stdout is read.
while (stdout.ready()) {
processLog.append(CharStreams.toString(stdout));
}
if (process.exitValue() != 0) {
throw new RuntimeException(String.format("Error during %s:", action) + "\n" + processLog);
}
return processLog.toString();
}
}