com.facebook.buck.android.CopyNativeLibraries.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.android.CopyNativeLibraries.java

Source

/*
 * 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.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.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 org.immutables.value.Value;

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;

/**
 * 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 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(new MkdirStep(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.add(new MakeCleanDirectoryStep(getProjectFilesystem(), getBinPath()));

        final Path pathToNativeLibs = getPathToNativeLibsDir();
        steps.add(new MakeCleanDirectoryStep(getProjectFilesystem(), pathToNativeLibs));

        final Path pathToNativeLibsAssets = getPathToNativeLibsAssetsDir();
        steps.add(new MakeCleanDirectoryStep(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(new AbstractExecutionStep("hash_native_libs") {
            @Override
            public StepExecutionResult execute(ExecutionContext context) {
                ProjectFilesystem filesystem = getProjectFilesystem();
                ImmutableList.Builder<String> metadataLines = ImmutableList.builder();
                try {
                    for (Path nativeLib : filesystem.getFilesUnderPath(getPathToAllLibsDir())) {
                        Sha1HashCode filesha1 = filesystem.computeSha1(nativeLib);
                        Path relativePath = getPathToAllLibsDir().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;
            }
        });

        buildableContext.recordArtifact(pathToNativeLibs);
        buildableContext.recordArtifact(pathToNativeLibsAssets);
        buildableContext.recordArtifact(pathToMetadataTxt);

        return steps.build();
    }

    @Nullable
    @Override
    public Path getPathToOutput() {
        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 = new MkdirStep(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(shs96c): 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());
        }
    }
}