blob: 13e3466439068b82b071ba5ce07d0d932f236009 [file] [log] [blame]
/*
* 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;
}
}