com.android.build.gradle.tasks.NdkBuildExternalNativeJsonGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.android.build.gradle.tasks.NdkBuildExternalNativeJsonGenerator.java

Source

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.android.build.gradle.tasks;

import static com.android.SdkConstants.CURRENT_PLATFORM;
import static com.android.SdkConstants.PLATFORM_WINDOWS;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.gradle.external.gnumake.NativeBuildConfigValueBuilder;
import com.android.build.gradle.external.gson.NativeBuildConfigValue;
import com.android.build.gradle.external.gson.PlainFileGsonTypeAdaptor;
import com.android.build.gradle.internal.core.Abi;
import com.android.build.gradle.internal.ndk.NdkHandler;
import com.android.builder.core.AndroidBuilder;
import com.android.ide.common.process.ProcessException;
import com.android.ide.common.process.ProcessInfoBuilder;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.google.gson.GsonBuilder;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.gradle.api.GradleException;

/**
 * ndk-build JSON generation logic. This is separated from the corresponding ndk-build task so that
 * JSON can be generated during configuration.
 */
class NdkBuildExternalNativeJsonGenerator extends ExternalNativeJsonGenerator {
    @NonNull
    private final File projectDir;

    NdkBuildExternalNativeJsonGenerator(@NonNull NdkHandler ndkHandler, int minSdkVersion,
            @NonNull String variantName, @NonNull Collection<Abi> abis, @NonNull AndroidBuilder androidBuilder,
            @NonNull File projectDir, @NonNull File sdkFolder, @NonNull File ndkFolder, @NonNull File soFolder,
            @NonNull File objFolder, @NonNull File jsonFolder, @NonNull File makeFile, boolean debuggable,
            @Nullable List<String> buildArguments, @Nullable List<String> cFlags, @Nullable List<String> cppFlags,
            @NonNull List<File> nativeBuildConfigurationsJsons) {
        super(ndkHandler, minSdkVersion, variantName, abis, androidBuilder, sdkFolder, ndkFolder, soFolder,
                new File(objFolder, "local"), // ndk-build create libraries in a "local" subfolder.
                jsonFolder, makeFile, debuggable, buildArguments, cFlags, cppFlags, nativeBuildConfigurationsJsons);
        this.projectDir = projectDir;
    }

    @Override
    void processBuildOutput(@NonNull String buildOutput, @NonNull String abi, int abiPlatformVersion)
            throws IOException {
        // Discover Application.mk if one exists next to Android.mk
        // If there is an Application.mk file next to Android.mk then pick it up.
        File applicationMk = new File(getMakeFile().getParent(), "Application.mk");

        // Write the captured ndk-build output to a file for diagnostic purposes.
        diagnostic("parse and convert ndk-build output to build configuration JSON");

        // Tasks, including the Exec task used to execute ndk-build, will execute in the same folder
        // as the module build.gradle. However, parsing of ndk-build output doesn't necessarily
        // happen within a task because it may be done at sync time. The parser needs to create
        // absolute paths for source files that have relative paths in the ndk-build output. For
        // this reason, we need to tell the parser the folder of the module build.gradle. This is
        // 'projectDir'.
        //
        // Example, if a project is set up as follows:
        //
        //   project/build.gradle
        //   project/app/build.gradle
        //
        // Then, right now, the current folder is 'project/' but ndk-build -n was executed in
        // 'project/app/'. For this reason, any relative paths in the ndk-build -n ouput will be
        // relative to 'project/app/' but a direct call now to getAbsolutePath() would produce a
        // path relative to 'project/' which is wrong.
        //
        // NOTE: CMake doesn't have the same issue because CMake JSON generation happens fully
        // within the Exec call which has 'project/app' as the current directory.
        NativeBuildConfigValue buildConfig = new NativeBuildConfigValueBuilder(getMakeFile(), projectDir)
                .addCommands(getBuildCommand(abi, abiPlatformVersion, applicationMk, false /* removeJobsFlag */),
                        getBuildCommand(abi, abiPlatformVersion, applicationMk, true /* removeJobsFlag */)
                                + " clean",
                        variantName, buildOutput)
                .build();

        if (applicationMk.exists()) {
            diagnostic("found application make file %s", applicationMk.getAbsolutePath());
            Preconditions.checkNotNull(buildConfig.buildFiles);
            buildConfig.buildFiles.add(applicationMk);
        }

        String actualResult = new GsonBuilder().registerTypeAdapter(File.class, new PlainFileGsonTypeAdaptor())
                .setPrettyPrinting().create().toJson(buildConfig);

        // Write the captured ndk-build output to JSON file
        File expectedJson = ExternalNativeBuildTaskUtils.getOutputJson(getJsonFolder(), abi);
        Files.write(actualResult, expectedJson, Charsets.UTF_8);
    }

