From 99b9248dc623f17e62bf3ba6e5de0127a6f8ec18 Mon Sep 17 00:00:00 2001 From: asteinb Date: Mon, 16 Apr 2018 09:52:54 -0700 Subject: Create asset merging action This action is a trimmed-down version of the resource merging action (no resources or manifests). Also, create a base class to collect boilerplate action code, and a new Exception to indicate that we should exit an action immediately (rather than throw and print the stack trace). RELNOTES: none PiperOrigin-RevId: 193054422 --- .../build/android/AbstractBusyBoxAction.java | 87 +++++++++++ .../build/android/AndroidAssetMergingAction.java | 159 +++++++++++++++++++++ .../devtools/build/android/AndroidDataMerger.java | 2 +- .../build/android/AndroidResourceMerger.java | 4 +- .../build/android/ResourceProcessorBusyBox.java | 6 + .../devtools/build/android/UserException.java | 31 ++++ 6 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 src/tools/android/java/com/google/devtools/build/android/AbstractBusyBoxAction.java create mode 100644 src/tools/android/java/com/google/devtools/build/android/AndroidAssetMergingAction.java create mode 100644 src/tools/android/java/com/google/devtools/build/android/UserException.java (limited to 'src/tools/android/java/com/google/devtools') diff --git a/src/tools/android/java/com/google/devtools/build/android/AbstractBusyBoxAction.java b/src/tools/android/java/com/google/devtools/build/android/AbstractBusyBoxAction.java new file mode 100644 index 0000000000..66657d427e --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AbstractBusyBoxAction.java @@ -0,0 +1,87 @@ +// Copyright 2018 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.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.OptionsParsingException; +import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** Abstract base class containing helper methods and error handling for BusyBox actions. */ +public abstract class AbstractBusyBoxAction { + private final OptionsParser optionsParser; + private final String description; + private final Stopwatch timer = Stopwatch.createUnstarted(); + + AbstractBusyBoxAction(OptionsParser optionsParser, String description) { + this.optionsParser = optionsParser; + this.description = description; + } + + public void invoke(String[] args) throws Exception { + try { + invokeWithoutExit(args); + } catch (UserException | OptionsParsingException e) { + getLogger().severe(e.getMessage()); + // In Bazel, users tend to assume that a stack trace indicates a bug in underlying Bazel code + // and ignore the content of the exception. If we know that the exception was actually their + // fault, we should just exit immediately rather than print a stack trace. + System.exit(1); + } catch (Exception e) { + getLogger().log(Level.SEVERE, "Unexpected", e); + throw e; + } + } + + /** Invokes the action without calling System.exit or catching exceptions, for use in testing */ + @VisibleForTesting + void invokeWithoutExit(String[] args) throws Exception { + timer.start(); + optionsParser.enableParamsFileSupport( + new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); + optionsParser.parse(args); + + try (ScopedTemporaryDirectory scopedTmp = new ScopedTemporaryDirectory(description + "_tmp"); + ExecutorServiceCloser executorService = ExecutorServiceCloser.createWithFixedPoolOf(15)) { + run(scopedTmp.getPath(), executorService); + } + + logCompletion(description); + } + + abstract void run(Path tmp, ExecutorServiceCloser executorService) throws Exception; + + abstract Logger getLogger(); + + T getOptions(Class clazz) { + return optionsParser.getOptions(clazz); + } + + /** + * Logs that this action or some portion of it completed successfully. + * + * @param completedAction the action that was completed, for example "parsing". A timestamp will + * be appended to this string. + */ + void logCompletion(String completedAction) { + getLogger() + .fine(String.format("%s finished at %sms", completedAction, timer.elapsed().toMillis())); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidAssetMergingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidAssetMergingAction.java new file mode 100644 index 0000000000..98081c72b0 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidAssetMergingAction.java @@ -0,0 +1,159 @@ +// Copyright 2018 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.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.build.android.Converters.SerializedAndroidDataConverter; +import com.google.devtools.build.android.Converters.SerializedAndroidDataListConverter; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionDocumentationCategory; +import com.google.devtools.common.options.OptionEffectTag; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Logger; + +/** An action that merges a library's assets (without using resources or manifests). */ +public class AndroidAssetMergingAction extends AbstractBusyBoxAction { + private static final Logger logger = Logger.getLogger(AndroidAssetMergingAction.class.getName()); + + public static void main(String[] args) throws Exception { + create().invoke(args); + } + + @VisibleForTesting + static void testingMain(String... args) throws Exception { + create().invokeWithoutExit(args); + } + + private static AndroidAssetMergingAction create() { + return new AndroidAssetMergingAction(OptionsParser.newOptionsParser(Options.class)); + } + + private AndroidAssetMergingAction(OptionsParser optionsParser) { + super(optionsParser, "Merge assets"); + } + + /** Flag specifications for this action. */ + public static final class Options extends OptionsBase { + + @Option( + name = "primaryData", + defaultValue = "null", + converter = SerializedAndroidDataConverter.class, + category = "input", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = + "The assets of the current target. The expected format is " + + SerializedAndroidData.EXPECTED_FORMAT + ) + public SerializedAndroidData primary; + + @Option( + name = "directData", + defaultValue = "", + converter = SerializedAndroidDataListConverter.class, + category = "input", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = + "Direct asset dependencies. These values will be used if not defined in the " + + "primary assets. The expected format is " + + SerializedAndroidData.EXPECTED_FORMAT + + "[&...]" + ) + public List directData; + + @Option( + name = "data", + defaultValue = "", + converter = SerializedAndroidDataListConverter.class, + category = "input", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = + "Transitive Data dependencies. These values will be used if not defined in the " + + "primary assets. The expected format is " + + SerializedAndroidData.EXPECTED_FORMAT + + "[&...]" + ) + public List transitiveData; + + @Option( + name = "assetsOutput", + defaultValue = "null", + converter = PathConverter.class, + category = "output", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Path to the write merged asset archive." + ) + public Path assetsOutput; + + @Option( + name = "throwOnAssetConflict", + defaultValue = "true", + category = "config", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "If passed, asset merge conflicts will be treated as errors instead of warnings" + ) + public boolean throwOnAssetConflict; + } + + @Override + void run(Path tmp, ExecutorServiceCloser executorService) throws Exception { + Options options = getOptions(Options.class); + Path mergedAssets = tmp.resolve("merged_assets"); + Path ignored = tmp.resolve("ignored"); + + Preconditions.checkNotNull(options.primary); + + MergedAndroidData mergedData = + AndroidResourceMerger.mergeData( + options.primary, + /* primaryManifest = */ null, + options.directData, + options.transitiveData, + /* resourcesOut = */ ignored, + mergedAssets, + /* cruncher = */ null, + VariantType.LIBRARY, + /* symbolsOut = */ null, + /* rclassWriter = */ null, + options.throwOnAssetConflict, + executorService); + + logCompletion("Merging"); + + Preconditions.checkState( + !Files.exists(ignored), + "The asset merging action should not produce non-asset merge results!"); + + ResourcesZip.from(ignored, mergedData.getAssetDir()) + .writeTo(options.assetsOutput, true /* compress */); + logCompletion("Create assets zip"); + } + + @Override + Logger getLogger() { + return logger; + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java b/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java index 20a63bf120..8874ff9ff1 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java @@ -35,7 +35,7 @@ import java.util.logging.Logger; /** Handles the Merging of ParsedAndroidData. */ class AndroidDataMerger { - public static class MergeConflictException extends RuntimeException { + public static class MergeConflictException extends UserException { private MergeConflictException(String message) { super(message); diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java index a5126c6c28..2b63b5647b 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java @@ -31,10 +31,10 @@ import java.util.logging.Logger; // TODO(bazel-team): Turn into an instance object, in order to use an external ExecutorService. public class AndroidResourceMerger { /** Thrown when there is a unexpected condition during merging. */ - public static class MergingException extends RuntimeException { + public static class MergingException extends UserException { private MergingException(Throwable e) { - super(e); + super("Error during merging", e); } private MergingException(String message) { diff --git a/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java b/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java index 6f120ae165..32a17e7ddd 100644 --- a/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java +++ b/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java @@ -143,6 +143,12 @@ public class ResourceProcessorBusyBox { void call(String[] args) throws Exception { Aapt2ResourceShrinkingAction.main(args); } + }, + MERGE_ASSETS() { + @Override + void call(String[] args) throws Exception { + AndroidAssetMergingAction.main(args); + } }; abstract void call(String[] args) throws Exception; diff --git a/src/tools/android/java/com/google/devtools/build/android/UserException.java b/src/tools/android/java/com/google/devtools/build/android/UserException.java new file mode 100644 index 0000000000..6efbe8c360 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/UserException.java @@ -0,0 +1,31 @@ +// Copyright 2018 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; + +/** + * An exception triggered by a user error (including problems with input). + * + *

In Bazel, users tend to assume that a stack trace indicates a bug in underlying Bazel code and + * ignore the content of the exception. If we know that the exception was actually their fault, we + * should just exit immediately rather than print a stack trace. + */ +public class UserException extends RuntimeException { + UserException(String message, Throwable e) { + super(message, e); + } + + UserException(String message) { + super(message); + } +} -- cgit v1.2.3