/*
* 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.android.common.SdkConstants;
import com.facebook.buck.android.NdkCxxPlatforms.TargetCpuType;
import com.facebook.buck.android.exopackage.ExopackageInstaller;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.AddToRuleKey;
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.RuleKeyAppendable;
import com.facebook.buck.rules.RuleKeyObjectSink;
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.step.fs.CopyStep;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.util.immutables.BuckStyleImmutable;
import com.facebook.buck.util.sha1.Sha1HashCode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Optional;
import javax.annotation.Nullable;
import org.immutables.value.Value;
/**
* A {@link com.facebook.buck.rules.BuildRule} that gathers shared objects generated by {@code
* ndk_library} and {@code prebuilt_native_library} rules into a directory. It also hashes the
* shared objects collected and stores this metadata in a text file, to be used later by {@link
* ExopackageInstaller}.
*/
public class CopyNativeLibraries extends AbstractBuildRule {
private final ImmutableSet<SourcePath> nativeLibDirectories;
@AddToRuleKey private final ImmutableSet<TargetCpuType> cpuFilters;
@AddToRuleKey private final ImmutableSet<StrippedObjectDescription> stripLibRules;
@AddToRuleKey private final ImmutableSet<StrippedObjectDescription> stripLibAssetRules;
private final String moduleName;
protected CopyNativeLibraries(
BuildRuleParams buildRuleParams,
ImmutableSet<SourcePath> nativeLibDirectories,
ImmutableSet<StrippedObjectDescription> stripLibRules,
ImmutableSet<StrippedObjectDescription> stripLibAssetRules,
ImmutableSet<TargetCpuType> cpuFilters,
String moduleName) {
super(buildRuleParams);
this.nativeLibDirectories = nativeLibDirectories;
this.stripLibRules = stripLibRules;
this.stripLibAssetRules = stripLibAssetRules;
this.cpuFilters = cpuFilters;
this.moduleName = moduleName;
Preconditions.checkArgument(
!nativeLibDirectories.isEmpty()
|| !stripLibRules.isEmpty()
|| !stripLibAssetRules.isEmpty(),
"There should be at least one native library to copy.");
}
public Path getPathToNativeLibsDir() {
return getBinPath().resolve("libs");
}
public Path getPathToNativeLibsAssetsDir() {
return getBinPath().resolve("assetLibs");
}
/**
* Returns the path that is the immediate parent of {@link #getPathToNativeLibsAssetsDir()} and
* {@link #getPathToNativeLibsDir()}.
*/
public Path getPathToAllLibsDir() {
return getBinPath();
}
public SourcePath getSourcePathToAllLibsDir() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), getPathToAllLibsDir());
}
public Path getPathToMetadataTxt() {
return getBinPath().resolve("metadata.txt");
}
private Path getBinPath() {
return BuildTargets.getScratchPath(
getProjectFilesystem(), getBuildTarget(), "__native_" + moduleName + "_%s__");
}
@VisibleForTesting
ImmutableSet<SourcePath> getNativeLibDirectories() {
return nativeLibDirectories;
}
@VisibleForTesting
ImmutableSet<StrippedObjectDescription> getStrippedObjectDescriptions() {
return ImmutableSet.<StrippedObjectDescription>builder()
.addAll(stripLibRules)
.addAll(stripLibAssetRules)
.build();
}
private void addStepsForCopyingStrippedNativeLibrariesOrAssets(
SourcePathResolver resolver,
ProjectFilesystem filesystem,
ImmutableSet<StrippedObjectDescription> strippedNativeLibrariesOrAssets,
Path destinationRootDir,
ImmutableList.Builder<Step> steps) {
for (StrippedObjectDescription strippedObject : strippedNativeLibrariesOrAssets) {
Optional<String> abiDirectoryComponent =
getAbiDirectoryComponent(strippedObject.getTargetCpuType());
Preconditions.checkState(abiDirectoryComponent.isPresent());
Path destination =
destinationRootDir
.resolve(abiDirectoryComponent.get())
.resolve(strippedObject.getStrippedObjectName());
steps.add(MkdirStep.of(getProjectFilesystem(), destination.getParent()));
steps.add(
CopyStep.forFile(
filesystem, resolver.getAbsolutePath(strippedObject.getSourcePath()), destination));
}
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
ImmutableList.Builder<Step> steps = ImmutableList.builder();
steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), getBinPath()));
final Path pathToNativeLibs = getPathToNativeLibsDir();
steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), pathToNativeLibs));
final Path pathToNativeLibsAssets = getPathToNativeLibsAssetsDir();
steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), pathToNativeLibsAssets));
for (SourcePath nativeLibDir : nativeLibDirectories.asList().reverse()) {
copyNativeLibrary(
getProjectFilesystem(),
context.getSourcePathResolver().getAbsolutePath(nativeLibDir),
pathToNativeLibs,
cpuFilters,
steps);
}
addStepsForCopyingStrippedNativeLibrariesOrAssets(
context.getSourcePathResolver(),
getProjectFilesystem(),
stripLibRules,
pathToNativeLibs,
steps);
addStepsForCopyingStrippedNativeLibrariesOrAssets(
context.getSourcePathResolver(),
getProjectFilesystem(),
stripLibAssetRules,
pathToNativeLibsAssets,
steps);
final Path pathToMetadataTxt = getPathToMetadataTxt();
steps.add(
createMetadataStep(getProjectFilesystem(), getPathToMetadataTxt(), getPathToAllLibsDir()));
buildableContext.recordArtifact(pathToNativeLibs);
buildableContext.recordArtifact(pathToNativeLibsAssets);
buildableContext.recordArtifact(pathToMetadataTxt);
return steps.build();
}
public static Step createMetadataStep(
ProjectFilesystem filesystem, Path pathToMetadataTxt, Path pathToAllLibsDir) {
return new AbstractExecutionStep("hash_native_libs") {
@Override
public StepExecutionResult execute(ExecutionContext context)
throws IOException, InterruptedException {
ImmutableList.Builder<String> metadataLines = ImmutableList.builder();
try {
for (Path nativeLib : filesystem.getFilesUnderPath(pathToAllLibsDir)) {
Sha1HashCode filesha1 = filesystem.computeSha1(nativeLib);
Path relativePath = pathToAllLibsDir.relativize(nativeLib);
metadataLines.add(String.format("%s %s", relativePath, filesha1));
}
filesystem.writeLinesToPath(metadataLines.build(), pathToMetadataTxt);
} catch (IOException e) {
context.logError(e, "There was an error hashing native libraries.");
return StepExecutionResult.ERROR;
}
return StepExecutionResult.SUCCESS;
}
};
}
@Nullable
@Override
public SourcePath getSourcePathToOutput() {
return null;
}
public static void copyNativeLibrary(
final ProjectFilesystem filesystem,
Path sourceDir,
final Path destinationDir,
ImmutableSet<TargetCpuType> cpuFilters,
ImmutableList.Builder<Step> steps) {
if (cpuFilters.isEmpty()) {
steps.add(
CopyStep.forDirectory(
filesystem, sourceDir, destinationDir, CopyStep.DirectoryMode.CONTENTS_ONLY));
} else {
for (TargetCpuType cpuType : cpuFilters) {
Optional<String> abiDirectoryComponent = getAbiDirectoryComponent(cpuType);
Preconditions.checkState(abiDirectoryComponent.isPresent());
final Path libSourceDir = sourceDir.resolve(abiDirectoryComponent.get());
Path libDestinationDir = destinationDir.resolve(abiDirectoryComponent.get());
final MkdirStep mkDirStep = MkdirStep.of(filesystem, libDestinationDir);
final CopyStep copyStep =
CopyStep.forDirectory(
filesystem, libSourceDir, libDestinationDir, CopyStep.DirectoryMode.CONTENTS_ONLY);
steps.add(
new Step() {
@Override
public StepExecutionResult execute(ExecutionContext context) {
// TODO(simons): Using a projectfilesystem here is almost definitely wrong.
// This is because each library may come from different build rules, which may be in
// different cells --- this check works by coincidence.
if (!filesystem.exists(libSourceDir)) {
return StepExecutionResult.SUCCESS;
}
if (mkDirStep.execute(context).isSuccess()
&& copyStep.execute(context).isSuccess()) {
return StepExecutionResult.SUCCESS;
}
return StepExecutionResult.ERROR;
}
@Override
public String getShortName() {
return "copy_native_libraries";
}
@Override
public String getDescription(ExecutionContext context) {
ImmutableList.Builder<String> stringBuilder = ImmutableList.builder();
stringBuilder.add(String.format("[ -d %s ]", libSourceDir.toString()));
stringBuilder.add(mkDirStep.getDescription(context));
stringBuilder.add(copyStep.getDescription(context));
return Joiner.on(" && ").join(stringBuilder.build());
}
});
}
}
// Rename native files named like "*-disguised-exe" to "lib*.so" so they will be unpacked
// by the Android package installer. Then they can be executed like normal binaries
// on the device.
steps.add(
new AbstractExecutionStep("rename_native_executables") {
@Override
public StepExecutionResult execute(ExecutionContext context) {
final ImmutableSet.Builder<Path> executablesBuilder = ImmutableSet.builder();
try {
filesystem.walkRelativeFileTree(
destinationDir,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
if (file.toString().endsWith("-disguised-exe")) {
executablesBuilder.add(file);
}
return FileVisitResult.CONTINUE;
}
});
for (Path exePath : executablesBuilder.build()) {
Path fakeSoPath =
Paths.get(
MorePaths.pathWithUnixSeparators(exePath)
.replaceAll("/([^/]+)-disguised-exe$", "/lib$1.so"));
filesystem.move(exePath, fakeSoPath);
}
} catch (IOException e) {
context.logError(e, "Renaming native executables failed.");
return StepExecutionResult.ERROR;
}
return StepExecutionResult.SUCCESS;
}
});
}
/**
* Native libraries compiled for different CPU architectures are placed in the respective ABI
* subdirectories, such as 'armeabi', 'armeabi-v7a', 'x86' and 'mips'. This looks at the cpu
* filter and returns the correct subdirectory. If cpu filter is not present or not supported,
* returns Optional.empty();
*/
private static Optional<String> getAbiDirectoryComponent(TargetCpuType cpuType) {
switch (cpuType) {
case ARM:
return Optional.of(SdkConstants.ABI_ARMEABI);
case ARMV7:
return Optional.of(SdkConstants.ABI_ARMEABI_V7A);
case ARM64:
return Optional.of(SdkConstants.ABI_ARM64_V8A);
case X86:
return Optional.of(SdkConstants.ABI_INTEL_ATOM);
case X86_64:
return Optional.of(SdkConstants.ABI_INTEL_ATOM64);
case MIPS:
return Optional.of(SdkConstants.ABI_MIPS);
default:
return Optional.empty();
}
}
@Value.Immutable
@BuckStyleImmutable
abstract static class AbstractStrippedObjectDescription implements RuleKeyAppendable {
public abstract SourcePath getSourcePath();
public abstract String getStrippedObjectName();
public abstract TargetCpuType getTargetCpuType();
@Override
public void appendToRuleKey(RuleKeyObjectSink sink) {
sink.setReflectively("sourcePath", getSourcePath())
.setReflectively("strippedObjectName", getStrippedObjectName())
.setReflectively("targetCpuType", getTargetCpuType());
}
}
}