blob: 1b673f5be5642a53924d2b5ab1cc6a2fde7c7cfd [file] [log] [blame]
/*
* 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.AndroidBinary.TargetCpuType;
import com.facebook.buck.cxx.ObjcopyStep;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Pair;
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.RuleKey;
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.fs.CopyStep;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
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.ImmutableSortedMap;
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.Map;
import javax.annotation.Nullable;
/**
* 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 com.facebook.buck.cli.ExopackageInstaller}.
*/
public class CopyNativeLibraries extends AbstractBuildRule {
private final ImmutableSet<Path> nativeLibDirectories;
private final ImmutableSet<TargetCpuType> cpuFilters;
/**
* A map of native libraries to copy in which are already filtered using the above CPU filter.
* The keys of the map are the tuple of {@link TargetCpuType} and shared library SONAME
* (e.g. <"x86", "libtest.so">), and the values of the map are the full paths to the libraries.
*/
private final ImmutableMap<TargetCpuType, NdkCxxPlatform> nativePlatforms;
private final ImmutableMap<Pair<TargetCpuType, String>, SourcePath> filteredNativeLibraries;
protected CopyNativeLibraries(
BuildRuleParams buildRuleParams,
SourcePathResolver resolver,
ImmutableSet<Path> nativeLibDirectories,
ImmutableSet<TargetCpuType> cpuFilters,
ImmutableMap<TargetCpuType, NdkCxxPlatform> nativePlatforms,
ImmutableMap<Pair<TargetCpuType, String>, SourcePath> filteredNativeLibraries) {
super(buildRuleParams, resolver);
this.nativeLibDirectories = nativeLibDirectories;
this.cpuFilters = cpuFilters;
this.filteredNativeLibraries = filteredNativeLibraries;
this.nativePlatforms = nativePlatforms;
Preconditions.checkArgument(
!nativeLibDirectories.isEmpty() || !filteredNativeLibraries.isEmpty(),
"There should be at least one native library to copy.");
}
public Path getPathToNativeLibsDir() {
return getBinPath().resolve("libs");
}
public Path getPathToMetadataTxt() {
return getBinPath().resolve("metadata.txt");
}
private Path getBinPath() {
return BuildTargets.getBinPath(getBuildTarget(), "__native_libs_%s__");
}
@Override
protected ImmutableCollection<Path> getInputsToCompareToOutput() {
return ImmutableList.of();
}
@Override
protected RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) {
builder.setReflectively("cpuFilters", cpuFilters);
// Hash in the pre-filtered native libraries we're pulling in.
ImmutableSortedMap<Pair<TargetCpuType, String>, SourcePath> sortedLibs =
ImmutableSortedMap.<Pair<TargetCpuType, String>, SourcePath>orderedBy(
Pair.<TargetCpuType, String>comparator())
.putAll(filteredNativeLibraries)
.build();
for (Map.Entry<Pair<TargetCpuType, String>, SourcePath> entry : sortedLibs.entrySet()) {
builder.setReflectively(
String.format("lib(%s, %s)", entry.getKey().getFirst(), entry.getKey().getSecond()),
entry.getValue());
}
return builder;
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context,
BuildableContext buildableContext) {
ImmutableList.Builder<Step> steps = ImmutableList.builder();
final Path pathToNativeLibs = getPathToNativeLibsDir();
steps.add(new MakeCleanDirectoryStep(pathToNativeLibs));
for (Path nativeLibDir : nativeLibDirectories.asList().reverse()) {
copyNativeLibrary(nativeLibDir, pathToNativeLibs, cpuFilters, steps);
}
// Copy in the pre-filtered native libraries.
for (Map.Entry<Pair<TargetCpuType, String>, SourcePath> entry :
filteredNativeLibraries.entrySet()) {
Optional<String> abiDirectoryComponent = getAbiDirectoryComponent(entry.getKey().getFirst());
Preconditions.checkState(abiDirectoryComponent.isPresent());
Path destination =
pathToNativeLibs
.resolve(abiDirectoryComponent.get())
.resolve(entry.getKey().getSecond());
NdkCxxPlatform platform =
Preconditions.checkNotNull(nativePlatforms.get(entry.getKey().getFirst()));
Path objcopy = platform.getObjcopy();
steps.add(new MkdirStep(destination.getParent()));
steps.add(
new ObjcopyStep(
objcopy,
ImmutableList.of("--strip-unneeded"),
getResolver().getPath(entry.getValue()),
destination));
}
final Path pathToMetadataTxt = getPathToMetadataTxt();
steps.add(
new AbstractExecutionStep("hash_native_libs") {
@Override
public int execute(ExecutionContext context) {
ProjectFilesystem filesystem = context.getProjectFilesystem();
ImmutableList.Builder<String> metadataLines = ImmutableList.builder();
try {
for (Path nativeLib : filesystem.getFilesUnderPath(pathToNativeLibs)) {
String filesha1 = filesystem.computeSha1(nativeLib);
Path relativePath = pathToNativeLibs.relativize(nativeLib);
metadataLines.add(String.format("%s %s", relativePath.toString(), filesha1));
}
filesystem.writeLinesToPath(metadataLines.build(), pathToMetadataTxt);
} catch (IOException e) {
context.logError(e, "There was an error hashing native libraries.");
return 1;
}
return 0;
}
});
buildableContext.recordArtifactsInDirectory(pathToNativeLibs);
buildableContext.recordArtifact(pathToMetadataTxt);
return steps.build();
}
@Nullable
@Override
public Path getPathToOutputFile() {
return null;
}
public static void copyNativeLibrary(Path sourceDir,
final Path destinationDir,
ImmutableSet<TargetCpuType> cpuFilters,
ImmutableList.Builder<Step> steps) {
if (cpuFilters.isEmpty()) {
steps.add(
CopyStep.forDirectory(
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 = new MkdirStep(libDestinationDir);
final CopyStep copyStep = CopyStep.forDirectory(
libSourceDir,
libDestinationDir,
CopyStep.DirectoryMode.CONTENTS_ONLY);
steps.add(
new Step() {
@Override
public int execute(ExecutionContext context) {
if (!context.getProjectFilesystem().exists(libSourceDir)) {
return 0;
}
if (mkDirStep.execute(context) == 0 && copyStep.execute(context) == 0) {
return 0;
}
return 1;
}
@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 int execute(ExecutionContext context) {
ProjectFilesystem filesystem = context.getProjectFilesystem();
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(
exePath.toString().replaceAll("/([^/]+)-disguised-exe$", "/lib$1.so"));
filesystem.move(exePath, fakeSoPath);
}
} catch (IOException e) {
context.logError(e, "Renaming native executables failed.");
return 1;
}
return 0;
}
});
}
/**
* 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.absent();
*/
private static Optional<String> getAbiDirectoryComponent(TargetCpuType cpuType) {
String component = null;
if (cpuType.equals(TargetCpuType.ARM)) {
component = SdkConstants.ABI_ARMEABI;
} else if (cpuType.equals(TargetCpuType.ARMV7)) {
component = SdkConstants.ABI_ARMEABI_V7A;
} else if (cpuType.equals(TargetCpuType.X86)) {
component = SdkConstants.ABI_INTEL_ATOM;
} else if (cpuType.equals(TargetCpuType.MIPS)) {
component = SdkConstants.ABI_MIPS;
}
return Optional.fromNullable(component);
}
}