| // 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(); |
| } |
| } |