| /* |
| * Copyright 2012-present Facebook, Inc. |
| * |
| * 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.facebook.buck.android; |
| |
| import static com.facebook.buck.rules.BuildableProperties.Kind.ANDROID; |
| import static com.facebook.buck.rules.BuildableProperties.Kind.PACKAGING; |
| |
| import com.facebook.buck.android.FilterResourcesStep.ResourceFilter; |
| import com.facebook.buck.android.ResourcesFilter.ResourceCompressionMode; |
| import com.facebook.buck.java.AccumulateClassNamesStep; |
| import com.facebook.buck.java.Classpaths; |
| import com.facebook.buck.java.HasClasspathEntries; |
| import com.facebook.buck.java.JavaLibrary; |
| import com.facebook.buck.java.Keystore; |
| import com.facebook.buck.model.BuildTarget; |
| import com.facebook.buck.model.BuildTargets; |
| import com.facebook.buck.rules.AbiRule; |
| import com.facebook.buck.rules.AbstractBuildRule; |
| import com.facebook.buck.rules.BuildContext; |
| import com.facebook.buck.rules.BuildRule; |
| import com.facebook.buck.rules.BuildRuleParams; |
| import com.facebook.buck.rules.BuildableContext; |
| import com.facebook.buck.rules.BuildableProperties; |
| import com.facebook.buck.rules.ExopackageInfo; |
| import com.facebook.buck.rules.ImmutableExopackageInfo; |
| import com.facebook.buck.rules.ImmutableSha1HashCode; |
| import com.facebook.buck.rules.InstallableApk; |
| import com.facebook.buck.rules.RuleKey; |
| import com.facebook.buck.rules.Sha1HashCode; |
| import com.facebook.buck.rules.SourcePath; |
| import com.facebook.buck.rules.SourcePathResolver; |
| import com.facebook.buck.shell.AbstractGenruleStep; |
| import com.facebook.buck.shell.EchoStep; |
| import com.facebook.buck.shell.SymlinkFilesIntoDirectoryStep; |
| import com.facebook.buck.step.AbstractExecutionStep; |
| import com.facebook.buck.step.ExecutionContext; |
| import com.facebook.buck.step.Step; |
| import com.facebook.buck.step.fs.MakeCleanDirectoryStep; |
| import com.facebook.buck.step.fs.MkdirStep; |
| import com.facebook.buck.util.Optionals; |
| import com.facebook.buck.zip.RepackZipEntriesStep; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Function; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Supplier; |
| import com.google.common.base.Suppliers; |
| import com.google.common.collect.FluentIterable; |
| import com.google.common.collect.ImmutableCollection; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSetMultimap; |
| import com.google.common.collect.ImmutableSortedMap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Multimap; |
| import com.google.common.hash.HashCode; |
| import com.google.common.io.Files; |
| |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.EnumSet; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * <pre> |
| * android_binary( |
| * name = 'messenger', |
| * manifest = 'AndroidManifest.xml', |
| * deps = [ |
| * '//src/com/facebook/messenger:messenger_library', |
| * ], |
| * ) |
| * </pre> |
| */ |
| public class AndroidBinary extends AbstractBuildRule implements |
| AbiRule, HasClasspathEntries, InstallableApk { |
| |
| private static final BuildableProperties PROPERTIES = new BuildableProperties(ANDROID, PACKAGING); |
| |
| /** |
| * This is the path from the root of the APK that should contain the metadata.txt and |
| * secondary-N.dex.jar files for secondary dexes. |
| */ |
| static final String SECONDARY_DEX_SUBDIR = "assets/secondary-program-dex-jars"; |
| |
| private final Optional<Path> proguardJarOverride; |
| private final String proguardMaxHeapSize; |
| |
| /** |
| * This list of package types is taken from the set of targets that the default build.xml provides |
| * for Android projects. |
| * <p> |
| * Note: not all package types are supported. If unsupported, will be treated as "DEBUG". |
| */ |
| static enum PackageType { |
| DEBUG, |
| INSTRUMENTED, |
| RELEASE, |
| TEST, |
| ; |
| |
| /** |
| * @return true if ProGuard should be used to obfuscate the output |
| */ |
| boolean isBuildWithObfuscation() { |
| return this == RELEASE; |
| } |
| |
| final boolean isCrunchPngFiles() { |
| return this == RELEASE; |
| } |
| } |
| |
| public static enum TargetCpuType { |
| ARM, |
| ARMV7, |
| X86, |
| MIPS, |
| } |
| |
| static enum ExopackageMode { |
| SECONDARY_DEX(1), |
| NATIVE_LIBRARY(2), |
| ; |
| |
| private final int code; |
| |
| private ExopackageMode(int code) { |
| this.code = code; |
| } |
| |
| public static boolean enabledForSecondaryDexes(EnumSet<ExopackageMode> modes) { |
| return modes.contains(SECONDARY_DEX); |
| } |
| |
| public static boolean enabledForNativeLibraries(EnumSet<ExopackageMode> modes) { |
| return modes.contains(NATIVE_LIBRARY); |
| } |
| |
| public static int toBitmask(EnumSet<ExopackageMode> modes) { |
| int bitmask = 0; |
| for (ExopackageMode mode : modes) { |
| bitmask |= mode.code; |
| } |
| return bitmask; |
| } |
| } |
| |
| private final SourcePath manifest; |
| private final Keystore keystore; |
| private final PackageType packageType; |
| private DexSplitMode dexSplitMode; |
| private final ImmutableSet<BuildTarget> buildTargetsToExcludeFromDex; |
| private final ProGuardObfuscateStep.SdkProguardType sdkProguardConfig; |
| private final Optional<Integer> optimizationPasses; |
| private final Optional<SourcePath> proguardConfig; |
| private final ResourceCompressionMode resourceCompressionMode; |
| private final ImmutableSet<TargetCpuType> cpuFilters; |
| private final ResourceFilter resourceFilter; |
| private final Path primaryDexPath; |
| private final EnumSet<ExopackageMode> exopackageModes; |
| private final ImmutableSortedSet<BuildRule> preprocessJavaClassesDeps; |
| private final Function<String, String> macroExpander; |
| private final Optional<String> preprocessJavaClassesBash; |
| protected final ImmutableSortedSet<JavaLibrary> rulesToExcludeFromDex; |
| protected final AndroidGraphEnhancementResult enhancementResult; |
| |
| AndroidBinary( |
| BuildRuleParams params, |
| SourcePathResolver resolver, |
| Optional<Path> proguardJarOverride, |
| String proguardMaxHeapSize, |
| SourcePath manifest, |
| Keystore keystore, |
| PackageType packageType, |
| DexSplitMode dexSplitMode, |
| Set<BuildTarget> buildTargetsToExcludeFromDex, |
| ProGuardObfuscateStep.SdkProguardType sdkProguardConfig, |
| Optional<Integer> proguardOptimizationPasses, |
| Optional<SourcePath> proguardConfig, |
| ResourceCompressionMode resourceCompressionMode, |
| Set<TargetCpuType> cpuFilters, |
| ResourceFilter resourceFilter, |
| EnumSet<ExopackageMode> exopackageModes, |
| Set<BuildRule> preprocessJavaClassesDeps, |
| Function<String, String> macroExpander, |
| Optional<String> preprocessJavaClassesBash, |
| ImmutableSortedSet<JavaLibrary> rulesToExcludeFromDex, |
| AndroidGraphEnhancementResult enhancementResult) { |
| super(params, resolver); |
| this.proguardJarOverride = proguardJarOverride; |
| this.proguardMaxHeapSize = proguardMaxHeapSize; |
| this.manifest = manifest; |
| this.keystore = keystore; |
| this.packageType = packageType; |
| this.dexSplitMode = dexSplitMode; |
| this.buildTargetsToExcludeFromDex = ImmutableSet.copyOf(buildTargetsToExcludeFromDex); |
| this.sdkProguardConfig = sdkProguardConfig; |
| this.optimizationPasses = proguardOptimizationPasses; |
| this.proguardConfig = proguardConfig; |
| this.resourceCompressionMode = resourceCompressionMode; |
| this.cpuFilters = ImmutableSet.copyOf(cpuFilters); |
| this.resourceFilter = resourceFilter; |
| this.exopackageModes = exopackageModes; |
| this.preprocessJavaClassesDeps = ImmutableSortedSet.copyOf(preprocessJavaClassesDeps); |
| this.macroExpander = macroExpander; |
| this.preprocessJavaClassesBash = preprocessJavaClassesBash; |
| this.rulesToExcludeFromDex = rulesToExcludeFromDex; |
| this.enhancementResult = enhancementResult; |
| this.primaryDexPath = getPrimaryDexPath(params.getBuildTarget()); |
| |
| if (ExopackageMode.enabledForSecondaryDexes(exopackageModes)) { |
| Preconditions.checkArgument(enhancementResult.getPreDexMerge().isPresent(), |
| "%s specified exopackage without pre-dexing, which is invalid.", |
| getBuildTarget()); |
| Preconditions.checkArgument(dexSplitMode.getDexStore() == DexStore.JAR, |
| "%s specified exopackage with secondary dex mode %s, " + |
| "which is invalid. (Only JAR is allowed.)", |
| getBuildTarget(), dexSplitMode.getDexStore()); |
| Preconditions.checkArgument(enhancementResult.getComputeExopackageDepsAbi().isPresent(), |
| "computeExopackageDepsAbi must be set if exopackage is true."); |
| } |
| } |
| |
| public static Path getPrimaryDexPath(BuildTarget buildTarget) { |
| return BuildTargets.getBinPath(buildTarget, ".dex/%s/classes.dex"); |
| } |
| |
| @Override |
| public BuildableProperties getProperties() { |
| return PROPERTIES; |
| } |
| |
| @Override |
| public RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) { |
| builder |
| .setReflectively("keystore", keystore.getBuildTarget()) |
| .setReflectively("packageType", packageType) |
| .setReflectively("sdkProguardConfig", sdkProguardConfig) |
| .setReflectively("optimizationPasses", optimizationPasses) |
| .setReflectively("resourceCompressionMode", resourceCompressionMode) |
| .setReflectively("cpuFilters", ImmutableSortedSet.copyOf(cpuFilters)) |
| .setReflectively("exopackageModes", exopackageModes) |
| .setReflectively("preprocessJavaClassesBash", preprocessJavaClassesBash) |
| .setReflectively("preprocessJavaClassesDeps", preprocessJavaClassesDeps) |
| .setReflectively("proguardJarOverride", proguardJarOverride); |
| |
| for (JavaLibrary library : rulesToExcludeFromDex) { |
| library.appendDetailsToRuleKey(builder); |
| } |
| |
| return dexSplitMode.appendToRuleKey(builder, "dexSplitMode"); |
| } |
| |
| public ImmutableSortedSet<JavaLibrary> getRulesToExcludeFromDex() { |
| return rulesToExcludeFromDex; |
| } |
| |
| public Set<BuildTarget> getBuildTargetsToExcludeFromDex() { |
| return buildTargetsToExcludeFromDex; |
| } |
| |
| public Optional<SourcePath> getProguardConfig() { |
| return proguardConfig; |
| } |
| |
| private boolean isCompressResources(){ |
| return resourceCompressionMode.isCompressResources(); |
| } |
| |
| public ResourceCompressionMode getResourceCompressionMode() { |
| return resourceCompressionMode; |
| } |
| |
| public ImmutableSet<TargetCpuType> getCpuFilters() { |
| return this.cpuFilters; |
| } |
| |
| public ResourceFilter getResourceFilter() { |
| return resourceFilter; |
| } |
| |
| public Function<String, String> getMacroExpander() { |
| return macroExpander; |
| } |
| |
| ProGuardObfuscateStep.SdkProguardType getSdkProguardConfig() { |
| return sdkProguardConfig; |
| } |
| |
| public Optional<Integer> getOptimizationPasses() { |
| return optimizationPasses; |
| } |
| |
| @VisibleForTesting |
| AndroidGraphEnhancementResult getEnhancementResult() { |
| return enhancementResult; |
| } |
| |
| /** The APK at this path is the final one that points to an APK that a user should install. */ |
| @Override |
| public Path getApkPath() { |
| return Paths.get(getUnsignedApkPath().replaceAll("\\.unsigned\\.apk$", ".apk")); |
| } |
| |
| @Override |
| public Path getPathToOutputFile() { |
| return getApkPath(); |
| } |
| |
| @Override |
| public ImmutableCollection<Path> getInputsToCompareToOutput() { |
| ImmutableList.Builder<SourcePath> sourcePaths = ImmutableList.builder(); |
| sourcePaths.add(manifest); |
| |
| Optionals.addIfPresent(proguardConfig, sourcePaths); |
| sourcePaths.addAll(dexSplitMode.getSourcePaths()); |
| |
| return getResolver().filterInputsToCompareToOutput(sourcePaths.build()); |
| } |
| |
| @Override |
| public ImmutableList<Step> getBuildSteps( |
| BuildContext context, |
| BuildableContext buildableContext) { |
| |
| ImmutableList.Builder<Step> steps = ImmutableList.builder(); |
| |
| // Create the .dex files if we aren't doing pre-dexing. |
| Path signedApkPath = getSignedApkPath(); |
| DexFilesInfo dexFilesInfo = addFinalDxSteps(context, buildableContext, steps); |
| |
| //// |
| // BE VERY CAREFUL adding any code below here. |
| // Any inputs to apkbuilder must be reflected in the hash returned by getAbiKeyForDeps. |
| //// |
| |
| AndroidPackageableCollection packageableCollection = |
| enhancementResult.getPackageableCollection(); |
| ImmutableSet<Path> nativeLibraryDirectories = ImmutableSet.of(); |
| if (!ExopackageMode.enabledForNativeLibraries(exopackageModes) && |
| enhancementResult.getCopyNativeLibraries().isPresent()) { |
| nativeLibraryDirectories = ImmutableSet.of( |
| enhancementResult.getCopyNativeLibraries().get().getPathToNativeLibsDir()); |
| } |
| |
| // Copy the transitive closure of native-libs-as-assets to a single directory, if any. |
| ImmutableSet<Path> nativeLibraryAsAssetDirectories; |
| if (!packageableCollection.getNativeLibAssetsDirectories().isEmpty()) { |
| Path pathForNativeLibsAsAssets = getPathForNativeLibsAsAssets(); |
| Path libSubdirectory = pathForNativeLibsAsAssets.resolve("assets").resolve("lib"); |
| steps.add(new MakeCleanDirectoryStep(libSubdirectory)); |
| for (Path nativeLibDir : packageableCollection.getNativeLibAssetsDirectories()) { |
| CopyNativeLibraries.copyNativeLibrary(nativeLibDir, libSubdirectory, cpuFilters, steps); |
| } |
| nativeLibraryAsAssetDirectories = ImmutableSet.of(pathForNativeLibsAsAssets); |
| } else { |
| nativeLibraryAsAssetDirectories = ImmutableSet.of(); |
| } |
| |
| // If non-english strings are to be stored as assets, pass them to ApkBuilder. |
| ImmutableSet.Builder<Path> zipFiles = ImmutableSet.builder(); |
| Optional<PackageStringAssets> packageStringAssets = enhancementResult.getPackageStringAssets(); |
| if (packageStringAssets.isPresent()) { |
| final Path pathToStringAssetsZip = packageStringAssets.get().getPathToStringAssetsZip(); |
| zipFiles.add(pathToStringAssetsZip); |
| } |
| |
| ImmutableSet<Path> allAssetDirectories = ImmutableSet.<Path>builder() |
| .addAll(nativeLibraryAsAssetDirectories) |
| .addAll(dexFilesInfo.secondaryDexDirs) |
| .build(); |
| |
| ApkBuilderStep apkBuilderCommand = new ApkBuilderStep( |
| enhancementResult.getAaptPackageResources().getResourceApkPath(), |
| getSignedApkPath(), |
| dexFilesInfo.primaryDexPath, |
| allAssetDirectories, |
| nativeLibraryDirectories, |
| zipFiles.build(), |
| ImmutableSortedSet.copyOf(packageableCollection.getPathsToThirdPartyJars()), |
| keystore.getPathToStore(), |
| keystore.getPathToPropertiesFile(), |
| /* debugMode */ false); |
| steps.add(apkBuilderCommand); |
| |
| |
| Path apkToAlign; |
| // Optionally, compress the resources file in the .apk. |
| if (this.isCompressResources()) { |
| Path compressedApkPath = getCompressedResourcesApkPath(); |
| apkToAlign = compressedApkPath; |
| RepackZipEntriesStep arscComp = new RepackZipEntriesStep( |
| signedApkPath, |
| compressedApkPath, |
| ImmutableSet.of("resources.arsc")); |
| steps.add(arscComp); |
| } else { |
| apkToAlign = signedApkPath; |
| } |
| |
| Path apkPath = getApkPath(); |
| ZipalignStep zipalign = new ZipalignStep(apkToAlign, apkPath); |
| steps.add(zipalign); |
| |
| // Inform the user where the APK can be found. |
| EchoStep success = new EchoStep( |
| String.format("built APK for %s at %s", |
| getBuildTarget().getFullyQualifiedName(), |
| apkPath)); |
| steps.add(success); |
| |
| buildableContext.recordArtifact(getApkPath()); |
| return steps.build(); |
| } |
| |
| @Override |
| public Sha1HashCode getAbiKeyForDeps() { |
| // For non-exopackages, there is no benefit to the ABI optimization, so we want to disable it. |
| // Returning our RuleKey has this effect because we will never get an ABI match after a |
| // RuleKey miss. |
| if (exopackageModes.isEmpty()) { |
| return ImmutableSha1HashCode.of(getRuleKey().toString()); |
| } |
| |
| return enhancementResult.getComputeExopackageDepsAbi().get().getAndroidBinaryAbiHash(); |
| } |
| |
| /** |
| * Adds steps to do the final dexing or dex merging before building the apk. |
| */ |
| private DexFilesInfo addFinalDxSteps( |
| BuildContext context, |
| BuildableContext buildableContext, |
| ImmutableList.Builder<Step> steps) { |
| AndroidPackageableCollection packageableCollection = |
| enhancementResult.getPackageableCollection(); |
| // Execute preprocess_java_classes_binary, if appropriate. |
| ImmutableSet<Path> classpathEntriesToDex; |
| if (preprocessJavaClassesBash.isPresent()) { |
| // Symlink everything in dexTransitiveDependencies.classpathEntriesToDex to the input |
| // directory. Expect parallel outputs in the output directory and update classpathEntriesToDex |
| // to reflect that. |
| final Path preprocessJavaClassesInDir = getBinPath("java_classes_preprocess_in_%s"); |
| final Path preprocessJavaClassesOutDir = getBinPath("java_classes_preprocess_out_%s"); |
| steps.add(new MakeCleanDirectoryStep(preprocessJavaClassesInDir)); |
| steps.add(new MakeCleanDirectoryStep(preprocessJavaClassesOutDir)); |
| steps.add(new SymlinkFilesIntoDirectoryStep( |
| context.getProjectRoot(), |
| enhancementResult.getClasspathEntriesToDex(), |
| preprocessJavaClassesInDir)); |
| classpathEntriesToDex = FluentIterable.from(enhancementResult.getClasspathEntriesToDex()) |
| .transform(new Function<Path, Path>() { |
| @Override |
| public Path apply(Path classpathEntry) { |
| return preprocessJavaClassesOutDir.resolve(classpathEntry); |
| } |
| }) |
| .toSet(); |
| |
| AbstractGenruleStep.CommandString commandString = new AbstractGenruleStep.CommandString( |
| /* cmd */ Optional.<String>absent(), |
| /* bash */ preprocessJavaClassesBash.transform(macroExpander), |
| /* cmdExe */ Optional.<String>absent()); |
| steps.add(new AbstractGenruleStep( |
| this.getBuildTarget(), |
| commandString, |
| context.getProjectRoot().resolve(preprocessJavaClassesInDir).toFile()) { |
| |
| @Override |
| protected void addEnvironmentVariables( |
| ExecutionContext context, |
| ImmutableMap.Builder<String, String> environmentVariablesBuilder) { |
| Function<Path, Path> absolutifier = context.getProjectFilesystem().getAbsolutifier(); |
| environmentVariablesBuilder.put( |
| "IN_JARS_DIR", absolutifier.apply(preprocessJavaClassesInDir).toString()); |
| environmentVariablesBuilder.put( |
| "OUT_JARS_DIR", absolutifier.apply(preprocessJavaClassesOutDir).toString()); |
| |
| AndroidPlatformTarget platformTarget = context.getAndroidPlatformTarget(); |
| String bootclasspath = Joiner.on(':').join( |
| Iterables.transform( |
| platformTarget.getBootclasspathEntries(), |
| absolutifier)); |
| |
| environmentVariablesBuilder.put("ANDROID_BOOTCLASSPATH", bootclasspath); |
| } |
| }); |
| |
| } else { |
| classpathEntriesToDex = enhancementResult.getClasspathEntriesToDex(); |
| } |
| |
| // Execute proguard if desired (transforms input classpaths). |
| if (packageType.isBuildWithObfuscation()) { |
| classpathEntriesToDex = addProguardCommands( |
| classpathEntriesToDex, |
| packageableCollection.getProguardConfigs(), |
| steps, |
| buildableContext); |
| } |
| |
| Supplier<Map<String, HashCode>> classNamesToHashesSupplier; |
| boolean classFilesHaveChanged = preprocessJavaClassesBash.isPresent() || |
| packageType.isBuildWithObfuscation(); |
| |
| if (classFilesHaveChanged) { |
| classNamesToHashesSupplier = addAccumulateClassNamesStep(classpathEntriesToDex, steps); |
| } else { |
| classNamesToHashesSupplier = packageableCollection.getClassNamesToHashesSupplier(); |
| } |
| |
| // Create the final DEX (or set of DEX files in the case of split dex). |
| // The APK building command needs to take a directory of raw files, so primaryDexPath |
| // can only contain .dex files from this build rule. |
| |
| // Create dex artifacts. If split-dex is used, the assets/ directory should contain entries |
| // that look something like the following: |
| // |
| // assets/secondary-program-dex-jars/metadata.txt |
| // assets/secondary-program-dex-jars/secondary-1.dex.jar |
| // assets/secondary-program-dex-jars/secondary-2.dex.jar |
| // assets/secondary-program-dex-jars/secondary-3.dex.jar |
| // |
| // The contents of the metadata.txt file should look like: |
| // secondary-1.dex.jar fffe66877038db3af2cbd0fe2d9231ed5912e317 secondary.dex01.Canary |
| // secondary-2.dex.jar b218a3ea56c530fed6501d9f9ed918d1210cc658 secondary.dex02.Canary |
| // secondary-3.dex.jar 40f11878a8f7a278a3f12401c643da0d4a135e1a secondary.dex03.Canary |
| // |
| // The scratch directories that contain the metadata.txt and secondary-N.dex.jar files must be |
| // listed in secondaryDexDirectoriesBuilder so that their contents will be compressed |
| // appropriately for Froyo. |
| ImmutableSet.Builder<Path> secondaryDexDirectoriesBuilder = ImmutableSet.builder(); |
| Optional<PreDexMerge> preDexMerge = enhancementResult.getPreDexMerge(); |
| if (!preDexMerge.isPresent()) { |
| steps.add(new MkdirStep(primaryDexPath.getParent())); |
| |
| addDexingSteps( |
| classpathEntriesToDex, |
| classNamesToHashesSupplier, |
| secondaryDexDirectoriesBuilder, |
| steps, |
| primaryDexPath); |
| } else if (!ExopackageMode.enabledForSecondaryDexes(exopackageModes)) { |
| secondaryDexDirectoriesBuilder.addAll(preDexMerge.get().getSecondaryDexDirectories()); |
| } |
| |
| return new DexFilesInfo(primaryDexPath, secondaryDexDirectoriesBuilder.build()); |
| } |
| |
| public Supplier<Map<String, HashCode>> addAccumulateClassNamesStep( |
| final ImmutableSet<Path> classPathEntriesToDex, |
| ImmutableList.Builder<Step> steps) { |
| final ImmutableMap.Builder<String, HashCode> builder = ImmutableMap.builder(); |
| |
| steps.add( |
| new AbstractExecutionStep("collect_all_class_names") { |
| @Override |
| public int execute(ExecutionContext context) { |
| for (Path path : classPathEntriesToDex) { |
| Optional<ImmutableSortedMap<String, HashCode>> hashes = |
| AccumulateClassNamesStep.calculateClassHashes(context, path); |
| if (!hashes.isPresent()) { |
| return 1; |
| } |
| builder.putAll(hashes.get()); |
| } |
| return 0; |
| } |
| }); |
| |
| return Suppliers.memoize( |
| new Supplier<Map<String, HashCode>>() { |
| @Override |
| public Map<String, HashCode> get() { |
| return builder.build(); |
| } |
| }); |
| } |
| |
| public AndroidPackageableCollection getAndroidPackageableCollection() { |
| return enhancementResult.getPackageableCollection(); |
| } |
| |
| /** |
| * All native-libs-as-assets are copied to this directory before running apkbuilder. |
| */ |
| private Path getPathForNativeLibsAsAssets() { |
| return getBinPath("__native_libs_as_assets_%s__"); |
| } |
| |
| public Keystore getKeystore() { |
| return keystore; |
| } |
| |
| public String getUnsignedApkPath() { |
| return BuildTargets.getGenPath(getBuildTarget(), "%s.unsigned.apk").toString(); |
| } |
| |
| /** The APK at this path will be signed, but not zipaligned. */ |
| private Path getSignedApkPath() { |
| return Paths.get(getUnsignedApkPath().replaceAll("\\.unsigned\\.apk$", ".signed.apk")); |
| } |
| |
| /** The APK at this path will have compressed resources, but will not be zipaligned. */ |
| private Path getCompressedResourcesApkPath() { |
| return Paths.get(getUnsignedApkPath().replaceAll("\\.unsigned\\.apk$", ".compressed.apk")); |
| } |
| |
| private Path getBinPath(String format) { |
| return BuildTargets.getBinPath(getBuildTarget(), format); |
| } |
| |
| @VisibleForTesting |
| Path getProguardOutputFromInputClasspath(Path classpathEntry) { |
| // Hehe, this is so ridiculously fragile. |
| Preconditions.checkArgument(!classpathEntry.isAbsolute(), |
| "Classpath entries should be relative rather than absolute paths: %s", |
| classpathEntry); |
| String obfuscatedName = |
| Files.getNameWithoutExtension(classpathEntry.toString()) + "-obfuscated.jar"; |
| Path dirName = classpathEntry.getParent(); |
| Path proguardConfigDir = enhancementResult.getAaptPackageResources() |
| .getPathToGeneratedProguardConfigDir(); |
| return proguardConfigDir.resolve(dirName).resolve(obfuscatedName); |
| } |
| |
| /** |
| * @return the resulting set of ProGuarded classpath entries to dex. |
| */ |
| @VisibleForTesting |
| ImmutableSet<Path> addProguardCommands( |
| Set<Path> classpathEntriesToDex, |
| Set<Path> depsProguardConfigs, |
| ImmutableList.Builder<Step> steps, |
| BuildableContext buildableContext) { |
| final ImmutableSetMultimap<JavaLibrary, Path> classpathEntriesMap = |
| getTransitiveClasspathEntries(); |
| ImmutableSet.Builder<Path> additionalLibraryJarsForProguardBuilder = ImmutableSet.builder(); |
| |
| for (JavaLibrary buildRule : rulesToExcludeFromDex) { |
| additionalLibraryJarsForProguardBuilder.addAll(classpathEntriesMap.get(buildRule)); |
| } |
| |
| // Create list of proguard Configs for the app project and its dependencies |
| ImmutableSet.Builder<Path> proguardConfigsBuilder = ImmutableSet.builder(); |
| proguardConfigsBuilder.addAll(depsProguardConfigs); |
| if (proguardConfig.isPresent()) { |
| proguardConfigsBuilder.add(getResolver().getPath(proguardConfig.get())); |
| } |
| |
| // Transform our input classpath to a set of output locations for each input classpath. |
| // TODO(devjasta): the output path we choose is the result of a slicing function against |
| // input classpath. This is fragile and should be replaced with knowledge of the BuildTarget. |
| final ImmutableMap<Path, Path> inputOutputEntries = FluentIterable |
| .from(classpathEntriesToDex) |
| .toMap(new Function<Path, Path>() { |
| @Override |
| public Path apply(Path classpathEntry) { |
| return getProguardOutputFromInputClasspath(classpathEntry); |
| } |
| }); |
| |
| Path proguardConfigDir = enhancementResult.getAaptPackageResources() |
| .getPathToGeneratedProguardConfigDir(); |
| // Run ProGuard on the classpath entries. |
| ProGuardObfuscateStep.create( |
| proguardJarOverride, |
| proguardMaxHeapSize, |
| proguardConfigDir.resolve("proguard.txt"), |
| proguardConfigsBuilder.build(), |
| sdkProguardConfig, |
| optimizationPasses, |
| inputOutputEntries, |
| additionalLibraryJarsForProguardBuilder.build(), |
| proguardConfigDir, |
| buildableContext, |
| steps); |
| |
| // Apply the transformed inputs to the classpath (this will modify deps.classpathEntriesToDex |
| // so that we're now dexing the proguarded artifacts). |
| return ImmutableSet.copyOf(inputOutputEntries.values()); |
| } |
| |
| /** |
| * Create dex artifacts for all of the individual directories of compiled .class files (or |
| * the obfuscated jar files if proguard is used). If split dex is used, multiple dex artifacts |
| * will be produced. |
| * @param classpathEntriesToDex Full set of classpath entries that must make |
| * their way into the final APK structure (but not necessarily into the |
| * primary dex). |
| * @param secondaryDexDirectories The contract for updating this builder must match that |
| * of {@link PreDexMerge#getSecondaryDexDirectories()}. |
| * @param steps List of steps to add to. |
| * @param primaryDexPath Output path for the primary dex file. |
| */ |
| @VisibleForTesting |
| void addDexingSteps( |
| Set<Path> classpathEntriesToDex, |
| Supplier<Map<String, HashCode>> classNamesToHashesSupplier, |
| ImmutableSet.Builder<Path> secondaryDexDirectories, |
| ImmutableList.Builder<Step> steps, |
| Path primaryDexPath) { |
| final Supplier<Set<Path>> primaryInputsToDex; |
| final Optional<Path> secondaryDexDir; |
| final Optional<Supplier<Multimap<Path, Path>>> secondaryOutputToInputs; |
| |
| if (shouldSplitDex()) { |
| Optional<Path> proguardFullConfigFile = Optional.absent(); |
| Optional<Path> proguardMappingFile = Optional.absent(); |
| if (packageType.isBuildWithObfuscation()) { |
| Path proguardConfigDir = enhancementResult.getAaptPackageResources() |
| .getPathToGeneratedProguardConfigDir(); |
| proguardFullConfigFile = Optional.of(proguardConfigDir.resolve("configuration.txt")); |
| proguardMappingFile = Optional.of(proguardConfigDir.resolve("mapping.txt")); |
| } |
| |
| // DexLibLoader expects that metadata.txt and secondary jar files are under this dir |
| // in assets. |
| |
| // Intermediate directory holding the primary split-zip jar. |
| Path splitZipDir = getBinPath("__%s_split_zip__"); |
| steps.add(new MakeCleanDirectoryStep(splitZipDir)); |
| Path primaryJarPath = splitZipDir.resolve("primary.jar"); |
| |
| Path secondaryJarMetaDirParent = splitZipDir.resolve("secondary_meta"); |
| Path secondaryJarMetaDir = secondaryJarMetaDirParent.resolve(SECONDARY_DEX_SUBDIR); |
| steps.add(new MakeCleanDirectoryStep(secondaryJarMetaDir)); |
| Path secondaryJarMeta = secondaryJarMetaDir.resolve("metadata.txt"); |
| |
| // Intermediate directory holding _ONLY_ the secondary split-zip jar files. This is |
| // important because SmartDexingCommand will try to dx every entry in this directory. It |
| // does this because it's impossible to know what outputs split-zip will generate until it |
| // runs. |
| final Path secondaryZipDir = getBinPath("__%s_secondary_zip__"); |
| steps.add(new MakeCleanDirectoryStep(secondaryZipDir)); |
| |
| // Run the split-zip command which is responsible for dividing the large set of input |
| // classpaths into a more compact set of jar files such that no one jar file when dexed will |
| // yield a dex artifact too large for dexopt or the dx method limit to handle. |
| Path zipSplitReportDir = getBinPath("__%s_split_zip_report__"); |
| steps.add(new MakeCleanDirectoryStep(zipSplitReportDir)); |
| SplitZipStep splitZipCommand = new SplitZipStep( |
| classpathEntriesToDex, |
| secondaryJarMeta, |
| primaryJarPath, |
| secondaryZipDir, |
| "secondary-%d.jar", |
| proguardFullConfigFile, |
| proguardMappingFile, |
| dexSplitMode, |
| dexSplitMode.getPrimaryDexScenarioFile().transform(getResolver().getPathFunction()), |
| dexSplitMode.getPrimaryDexClassesFile().transform(getResolver().getPathFunction()), |
| dexSplitMode.getSecondaryDexHeadClassesFile().transform(getResolver().getPathFunction()), |
| dexSplitMode.getSecondaryDexTailClassesFile().transform(getResolver().getPathFunction()), |
| zipSplitReportDir); |
| steps.add(splitZipCommand); |
| |
| // Add the secondary dex directory that has yet to be created, but will be by the |
| // smart dexing command. Smart dex will handle "cleaning" this directory properly. |
| Path secondaryDexParentDir = getBinPath("__%s_secondary_dex__/"); |
| secondaryDexDir = Optional.of(secondaryDexParentDir.resolve(SECONDARY_DEX_SUBDIR)); |
| steps.add(new MkdirStep(secondaryDexDir.get())); |
| |
| if (dexSplitMode.getDexStore() == DexStore.RAW) { |
| secondaryDexDirectories.add(secondaryDexDir.get()); |
| } else { |
| secondaryDexDirectories.add(secondaryJarMetaDirParent); |
| secondaryDexDirectories.add(secondaryDexParentDir); |
| } |
| |
| // Adjust smart-dex inputs for the split-zip case. |
| primaryInputsToDex = Suppliers.<Set<Path>>ofInstance(ImmutableSet.of(primaryJarPath)); |
| Supplier<Multimap<Path, Path>> secondaryOutputToInputsMap = |
| splitZipCommand.getOutputToInputsMapSupplier(secondaryDexDir.get()); |
| secondaryOutputToInputs = Optional.of(secondaryOutputToInputsMap); |
| } else { |
| // Simple case where our inputs are the natural classpath directories and we don't have |
| // to worry about secondary jar/dex files. |
| primaryInputsToDex = Suppliers.ofInstance(classpathEntriesToDex); |
| secondaryDexDir = Optional.absent(); |
| secondaryOutputToInputs = Optional.absent(); |
| } |
| |
| HashInputJarsToDexStep hashInputJarsToDexStep = new HashInputJarsToDexStep( |
| primaryInputsToDex, |
| secondaryOutputToInputs, |
| classNamesToHashesSupplier); |
| steps.add(hashInputJarsToDexStep); |
| |
| // Stores checksum information from each invocation to intelligently decide when dx needs |
| // to be re-run. |
| Path successDir = getBinPath("__%s_smart_dex__/.success"); |
| steps.add(new MkdirStep(successDir)); |
| |
| // Add the smart dexing tool that is capable of avoiding the external dx invocation(s) if |
| // it can be shown that the inputs have not changed. It also parallelizes dx invocations |
| // where applicable. |
| // |
| // Note that by not specifying the number of threads this command will use it will select an |
| // optimal default regardless of the value of --num-threads. This decision was made with the |
| // assumption that --num-threads specifies the threading of build rule execution and does not |
| // directly apply to the internal threading/parallelization details of various build commands |
| // being executed. For example, aapt is internally threaded by default when preprocessing |
| // images. |
| EnumSet<DxStep.Option> dxOptions = PackageType.RELEASE.equals(packageType) |
| ? EnumSet.noneOf(DxStep.Option.class) |
| : EnumSet.of(DxStep.Option.NO_OPTIMIZE); |
| SmartDexingStep smartDexingCommand = new SmartDexingStep( |
| primaryDexPath, |
| primaryInputsToDex, |
| secondaryDexDir, |
| secondaryOutputToInputs, |
| hashInputJarsToDexStep, |
| successDir, |
| Optional.<Integer>absent(), |
| dxOptions); |
| steps.add(smartDexingCommand); |
| } |
| |
| @Override |
| public Path getManifestPath() { |
| return enhancementResult.getAaptPackageResources().getAndroidManifestXml(); |
| } |
| |
| boolean shouldSplitDex() { |
| return dexSplitMode.isShouldSplitDex(); |
| } |
| |
| @Override |
| public Optional<ExopackageInfo> getExopackageInfo() { |
| boolean shouldInstall = false; |
| |
| ImmutableExopackageInfo.Builder builder = ImmutableExopackageInfo.builder(); |
| if (ExopackageMode.enabledForSecondaryDexes(exopackageModes)) { |
| PreDexMerge preDexMerge = enhancementResult.getPreDexMerge().get(); |
| builder.setDexInfo( |
| ImmutableExopackageInfo.DexInfo.of( |
| preDexMerge.getMetadataTxtPath(), |
| preDexMerge.getDexDirectory())); |
| shouldInstall = true; |
| } |
| |
| if (ExopackageMode.enabledForNativeLibraries(exopackageModes) && |
| enhancementResult.getCopyNativeLibraries().isPresent()) { |
| CopyNativeLibraries copyNativeLibraries = enhancementResult.getCopyNativeLibraries().get(); |
| builder.setNativeLibsInfo( |
| ImmutableExopackageInfo.NativeLibsInfo.of( |
| copyNativeLibraries.getPathToMetadataTxt(), |
| copyNativeLibraries.getPathToNativeLibsDir())); |
| shouldInstall = true; |
| } |
| |
| if (!shouldInstall) { |
| return Optional.absent(); |
| } |
| |
| ExopackageInfo exopackageInfo = builder.build(); |
| return Optional.of(exopackageInfo); |
| } |
| |
| public ImmutableSortedSet<BuildRule> getClasspathDeps() { |
| return getDeclaredDeps(); |
| } |
| |
| @Override |
| public ImmutableSetMultimap<JavaLibrary, Path> getTransitiveClasspathEntries() { |
| // This is used primarily for buck audit classpath. |
| return Classpaths.getClasspathEntries(getClasspathDeps()); |
| } |
| |
| /** |
| * Encapsulates the information about dexing output that must be passed to ApkBuilder. |
| */ |
| private static class DexFilesInfo { |
| final Path primaryDexPath; |
| final ImmutableSet<Path> secondaryDexDirs; |
| |
| DexFilesInfo(Path primaryDexPath, ImmutableSet<Path> secondaryDexDirs) { |
| this.primaryDexPath = primaryDexPath; |
| this.secondaryDexDirs = secondaryDexDirs; |
| } |
| } |
| } |