/* * Copyright 2014-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 com.facebook.buck.android.AndroidBinary.ExopackageMode; import com.facebook.buck.log.Logger; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.rules.AbstractBuildRule; import com.facebook.buck.rules.BuildContext; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildableContext; import com.facebook.buck.rules.ExplicitBuildTargetSourcePath; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.step.AbstractExecutionStep; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.StepExecutionResult; import com.facebook.buck.util.sha1.Sha1HashCode; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; import java.util.Map; import java.util.Optional; /** * A build rule that hashes all of the files that go into an exopackage APK. This is used by * AndroidBinaryRule to compute the ABI hash of its deps. */ public class ComputeExopackageDepsAbi extends AbstractBuildRule { private static final Logger LOG = Logger.get(ComputeExopackageDepsAbi.class); private final EnumSet<ExopackageMode> exopackageModes; private final AndroidPackageableCollection packageableCollection; private final Optional<ImmutableMap<APKModule, CopyNativeLibraries>> copyNativeLibraries; private final Optional<PreDexMerge> preDexMerge; private final Path abiPath; public ComputeExopackageDepsAbi( BuildRuleParams params, EnumSet<ExopackageMode> exopackageModes, AndroidPackageableCollection packageableCollection, Optional<ImmutableMap<APKModule, CopyNativeLibraries>> copyNativeLibraries, Optional<PreDexMerge> preDexMerge) { super(params); Preconditions.checkArgument(!exopackageModes.isEmpty()); this.exopackageModes = exopackageModes; this.packageableCollection = packageableCollection; this.copyNativeLibraries = copyNativeLibraries; this.preDexMerge = preDexMerge; this.abiPath = BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "%s/exopackage.abi"); } public SourcePath getAbiPath() { return new ExplicitBuildTargetSourcePath(getBuildTarget(), abiPath); } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, final BuildableContext buildableContext) { return ImmutableList.of( new AbstractExecutionStep("compute_android_binary_deps_abi") { @Override public StepExecutionResult execute(ExecutionContext executionContext) { try { // TODO(cjhopman): Rather than calculate this hash ourselves, we should be able to // just add all these files to the AndroidBinary's rulekey and rely on the input-based // rulekey calculation. // For exopackages, the only significant thing android_binary does is apkbuilder, // so we need to include all of the apkbuilder inputs in the ABI key. final Hasher hasher = Hashing.sha1().newHasher(); // The primary dex is always added. Sha1HashCode primaryDexHash = Preconditions.checkNotNull(preDexMerge.get().getPrimaryDexHash()); LOG.verbose("primary dex = %s", primaryDexHash); primaryDexHash.update(hasher); // We currently don't use any resource directories, so nothing to add there. // Collect files whose sha1 hashes need to be added to our ABI key. // This maps from the file to the role it plays, so changing (for example) // a native library to an asset will change the ABI key. // We assume that these are all order-insensitive to avoid having our ABI key // affected by filesystem iteration order. // If exopackage is disabled for secondary dexes, we need to hash the secondary dex // files that end up in the APK. PreDexMerge already hashes those files, so we can // just hash the summary of those hashes. if (!ExopackageMode.enabledForSecondaryDexes(exopackageModes) && preDexMerge.isPresent()) { addToHash(hasher, "secondary_dexes", preDexMerge.get().getMetadataTxtPath()); } // If exopackage is disabled for native libraries, we add them in apkbuilder, so we // need to include their hashes. AndroidTransitiveDependencies doesn't provide // BuildRules, only paths. We could augment it, but our current native libraries are // small enough that we can just hash them all without too much of a perf hit. if (!ExopackageMode.enabledForNativeLibraries(exopackageModes) && copyNativeLibraries.isPresent()) { for (Map.Entry<APKModule, CopyNativeLibraries> entry : copyNativeLibraries.get().entrySet()) { addToHash( hasher, "native_libs_" + entry.getKey().getName(), entry.getValue().getPathToMetadataTxt()); } } // In native exopackage mode, we include a bundle of fake // libraries that makes multi-arch Android always put our application // in 32-bit mode. if (ExopackageMode.enabledForNativeLibraries(exopackageModes)) { String fakeNativeLibraryBundle = System.getProperty("buck.native_exopackage_fake_path"); Preconditions.checkNotNull( fakeNativeLibraryBundle, "fake native bundle not specified in properties."); Path fakePath = Paths.get(fakeNativeLibraryBundle); Preconditions.checkState( fakePath.isAbsolute(), "Expected fake path to be absolute: %s", fakePath); addToHash(hasher, "fake_native_libs", fakePath, fakePath.getFileName()); } // Same deal for native libs as assets. final ImmutableSortedMap.Builder<Path, Path> allNativeFiles = ImmutableSortedMap.naturalOrder(); for (SourcePath libDir : packageableCollection.getNativeLibAssetsDirectories().values()) { // A SourcePath may not come from the same ProjectFilesystem as the step. Yay. The // `getFilesUnderPath` method returns files relative to the ProjectFilesystem's root // and so they may not exist, but we could go and do some path manipulation to // figure out where they are. Since we'll have to do the work anyway, let's just // handle things ourselves. final Path root = context.getSourcePathResolver().getAbsolutePath(libDir); Files.walkFileTree( root, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { allNativeFiles.put(file, root.relativize(file)); return FileVisitResult.CONTINUE; } }); } for (Map.Entry<Path, Path> entry : allNativeFiles.build().entrySet()) { addToHash(hasher, "native_lib_as_asset", entry.getKey(), entry.getValue()); } // Resources get copied from third-party JARs, so hash them. for (SourcePath jar : ImmutableSortedSet.copyOf(packageableCollection.getPathsToThirdPartyJars())) { addToHash(context.getSourcePathResolver(), hasher, "third-party jar", jar); } String abiHash = hasher.hash().toString(); LOG.verbose("ABI hash = %s", abiHash); getProjectFilesystem().createParentDirs(abiPath); getProjectFilesystem().writeContentsToPath(abiHash, abiPath); buildableContext.recordArtifact(abiPath); return StepExecutionResult.SUCCESS; } catch (IOException e) { executionContext.logError(e, "Error computing ABI hash."); return StepExecutionResult.ERROR; } } }); } private void addToHash(SourcePathResolver resolver, Hasher hasher, String role, SourcePath path) throws IOException { addToHash(hasher, role, resolver.getAbsolutePath(path), resolver.getRelativePath(path)); } private void addToHash(Hasher hasher, String role, Path path) throws IOException { Preconditions.checkState(!path.isAbsolute(), "Input path must not be absolute: %s", path); addToHash(hasher, role, getProjectFilesystem().resolve(path), path); } private void addToHash(Hasher hasher, String role, Path absolutePath, Path relativePath) throws IOException { // No need to check relative path. That's already been done for us. Preconditions.checkState( absolutePath.isAbsolute(), "Expected absolute path to be absolute: %s", absolutePath); hasher.putUnencodedChars(relativePath.toString()); hasher.putByte((byte) 0); Sha1HashCode fileSha1 = getProjectFilesystem().computeSha1(absolutePath); fileSha1.update(hasher); hasher.putByte((byte) 0); hasher.putUnencodedChars(role); hasher.putByte((byte) 0); LOG.verbose("file %s(%s) = %s", relativePath, role, fileSha1); } @Override public SourcePath getSourcePathToOutput() { return new ExplicitBuildTargetSourcePath(getBuildTarget(), abiPath); } }