| // 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 <flag> @<file>. | |
| */ | |
| 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(); | |
| } | |
| } |