    /**
     * Get the process builder with -n flag. This will tell ndk-build to emit the steps that it
     * would do to execute the build.
     */
    @NonNull
    @Override
    ProcessInfoBuilder getProcessBuilder(@NonNull String abi, int abiPlatformVersion, @NonNull File outputJson) {
        checkConfiguration();
        // Discover Application.mk if one exists next to Android.mk
        // If there is an Application.mk file next to Android.mk then pick it up.
        File applicationMk = new File(getMakeFile().getParent(), "Application.mk");
        ProcessInfoBuilder builder = new ProcessInfoBuilder();
        builder.setExecutable(getNdkBuild())
                .addArgs(getBaseArgs(abi, abiPlatformVersion, applicationMk, false /* removeJobsFlag */))
                // Disable response files so we can parse the command line.
                .addArgs("APP_SHORT_COMMANDS=false").addArgs("LOCAL_SHORT_COMMANDS=false").addArgs("-B") // Build as if clean
                .addArgs("-n");
        return builder;
    }

    @NonNull
    @Override
    String executeProcess(@NonNull String abi, int abiPlatformVersion, @NonNull File outputJsonDir)
            throws ProcessException, IOException {
        return ExternalNativeBuildTaskUtils.executeBuildProcessAndLogError(androidBuilder,
                getProcessBuilder(abi, abiPlatformVersion, outputJsonDir), false /* logStdioToInfo */);
    }

    @NonNull
    @Override
    public NativeBuildSystem getNativeBuildSystem() {
        return NativeBuildSystem.NDK_BUILD;
    }

    @NonNull
    @Override
    Map<Abi, File> getStlSharedObjectFiles() {
        return Maps.newHashMap();
    }

    /** Get the path of the ndk-build script. */
    @NonNull
    private String getNdkBuild() {
        String tool = "ndk-build";
        if (isWindows()) {
            tool += ".cmd";
        }
        File toolFile = new File(getNdkFolder(), tool);

        try {
            // Attempt to shorten ndkFolder which may have segments of "path\.."
            // File#getAbsolutePath doesn't do this.
            return toolFile.getCanonicalPath();
        } catch (IOException e) {
            warn("Attempted to get ndkFolder canonical path and failed: %s\n" + "Falling back to absolute path.",
                    e);
            return toolFile.getAbsolutePath();
        }
    }

    /**
     * If the make file is a directory then get the implied file, otherwise return the path.
     */
    @NonNull
    private File getMakeFile() {
        if (getMakefile().isDirectory()) {
            return new File(getMakefile(), "Android.mk");
        }
        return getMakefile();
    }

    /**
     * Check whether the configuration looks good enough to generate JSON files and expect that
     * the result will be valid.
     */
    private void checkConfiguration() {
        List<String> configurationErrors = getConfigurationErrors();
        if (!configurationErrors.isEmpty()) {
            throw new GradleException(Joiner.on("\n").join(configurationErrors));
        }
    }

