diff options
author | 2017-03-15 18:34:58 +0000 | |
---|---|---|
committer | 2017-03-16 08:36:33 +0000 | |
commit | 6ff407df1fd6f5ccfab6d1a65c9e6708e719b61a (patch) | |
tree | 2069712517a3686cb5ccd26c36cb3b34367700d6 | |
parent | d7b23448c4fa067b1056848a9bf2e5dd3e6b45d6 (diff) |
Breaking up is hard to do: AndroidResourceProcessor
* Extract merging methods to a static class
* Extract output methods to a static class
* Extract manifest processing methods to a class
* Move ExecutorCloserService to the top level
--
PiperOrigin-RevId: 150219121
MOS_MIGRATED_REVID=150219121
13 files changed, 894 insertions, 782 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java b/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java index 66d921b384..2dea78e650 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java @@ -132,7 +132,7 @@ public class AarGeneratorAction { // There aren't any dependencies, but we merge to combine primary resources from different // res/assets directories into a single res and single assets directory. MergedAndroidData mergedData = - resourceProcessor.mergeData( + AndroidResourceMerger.mergeData( options.mainData, ImmutableList.<DependencyAndroidData>of(), ImmutableList.<DependencyAndroidData>of(), 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 new file mode 100644 index 0000000000..49020a4563 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidManifestProcessor.java @@ -0,0 +1,312 @@ +// 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 static java.nio.charset.StandardCharsets.UTF_8; + +import com.android.builder.core.VariantType; +import com.android.manifmerger.ManifestMerger2; +import com.android.manifmerger.ManifestMerger2.Invoker; +import com.android.manifmerger.ManifestMerger2.Invoker.Feature; +import com.android.manifmerger.ManifestMerger2.MergeFailureException; +import com.android.manifmerger.ManifestMerger2.MergeType; +import com.android.manifmerger.ManifestMerger2.SystemProperty; +import com.android.manifmerger.MergingReport; +import com.android.manifmerger.MergingReport.MergedManifestKind; +import com.android.manifmerger.PlaceholderHandler; +import com.android.utils.Pair; +import com.android.utils.StdLogger; +import com.google.common.base.Function; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; + +/** Provides manifest processing oriented tools. */ +public class AndroidManifestProcessor { + private static final ImmutableMap<SystemProperty, String> SYSTEM_PROPERTY_NAMES = + Maps.toMap( + Arrays.asList(SystemProperty.values()), + new Function<SystemProperty, String>() { + @Override + public String apply(SystemProperty property) { + if (property == SystemProperty.PACKAGE) { + return "applicationId"; + } else { + return property.toCamelCase(); + } + } + }); + + /** Creates a new processor with the appropriate logger. */ + public static AndroidManifestProcessor with(StdLogger stdLogger) { + return new AndroidManifestProcessor(stdLogger); + } + + private final StdLogger stdLogger; + + private AndroidManifestProcessor(StdLogger stdLogger) { + this.stdLogger = stdLogger; + } + + /** + * Merge several manifests into one and perform placeholder substitutions. This operation uses + * Gradle semantics. + * + * @param manifest The primary manifest of the merge. + * @param mergeeManifests Manifests to be merged into {@code manifest}. + * @param mergeType Whether the merger should operate in application or library mode. + * @param values A map of strings to be used as manifest placeholders and overrides. packageName + * is the only disallowed value and will be ignored. + * @param output The path to write the resultant manifest to. + * @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 IOException if there was a problem writing the merged manifest. + */ + // TODO(corysmith): Extract manifest processing. + public Path mergeManifest( + Path manifest, + Map<Path, String> mergeeManifests, + MergeType mergeType, + Map<String, String> values, + Path output, + Path logFile) + throws IOException { + if (mergeeManifests.isEmpty() && values.isEmpty()) { + return manifest; + } + + Invoker<?> manifestMerger = ManifestMerger2.newMerger(manifest.toFile(), stdLogger, mergeType); + MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED; + if (mergeType == MergeType.APPLICATION) { + manifestMerger.withFeatures(Feature.REMOVE_TOOLS_DECLARATIONS); + } + + // Add mergee manifests + List<Pair<String, File>> libraryManifests = new ArrayList<>(); + for (Entry<Path, String> mergeeManifest : mergeeManifests.entrySet()) { + libraryManifests.add(Pair.of(mergeeManifest.getValue(), mergeeManifest.getKey().toFile())); + } + manifestMerger.addLibraryManifests(libraryManifests); + + // Extract SystemProperties from the provided values. + Map<String, Object> placeholders = new HashMap<>(); + 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))); + + // The manifest merger does not allow explicitly specifying either applicationId or + // packageName as placeholders if SystemProperty.PACKAGE is specified. It forces these + // placeholders to have the same value as specified by SystemProperty.PACKAGE. + if (property == SystemProperty.PACKAGE) { + placeholders.remove(PlaceholderHandler.APPLICATION_ID); + placeholders.remove(PlaceholderHandler.PACKAGE_NAME); + } + } + } + + // Add placeholders for all values. + // packageName is populated from either the applicationId override or from the manifest itself; + // it cannot be manually specified. + placeholders.remove(PlaceholderHandler.PACKAGE_NAME); + manifestMerger.setPlaceHolderValues(placeholders); + + try { + MergingReport mergingReport = manifestMerger.merge(); + + if (logFile != null) { + logFile.getParent().toFile().mkdirs(); + try (PrintStream stream = new PrintStream(logFile.toFile())) { + mergingReport.log(new AndroidResourceProcessor.PrintStreamLogger(stream)); + } + } + switch (mergingReport.getResult()) { + case WARNING: + mergingReport.log(stdLogger); + Files.createDirectories(output.getParent()); + writeMergedManifest(mergedManifestKind, mergingReport, output); + break; + case SUCCESS: + Files.createDirectories(output.getParent()); + writeMergedManifest(mergedManifestKind, mergingReport, output); + break; + case ERROR: + mergingReport.log(stdLogger); + throw new RuntimeException(mergingReport.getReportString()); + default: + throw new RuntimeException("Unhandled result type : " + mergingReport.getResult()); + } + } catch (MergeFailureException e) { + throw new RuntimeException(e); + } + + return output; + } + + public MergedAndroidData processManifest( + VariantType variantType, + String customPackageForR, + String applicationId, + int versionCode, + String versionName, + MergedAndroidData primaryData, + Path processedManifest) + throws IOException { + + ManifestMerger2.MergeType mergeType = + variantType == VariantType.DEFAULT + ? ManifestMerger2.MergeType.APPLICATION + : ManifestMerger2.MergeType.LIBRARY; + + String newManifestPackage = + variantType == VariantType.DEFAULT ? applicationId : customPackageForR; + + if (versionCode != -1 || versionName != null || newManifestPackage != null) { + 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); + } + + try { + 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 RuntimeException(mergingReport.getReportString()); + default: + throw new RuntimeException("Unhandled result type : " + mergingReport.getResult()); + } + } catch (IOException | MergeFailureException e) { + throw new RuntimeException(e); + } + return new MergedAndroidData( + primaryData.getResourceDir(), primaryData.getAssetDir(), processedManifest); + } + return primaryData; + } + + /** + * Overwrite the package attribute of {@code <manifest>} in an AndroidManifest.xml file. + * + * @param manifest The input manifest. + * @param customPackage The package to write to the manifest. + * @param output The output manifest to generate. + * @return The output manifest if generated or the input manifest if no overwriting is required. + */ + /* TODO(apell): switch from custom xml parsing to Gradle merger with NO_PLACEHOLDER_REPLACEMENT + * set when android common is updated to version 2.5.0. + */ + public Path writeManifestPackage(Path manifest, String customPackage, Path output) { + if (Strings.isNullOrEmpty(customPackage)) { + return manifest; + } + try { + Files.createDirectories(output.getParent()); + XMLEventReader reader = + XMLInputFactory.newInstance() + .createXMLEventReader(Files.newInputStream(manifest), UTF_8.name()); + XMLEventWriter writer = + XMLOutputFactory.newInstance() + .createXMLEventWriter(Files.newOutputStream(output), UTF_8.name()); + XMLEventFactory eventFactory = XMLEventFactory.newInstance(); + while (reader.hasNext()) { + XMLEvent event = reader.nextEvent(); + if (event.isStartElement() + && event.asStartElement().getName().toString().equalsIgnoreCase("manifest")) { + StartElement element = event.asStartElement(); + @SuppressWarnings("unchecked") + Iterator<Attribute> attributes = element.getAttributes(); + ImmutableList.Builder<Attribute> newAttributes = ImmutableList.builder(); + while (attributes.hasNext()) { + Attribute attr = attributes.next(); + if (attr.getName().toString().equalsIgnoreCase("package")) { + newAttributes.add(eventFactory.createAttribute("package", customPackage)); + } else { + newAttributes.add(attr); + } + } + writer.add( + eventFactory.createStartElement( + element.getName(), newAttributes.build().iterator(), element.getNamespaces())); + } else { + writer.add(event); + } + } + writer.flush(); + } catch (XMLStreamException | FactoryConfigurationError | IOException e) { + throw new RuntimeException(e); + } + + return output; + } + + public void writeMergedManifest( + MergedManifestKind mergedManifestKind, MergingReport mergingReport, Path manifestOut) + throws IOException { + String manifestContents = mergingReport.getMergedDocument(mergedManifestKind); + String annotatedDocument = mergingReport.getMergedDocument(MergedManifestKind.BLAME); + stdLogger.verbose(annotatedDocument); + Files.write(manifestOut, manifestContents.getBytes(UTF_8)); + } +} 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 new file mode 100644 index 0000000000..0ab4c32275 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java @@ -0,0 +1,149 @@ +// 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 com.android.annotations.Nullable; +import com.android.builder.core.VariantType; +import com.android.ide.common.internal.PngCruncher; +import com.android.ide.common.res2.MergingException; +import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** Collects all the functionationality for an action to merge resources. */ +public class AndroidResourceMerger { + static final Logger logger = Logger.getLogger(AndroidResourceProcessor.class.getName()); + + /** Merges all secondary resources with the primary resources. */ + static MergedAndroidData mergeData( + final ParsedAndroidData primary, + final Path primaryManifest, + final List<? extends SerializedAndroidData> direct, + final List<? extends SerializedAndroidData> transitive, + final Path resourcesOut, + final Path assetsOut, + @Nullable final PngCruncher cruncher, + final VariantType type, + @Nullable final Path symbolsOut, + @Nullable AndroidResourceClassWriter rclassWriter) + throws MergingException { + Stopwatch timer = Stopwatch.createStarted(); + final ListeningExecutorService executorService = + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(15)); + try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { + AndroidDataMerger merger = AndroidDataMerger.createWithPathDeduplictor(executorService); + UnwrittenMergedAndroidData merged = + merger.loadAndMerge( + transitive, direct, primary, primaryManifest, type != VariantType.LIBRARY); + logger.fine(String.format("merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + timer.reset().start(); + if (symbolsOut != null) { + AndroidDataSerializer serializer = AndroidDataSerializer.create(); + merged.serializeTo(serializer); + serializer.flushTo(symbolsOut); + logger.fine( + String.format( + "serialize merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + timer.reset().start(); + } + if (rclassWriter != null) { + merged.writeResourceClass(rclassWriter); + logger.fine( + String.format("write classes finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + timer.reset().start(); + } + AndroidDataWriter writer = + AndroidDataWriter.createWith( + resourcesOut.getParent(), resourcesOut, assetsOut, cruncher, executorService); + return merged.write(writer); + } catch (IOException e) { + throw MergingException.wrapException(e).build(); + } finally { + logger.fine( + String.format("write merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } + } + + /** + * Merges all secondary resources with the primary resources, given that the primary resources + * have not yet been parsed and serialized. + */ + public static MergedAndroidData mergeData( + final UnvalidatedAndroidData primary, + final List<? extends SerializedAndroidData> direct, + final List<? extends SerializedAndroidData> transitive, + final Path resourcesOut, + final Path assetsOut, + @Nullable final PngCruncher cruncher, + final VariantType type, + @Nullable final Path symbolsOut) + throws MergingException { + try { + final ParsedAndroidData parsedPrimary = ParsedAndroidData.from(primary); + return mergeData( + parsedPrimary, + primary.getManifest(), + direct, + transitive, + resourcesOut, + assetsOut, + cruncher, + type, + symbolsOut, + null /* rclassWriter */); + } catch (IOException e) { + throw MergingException.wrapException(e).build(); + } + } + + /** + * Merges all secondary resources with the primary resources, given that the primary resources + * have been separately parsed and serialized. + */ + public static MergedAndroidData mergeData( + final SerializedAndroidData primary, + final Path primaryManifest, + final List<? extends SerializedAndroidData> direct, + final List<? extends SerializedAndroidData> transitive, + final Path resourcesOut, + final Path assetsOut, + @Nullable final PngCruncher cruncher, + final VariantType type, + @Nullable final Path symbolsOut, + @Nullable final AndroidResourceClassWriter rclassWriter) + throws MergingException { + final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder(); + final AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + primary.deserialize(deserializer, primaryBuilder.consumers()); + ParsedAndroidData primaryData = primaryBuilder.build(); + return mergeData( + primaryData, + primaryManifest, + direct, + transitive, + resourcesOut, + assetsOut, + cruncher, + type, + symbolsOut, + rclassWriter); + } +} 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 0e2501e773..f5f49f6894 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 @@ -164,8 +164,6 @@ public class AndroidResourceMergingAction { AaptConfigOptions aaptConfigOptions = optionsParser.getOptions(AaptConfigOptions.class); Options options = optionsParser.getOptions(Options.class); - final AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(stdLogger); - Preconditions.checkNotNull(options.primaryData); Preconditions.checkNotNull(options.primaryManifest); Preconditions.checkNotNull(options.classJarOutput); @@ -189,7 +187,7 @@ public class AndroidResourceMergingAction { resourceClassWriter.setIncludeJavaFile(false); final MergedAndroidData mergedData = - resourceProcessor.mergeData( + AndroidResourceMerger.mergeData( options.primaryData, options.primaryManifest, options.directData, @@ -208,18 +206,19 @@ public class AndroidResourceMergingAction { // the manifests compatible with the old manifest merger. if (options.manifestOutput != null) { MergedAndroidData processedData = - resourceProcessor.processManifest( - packageType, - options.packageForR, - null, /* applicationId */ - -1, /* versionCode */ - null, /* versionName */ - mergedData, - processedManifest); - resourceProcessor.copyManifestToOutput(processedData, options.manifestOutput); + AndroidManifestProcessor.with(stdLogger) + .processManifest( + packageType, + options.packageForR, + null, /* applicationId */ + -1, /* versionCode */ + null, /* versionName */ + mergedData, + processedManifest); + AndroidResourceOutputs.copyManifestToOutput(processedData, options.manifestOutput); } - resourceProcessor.createClassJar(generatedSources, options.classJarOutput); + AndroidResourceOutputs.createClassJar(generatedSources, options.classJarOutput); logger.fine( String.format("Create classJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); @@ -235,11 +234,8 @@ public class AndroidResourceMergingAction { // For now, try compressing the library resources that we pass to the validator. This takes // extra CPU resources to pack and unpack (~2x), but can reduce the zip size (~4x). - resourceProcessor.createResourcesZip( - resourcesDir, - mergedData.getAssetDir(), - options.resourcesOutput, - true /* compress */); + AndroidResourceOutputs.createResourcesZip( + resourcesDir, mergedData.getAssetDir(), options.resourcesOutput, true /* compress */); logger.fine( String.format( "Create resources.zip finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); @@ -250,8 +246,6 @@ public class AndroidResourceMergingAction { } catch (Exception e) { logger.log(Level.SEVERE, "Unexpected", e); throw e; - } finally { - resourceProcessor.shutdown(); } logger.fine(String.format("Resources merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); } 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 new file mode 100644 index 0000000000..472e71f57e --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java @@ -0,0 +1,336 @@ +// 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 static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Joiner; +import com.google.common.collect.Ordering; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** Collects all the functionationality for an action to create the final output artifacts. */ +public class AndroidResourceOutputs { + + /** A FileVisitor that will add all R class files to be stored in a zip archive. */ + static final class ClassJarBuildingVisitor extends ZipBuilderVisitor { + + ClassJarBuildingVisitor(ZipOutputStream zip, Path root) { + super(zip, root, null); + } + + private byte[] manifestContent() throws IOException { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + Attributes.Name createdBy = new Attributes.Name("Created-By"); + if (attributes.getValue(createdBy) == null) { + attributes.put(createdBy, "bazel"); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + manifest.write(out); + return out.toByteArray(); + } + + @Override + protected void writeFileEntry(Path file) throws IOException { + Path filename = file.getFileName(); + String name = filename.toString(); + if (name.endsWith(".class")) { + byte[] content = Files.readAllBytes(file); + addEntry(file, content); + } + } + + void writeManifestContent() throws IOException { + addEntry(root.resolve(JarFile.MANIFEST_NAME), manifestContent()); + } + } + + /** A FileVisitor that will add all R.java files to be stored in a zip archive. */ + static final class SymbolFileSrcJarBuildingVisitor extends ZipBuilderVisitor { + + static final Pattern ID_PATTERN = + Pattern.compile("public static int ([\\w\\.]+)=0x[0-9A-fa-f]+;"); + static final Pattern INNER_CLASS = + Pattern.compile("public static class ([a-z_]*) \\{(.*?)\\}", Pattern.DOTALL); + static final Pattern PACKAGE_PATTERN = + Pattern.compile("\\s*package ([a-zA-Z_$][a-zA-Z\\d_$]*(?:\\.[a-zA-Z_$][a-zA-Z\\d_$]*)*)"); + + private final boolean staticIds; + + private SymbolFileSrcJarBuildingVisitor(ZipOutputStream zip, Path root, boolean staticIds) { + super(zip, root, null); + this.staticIds = staticIds; + } + + private String replaceIdsWithStaticIds(String contents) { + Matcher packageMatcher = PACKAGE_PATTERN.matcher(contents); + if (!packageMatcher.find()) { + return contents; + } + String pkg = packageMatcher.group(1); + StringBuffer out = new StringBuffer(); + Matcher innerClassMatcher = INNER_CLASS.matcher(contents); + while (innerClassMatcher.find()) { + String resourceType = innerClassMatcher.group(1); + Matcher idMatcher = ID_PATTERN.matcher(innerClassMatcher.group(2)); + StringBuffer resourceIds = new StringBuffer(); + while (idMatcher.find()) { + String javaId = idMatcher.group(1); + idMatcher.appendReplacement( + resourceIds, + String.format( + "public static int %s=0x%08X;", javaId, Objects.hash(pkg, resourceType, javaId))); + } + idMatcher.appendTail(resourceIds); + innerClassMatcher.appendReplacement( + out, + String.format("public static class %s {%s}", resourceType, resourceIds.toString())); + } + innerClassMatcher.appendTail(out); + return out.toString(); + } + + @Override + protected void writeFileEntry(Path file) throws IOException { + if (file.getFileName().endsWith("R.java")) { + byte[] content = Files.readAllBytes(file); + if (staticIds) { + content = + replaceIdsWithStaticIds(UTF_8.decode(ByteBuffer.wrap(content)).toString()) + .getBytes(UTF_8); + } + addEntry(file, content); + } + } + } + + /** A FileVisitor that will add all files to be stored in a zip archive. */ + static class ZipBuilderVisitor extends SimpleFileVisitor<Path> { + + // ZIP timestamps have a resolution of 2 seconds. + // see http://www.info-zip.org/FAQ.html#limits + private static final long MINIMUM_TIMESTAMP_INCREMENT = 2000L; + // The earliest date representable in a zip file, 1-1-1980 (the DOS epoch). + private static final long ZIP_EPOCH = 315561600000L; + + private final String directoryPrefix; + private final Collection<Path> paths = new ArrayList<>(); + protected final Path root; + private int storageMethod = ZipEntry.STORED; + private final ZipOutputStream zip; + + ZipBuilderVisitor(ZipOutputStream zip, Path root, String directory) { + this.zip = zip; + this.root = root; + this.directoryPrefix = directory; + } + + protected void addEntry(Path file, byte[] content) throws IOException { + String prefix = directoryPrefix != null ? (directoryPrefix + "/") : ""; + String relativeName = root.relativize(file).toString(); + ZipEntry entry = new ZipEntry(prefix + relativeName); + entry.setMethod(storageMethod); + entry.setTime(normalizeTime(relativeName)); + entry.setSize(content.length); + CRC32 crc32 = new CRC32(); + crc32.update(content); + entry.setCrc(crc32.getValue()); + + zip.putNextEntry(entry); + zip.write(content); + zip.closeEntry(); + } + + /** + * Normalize timestamps for deterministic builds. Stamp .class files to be a bit newer than + * .java files. See: {@link + * com.google.devtools.build.buildjar.jarhelper.JarHelper#normalizedTimestamp(String)} + */ + protected long normalizeTime(String filename) { + if (filename.endsWith(".class")) { + return ZIP_EPOCH + MINIMUM_TIMESTAMP_INCREMENT; + } else { + return ZIP_EPOCH; + } + } + + public void setCompress(boolean compress) { + storageMethod = compress ? ZipEntry.DEFLATED : ZipEntry.STORED; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + paths.add(file); + return FileVisitResult.CONTINUE; + } + + /** + * Iterate through collected file paths in a deterministic order and write to the zip. + * + * @throws IOException if there is an error reading from the source or writing to the zip. + */ + void writeEntries() throws IOException { + for (Path path : Ordering.natural().immutableSortedCopy(paths)) { + writeFileEntry(path); + } + } + + protected void writeFileEntry(Path file) throws IOException { + byte[] content = Files.readAllBytes(file); + addEntry(file, content); + } + } + + static final Pattern HEX_REGEX = Pattern.compile("0x[0-9A-Fa-f]{8}"); + + /** + * Copies the AndroidManifest.xml to the specified output location. + * + * @param androidData 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) { + try { + Files.createDirectories(manifestOut.getParent()); + Files.copy(androidData.getManifest(), manifestOut); + // Set to the epoch for caching purposes. + Files.setLastModifiedTime(manifestOut, FileTime.fromMillis(0L)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Copies the R.txt to the expected place. + * + * @param generatedSourceRoot The path to the generated R.txt. + * @param rOutput The Path to write the R.txt. + * @param staticIds Boolean that indicates if the ids should be set to 0x1 for caching purposes. + */ + public static void copyRToOutput(Path generatedSourceRoot, Path rOutput, boolean staticIds) { + try { + Files.createDirectories(rOutput.getParent()); + final Path source = generatedSourceRoot.resolve("R.txt"); + if (Files.exists(source)) { + if (staticIds) { + String contents = + HEX_REGEX + .matcher(Joiner.on("\n").join(Files.readAllLines(source, UTF_8))) + .replaceAll("0x1"); + Files.write(rOutput, contents.getBytes(UTF_8)); + } else { + Files.copy(source, rOutput); + } + } else { + // The R.txt wasn't generated, create one for future inheritance, as Bazel always requires + // outputs. This state occurs when there are no resource directories. + Files.createFile(rOutput); + } + // Set to the epoch for caching purposes. + Files.setLastModifiedTime(rOutput, FileTime.fromMillis(0L)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** Creates a zip archive from all found R.class (and inner class) files. */ + public static void createClassJar(Path generatedClassesRoot, Path classJar) { + try { + Files.createDirectories(classJar.getParent()); + try (final ZipOutputStream zip = + new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(classJar)))) { + ClassJarBuildingVisitor visitor = + new ClassJarBuildingVisitor(zip, generatedClassesRoot); + Files.walkFileTree(generatedClassesRoot, visitor); + visitor.writeEntries(); + visitor.writeManifestContent(); + } + // Set to the epoch for caching purposes. + Files.setLastModifiedTime(classJar, FileTime.fromMillis(0L)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates a zip file containing the provided android resources and assets. + * + * @param resourcesRoot The root containing android resources to be written. + * @param assetsRoot The root containing android assets to be written. + * @param output The path to write the zip file + * @param compress Whether or not to compress the content + * @throws IOException + */ + public static void createResourcesZip( + Path resourcesRoot, Path assetsRoot, Path output, boolean compress) throws IOException { + try (ZipOutputStream zout = + new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(output)))) { + if (Files.exists(resourcesRoot)) { + ZipBuilderVisitor visitor = + new ZipBuilderVisitor(zout, resourcesRoot, "res"); + visitor.setCompress(compress); + Files.walkFileTree(resourcesRoot, visitor); + visitor.writeEntries(); + } + if (Files.exists(assetsRoot)) { + ZipBuilderVisitor visitor = + new ZipBuilderVisitor(zout, assetsRoot, "assets"); + visitor.setCompress(compress); + Files.walkFileTree(assetsRoot, visitor); + visitor.writeEntries(); + } + } + } + + /** Creates a zip archive from all found R.java files. */ + public static void createSrcJar(Path generatedSourcesRoot, Path srcJar, boolean staticIds) { + try { + Files.createDirectories(srcJar.getParent()); + try (final ZipOutputStream zip = + new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(srcJar)))) { + SymbolFileSrcJarBuildingVisitor visitor = + new SymbolFileSrcJarBuildingVisitor( + zip, generatedSourcesRoot, staticIds); + Files.walkFileTree(generatedSourcesRoot, visitor); + visitor.writeEntries(); + } + // Set to the epoch for caching purposes. + Files.setLastModifiedTime(srcJar, FileTime.fromMillis(0L)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} 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 1001cb578f..07ddb400dc 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 @@ -245,7 +245,7 @@ public class AndroidResourceProcessingAction { .asList(); final MergedAndroidData mergedData = - resourceProcessor.mergeData( + AndroidResourceMerger.mergeData( options.primaryData, options.directData, options.transitiveData, @@ -268,18 +268,19 @@ public class AndroidResourceProcessingAction { "Density filtering finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); MergedAndroidData processedData = - resourceProcessor.processManifest( - options.packageType, - options.packageForR, - options.applicationId, - options.versionCode, - options.versionName, - filteredData, - processedManifest); + AndroidManifestProcessor.with(STD_LOGGER) + .processManifest( + options.packageType, + options.packageForR, + options.applicationId, + options.versionCode, + options.versionName, + filteredData, + processedManifest); // Write manifestOutput now before the dummy manifest is created. if (options.manifestOutput != null) { - resourceProcessor.copyManifestToOutput(processedData, options.manifestOutput); + AndroidResourceOutputs.copyManifestToOutput(processedData, options.manifestOutput); } if (options.packageType == VariantType.LIBRARY) { @@ -313,19 +314,15 @@ public class AndroidResourceProcessingAction { logger.fine(String.format("aapt finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); if (options.srcJarOutput != null) { - resourceProcessor.createSrcJar( - generatedSources, - options.srcJarOutput, - VariantType.LIBRARY == options.packageType); + AndroidResourceOutputs.createSrcJar( + generatedSources, options.srcJarOutput, VariantType.LIBRARY == options.packageType); } if (options.rOutput != null) { - resourceProcessor.copyRToOutput( - generatedSources, - options.rOutput, - VariantType.LIBRARY == options.packageType); + AndroidResourceOutputs.copyRToOutput( + generatedSources, options.rOutput, VariantType.LIBRARY == options.packageType); } if (options.resourcesOutput != null) { - resourceProcessor.createResourcesZip( + AndroidResourceOutputs.createResourcesZip( processedData.getResourceDir(), processedData.getAssetDir(), options.resourcesOutput, diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java index f1fbf03149..e33928d19c 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java @@ -28,35 +28,18 @@ import com.android.builder.model.AaptOptions; import com.android.ide.common.internal.CommandLineRunner; import com.android.ide.common.internal.ExecutorSingleton; import com.android.ide.common.internal.LoggedErrorException; -import com.android.ide.common.internal.PngCruncher; import com.android.ide.common.res2.MergingException; import com.android.io.FileWrapper; import com.android.io.StreamException; -import com.android.manifmerger.ManifestMerger2; -import com.android.manifmerger.ManifestMerger2.Invoker; -import com.android.manifmerger.ManifestMerger2.Invoker.Feature; -import com.android.manifmerger.ManifestMerger2.MergeFailureException; -import com.android.manifmerger.ManifestMerger2.MergeType; -import com.android.manifmerger.ManifestMerger2.SystemProperty; -import com.android.manifmerger.MergingReport; -import com.android.manifmerger.MergingReport.MergedManifestKind; -import com.android.manifmerger.PlaceholderHandler; import com.android.repository.Revision; import com.android.utils.ILogger; -import com.android.utils.Pair; import com.android.utils.StdLogger; import com.android.xml.AndroidManifest; -import com.google.common.base.Function; import com.google.common.base.Joiner; -import com.google.common.base.Stopwatch; -import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; -import com.google.common.collect.Maps; import com.google.common.collect.Multimap; -import com.google.common.collect.Ordering; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -70,60 +53,31 @@ import com.google.devtools.common.options.Converters.CommaSeparatedOptionListCon import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.TriState; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.PrintStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; -import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import javax.xml.stream.FactoryConfigurationError; -import javax.xml.stream.XMLEventFactory; -import javax.xml.stream.XMLEventReader; -import javax.xml.stream.XMLEventWriter; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.events.Attribute; -import javax.xml.stream.events.StartElement; -import javax.xml.stream.events.XMLEvent; import javax.xml.xpath.XPathExpressionException; /** * Provides a wrapper around the AOSP build tools for resource processing. */ public class AndroidResourceProcessor { - private static final Logger logger = Logger.getLogger(AndroidResourceProcessor.class.getName()); + static final Logger logger = Logger.getLogger(AndroidResourceProcessor.class.getName()); /** * Options class containing flags for Aapt setup. @@ -256,165 +210,12 @@ public class AndroidResourceProcessor { } } - /** Shutdowns and verifies that no tasks are running in the executor service. */ - private static final class ExecutorServiceCloser implements Closeable { - private final ListeningExecutorService executorService; - private ExecutorServiceCloser(ListeningExecutorService executorService) { - this.executorService = executorService; - } - - @Override - public void close() throws IOException { - List<Runnable> unfinishedTasks = executorService.shutdownNow(); - if (!unfinishedTasks.isEmpty()) { - throw new IOException( - "Shutting down the executor with unfinished tasks:" + unfinishedTasks); - } - } - - public static Closeable createWith(ListeningExecutorService executorService) { - return new ExecutorServiceCloser(executorService); - } - } - - private static final ImmutableMap<SystemProperty, String> SYSTEM_PROPERTY_NAMES = Maps.toMap( - Arrays.asList(SystemProperty.values()), new Function<SystemProperty, String>() { - @Override - public String apply(SystemProperty property) { - if (property == SystemProperty.PACKAGE) { - return "applicationId"; - } else { - return property.toCamelCase(); - } - } - }); - - private static final Pattern HEX_REGEX = Pattern.compile("0x[0-9A-Fa-f]{8}"); private final StdLogger stdLogger; public AndroidResourceProcessor(StdLogger stdLogger) { this.stdLogger = stdLogger; } - /** - * Copies the R.txt to the expected place. - * - * @param generatedSourceRoot The path to the generated R.txt. - * @param rOutput The Path to write the R.txt. - * @param staticIds Boolean that indicates if the ids should be set to 0x1 for caching purposes. - */ - public void copyRToOutput(Path generatedSourceRoot, Path rOutput, boolean staticIds) { - try { - Files.createDirectories(rOutput.getParent()); - final Path source = generatedSourceRoot.resolve("R.txt"); - if (Files.exists(source)) { - if (staticIds) { - String contents = - HEX_REGEX - .matcher(Joiner.on("\n").join(Files.readAllLines(source, UTF_8))) - .replaceAll("0x1"); - Files.write(rOutput, contents.getBytes(UTF_8)); - } else { - Files.copy(source, rOutput); - } - } else { - // The R.txt wasn't generated, create one for future inheritance, as Bazel always requires - // outputs. This state occurs when there are no resource directories. - Files.createFile(rOutput); - } - // Set to the epoch for caching purposes. - Files.setLastModifiedTime(rOutput, FileTime.fromMillis(0L)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Creates a zip archive from all found R.java files. - */ - public void createSrcJar(Path generatedSourcesRoot, Path srcJar, boolean staticIds) { - try { - Files.createDirectories(srcJar.getParent()); - try (final ZipOutputStream zip = new ZipOutputStream( - new BufferedOutputStream(Files.newOutputStream(srcJar)))) { - SymbolFileSrcJarBuildingVisitor visitor = - new SymbolFileSrcJarBuildingVisitor(zip, generatedSourcesRoot, staticIds); - Files.walkFileTree(generatedSourcesRoot, visitor); - visitor.writeEntries(); - } - // Set to the epoch for caching purposes. - Files.setLastModifiedTime(srcJar, FileTime.fromMillis(0L)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Creates a zip archive from all found R.class (and inner class) files. - */ - public void createClassJar(Path generatedClassesRoot, Path classJar) { - try { - Files.createDirectories(classJar.getParent()); - try (final ZipOutputStream zip = new ZipOutputStream( - new BufferedOutputStream(Files.newOutputStream(classJar)))) { - ClassJarBuildingVisitor visitor = new ClassJarBuildingVisitor(zip, generatedClassesRoot); - Files.walkFileTree(generatedClassesRoot, visitor); - visitor.writeEntries(); - visitor.writeManifestContent(); - } - // Set to the epoch for caching purposes. - Files.setLastModifiedTime(classJar, FileTime.fromMillis(0L)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Copies the AndroidManifest.xml to the specified output location. - * - * @param androidData The MergedAndroidData which contains the manifest to be written to - * manifestOut. - * @param manifestOut The Path to write the AndroidManifest.xml. - */ - public void copyManifestToOutput(MergedAndroidData androidData, Path manifestOut) { - try { - Files.createDirectories(manifestOut.getParent()); - Files.copy(androidData.getManifest(), manifestOut); - // Set to the epoch for caching purposes. - Files.setLastModifiedTime(manifestOut, FileTime.fromMillis(0L)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Creates a zip file containing the provided android resources and assets. - * - * @param resourcesRoot The root containing android resources to be written. - * @param assetsRoot The root containing android assets to be written. - * @param output The path to write the zip file - * @param compress Whether or not to compress the content - * @throws IOException - */ - public void createResourcesZip(Path resourcesRoot, Path assetsRoot, Path output, boolean compress) - throws IOException { - try (ZipOutputStream zout = new ZipOutputStream( - new BufferedOutputStream(Files.newOutputStream(output)))) { - if (Files.exists(resourcesRoot)) { - ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, resourcesRoot, "res"); - visitor.setCompress(compress); - Files.walkFileTree(resourcesRoot, visitor); - visitor.writeEntries(); - } - if (Files.exists(assetsRoot)) { - ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, assetsRoot, "assets"); - visitor.setCompress(compress); - Files.walkFileTree(assetsRoot, visitor); - visitor.writeEntries(); - } - } - } - // TODO(bazel-team): Clean up this method call -- 13 params is too many. /** Processes resources for generated sources, configs and packaging resources. */ public void processResources( @@ -847,77 +648,8 @@ public class AndroidResourceProcessor { return outputPaths.build(); } - public MergedAndroidData processManifest( - VariantType variantType, - String customPackageForR, - String applicationId, - int versionCode, - String versionName, - MergedAndroidData primaryData, - Path processedManifest) - throws IOException { - - ManifestMerger2.MergeType mergeType = - variantType == VariantType.DEFAULT - ? ManifestMerger2.MergeType.APPLICATION - : ManifestMerger2.MergeType.LIBRARY; - - String newManifestPackage = - variantType == VariantType.DEFAULT ? applicationId : customPackageForR; - - if (versionCode != -1 || versionName != null || newManifestPackage != null) { - 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); - } - - try { - 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 RuntimeException(mergingReport.getReportString()); - default: - throw new RuntimeException("Unhandled result type : " + mergingReport.getResult()); - } - } catch (IOException | MergeFailureException e) { - throw new RuntimeException(e); - } - return new MergedAndroidData( - primaryData.getResourceDir(), primaryData.getAssetDir(), processedManifest); - } - return primaryData; - } - - /** - * A logger that will print messages to a target OutputStream. - */ - private static final class PrintStreamLogger implements ILogger { + /** A logger that will print messages to a target OutputStream. */ + static final class PrintStreamLogger implements ILogger { private final PrintStream out; public PrintStreamLogger(PrintStream stream) { @@ -950,110 +682,6 @@ public class AndroidResourceProcessor { } } - /** - * Merge several manifests into one and perform placeholder substitutions. This operation uses - * Gradle semantics. - * - * @param manifest The primary manifest of the merge. - * @param mergeeManifests Manifests to be merged into {@code manifest}. - * @param mergeType Whether the merger should operate in application or library mode. - * @param values A map of strings to be used as manifest placeholders and overrides. packageName - * is the only disallowed value and will be ignored. - * @param output The path to write the resultant manifest to. - * @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 IOException if there was a problem writing the merged manifest. - */ - public Path mergeManifest( - Path manifest, - Map<Path, String> mergeeManifests, - MergeType mergeType, - Map<String, String> values, - Path output, - Path logFile) - throws IOException { - if (mergeeManifests.isEmpty() && values.isEmpty()) { - return manifest; - } - - Invoker<?> manifestMerger = ManifestMerger2.newMerger(manifest.toFile(), stdLogger, mergeType); - MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED; - if (mergeType == MergeType.APPLICATION) { - manifestMerger.withFeatures(Feature.REMOVE_TOOLS_DECLARATIONS); - } - - // Add mergee manifests - List<Pair<String, File>> libraryManifests = new ArrayList<>(); - for (Entry<Path, String> mergeeManifest : mergeeManifests.entrySet()) { - libraryManifests.add(Pair.of(mergeeManifest.getValue(), mergeeManifest.getKey().toFile())); - } - manifestMerger.addLibraryManifests(libraryManifests); - - // Extract SystemProperties from the provided values. - Map<String, Object> placeholders = new HashMap<>(); - 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))); - - // The manifest merger does not allow explicitly specifying either applicationId or - // packageName as placeholders if SystemProperty.PACKAGE is specified. It forces these - // placeholders to have the same value as specified by SystemProperty.PACKAGE. - if (property == SystemProperty.PACKAGE) { - placeholders.remove(PlaceholderHandler.APPLICATION_ID); - placeholders.remove(PlaceholderHandler.PACKAGE_NAME); - } - } - } - - // Add placeholders for all values. - // packageName is populated from either the applicationId override or from the manifest itself; - // it cannot be manually specified. - placeholders.remove(PlaceholderHandler.PACKAGE_NAME); - manifestMerger.setPlaceHolderValues(placeholders); - - try { - MergingReport mergingReport = manifestMerger.merge(); - - if (logFile != null) { - logFile.getParent().toFile().mkdirs(); - try (PrintStream stream = new PrintStream(logFile.toFile())) { - mergingReport.log(new PrintStreamLogger(stream)); - } - } - switch (mergingReport.getResult()) { - case WARNING: - mergingReport.log(stdLogger); - Files.createDirectories(output.getParent()); - writeMergedManifest(mergedManifestKind, mergingReport, output); - break; - case SUCCESS: - Files.createDirectories(output.getParent()); - writeMergedManifest(mergedManifestKind, mergingReport, output); - break; - case ERROR: - mergingReport.log(stdLogger); - throw new RuntimeException(mergingReport.getReportString()); - default: - throw new RuntimeException("Unhandled result type : " + mergingReport.getResult()); - } - } catch (MergeFailureException e) { - throw new RuntimeException(e); - } - - return output; - } - - private void writeMergedManifest( - MergedManifestKind mergedManifestKind, MergingReport mergingReport, Path manifestOut) - throws IOException { - String manifestContents = mergingReport.getMergedDocument(mergedManifestKind); - String annotatedDocument = mergingReport.getMergedDocument(MergedManifestKind.BLAME); - stdLogger.verbose(annotatedDocument); - Files.write(manifestOut, manifestContents.getBytes(UTF_8)); - } - public void writeDummyManifestForAapt(Path dummyManifest, String packageForR) throws IOException { Files.createDirectories(dummyManifest.getParent()); Files.write(dummyManifest, String.format( @@ -1064,173 +692,6 @@ public class AndroidResourceProcessor { } /** - * Overwrite the package attribute of {@code <manifest>} in an AndroidManifest.xml file. - * - * @param manifest The input manifest. - * @param customPackage The package to write to the manifest. - * @param output The output manifest to generate. - * @return The output manifest if generated or the input manifest if no overwriting is required. - */ - /* TODO(apell): switch from custom xml parsing to Gradle merger with NO_PLACEHOLDER_REPLACEMENT - * set when android common is updated to version 2.5.0. - */ - public Path writeManifestPackage(Path manifest, String customPackage, Path output) { - if (Strings.isNullOrEmpty(customPackage)) { - return manifest; - } - try { - Files.createDirectories(output.getParent()); - XMLEventReader reader = - XMLInputFactory.newInstance() - .createXMLEventReader(Files.newInputStream(manifest), UTF_8.name()); - XMLEventWriter writer = - XMLOutputFactory.newInstance() - .createXMLEventWriter(Files.newOutputStream(output), UTF_8.name()); - XMLEventFactory eventFactory = XMLEventFactory.newInstance(); - while (reader.hasNext()) { - XMLEvent event = reader.nextEvent(); - if (event.isStartElement() - && event.asStartElement().getName().toString().equalsIgnoreCase("manifest")) { - StartElement element = event.asStartElement(); - @SuppressWarnings("unchecked") - Iterator<Attribute> attributes = element.getAttributes(); - ImmutableList.Builder<Attribute> newAttributes = ImmutableList.builder(); - while (attributes.hasNext()) { - Attribute attr = attributes.next(); - if (attr.getName().toString().equalsIgnoreCase("package")) { - newAttributes.add(eventFactory.createAttribute("package", customPackage)); - } else { - newAttributes.add(attr); - } - } - writer.add( - eventFactory.createStartElement( - element.getName(), newAttributes.build().iterator(), element.getNamespaces())); - } else { - writer.add(event); - } - } - writer.flush(); - } catch (XMLStreamException | FactoryConfigurationError | IOException e) { - throw new RuntimeException(e); - } - - return output; - } - - /** - * Merges all secondary resources with the primary resources, given that the primary resources - * have not yet been parsed and serialized. - */ - public MergedAndroidData mergeData( - final UnvalidatedAndroidData primary, - final List<? extends SerializedAndroidData> direct, - final List<? extends SerializedAndroidData> transitive, - final Path resourcesOut, - final Path assetsOut, - @Nullable final PngCruncher cruncher, - final VariantType type, - @Nullable final Path symbolsOut) - throws MergingException { - try { - final ParsedAndroidData parsedPrimary = ParsedAndroidData.from(primary); - return mergeData(parsedPrimary, primary.getManifest(), direct, transitive, - resourcesOut, assetsOut, cruncher, type, symbolsOut, null /* rclassWriter */); - } catch (IOException e) { - throw MergingException.wrapException(e).build(); - } - } - - /** - * Merges all secondary resources with the primary resources, given that the primary resources - * have been separately parsed and serialized. - */ - public MergedAndroidData mergeData( - final SerializedAndroidData primary, - final Path primaryManifest, - final List<? extends SerializedAndroidData> direct, - final List<? extends SerializedAndroidData> transitive, - final Path resourcesOut, - final Path assetsOut, - @Nullable final PngCruncher cruncher, - final VariantType type, - @Nullable final Path symbolsOut, - @Nullable final AndroidResourceClassWriter rclassWriter) - throws MergingException { - final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder(); - final AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); - primary.deserialize(deserializer, primaryBuilder.consumers()); - ParsedAndroidData primaryData = primaryBuilder.build(); - return mergeData( - primaryData, - primaryManifest, - direct, - transitive, - resourcesOut, - assetsOut, - cruncher, - type, - symbolsOut, - rclassWriter); - } - - /** - * Merges all secondary resources with the primary resources. - */ - private MergedAndroidData mergeData( - final ParsedAndroidData primary, - final Path primaryManifest, - final List<? extends SerializedAndroidData> direct, - final List<? extends SerializedAndroidData> transitive, - final Path resourcesOut, - final Path assetsOut, - @Nullable final PngCruncher cruncher, - final VariantType type, - @Nullable final Path symbolsOut, - @Nullable AndroidResourceClassWriter rclassWriter) - throws MergingException { - Stopwatch timer = Stopwatch.createStarted(); - final ListeningExecutorService executorService = - MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(15)); - try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { - AndroidDataMerger merger = AndroidDataMerger.createWithPathDeduplictor(executorService); - UnwrittenMergedAndroidData merged = - merger.loadAndMerge( - transitive, - direct, - primary, - primaryManifest, - type != VariantType.LIBRARY); - logger.fine(String.format("merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); - timer.reset().start(); - if (symbolsOut != null) { - AndroidDataSerializer serializer = AndroidDataSerializer.create(); - merged.serializeTo(serializer); - serializer.flushTo(symbolsOut); - logger.fine( - String.format( - "serialize merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); - timer.reset().start(); - } - if (rclassWriter != null) { - merged.writeResourceClass(rclassWriter); - logger.fine( - String.format("write classes finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); - timer.reset().start(); - } - AndroidDataWriter writer = - AndroidDataWriter.createWith( - resourcesOut.getParent(), resourcesOut, assetsOut, cruncher, executorService); - return merged.write(writer); - } catch (IOException e) { - throw MergingException.wrapException(e).build(); - } finally { - logger.fine( - String.format("write merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); - } - } - - /** * Shutdown AOSP utilized thread-pool. */ public void shutdown() { @@ -1269,183 +730,6 @@ public class AndroidResourceProcessor { return deserializedDataBuilder.build(); } - /** - * A FileVisitor that will add all files to be stored in a zip archive. - */ - private static class ZipBuilderVisitor extends SimpleFileVisitor<Path> { - - // The earliest date representable in a zip file, 1-1-1980 (the DOS epoch). - private static final long ZIP_EPOCH = 315561600000L; - // ZIP timestamps have a resolution of 2 seconds. - // see http://www.info-zip.org/FAQ.html#limits - private static final long MINIMUM_TIMESTAMP_INCREMENT = 2000L; - - private final ZipOutputStream zip; - protected final Path root; - private final String directoryPrefix; - private int storageMethod = ZipEntry.STORED; - private final Collection<Path> paths = new ArrayList<>(); - - ZipBuilderVisitor(ZipOutputStream zip, Path root, String directory) { - this.zip = zip; - this.root = root; - this.directoryPrefix = directory; - } - - public void setCompress(boolean compress) { - storageMethod = compress ? ZipEntry.DEFLATED : ZipEntry.STORED; - } - - /** - * Iterate through collected file paths in a deterministic order and write to the zip. - * - * @throws IOException if there is an error reading from the source or writing to the zip. - */ - void writeEntries() throws IOException { - for (Path path : Ordering.natural().immutableSortedCopy(paths)) { - writeFileEntry(path); - } - } - - /** - * Normalize timestamps for deterministic builds. Stamp .class files to be a bit newer - * than .java files. See: - * {@link com.google.devtools.build.buildjar.jarhelper.JarHelper#normalizedTimestamp(String)} - */ - protected long normalizeTime(String filename) { - if (filename.endsWith(".class")) { - return ZIP_EPOCH + MINIMUM_TIMESTAMP_INCREMENT; - } else { - return ZIP_EPOCH; - } - } - - protected void addEntry(Path file, byte[] content) throws IOException { - String prefix = directoryPrefix != null ? (directoryPrefix + "/") : ""; - String relativeName = root.relativize(file).toString(); - ZipEntry entry = new ZipEntry(prefix + relativeName); - entry.setMethod(storageMethod); - entry.setTime(normalizeTime(relativeName)); - entry.setSize(content.length); - CRC32 crc32 = new CRC32(); - crc32.update(content); - entry.setCrc(crc32.getValue()); - - zip.putNextEntry(entry); - zip.write(content); - zip.closeEntry(); - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - paths.add(file); - return FileVisitResult.CONTINUE; - } - - protected void writeFileEntry(Path file) throws IOException { - byte[] content = Files.readAllBytes(file); - addEntry(file, content); - } - } - - /** - * A FileVisitor that will add all R.java files to be stored in a zip archive. - */ - private static final class SymbolFileSrcJarBuildingVisitor extends ZipBuilderVisitor { - - static final Pattern PACKAGE_PATTERN = - Pattern.compile("\\s*package ([a-zA-Z_$][a-zA-Z\\d_$]*(?:\\.[a-zA-Z_$][a-zA-Z\\d_$]*)*)"); - static final Pattern ID_PATTERN = - Pattern.compile("public static int ([\\w\\.]+)=0x[0-9A-fa-f]+;"); - static final Pattern INNER_CLASS = - Pattern.compile("public static class ([a-z_]*) \\{(.*?)\\}", Pattern.DOTALL); - - private final boolean staticIds; - - private SymbolFileSrcJarBuildingVisitor(ZipOutputStream zip, Path root, boolean staticIds) { - super(zip, root, null); - this.staticIds = staticIds; - } - - private String replaceIdsWithStaticIds(String contents) { - Matcher packageMatcher = PACKAGE_PATTERN.matcher(contents); - if (!packageMatcher.find()) { - return contents; - } - String pkg = packageMatcher.group(1); - StringBuffer out = new StringBuffer(); - Matcher innerClassMatcher = INNER_CLASS.matcher(contents); - while (innerClassMatcher.find()) { - String resourceType = innerClassMatcher.group(1); - Matcher idMatcher = ID_PATTERN.matcher(innerClassMatcher.group(2)); - StringBuffer resourceIds = new StringBuffer(); - while (idMatcher.find()) { - String javaId = idMatcher.group(1); - idMatcher.appendReplacement( - resourceIds, - String.format( - "public static int %s=0x%08X;", javaId, Objects.hash(pkg, resourceType, javaId))); - } - idMatcher.appendTail(resourceIds); - innerClassMatcher.appendReplacement( - out, - String.format("public static class %s {%s}", resourceType, resourceIds.toString())); - } - innerClassMatcher.appendTail(out); - return out.toString(); - } - - @Override - protected void writeFileEntry(Path file) throws IOException { - if (file.getFileName().endsWith("R.java")) { - byte[] content = Files.readAllBytes(file); - if (staticIds) { - content = - replaceIdsWithStaticIds(UTF_8.decode(ByteBuffer.wrap(content)).toString()) - .getBytes(UTF_8); - } - addEntry(file, content); - } - } - } - - /** - * A FileVisitor that will add all R class files to be stored in a zip archive. - */ - private static final class ClassJarBuildingVisitor extends ZipBuilderVisitor { - - ClassJarBuildingVisitor(ZipOutputStream zip, Path root) { - super(zip, root, null); - } - - @Override - protected void writeFileEntry(Path file) throws IOException { - Path filename = file.getFileName(); - String name = filename.toString(); - if (name.endsWith(".class")) { - byte[] content = Files.readAllBytes(file); - addEntry(file, content); - } - } - - private byte[] manifestContent() throws IOException { - Manifest manifest = new Manifest(); - Attributes attributes = manifest.getMainAttributes(); - attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); - Attributes.Name createdBy = new Attributes.Name("Created-By"); - if (attributes.getValue(createdBy) == null) { - attributes.put(createdBy, "bazel"); - } - ByteArrayOutputStream out = new ByteArrayOutputStream(); - manifest.write(out); - return out.toByteArray(); - } - - void writeManifestContent() throws IOException { - addEntry(root.resolve(JarFile.MANIFEST_NAME), manifestContent()); - } - } - /** Task to deserialize resources from a path. */ private static final class Deserialize implements Callable<Boolean> { diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceValidatorAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceValidatorAction.java index f05012ba24..40ca01c819 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceValidatorAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceValidatorAction.java @@ -145,10 +145,10 @@ public class AndroidResourceValidatorAction { null /* publicResourcesOut */); logger.fine(String.format("aapt finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); - resourceProcessor.copyRToOutput( + AndroidResourceOutputs.copyRToOutput( generatedSources, options.rOutput, VariantType.LIBRARY == packageType); - resourceProcessor.createSrcJar( + AndroidResourceOutputs.createSrcJar( generatedSources, options.srcJarOutput, VariantType.LIBRARY == packageType); } catch (Exception e) { logger.log(java.util.logging.Level.SEVERE, "Unexpected", e); 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 new file mode 100644 index 0000000000..374c8a5db0 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/ExecutorServiceCloser.java @@ -0,0 +1,40 @@ +// 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 com.google.common.util.concurrent.ListeningExecutorService; +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +/** Shutdowns and verifies that no tasks are running in the executor service. */ +final class ExecutorServiceCloser implements Closeable { + private final ListeningExecutorService executorService; + + private ExecutorServiceCloser(ListeningExecutorService executorService) { + this.executorService = executorService; + } + + @Override + public void close() throws IOException { + List<Runnable> unfinishedTasks = executorService.shutdownNow(); + if (!unfinishedTasks.isEmpty()) { + throw new IOException("Shutting down the executor with unfinished tasks:" + unfinishedTasks); + } + } + + public static Closeable createWith(ListeningExecutorService executorService) { + return new ExecutorServiceCloser(executorService); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/LibraryRClassGeneratorAction.java b/src/tools/android/java/com/google/devtools/build/android/LibraryRClassGeneratorAction.java index 59a200443f..51c771946b 100644 --- a/src/tools/android/java/com/google/devtools/build/android/LibraryRClassGeneratorAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/LibraryRClassGeneratorAction.java @@ -106,7 +106,7 @@ public class LibraryRClassGeneratorAction { logger.fine( String.format("R writing finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); - resourceProcessor.createClassJar(scopedTmp.getPath(), options.classJarOutput); + AndroidResourceOutputs.createClassJar(scopedTmp.getPath(), options.classJarOutput); logger.fine( String.format( "Creating class jar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); 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 09c3b92946..3e366e1625 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 @@ -156,10 +156,9 @@ public class ManifestMergerAction { optionsParser.parseAndExitUponError(args); options = optionsParser.getOptions(Options.class); - final AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(stdLogger); - try { Path mergedManifest; + AndroidManifestProcessor manifestProcessor = AndroidManifestProcessor.with(stdLogger); if (options.mergeType == MergeType.APPLICATION) { // Remove uses-permission tags from mergees before the merge. Path tmp = Files.createTempDirectory("manifest_merge_tmp"); @@ -172,17 +171,19 @@ public class ManifestMergerAction { } // Ignore custom package at the binary level. - mergedManifest = resourceProcessor.mergeManifest( - options.manifest, - mergeeManifests.build(), - options.mergeType, - options.manifestValues, - options.manifestOutput, - options.log); + mergedManifest = + manifestProcessor.mergeManifest( + options.manifest, + mergeeManifests.build(), + options.mergeType, + options.manifestValues, + options.manifestOutput, + options.log); } else { // Only need to stamp custom package into the library level. - mergedManifest = resourceProcessor.writeManifestPackage( - options.manifest, options.customPackage, options.manifestOutput); + mergedManifest = + manifestProcessor.writeManifestPackage( + options.manifest, options.customPackage, options.manifestOutput); } if (!mergedManifest.equals(options.manifestOutput)) { @@ -194,8 +195,6 @@ public class ManifestMergerAction { } catch (IOException e) { logger.log(SEVERE, "Error during merging manifests", e); throw e; - } finally { - resourceProcessor.shutdown(); } } } diff --git a/src/tools/android/java/com/google/devtools/build/android/RClassGeneratorAction.java b/src/tools/android/java/com/google/devtools/build/android/RClassGeneratorAction.java index 18baf65393..1e97859311 100644 --- a/src/tools/android/java/com/google/devtools/build/android/RClassGeneratorAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/RClassGeneratorAction.java @@ -150,7 +150,7 @@ public class RClassGeneratorAction { } // We write .class files to temp, then jar them up after (we create a dummy jar, even if // there are no class files). - resourceProcessor.createClassJar(classOutPath, options.classJarOutput); + AndroidResourceOutputs.createClassJar(classOutPath, options.classJarOutput); logger.fine( String.format("createClassJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); } finally { diff --git a/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java b/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java index 76b5433702..ece53c6e03 100644 --- a/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java @@ -255,14 +255,15 @@ public class ResourceShrinkerAction { null /* publicResourcesOut */, null /* dataBindingInfoOut */); if (options.shrunkResources != null) { - resourceProcessor.createResourcesZip(shrunkResources, resourceFiles.resolve("assets"), - options.shrunkResources, false /* compress */); + AndroidResourceOutputs.createResourcesZip( + shrunkResources, + resourceFiles.resolve("assets"), + options.shrunkResources, + false /* compress */); } if (options.rTxtOutput != null) { - resourceProcessor.copyRToOutput( - generatedSources, - options.rTxtOutput, - options.packageType == VariantType.LIBRARY); + AndroidResourceOutputs.copyRToOutput( + generatedSources, options.rTxtOutput, options.packageType == VariantType.LIBRARY); } logger.fine(String.format("Packing resources finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); |