diff options
author | 2017-08-23 15:24:31 +0200 | |
---|---|---|
committer | 2017-08-23 15:46:23 +0200 | |
commit | e516a101bb615f064d6622a5d4add541617b8c1f (patch) | |
tree | 93b6a3989b048a1cd35d9c1b37f411f77ec556bc /src/tools/android/java/com/google/devtools | |
parent | 296cb428546a7e966065b03137eca362efb9f34c (diff) |
Adds compile action for aapt2: Aapt2ResourcePackagingAction
Adds a ManifestContainer interface to centralize the types that provide manifests.
Adds PackagedResources to represent a linked dexless resource apk
RELNOTES: None
PiperOrigin-RevId: 166193049
Diffstat (limited to 'src/tools/android/java/com/google/devtools')
21 files changed, 821 insertions, 178 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/Aapt2ResourcePackagingAction.java b/src/tools/android/java/com/google/devtools/build/android/Aapt2ResourcePackagingAction.java new file mode 100644 index 0000000000..5e168414c9 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/Aapt2ResourcePackagingAction.java @@ -0,0 +1,192 @@ +// Copyright 2015 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 static com.google.common.collect.Streams.concat; +import static java.util.stream.Collectors.toList; + +import com.android.utils.StdLogger; +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.devtools.build.android.AndroidResourceMerger.MergingException; +import com.google.devtools.build.android.AndroidResourceProcessingAction.Options; +import com.google.devtools.build.android.aapt2.Aapt2ConfigOptions; +import com.google.devtools.build.android.aapt2.CompiledResources; +import com.google.devtools.build.android.aapt2.ResourceCompiler; +import com.google.devtools.build.android.aapt2.ResourceLinker; +import com.google.devtools.build.android.aapt2.StaticLibrary; +import com.google.devtools.common.options.OptionsParser; +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * Provides an entry point for the resource processing using the AOSP build tools. + * + * <pre> + * Example Usage: + * java/com/google/build/android/Aapt2ResourcePackagingAction\ + * --sdkRoot path/to/sdk\ + * --aapt path/to/sdk/aapt\ + * --annotationJar path/to/sdk/annotationJar\ + * --adb path/to/sdk/adb\ + * --zipAlign path/to/sdk/zipAlign\ + * --androidJar path/to/sdk/androidJar\ + * --manifestOutput path/to/manifest\ + * --primaryData path/to/resources:path/to/assets:path/to/manifest\ + * --data p/t/res1:p/t/assets1:p/t/1/AndroidManifest.xml:p/t/1/R.txt:symbols,\ + * p/t/res2:p/t/assets2:p/t/2/AndroidManifest.xml:p/t/2/R.txt:symbols\ + * --packagePath path/to/write/archive.ap_\ + * --srcJarOutput path/to/write/archive.srcjar + * </pre> + */ +public class Aapt2ResourcePackagingAction { + + private static final StdLogger STD_LOGGER = new StdLogger(StdLogger.Level.WARNING); + + private static final Logger logger = + Logger.getLogger(Aapt2ResourcePackagingAction.class.getName()); + + private static Aapt2ConfigOptions aaptConfigOptions; + private static Options options; + + public static void main(String[] args) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + OptionsParser optionsParser = + OptionsParser.newOptionsParser(Options.class, Aapt2ConfigOptions.class); + optionsParser.enableParamsFileSupport(FileSystems.getDefault()); + optionsParser.parseAndExitUponError(args); + aaptConfigOptions = optionsParser.getOptions(Aapt2ConfigOptions.class); + options = optionsParser.getOptions(Options.class); + + try (ScopedTemporaryDirectory scopedTmp = + new ScopedTemporaryDirectory("android_resources_tmp")) { + final Path tmp = scopedTmp.getPath(); + final Path mergedAssets = tmp.resolve("merged_assets"); + final Path mergedResources = tmp.resolve("merged_resources"); + + final Path densityManifest = tmp.resolve("manifest-filtered/AndroidManifest.xml"); + + final Path processedManifest = tmp.resolve("manifest-processed/AndroidManifest.xml"); + final Path databindingResourcesRoot = + Files.createDirectories(tmp.resolve("android_data_binding_resources")); + final Path compiledResources = Files.createDirectories(tmp.resolve("compiled")); + final Path linkedOut = Files.createDirectories(tmp.resolve("linked")); + + logger.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + + // Checks for merge conflicts. + MergedAndroidData mergedAndroidData = + AndroidResourceMerger.mergeData( + options.primaryData, + options.directData, + options.transitiveData, + mergedResources, + mergedAssets, + null /* cruncher. Aapt2 automatically chooses to crunch or not. */, + options.packageType, + options.symbolsOut, + options.prefilteredResources, + false /* throwOnResourceConflict */); + + logger.fine(String.format("Merging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + + final List<String> densitiesToFilter = + options.prefilteredResources.isEmpty() + ? options.densities + : Collections.<String>emptyList(); + final ListeningExecutorService executorService = ExecutorServiceCloser.createDefaultService(); + try (final Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { + final ResourceCompiler compiler = + ResourceCompiler.create( + executorService, + compiledResources, + aaptConfigOptions.aapt2, + aaptConfigOptions.buildToolsVersion); + + CompiledResources compiled = + options + .primaryData + .processDataBindings(options.dataBindingInfoOut, databindingResourcesRoot) + .compile(compiler, compiledResources) + .processManifest( + manifest -> + AndroidManifestProcessor.with(STD_LOGGER) + .processManifest( + options.applicationId, + options.versionCode, + options.versionName, + manifest, + processedManifest)) + .processManifest( + manifest -> + new DensitySpecificManifestProcessor(options.densities, densityManifest) + .process(manifest)); + + // Write manifestOutput now before the dummy manifest is created. + if (options.manifestOutput != null) { + AndroidResourceOutputs.copyManifestToOutput(compiled, options.manifestOutput); + } + + List<StaticLibrary> dependencies = + // Last defined dependencies will overwrite previous one, so always place direct + // after transitive. + concat(options.transitiveData.stream(), options.directData.stream()) + .map(DependencyAndroidData::getStaticLibrary) + .collect(toList()); + + ResourceLinker.create(aaptConfigOptions.aapt2, linkedOut) + .dependencies(ImmutableList.of(StaticLibrary.from(aaptConfigOptions.androidJar))) + .include(dependencies) + .buildVersion(aaptConfigOptions.buildToolsVersion) + .filterToDensity(densitiesToFilter) + .link(compiled) + .copyPackageTo(options.packagePath) + .copyProguardTo(options.proguardOutput) + .copyMainDexProguardTo(options.mainDexProguardOutput) + .createSourceJar(options.srcJarOutput) + .copyRTxtTo(options.rOutput); + logger.fine(String.format("aapt2 finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } + if (options.resourcesOutput != null) { + // The compiled resources and the merged resources should be the same. + // TODO(corysmith): Decompile or otherwise provide the exact resources in the apk. + AndroidResourceOutputs.createResourcesZip( + mergedAndroidData.getResourceDir(), + mergedAndroidData.getAssetDir(), + options.resourcesOutput, + false /* compress */); + } + logger.fine( + String.format("Packaging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } catch (MergingException e) { + logger.log(java.util.logging.Level.SEVERE, "Error during merging resources", e); + throw e; + } catch (IOException e) { + logger.log(java.util.logging.Level.SEVERE, "Error during processing resources", e); + throw e; + } catch (Exception e) { + logger.log(java.util.logging.Level.SEVERE, "Unexpected", e); + throw e; + } + logger.fine(String.format("Resources processed in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidManifestProcessor.java b/src/tools/android/java/com/google/devtools/build/android/AndroidManifestProcessor.java index 93364a6fd7..be6738fa3b 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidManifestProcessor.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidManifestProcessor.java @@ -57,6 +57,18 @@ import javax.xml.stream.events.XMLEvent; /** Provides manifest processing oriented tools. */ public class AndroidManifestProcessor { + + /** Wrapper exception for errors thrown during manifest processing. */ + public static class ManifestProcessingException extends RuntimeException { + public ManifestProcessingException(Throwable e) { + super(e); + } + + public ManifestProcessingException(String message) { + super(message); + } + } + private static final ImmutableMap<SystemProperty, String> SYSTEM_PROPERTY_NAMES = Maps.toMap( Arrays.asList(SystemProperty.values()), @@ -109,7 +121,7 @@ public class AndroidManifestProcessor { * @param logFile The path to write the merger log to. * @return The path of the resultant manifest, either {@code output}, or {@code manifest} if no * merging was required. - * @throws MergeErrorException if a merge error was encountered during merging. + * @throws ManifestProcessingException if there was a problem writing the merged manifest. */ // TODO(corysmith): Extract manifest processing. public Path mergeManifest( @@ -118,7 +130,8 @@ public class AndroidManifestProcessor { MergeType mergeType, Map<String, String> values, Path output, - Path logFile) throws MergeErrorException { + Path logFile) + throws ManifestProcessingException { if (mergeeManifests.isEmpty() && values.isEmpty()) { return manifest; } @@ -141,8 +154,7 @@ public class AndroidManifestProcessor { placeholders.putAll(values); for (SystemProperty property : SystemProperty.values()) { if (values.containsKey(SYSTEM_PROPERTY_NAMES.get(property))) { - manifestMerger.setOverride( - property, values.get(SYSTEM_PROPERTY_NAMES.get(property))); + manifestMerger.setOverride(property, values.get(SYSTEM_PROPERTY_NAMES.get(property))); // The manifest merger does not allow explicitly specifying either applicationId or // packageName as placeholders if SystemProperty.PACKAGE is specified. It forces these @@ -181,31 +193,19 @@ public class AndroidManifestProcessor { break; case ERROR: mergingReport.log(stdLogger); - throw new MergeErrorException(mergingReport); + throw new ManifestProcessingException(mergingReport.getReportString()); default: - throw new RuntimeException("Unhandled result type : " + mergingReport.getResult()); + throw new ManifestProcessingException( + "Unhandled result type : " + mergingReport.getResult()); } - } catch (IOException | MergeFailureException e) { - throw new RuntimeException(e); + } catch (MergeFailureException | IOException e) { + throw new ManifestProcessingException(e); } return output; } - /** - * Stamp specific properties into the manifest tag of the given manifest. - * - * @param variantType The type of rule the manifest belongs to, determining the stamping behavior. - * @param customPackageForR The package attribute to stamp if not a binary manifest. - * @param applicationId The package attribute for a binary manifest. - * @param versionCode The android:versionCode attribute to stamp. - * @param versionName The android:versionName attribute to stamp. - * @param primaryData The {@link MergedAndroidData} that contains the manifest to modify. - * @param processedManifest The path to write the modified manifest. - * @return A {@link MergedAndroidData} containing the modified manifest, or {@code primaryData} if - * no modification was required. - * @throws MergeErrorException if a merge error was encountered during merging. - */ + /** Process a manifest for a library or a binary and return the merged android data. */ public MergedAndroidData processManifest( VariantType variantType, String customPackageForR, @@ -214,7 +214,7 @@ public class AndroidManifestProcessor { String versionName, MergedAndroidData primaryData, Path processedManifest) - throws MergeErrorException { + throws IOException { ManifestMerger2.MergeType mergeType = variantType == VariantType.DEFAULT @@ -225,55 +225,91 @@ public class AndroidManifestProcessor { variantType == VariantType.DEFAULT ? applicationId : customPackageForR; if (versionCode != -1 || versionName != null || newManifestPackage != null) { - try { - Files.createDirectories(processedManifest.getParent()); - - // The generics on Invoker don't make sense, so ignore them. - @SuppressWarnings("unchecked") - Invoker<?> manifestMergerInvoker = - ManifestMerger2.newMerger(primaryData.getManifest().toFile(), stdLogger, mergeType); - // Stamp new package - if (newManifestPackage != null) { - manifestMergerInvoker.setOverride(SystemProperty.PACKAGE, newManifestPackage); - } - // Stamp version and applicationId (if provided) into the manifest - if (versionCode > 0) { - manifestMergerInvoker.setOverride( - SystemProperty.VERSION_CODE, String.valueOf(versionCode)); - } - if (versionName != null) { - manifestMergerInvoker.setOverride(SystemProperty.VERSION_NAME, versionName); - } - - MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED; - if (mergeType == ManifestMerger2.MergeType.APPLICATION) { - manifestMergerInvoker.withFeatures(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS); - } - - MergingReport mergingReport = manifestMergerInvoker.merge(); - switch (mergingReport.getResult()) { - case WARNING: - mergingReport.log(stdLogger); - writeMergedManifest(mergedManifestKind, mergingReport, processedManifest); - break; - case SUCCESS: - writeMergedManifest(mergedManifestKind, mergingReport, processedManifest); - break; - case ERROR: - mergingReport.log(stdLogger); - throw new MergeErrorException(mergingReport); - default: - throw new RuntimeException("Unhandled result type : " + mergingReport.getResult()); - } - } catch (IOException | MergeFailureException e) { - throw new RuntimeException(e); - } + processManifest( + versionCode, + versionName, + primaryData.getManifest(), + processedManifest, + mergeType, + newManifestPackage); return new MergedAndroidData( primaryData.getResourceDir(), primaryData.getAssetDir(), processedManifest); } return primaryData; } + /** Processes the manifest for a binary and return the manifest Path. */ + public Path processManifest( + String applicationId, + int versionCode, + String versionName, + Path manifest, + Path processedManifest) { + + ManifestMerger2.MergeType mergeType = ManifestMerger2.MergeType.APPLICATION; + + String newManifestPackage = applicationId; + + if (versionCode != -1 || versionName != null || newManifestPackage != null) { + processManifest( + versionCode, versionName, manifest, processedManifest, mergeType, newManifestPackage); + return processedManifest; + } + return manifest; + } + + private void processManifest( + int versionCode, + String versionName, + Path primaryManifest, + Path processedManifest, + MergeType mergeType, + String newManifestPackage) { + try { + Files.createDirectories(processedManifest.getParent()); + + // The generics on Invoker don't make sense, so ignore them. + @SuppressWarnings("unchecked") + Invoker<?> manifestMergerInvoker = + ManifestMerger2.newMerger(primaryManifest.toFile(), stdLogger, mergeType); + // Stamp new package + if (newManifestPackage != null) { + manifestMergerInvoker.setOverride(SystemProperty.PACKAGE, newManifestPackage); + } + // Stamp version and applicationId (if provided) into the manifest + if (versionCode > 0) { + manifestMergerInvoker.setOverride(SystemProperty.VERSION_CODE, String.valueOf(versionCode)); + } + if (versionName != null) { + manifestMergerInvoker.setOverride(SystemProperty.VERSION_NAME, versionName); + } + + MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED; + if (mergeType == MergeType.APPLICATION) { + manifestMergerInvoker.withFeatures(Feature.REMOVE_TOOLS_DECLARATIONS); + } + + MergingReport mergingReport = manifestMergerInvoker.merge(); + switch (mergingReport.getResult()) { + case WARNING: + mergingReport.log(stdLogger); + writeMergedManifest(mergedManifestKind, mergingReport, processedManifest); + break; + case SUCCESS: + writeMergedManifest(mergedManifestKind, mergingReport, processedManifest); + break; + case ERROR: + mergingReport.log(stdLogger); + throw new ManifestProcessingException(mergingReport.getReportString()); + default: + throw new ManifestProcessingException( + "Unhandled result type : " + mergingReport.getResult()); + } + } catch (IOException | MergeFailureException e) { + throw new ManifestProcessingException(e); + } + } + /** * Overwrite the package attribute of {@code <manifest>} in an AndroidManifest.xml file. * @@ -323,18 +359,22 @@ public class AndroidManifestProcessor { } writer.flush(); } catch (XMLStreamException | FactoryConfigurationError | IOException e) { - throw new RuntimeException(e); + throw new ManifestProcessingException(e); } return output; } - private void writeMergedManifest( + public void writeMergedManifest( MergedManifestKind mergedManifestKind, MergingReport mergingReport, Path manifestOut) - throws IOException { + throws ManifestProcessingException { String manifestContents = mergingReport.getMergedDocument(mergedManifestKind); String annotatedDocument = mergingReport.getMergedDocument(MergedManifestKind.BLAME); stdLogger.verbose(annotatedDocument); - Files.write(manifestOut, manifestContents.getBytes(UTF_8)); + try { + Files.write(manifestOut, manifestContents.getBytes(UTF_8)); + } catch (IOException e) { + throw new ManifestProcessingException(e); + } } } diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java index 6840ac8cc4..1fb31561ed 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java @@ -22,7 +22,6 @@ import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.io.Files; import com.google.devtools.build.android.AndroidDataMerger.MergeConflictException; -import com.google.devtools.build.android.AndroidManifestProcessor.MergeErrorException; import com.google.devtools.build.android.AndroidResourceMerger.MergingException; import com.google.devtools.build.android.AndroidResourceProcessor.AaptConfigOptions; import com.google.devtools.build.android.Converters.ExistingPathConverter; @@ -287,7 +286,7 @@ public class AndroidResourceMergingAction { } catch (MergingException e) { logger.log(Level.SEVERE, "Error during merging resources", e); throw e; - } catch (MergeErrorException e) { + } catch (AndroidManifestProcessor.ManifestProcessingException e) { System.exit(1); } catch (Exception e) { logger.log(Level.SEVERE, "Unexpected", e); diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java index 3cad94c83f..97c261610e 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java @@ -15,6 +15,7 @@ package com.google.devtools.build.android; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.Ordering; @@ -47,6 +48,7 @@ import java.util.zip.ZipOutputStream; /** Collects all the functionationality for an action to create the final output artifacts. */ public class AndroidResourceOutputs { + @VisibleForTesting static class ZipBuilder implements Closeable { // ZIP timestamps have a resolution of 2 seconds. // see http://www.info-zip.org/FAQ.html#limits @@ -88,7 +90,8 @@ public class AndroidResourceOutputs { // Fix the path for windows. String relativeName = rawName.replace('\\', '/'); // Make sure the zip entry is not absolute. - Preconditions.checkArgument(!relativeName.startsWith("/")); + Preconditions.checkArgument( + !relativeName.startsWith("/"), "Cannot add absolute resources %s", relativeName); ZipEntry entry = new ZipEntry(relativeName); entry.setMethod(storageMethod); entry.setTime(normalizeTime(relativeName)); @@ -254,14 +257,13 @@ public class AndroidResourceOutputs { /** * Copies the AndroidManifest.xml to the specified output location. * - * @param androidData The MergedAndroidData which contains the manifest to be written to - * manifestOut. + * @param provider The MergedAndroidData which contains the manifest to be written to manifestOut. * @param manifestOut The Path to write the AndroidManifest.xml. */ - public static void copyManifestToOutput(MergedAndroidData androidData, Path manifestOut) { + public static void copyManifestToOutput(ManifestContainer provider, Path manifestOut) { try { Files.createDirectories(manifestOut.getParent()); - Files.copy(androidData.getManifest(), manifestOut); + Files.copy(provider.getManifest(), manifestOut); // Set to the epoch for caching purposes. Files.setLastModifiedTime(manifestOut, FileTime.fromMillis(0L)); } catch (IOException e) { @@ -381,7 +383,7 @@ public class AndroidResourceOutputs { } /** Collects all the compiled resources into an archive, normalizing the paths to the root. */ - public static void archiveCompiledResources( + public static Path archiveCompiledResources( final Path archiveOut, final Path databindingResourcesRoot, final Path compiledRoot, @@ -389,6 +391,7 @@ public class AndroidResourceOutputs { throws IOException { final Path relativeDatabindingProcessedResources = databindingResourcesRoot.getRoot().relativize(databindingResourcesRoot); + try (ZipBuilder builder = ZipBuilder.createFor(archiveOut)) { for (Path artifact : compiledArtifacts) { Path relativeName = artifact; @@ -407,5 +410,6 @@ public class AndroidResourceOutputs { builder.addEntry(relativeName.toString(), Files.readAllBytes(artifact), ZipEntry.STORED); } } + return archiveOut; } } diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java index 18b89c998d..cfee8a88e0 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java @@ -23,7 +23,6 @@ import com.android.utils.StdLogger; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.android.AndroidDataMerger.MergeConflictException; -import com.google.devtools.build.android.AndroidManifestProcessor.MergeErrorException; import com.google.devtools.build.android.AndroidResourceMerger.MergingException; import com.google.devtools.build.android.AndroidResourceProcessor.AaptConfigOptions; import com.google.devtools.build.android.AndroidResourceProcessor.FlagAaptOptions; @@ -298,12 +297,14 @@ public class AndroidResourceProcessingAction { ) public List<String> prefilteredResources; - @Option(name = "throwOnResourceConflict", - defaultValue = "false", - category = "config", - documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, - effectTags = {OptionEffectTag.UNKNOWN}, - help = "If passed, resource merge conflicts will be treated as errors instead of warnings") + @Option( + name = "throwOnResourceConflict", + defaultValue = "false", + category = "config", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "If passed, resource merge conflicts will be treated as errors instead of warnings" + ) public boolean throwOnResourceConflict; } @@ -452,7 +453,7 @@ public class AndroidResourceProcessingAction { | UnrecognizedSplitsException e) { logger.log(java.util.logging.Level.SEVERE, "Error during processing resources", e); throw e; - } catch (MergeErrorException e) { + } catch (AndroidManifestProcessor.ManifestProcessingException e) { System.exit(1); } catch (Exception e) { logger.log(java.util.logging.Level.SEVERE, "Unexpected", e); diff --git a/src/tools/android/java/com/google/devtools/build/android/Converters.java b/src/tools/android/java/com/google/devtools/build/android/Converters.java index db88ade4d1..26d918b278 100644 --- a/src/tools/android/java/com/google/devtools/build/android/Converters.java +++ b/src/tools/android/java/com/google/devtools/build/android/Converters.java @@ -465,15 +465,15 @@ public final class Converters { /** Converts a list of static library strings into paths. */ @Deprecated public static class StaticLibraryListConverter implements Converter<List<StaticLibrary>> { - static final Splitter SPLITTER = Splitter.on(File.pathSeparator); + static final Splitter SPLITTER = Splitter.on(File.pathSeparatorChar); static final StaticLibraryConverter libraryConverter = new StaticLibraryConverter(); @Override public List<StaticLibrary> convert(String input) throws OptionsParsingException { final Builder<StaticLibrary> builder = ImmutableList.<StaticLibrary>builder(); - for (String unused : SPLITTER.splitToList(input)) { - builder.add(libraryConverter.convert(input)); + for (String path : SPLITTER.splitToList(input)) { + builder.add(libraryConverter.convert(path)); } return builder.build(); } @@ -486,7 +486,6 @@ public final class Converters { /** Converts a static library string into path. */ public static class StaticLibraryConverter implements Converter<StaticLibrary> { - static final Splitter SPLITTER = Splitter.on(File.pathSeparator); static final PathConverter pathConverter = new PathConverter(true); diff --git a/src/tools/android/java/com/google/devtools/build/android/DensitySpecificManifestProcessor.java b/src/tools/android/java/com/google/devtools/build/android/DensitySpecificManifestProcessor.java index 5616a2adb5..8a08978f94 100644 --- a/src/tools/android/java/com/google/devtools/build/android/DensitySpecificManifestProcessor.java +++ b/src/tools/android/java/com/google/devtools/build/android/DensitySpecificManifestProcessor.java @@ -101,7 +101,7 @@ public class DensitySpecificManifestProcessor { * * @throws ManifestProcessingException when the manifest cannot be properly modified. */ - public Path process(Path manifest) throws ManifestProcessingException { + public Path process(Path manifest) { if (densities.isEmpty()) { return manifest; } @@ -111,9 +111,10 @@ public class DensitySpecificManifestProcessor { NodeList manifestElements = doc.getElementsByTagName("manifest"); if (manifestElements.getLength() != 1) { - throw new ManifestProcessingException( - String.format("Manifest %s does not contain exactly one <manifest> tag. " - + "It contains %d.", manifest, manifestElements.getLength())); + throw new AndroidManifestProcessor.ManifestProcessingException( + String.format( + "Manifest %s does not contain exactly one <manifest> tag. " + "It contains %d.", + manifest, manifestElements.getLength())); } Node manifestElement = manifestElements.item(0); @@ -170,7 +171,7 @@ public class DensitySpecificManifestProcessor { return out; } catch (ParserConfigurationException | SAXException | IOException | TransformerException e) { - throw new ManifestProcessingException(e.getMessage()); + throw new AndroidManifestProcessor.ManifestProcessingException(e.getMessage()); } } } diff --git a/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java index be7977ff60..f7ed888690 100644 --- a/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java +++ b/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android; import com.android.builder.dependency.SymbolFileProvider; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.aapt2.StaticLibrary; import java.io.File; import java.nio.file.FileSystem; import java.nio.file.FileSystems; @@ -26,19 +27,18 @@ import java.util.regex.Pattern; /** * Contains the assets, resources, manifest and resource symbols for an android_library dependency. * - * <p> - * This class serves the role of both a processed MergedAndroidData and a dependency exported from - * another invocation of the AndroidResourcesProcessorAction. Since it's presumed to be cheaper to - * only pass the derived artifact (rTxt) rather that the entirety of the processed dependencies (png - * crunching and resource processing should be saved for the final AndroidResourcesProcessorAction - * invocation) AndroidData can have multiple roots for resources and assets. - * </p> + * <p>This class serves the role of both a processed MergedAndroidData and a dependency exported + * from another invocation of the AndroidResourcesProcessorAction. Since it's presumed to be cheaper + * to only pass the derived artifact (rTxt) rather that the entirety of the processed dependencies + * (png crunching and resource processing should be saved for the final + * AndroidResourcesProcessorAction invocation) AndroidData can have multiple roots for resources and + * assets. */ class DependencyAndroidData extends SerializedAndroidData { - private static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+:.+(:.*)?"); + private static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+:.+(:.*){0,2}"); public static final String EXPECTED_FORMAT = - "resources[#resources]:assets[#assets]:manifest:r.txt:symbols.bin"; + "resources[#resources]:assets[#assets]:manifest:r.txt:symbols.bin:static.library.ap_"; public static DependencyAndroidData valueOf(String text) { return valueOf(text, FileSystems.getDefault()); @@ -47,33 +47,41 @@ class DependencyAndroidData extends SerializedAndroidData { @VisibleForTesting static DependencyAndroidData valueOf(String text, FileSystem fileSystem) { if (!VALID_REGEX.matcher(text).find()) { - throw new IllegalArgumentException( - text + " is not in the format '" + EXPECTED_FORMAT + "'"); + throw new IllegalArgumentException(text + " is not in the format '" + EXPECTED_FORMAT + "'"); } String[] parts = text.split(":"); // TODO(bazel-team): Handle the symbols.bin file. // The local symbols.bin is optional -- if it is missing, we'll use the full R.txt + Path rTxt = exists(fileSystem.getPath(parts[3])); + ImmutableList<Path> assetDirs = + parts[1].length() == 0 ? ImmutableList.<Path>of() : splitPaths(parts[1], fileSystem); return new DependencyAndroidData( splitPaths(parts[0], fileSystem), - parts[1].length() == 0 ? ImmutableList.<Path>of() : splitPaths(parts[1], fileSystem), + assetDirs, exists(fileSystem.getPath(parts[2])), - exists(fileSystem.getPath(parts[3])), - parts.length == 5 ? fileSystem.getPath(parts[4]) : null); + rTxt, + parts.length > 4 ? fileSystem.getPath(parts[4]) : null, + parts.length > 5 + ? StaticLibrary.from(exists(fileSystem.getPath(parts[5])), rTxt, assetDirs) + : null); } private final Path manifest; private final Path rTxt; + private final StaticLibrary staticLibrary; public DependencyAndroidData( ImmutableList<Path> resourceDirs, ImmutableList<Path> assetDirs, Path manifest, Path rTxt, - Path symbols) { + Path symbols, + StaticLibrary staticLibrary) { // Use the manifest as a label for now. super(resourceDirs, assetDirs, manifest.toString(), symbols); this.manifest = manifest; this.rTxt = rTxt; + this.staticLibrary = staticLibrary; } public SymbolFileProvider asSymbolFileProvider() { @@ -110,6 +118,10 @@ class DependencyAndroidData extends SerializedAndroidData { }; } + public StaticLibrary getStaticLibrary() { + return staticLibrary; + } + @Override public String toString() { return String.format( diff --git a/src/tools/android/java/com/google/devtools/build/android/ExecutorServiceCloser.java b/src/tools/android/java/com/google/devtools/build/android/ExecutorServiceCloser.java index 374c8a5db0..cc99342ddf 100644 --- a/src/tools/android/java/com/google/devtools/build/android/ExecutorServiceCloser.java +++ b/src/tools/android/java/com/google/devtools/build/android/ExecutorServiceCloser.java @@ -14,9 +14,11 @@ package com.google.devtools.build.android; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.io.Closeable; import java.io.IOException; import java.util.List; +import java.util.concurrent.Executors; /** Shutdowns and verifies that no tasks are running in the executor service. */ final class ExecutorServiceCloser implements Closeable { @@ -37,4 +39,18 @@ final class ExecutorServiceCloser implements Closeable { public static Closeable createWith(ListeningExecutorService executorService) { return new ExecutorServiceCloser(executorService); } + + /** + * Creates a {@link ListeningExecutorService} with a sane sized thread pool based on our current + * metrics. + */ + public static ListeningExecutorService createDefaultService() { + // The reported availableProcessors may be higher than the actual resources + // (on a shared system). On the other hand, a lot of the work is I/O, so it's not completely + // CPU bound. As a compromise, divide by 2 the reported availableProcessors. + int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); + final ListeningExecutorService executorService = + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(numThreads)); + return executorService; + } } diff --git a/src/tools/android/java/com/google/devtools/build/android/ManifestContainer.java b/src/tools/android/java/com/google/devtools/build/android/ManifestContainer.java new file mode 100644 index 0000000000..7b672be37d --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/ManifestContainer.java @@ -0,0 +1,21 @@ +// 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.android; + +import java.nio.file.Path; + +/** Represents a container that can be converted to a manifest. */ +public interface ManifestContainer { + Path getManifest(); +} diff --git a/src/tools/android/java/com/google/devtools/build/android/ManifestMergerAction.java b/src/tools/android/java/com/google/devtools/build/android/ManifestMergerAction.java index 2bd9a7756b..5f0681e8df 100644 --- a/src/tools/android/java/com/google/devtools/build/android/ManifestMergerAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/ManifestMergerAction.java @@ -18,7 +18,6 @@ import static java.util.logging.Level.SEVERE; import com.android.manifmerger.ManifestMerger2.MergeType; import com.android.utils.StdLogger; import com.google.common.collect.ImmutableMap; -import com.google.devtools.build.android.AndroidManifestProcessor.MergeErrorException; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.ExistingPathStringDictionaryConverter; import com.google.devtools.build.android.Converters.MergeTypeConverter; @@ -224,7 +223,7 @@ public class ManifestMergerAction { // Set to the epoch for caching purposes. Files.setLastModifiedTime(options.manifestOutput, FileTime.fromMillis(0L)); - } catch (MergeErrorException e) { + } catch (AndroidManifestProcessor.ManifestProcessingException e) { System.exit(1); } catch (Exception e) { logger.log(SEVERE, "Error during merging manifests", e); diff --git a/src/tools/android/java/com/google/devtools/build/android/MergedAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/MergedAndroidData.java index a69b02d71b..431d205e12 100644 --- a/src/tools/android/java/com/google/devtools/build/android/MergedAndroidData.java +++ b/src/tools/android/java/com/google/devtools/build/android/MergedAndroidData.java @@ -17,15 +17,15 @@ import java.nio.file.Path; /** * Represents the AndroidData before processing, after merging. - * - * <p> - * The life cycle of AndroidData goes: + * + * <p>The life cycle of AndroidData goes: + * * <pre> * UnvalidatedAndroidData -> MergedAndroidData -> DensityFilteredAndroidData * -> DependencyAndroidData * </pre> */ -class MergedAndroidData { +class MergedAndroidData implements ManifestContainer { private Path resourceDir; private Path assetDir; @@ -45,6 +45,7 @@ class MergedAndroidData { return assetDir; } + @Override public Path getManifest() { return manifest; } 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 5e89dcdec5..991a5d5bf7 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 @@ -114,6 +114,12 @@ public class ResourceProcessorBusyBox { void call(String[] args) throws Exception { CompileLibraryResourcesAction.main(args); } + }, + AAPT2_PACKAGE() { + @Override + void call(String[] args) throws Exception { + Aapt2ResourcePackagingAction.main(args); + } }; abstract void call(String[] args) throws Exception; @@ -139,7 +145,8 @@ public class ResourceProcessorBusyBox { help = "The processing tool to execute. " + "Valid tools: PACKAGE, VALIDATE, GENERATE_BINARY_R, GENERATE_LIBRARY_R, PARSE, " - + "MERGE, GENERATE_AAR, SHRINK, MERGE_MANIFEST, COMPILE_LIBRARY_RESOURCES." + + "MERGE, GENERATE_AAR, SHRINK, MERGE_MANIFEST, COMPILE_LIBRARY_RESOURCES, " + + "AAPT2_PACKAGE." ) public Tool tool; } diff --git a/src/tools/android/java/com/google/devtools/build/android/SerializedAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/SerializedAndroidData.java index ea744c53e5..b2a81d345d 100644 --- a/src/tools/android/java/com/google/devtools/build/android/SerializedAndroidData.java +++ b/src/tools/android/java/com/google/devtools/build/android/SerializedAndroidData.java @@ -47,7 +47,7 @@ public class SerializedAndroidData { splitPaths(parts[0], fileSystem), splitPaths(parts[1], fileSystem), parts[2], - parts.length > 3 ? fileSystem.getPath(parts[3]) : null); + parts.length > 3 ? exists(fileSystem.getPath(parts[3])) : null); } protected static ImmutableList<Path> splitPaths(String pathsString, FileSystem fileSystem) { diff --git a/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java index 20717d6005..74976543e9 100644 --- a/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java +++ b/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java @@ -15,20 +15,25 @@ package com.google.devtools.build.android; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.aapt2.CompiledResources; +import com.google.devtools.build.android.aapt2.ResourceCompiler; +import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Path; +import java.util.List; import java.util.Objects; +import java.util.concurrent.ExecutionException; import java.util.regex.Pattern; /** * Android data that has yet to be merged and validated, the primary data for the Processor. * - * <p>The life cycle of AndroidData goes: - * {@link UnvalidatedAndroidData} -> {@link MergedAndroidData} -> {@link DensityFilteredAndroidData} - * -> {@link DependencyAndroidData} + * <p>The life cycle of AndroidData goes: {@link UnvalidatedAndroidData} -> {@link + * MergedAndroidData} -> {@link DensityFilteredAndroidData} -> {@link DependencyAndroidData} */ class UnvalidatedAndroidData extends UnvalidatedAndroidDirectories { + private static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+"); public static final String EXPECTED_FORMAT = "resources[#resources]:assets[#assets]:manifest"; @@ -86,4 +91,39 @@ class UnvalidatedAndroidData extends UnvalidatedAndroidDirectories { && Objects.equals(other.manifest, manifest); } + public CompiledResources compile(ResourceCompiler compiler, Path workingDirectory) + throws IOException, ExecutionException, InterruptedException { + for (Path resourceDir : resourceDirs) { + compiler.queueDirectoryForCompilation(resourceDir); + } + return archiveCompiledResources( + compiler.getCompiledArtifacts(), + workingDirectory, + workingDirectory.resolve("compiled.zip")); + } + + protected CompiledResources archiveCompiledResources( + List<Path> resources, Path workingDirectory, Path output) throws IOException { + return CompiledResources.from( + AndroidResourceOutputs.archiveCompiledResources( + output, workingDirectory, workingDirectory, resources), + manifest, + assetDirs); + } + + public UnvalidatedAndroidData processDataBindings( + Path dataBindingInfoOut, Path dataBindingWorkingDirectory) { + + return new UnvalidatedAndroidData(resourceDirs, assetDirs, manifest) { + @Override + protected CompiledResources archiveCompiledResources( + List<Path> resources, Path workingDirectory, Path output) throws IOException { + return CompiledResources.from( + AndroidResourceOutputs.archiveCompiledResources( + output, dataBindingWorkingDirectory, workingDirectory, resources), + manifest, + assetDirs); + } + }; + } } diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/Aapt2ConfigOptions.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/Aapt2ConfigOptions.java index bfe57af3a1..ada69bc1a3 100644 --- a/src/tools/android/java/com/google/devtools/build/android/aapt2/Aapt2ConfigOptions.java +++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/Aapt2ConfigOptions.java @@ -47,4 +47,15 @@ public class Aapt2ConfigOptions extends OptionsBase { help = "Version of the build tools (e.g. aapt) being used, e.g. 23.0.2" ) public Revision buildToolsVersion; + + @Option( + name = "androidJar", + defaultValue = "null", + converter = ExistingPathConverter.class, + category = "tool", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Path to the android jar for resource packaging and building apks." + ) + public Path androidJar; } diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/CompiledResources.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/CompiledResources.java index 65f9c1a9a6..81d94fb244 100644 --- a/src/tools/android/java/com/google/devtools/build/android/aapt2/CompiledResources.java +++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/CompiledResources.java @@ -13,32 +13,56 @@ // limitations under the License. package com.google.devtools.build.android.aapt2; +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.ManifestContainer; import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nullable; /** * Contains reference to the aapt2 generated .flat file archive and a manifest. * * <p>This represents the state between the aapt2 compile and link actions. */ -public class CompiledResources { +public class CompiledResources implements ManifestContainer { private final Path resources; private final Path manifest; + private final List<Path> assetsDirs; - private CompiledResources(Path resources, Path manifest) { + private CompiledResources(Path resources, Path manifest, List<Path> assetsDirs) { this.resources = resources; this.manifest = manifest; + this.assetsDirs = assetsDirs; } public static CompiledResources from(Path resources, Path manifest) { - return new CompiledResources(resources, manifest); + return from(resources, manifest, null); } - public Path asZip() { + public static CompiledResources from( + Path resources, Path manifest, @Nullable List<Path> assetDirs) { + return new CompiledResources( + resources, manifest, Optional.ofNullable(assetDirs).orElseGet(ImmutableList::of)); + } + + public Path getZip() { return resources; } - public Path asManifest() { + @Override + public Path getManifest() { return manifest; } + + public List<String> getAssetsStrings() { + return assetsDirs.stream().map(Path::toString).collect(Collectors.toList()); + } + + public CompiledResources processManifest(Function<Path, Path> processManifest) { + return new CompiledResources(resources, processManifest.apply(manifest), assetsDirs); + } } diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/PackagedResources.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/PackagedResources.java new file mode 100644 index 0000000000..77c9c66cc6 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/PackagedResources.java @@ -0,0 +1,88 @@ +// 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.android.aapt2; + +import com.google.devtools.build.android.AndroidResourceOutputs; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Represents the packaged, flattened resources. */ +public class PackagedResources { + + private final Path apk; + private final Path rTxt; + private final Path proguardConfig; + private final Path mainDexProguard; + private final Path javaSourceDirectory; + + private PackagedResources( + Path apk, Path rTxt, Path proguardConfig, Path mainDexProguard, Path javaSourceDirectory) { + this.apk = apk; + this.rTxt = rTxt; + this.proguardConfig = proguardConfig; + this.mainDexProguard = mainDexProguard; + this.javaSourceDirectory = javaSourceDirectory; + } + + public PackagedResources copyPackageTo(Path packagePath) throws IOException { + return new PackagedResources( + copy(apk, packagePath), rTxt, proguardConfig, mainDexProguard, javaSourceDirectory); + } + + public PackagedResources copyRTxtTo(Path rOutput) throws IOException { + if (rOutput == null) { + return this; + } + return new PackagedResources( + apk, copy(rTxt, rOutput), proguardConfig, mainDexProguard, javaSourceDirectory); + } + + private Path copy(Path from, Path out) throws IOException { + Files.createDirectories(out.getParent()); + Files.copy(from, out); + return out; + } + + public PackagedResources copyProguardTo(Path proguardOut) throws IOException { + if (proguardOut == null) { + return this; + } + return new PackagedResources( + apk, rTxt, copy(proguardConfig, proguardOut), mainDexProguard, javaSourceDirectory); + } + + public PackagedResources copyMainDexProguardTo(Path mainDexProguardOut) throws IOException { + if (mainDexProguardOut == null) { + return this; + } + return of( + apk, rTxt, proguardConfig, copy(mainDexProguard, mainDexProguardOut), javaSourceDirectory); + } + + public PackagedResources createSourceJar(Path sourceJarPath) throws IOException { + if (sourceJarPath == null) { + return this; + } + AndroidResourceOutputs.createSrcJar(javaSourceDirectory, sourceJarPath, false); + return of(apk, rTxt, proguardConfig, mainDexProguard, sourceJarPath); + } + + public static PackagedResources of( + Path outPath, Path rTxt, Path proguardConfig, Path mainDexProguard, Path javaSourceDirectory) + throws IOException { + return new PackagedResources( + outPath, rTxt, proguardConfig, mainDexProguard, javaSourceDirectory); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceCompiler.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceCompiler.java index 240f8e0d04..e13dc613ca 100644 --- a/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceCompiler.java +++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceCompiler.java @@ -16,15 +16,11 @@ package com.google.devtools.build.android.aapt2; import com.android.builder.core.VariantType; import com.android.repository.Revision; -import com.google.common.base.Joiner; -import com.google.common.io.CharStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.devtools.build.android.AaptCommandBuilder; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -34,9 +30,11 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; /** Invokes aapt2 to compile resources. */ public class ResourceCompiler { + private static final Logger logger = Logger.getLogger(ResourceCompiler.class.getName()); private final CompilingVisitor compilingVisitor; @@ -57,8 +55,7 @@ public class ResourceCompiler { @Override public Path call() throws Exception { - List<String> processLog = new ArrayList<>(); - AaptCommandBuilder commandBuilder = + logger.fine( new AaptCommandBuilder(aapt2) .forBuildToolsVersion(buildToolsVersion) .forVariantType(VariantType.LIBRARY) @@ -66,20 +63,8 @@ public class ResourceCompiler { .add("-v") .add("--legacy") .add("-o", compiledResourcesOut.toString()) - .add(file.toString()); - final Process process = - new ProcessBuilder().command(commandBuilder.build()).redirectErrorStream(true).start(); - processLog.add("Command:"); - processLog.add(commandBuilder.build().toString()); - final InputStreamReader stdout = - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8); - while (process.isAlive()) { - processLog.add(CharStreams.toString(stdout)); - } - if (process.exitValue() != 0) { - throw new RuntimeException( - "Error compiling " + file + "\n" + Joiner.on("\n").join(processLog)); - } + .add(file.toString()) + .execute("Compiling " + file)); String type = file.getParent().getFileName().toString(); String filename = file.getFileName().toString(); if (type.startsWith("values")) { diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java index eb17f2dc9f..fa9edc3044 100644 --- a/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java +++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java @@ -15,18 +15,43 @@ package com.google.devtools.build.android.aapt2; import com.android.builder.core.VariantType; import com.android.repository.Revision; +import com.google.common.base.Joiner; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.io.ByteStreams; import com.google.devtools.build.android.AaptCommandBuilder; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.zip.ZipFile; /** Performs linking of {@link CompiledResources} using aapt2. */ public class ResourceLinker { + /** Represents errors thrown during linking. */ + public static class LinkError extends RuntimeException { + public LinkError(Throwable e) { + super(e); + } + } + + private static final Logger logger = Logger.getLogger(ResourceLinker.class.getName()); + private final Path aapt2; - private List<StaticLibrary> libraries; - private Revision buildToolsVersion; private final Path workingDirectory; + private List<StaticLibrary> linkAgainst = ImmutableList.of(); + private Revision buildToolsVersion; + private String density; + private Path androidJar; + private List<String> uncompressedExtensions = ImmutableList.of(); + private List<String> resourceConfigs = ImmutableList.of(); + private Path baseApk; + private List<StaticLibrary> include; private ResourceLinker(Path aapt2, Path workingDirectory) { this.aapt2 = aapt2; @@ -37,9 +62,15 @@ public class ResourceLinker { return new ResourceLinker(aapt2, workingDirectory); } - /** Dependent static libraries to be linked to. */ - public ResourceLinker dependencies(List<StaticLibrary> libraries) { - this.libraries = libraries; + /** Dependent static to be linked against. */ + public ResourceLinker dependencies(List<StaticLibrary> linkAgainst) { + this.linkAgainst = linkAgainst; + return this; + } + + /** Dependent static libraries to be included in the binary. */ + public ResourceLinker include(List<StaticLibrary> include) { + this.include = include; return this; } @@ -48,26 +79,147 @@ public class ResourceLinker { return this; } + public ResourceLinker baseApkToLinkAgainst(Path baseApk) { + this.baseApk = baseApk; + return this; + } + + public ResourceLinker filterToDensity(List<String> densitiesToFilter) { + if (densitiesToFilter.size() > 1) { + throw new UnsupportedOperationException("Multiple densities not yet supported with aapt2"); + } + if (densitiesToFilter.size() > 0) { + density = Iterables.getOnlyElement(densitiesToFilter); + } + return this; + } + /** * Statically links the {@link CompiledResources} with the dependencies to produce a {@link * StaticLibrary}. * * @throws IOException */ - public StaticLibrary linkStatically(CompiledResources resources) throws IOException { + public StaticLibrary linkStatically(CompiledResources resources) { final Path outPath = workingDirectory.resolve("lib.ap_"); - new AaptCommandBuilder(aapt2) - .forBuildToolsVersion(buildToolsVersion) - .forVariantType(VariantType.LIBRARY) - .add("link") - .add("--manifest", resources.asManifest()) - .add("--static-lib") - .add("--output-text-symbols", workingDirectory) - .add("-o", outPath) - .add("--auto-add-overlay") - .addRepeated("-I", StaticLibrary.toPathStrings(libraries)) - .add("-R", resources.asZip()) - .execute(String.format("Linking %s", resources)); - return StaticLibrary.from(outPath); + Path rTxt = workingDirectory.resolve("R.txt"); + + try { + logger.fine( + new AaptCommandBuilder(aapt2) + .forBuildToolsVersion(buildToolsVersion) + .forVariantType(VariantType.LIBRARY) + .add("link") + .add("--manifest", resources.getManifest()) + .add("--static-lib") + .add("--no-static-lib-packages") + .whenVersionIsAtLeast(new Revision(23)) + .thenAdd("--no-version-vectors") + .addRepeated("-R", unzipCompiledResources(resources.getZip())) + .addRepeated("-I", StaticLibrary.toPathStrings(linkAgainst)) + .add("--auto-add-overlay") + .add("-o", outPath) + .add("--java", workingDirectory.resolve("java")) // java needed to create R.txt + .add("--output-text-symbols", rTxt) + .execute(String.format("Statically linking %s", resources))); + return StaticLibrary.from(outPath, rTxt); + } catch (IOException e) { + throw new LinkError(e); + } + } + + public PackagedResources link(CompiledResources compiled) { + final Path outPath = workingDirectory.resolve("bin.ap_"); + Path rTxt = workingDirectory.resolve("R.txt"); + Path proguardConfig = workingDirectory.resolve("proguard.cfg"); + Path mainDexProguard = workingDirectory.resolve("proguard.maindex.cfg"); + Path javaSourceDirectory = workingDirectory.resolve("java"); + + try { + logger.fine( + new AaptCommandBuilder(aapt2) + .forBuildToolsVersion(buildToolsVersion) + .forVariantType(VariantType.DEFAULT) + .add("link") + .whenVersionIsAtLeast(new Revision(23)) + .thenAdd("--no-version-vectors") + .add("--no-static-lib-packages") + .when(Level.FINE.equals(logger.getLevel())) + .thenAdd("-v") + .add("--manifest", compiled.getManifest()) + .add("--auto-add-overlay") + .addRepeated("-A", compiled.getAssetsStrings()) + .addRepeated("-I", StaticLibrary.toPathStrings(linkAgainst)) + .addRepeated("-R", StaticLibrary.toPathStrings(include)) + .addRepeated("-R", unzipCompiledResources(compiled.getZip())) + // Never compress apks. + .add("-0", "apk") + // Add custom no-compress extensions. + .addRepeated("-0", uncompressedExtensions) + .addRepeated("-A", StaticLibrary.toAssetPaths(include)) + .when(density != null) + .thenAdd("--preferred-density", density) + // Filter by resource configuration type. + .when(!resourceConfigs.isEmpty()) + .thenAdd("-c", Joiner.on(',').join(resourceConfigs)) + .add("--output-text-symbols", rTxt) + .add("--java", javaSourceDirectory) + .add("--proguard", proguardConfig) + .add("--proguard-main-dex", mainDexProguard) + .add("-o", outPath) + .execute(String.format("Linking %s", compiled.getManifest()))); + return PackagedResources.of( + outPath, rTxt, proguardConfig, mainDexProguard, javaSourceDirectory); + } catch (IOException e) { + throw new LinkError(e); + } + } + + private List<String> unzipCompiledResources(Path resourceZip) throws IOException { + final ZipFile zipFile = new ZipFile(resourceZip.toFile()); + return zipFile + .stream() + .map( + entry -> { + final Path resolve = workingDirectory.resolve(entry.getName()); + try { + Files.createDirectories(resolve.getParent()); + return Files.write(resolve, ByteStreams.toByteArray(zipFile.getInputStream(entry))); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .map(Path::toString) + .collect(Collectors.toList()); + } + + public ResourceLinker storeUncompressed(List<String> uncompressedExtensions) { + this.uncompressedExtensions = uncompressedExtensions; + return this; + } + + public ResourceLinker includeOnlyConfigs(List<String> resourceConfigs) { + this.resourceConfigs = resourceConfigs; + return this; + } + + public ResourceLinker using(Path androidJar) { + this.androidJar = androidJar; + return this; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("aapt2", aapt2) + .add("linkAgainst", linkAgainst) + .add("buildToolsVersion", buildToolsVersion) + .add("workingDirectory", workingDirectory) + .add("density", density) + .add("androidJar", androidJar) + .add("uncompressedExtensions", uncompressedExtensions) + .add("resourceConfigs", resourceConfigs) + .add("baseApk", baseApk) + .toString(); } } diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/StaticLibrary.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/StaticLibrary.java index 8828e87042..fc574f89ff 100644 --- a/src/tools/android/java/com/google/devtools/build/android/aapt2/StaticLibrary.java +++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/StaticLibrary.java @@ -13,7 +13,10 @@ // limitations under the License. package com.google.devtools.build.android.aapt2; -import com.google.common.base.Function; +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import java.io.IOException; import java.nio.file.Files; @@ -21,47 +24,95 @@ import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Optional; +import javax.annotation.Nullable; -/** A library generated by aapt2. */ +/** A static library generated by aapt2. */ public class StaticLibrary { private final Path library; private final Optional<Path> rTxt; + private final Optional<List<Path>> assets; - private static final Function<StaticLibrary, String> LIBRARY_TO_STRING = - input -> input.library.toString(); - - private StaticLibrary(Path library, Optional<Path> rTxt) { + private StaticLibrary(Path library, Optional<Path> rTxt, Optional<List<Path>> assets) { this.library = library; this.rTxt = rTxt; + this.assets = assets; } public static StaticLibrary from(Path library) { - return new StaticLibrary(library, Optional.empty()); + return of(library, Optional.empty(), Optional.empty()); } public static StaticLibrary from(Path library, Path rTxt) { - return new StaticLibrary(library, Optional.of(rTxt)); + return of(library, Optional.of(rTxt), Optional.empty()); + } + + private static StaticLibrary of(Path library, Optional<Path> rTxt, Optional<List<Path>> assets) { + return new StaticLibrary(library, rTxt, assets); } public StaticLibrary copyLibraryTo(Path target) throws IOException { - return new StaticLibrary(Files.copy(library, target), rTxt); + return of(Files.copy(library, target), rTxt, assets); } - public StaticLibrary copyRTxtTo(final Path target) { - return new StaticLibrary( + public StaticLibrary copyRTxtTo(@Nullable final Path target) { + return of( library, rTxt.map( input -> { + if (target == null) { + return input; + } try { return Files.copy(input, target); } catch (IOException e) { throw new RuntimeException(e); } - })); + }), + assets); + } + + @VisibleForTesting + public String asLibraryPathString() { + return fixForAapt2(library).toString(); + } + + // TODO(b/64140845): aapt2 doesn't recognize .ap_ as a valid library. + private static Path fixForAapt2(Path library) { + if (library.getFileName().toString().endsWith(".ap_")) { + Path fixed = library.resolveSibling(library.getFileName().toString().replace(".ap_", ".apk")); + // The file has already been "fixed" + if (Files.exists(fixed)) { + return fixed; + } + try { + + return Files.copy(library, fixed); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return library; + } + + private List<Path> asAssetPathStrings() { + return assets.orElse(ImmutableList.of()); } public static Collection<String> toPathStrings(List<StaticLibrary> libraries) { - return Lists.transform(libraries, LIBRARY_TO_STRING); + return Lists.transform(libraries, StaticLibrary::asLibraryPathString); + } + + public static StaticLibrary from(Path library, Path rTxt, ImmutableList<Path> assetDirs) { + return of(library, Optional.ofNullable(rTxt), Optional.ofNullable(assetDirs)); + } + + public static Collection<String> toAssetPaths(List<StaticLibrary> libraries) { + return libraries + .stream() + .map(StaticLibrary::asAssetPathStrings) + .flatMap(List::stream) + .map(Object::toString) + .collect(toImmutableList()); } } |