    /** Get the base list of arguments for invoking ndk-build. */
    @NonNull
    private List<String> getBaseArgs(@NonNull String abi, int abiPlatformVersion, @NonNull File applicationMk,
            boolean removeJobsFlag) {
        List<String> result = Lists.newArrayList();
        result.add("NDK_PROJECT_PATH=null");
        result.add("APP_BUILD_SCRIPT=" + getMakeFile());

        if (applicationMk.exists()) {
            // NDK_APPLICATION_MK specifies the Application.mk file.
            result.add("NDK_APPLICATION_MK=" + applicationMk.getAbsolutePath());
        }

        // APP_ABI and NDK_ALL_ABIS work together. APP_ABI is the specific ABI for this build.
        // NDK_ALL_ABIS is the universe of all ABIs for this build. NDK_ALL_ABIS is set to just the
        // current ABI. If we don't do this, then ndk-build will erase build artifacts for all abis
        // aside from the current.
        result.add("APP_ABI=" + abi);
        result.add("NDK_ALL_ABIS=" + abi);

        if (isDebuggable()) {
            result.add("NDK_DEBUG=1");
        } else {
            result.add("NDK_DEBUG=0");
        }

        result.add("APP_PLATFORM=android-" + abiPlatformVersion);

        // getObjFolder is set to the "local" subfolder in the user specified directory, therefore,
        // NDK_OUT should be set to getObjFolder().getParent() instead of getObjFolder().
        String ndkOut = getObjFolder().getParent();
        if (CURRENT_PLATFORM == PLATFORM_WINDOWS) {
            // Due to b.android.com/219225, NDK_OUT on Windows requires forward slashes.
            // ndk-build.cmd is supposed to escape the back-slashes but it doesn't happen.
            // Workaround here by replacing back slash with forward.
            // ndk-build will have a fix for this bug in r14 but this gradle fix will make it
            // work back to r13, r12, r11, and r10.
            ndkOut = ndkOut.replace('\\', '/');
        }
        result.add("NDK_OUT=" + ndkOut);

        result.add("NDK_LIBS_OUT=" + getSoFolder().getAbsolutePath());

        for (String flag : getcFlags()) {
            result.add(String.format("APP_CFLAGS+=\"%s\"", flag));
        }

        for (String flag : getCppFlags()) {
            result.add(String.format("APP_CPPFLAGS+=\"%s\"", flag));
        }

        boolean skipNextArgument = false;
        for (String argument : getBuildArguments()) {
            // Jobs flag is removed for clean command because Make has issues running
            // cleans in parallel. See b.android.com/214558
            if (removeJobsFlag && argument.equals("-j")) {
                // This is the arguments "-j" "4" case. We need to skip the current argument
                // which is "-j" as well as the next argument, "4".
                skipNextArgument = true;
                continue;
            }
            if (removeJobsFlag && argument.equals("--jobs")) {
                // This is the arguments "--jobs" "4" case. We need to skip the current argument
                // which is "--jobs" as well as the next argument, "4".
                skipNextArgument = true;
                continue;
            }
            if (skipNextArgument) {
                // Skip the argument following "--jobs" or "-j"
                skipNextArgument = false;
                continue;
            }
            if (removeJobsFlag && (argument.startsWith("-j") || argument.startsWith("--jobs="))) {
                // This is the "-j4" or "--jobs=4" case.
                continue;
            }

            result.add(argument);
        }

        return result;
    }

    /** Get the build command */
    @NonNull
    private String getBuildCommand(@NonNull String abi, int abiPlatformVersion, @NonNull File applicationMk,
            boolean removeJobsFlag) {
        return getNdkBuild() + " "
                + Joiner.on(" ").join(getBaseArgs(abi, abiPlatformVersion, applicationMk, removeJobsFlag));
    }

    /**
     * Construct list of errors that can be known at configuration time.
     */
    @NonNull
    private List<String> getConfigurationErrors() {
        List<String> messages = Lists.newArrayList();
        if (getMakefile().isDirectory()) {
            messages.add(String.format(
                    "Gradle project ndkBuild.path %s is a folder. " + "Only files (like Android.mk) are allowed.",
                    getMakefile()));
        } else if (!getMakefile().exists()) {
            messages.add(
                    String.format("Gradle project ndkBuild.path is %s but that file doesn't exist", getMakefile()));
        }

        messages.addAll(getBaseConfigurationErrors());
        return messages;
    }
}