blob: 903029e5a8b5bdc14188d1ecdddd715b26069a63 [file] [log] [blame]
/*
* Copyright (C) 2013 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 static com.android.build.gradle.options.LongOption.DEPRECATED_NDK_COMPILE_LEASE;
import static com.android.build.gradle.options.NdkLease.DEPRECATED_NDK_COMPILE_LEASE_DAYS;
import com.android.annotations.NonNull;
import com.android.build.gradle.internal.core.GradleVariantConfiguration;
import com.android.build.gradle.internal.dsl.CoreNdkOptions;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.tasks.NdkTask;
import com.android.build.gradle.internal.tasks.TaskInputHelper;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.build.gradle.options.BooleanOption;
import com.android.build.gradle.options.NdkLease;
import com.android.ide.common.process.LoggedProcessOutputHandler;
import com.android.ide.common.process.ProcessException;
import com.android.ide.common.process.ProcessInfoBuilder;
import com.android.ide.common.process.ProcessOutputHandler;
import com.android.ide.common.util.ReferenceHolder;
import com.android.sdklib.IAndroidTarget;
import com.android.utils.FileUtils;
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.io.Files;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.incremental.IncrementalTaskInputs;
import org.gradle.api.tasks.incremental.InputFileDetails;
import org.gradle.api.tasks.util.PatternSet;
public class NdkCompile extends NdkTask {
private static String getAlternatives(File generatedMakefile, String urlSuffix) {
String generatedAndridMk = "";
if (generatedMakefile != null) {
generatedAndridMk =
String.format(
" To get started, you can use the sample ndk-build script the Android\n"
+ " plugin generated for you at:\n"
+ " %s\n",
generatedMakefile);
}
return String.format(
"Consider using CMake or ndk-build integration. For more information, go to:\n"
+ " https://d.android.com/r/studio-ui/add-native-code.html%s\n"
+ "%s"
+ "Alternatively, you can use the experimental plugin:\n"
+ " https://developer.android.com/r/tools/experimental-plugin.html\n",
urlSuffix, generatedAndridMk);
}
private FileCollection sourceFolders;
private File generatedMakefile;
private boolean debuggable;
private File soFolder;
private File objFolder;
private File ndkDirectory;
private boolean ndkRenderScriptMode;
private boolean ndkCygwinMode;
private boolean isForTesting;
private boolean isUseDeprecatedNdkFlag;
private boolean isDeprecatedNdkCompileLeaseExpired;
@OutputFile
public File getGeneratedMakefile() {
return generatedMakefile;
}
public void setGeneratedMakefile(File generatedMakefile) {
this.generatedMakefile = generatedMakefile;
}
@Input
public boolean isDebuggable() {
return debuggable;
}
public void setDebuggable(boolean debuggable) {
this.debuggable = debuggable;
}
@OutputDirectory
public File getSoFolder() {
return soFolder;
}
public void setSoFolder(File soFolder) {
this.soFolder = soFolder;
}
@OutputDirectory
public File getObjFolder() {
return objFolder;
}
public void setObjFolder(File objFolder) {
this.objFolder = objFolder;
}
@Optional
@Input
public File getNdkDirectory() {
return ndkDirectory;
}
public void setNdkDirectory(File ndkDirectory) {
this.ndkDirectory = ndkDirectory;
}
@Input
public boolean isNdkRenderScriptMode() {
return ndkRenderScriptMode;
}
public void setNdkRenderScriptMode(boolean ndkRenderScriptMode) {
this.ndkRenderScriptMode = ndkRenderScriptMode;
}
@Input
public boolean isNdkCygwinMode() {
return ndkCygwinMode;
}
public void setNdkCygwinMode(boolean ndkCygwinMode) {
this.ndkCygwinMode = ndkCygwinMode;
}
@Input
public boolean isForTesting() {
return isForTesting;
}
public void setForTesting(boolean forTesting) {
isForTesting = forTesting;
}
@Input
public boolean isDeprecatedNdkCompileLeaseExpired() {
return isDeprecatedNdkCompileLeaseExpired;
}
@Input
public boolean isUseDeprecatedNdkFlag() {
return isUseDeprecatedNdkFlag;
}
@SkipWhenEmpty
@InputFiles
public FileTree getSource() {
return sourceFolders.getAsFileTree();
}
private static String getAlternativesAndLeaseNotice(File generatedMakefile, String urlSuffix) {
return String.format(
getAlternatives(generatedMakefile, urlSuffix)
+ "To continue using the deprecated NDK compile for another %s days, "
+ "set \n"
+ "%s=%s in gradle.properties",
DEPRECATED_NDK_COMPILE_LEASE_DAYS,
DEPRECATED_NDK_COMPILE_LEASE.getPropertyName(),
NdkLease.getFreshDeprecatedNdkCompileLease());
}
@TaskAction
void taskAction(IncrementalTaskInputs inputs) throws IOException, ProcessException {
FileTree sourceFileTree = getSource();
Set<File> sourceFiles =
sourceFileTree.matching(new PatternSet().exclude("**/*.h")).getFiles();
File makefile = getGeneratedMakefile();
if (isUseDeprecatedNdkFlag) {
writeMakefile(sourceFiles, makefile);
throw new RuntimeException(
String.format(
"Error: Flag %s is no longer supported and will be removed in the next "
+ "version of Android Studio. Please switch to a supported "
+ "build system.\n%s",
BooleanOption.ENABLE_DEPRECATED_NDK.getPropertyName(),
getAlternativesAndLeaseNotice(makefile, "#ndkCompile")));
}
if (isDeprecatedNdkCompileLeaseExpired) {
writeMakefile(sourceFiles, makefile);
// Normally, we would catch the user when they try to configure the NDK, but NDK do
// not need to be configured by default. Throw this exception during task execution in
// case we miss it.
throw new RuntimeException(
"Error: Your project contains C++ files but it is not using a supported "
+ "native build system.\n"
+ getAlternatives(null, ""));
}
if (sourceFiles.isEmpty()) {
makefile.delete();
FileUtils.cleanOutputDir(getSoFolder());
FileUtils.cleanOutputDir(getObjFolder());
return;
}
if (ndkDirectory == null || !ndkDirectory.isDirectory()) {
throw new GradleException(
"NDK not configured.\n" +
"Download the NDK from http://developer.android.com/tools/sdk/ndk/." +
"Then add ndk.dir=path/to/ndk in local.properties.\n" +
"(On Windows, make sure you escape backslashes, e.g. C:\\\\ndk rather than C:\\ndk)");
}
final ReferenceHolder<Boolean> generateMakeFile = ReferenceHolder.of(false);
if (!inputs.isIncremental()) {
getLogger().info("Unable do incremental execution: full task run");
generateMakeFile.setValue(true);
FileUtils.cleanOutputDir(getSoFolder());
FileUtils.cleanOutputDir(getObjFolder());
} else {
// look for added or removed files *only*
inputs.outOfDate(new Action<InputFileDetails>() {
@Override
public void execute(InputFileDetails change) {
if (change.isAdded()) {
generateMakeFile.setValue(true);
}
}
});
inputs.removed(new Action<InputFileDetails>() {
@Override
public void execute(InputFileDetails change) {
generateMakeFile.setValue(true);
}
});
}
if (generateMakeFile.getValue()) {
writeMakefile(sourceFiles, makefile);
}
getLogger()
.warn(
"Warning: Deprecated NDK integration enabled by "
+ DEPRECATED_NDK_COMPILE_LEASE.getPropertyName()
+ " flag in gradle.properties will be removed from Android Gradle "
+ "plugin in the next version.\n"
+ getAlternatives(makefile, "#ndkCompile"));
// now build
runNdkBuild(ndkDirectory, makefile);
}
private void writeMakefile(@NonNull Set<File> sourceFiles, @NonNull File makefile)
throws IOException {
CoreNdkOptions ndk = getNdkConfig();
Preconditions.checkNotNull(ndk, "Ndk config should be set");
StringBuilder sb = new StringBuilder();
sb.append("LOCAL_PATH := $(call my-dir)\n" +
"include $(CLEAR_VARS)\n\n");
String moduleName = ndk.getModuleName() != null ? ndk.getModuleName() : getProject().getName();
if (isForTesting) {
moduleName = moduleName + "_test";
}
sb.append("LOCAL_MODULE := ").append(moduleName).append('\n');
if (ndk.getcFlags() != null) {
sb.append("LOCAL_CFLAGS := ").append(ndk.getcFlags()).append('\n');
}
// To support debugging from Android Studio.
sb.append("LOCAL_LDFLAGS := -Wl,--build-id\n");
List<String> fullLdlibs = Lists.newArrayList();
if (ndk.getLdLibs() != null) {
fullLdlibs.addAll(ndk.getLdLibs());
}
if (isNdkRenderScriptMode()) {
fullLdlibs.add("dl");
fullLdlibs.add("log");
fullLdlibs.add("jnigraphics");
fullLdlibs.add("RScpp_static");
}
if (!fullLdlibs.isEmpty()) {
sb.append("LOCAL_LDLIBS := \\\n");
for (String lib : fullLdlibs) {
sb.append("\t-l").append(lib).append(" \\\n");
}
sb.append('\n');
}
sb.append("LOCAL_SRC_FILES := \\\n");
for (File sourceFile : sourceFiles) {
sb.append('\t').append(sourceFile.getAbsolutePath()).append(" \\\n");
}
sb.append('\n');
for (File sourceFolder : sourceFolders.getFiles()) {
sb.append("LOCAL_C_INCLUDES += ").append(sourceFolder.getAbsolutePath()).append('\n');
}
if (isNdkRenderScriptMode()) {
sb.append("LOCAL_LDFLAGS += -L$(call host-path,$(TARGET_C_INCLUDES)/../lib/rs)\n");
sb.append("LOCAL_C_INCLUDES += $(TARGET_C_INCLUDES)/rs/cpp\n");
sb.append("LOCAL_C_INCLUDES += $(TARGET_C_INCLUDES)/rs\n");
sb.append("LOCAL_C_INCLUDES += $(TARGET_OBJS)/$(LOCAL_MODULE)\n");
}
sb.append("\ninclude $(BUILD_SHARED_LIBRARY)\n");
Files.write(sb.toString(), makefile, Charsets.UTF_8);
}
private void runNdkBuild(@NonNull File ndkLocation, @NonNull File makefile)
throws ProcessException {
CoreNdkOptions ndk = getNdkConfig();
ProcessInfoBuilder builder = new ProcessInfoBuilder();
String exe = ndkLocation.getAbsolutePath() + File.separator + "ndk-build";
if (CURRENT_PLATFORM == PLATFORM_WINDOWS && !ndkCygwinMode) {
exe += ".cmd";
}
builder.setExecutable(exe);
builder.addArgs(
"NDK_PROJECT_PATH=null",
"APP_BUILD_SCRIPT=" + makefile.getAbsolutePath());
// target
IAndroidTarget target = getBuilder().getTarget();
if (!target.isPlatform()) {
target = target.getParent();
}
builder.addArgs("APP_PLATFORM=" + target.hashString());
// temp out
builder.addArgs("NDK_OUT=" + getObjFolder().getAbsolutePath());
// libs out
builder.addArgs("NDK_LIBS_OUT=" + getSoFolder().getAbsolutePath());
// debug builds
if (isDebuggable()) {
builder.addArgs("NDK_DEBUG=1");
}
if (ndk.getStl() != null) {
builder.addArgs("APP_STL=" + ndk.getStl());
}
Set<String> abiFilters = ndk.getAbiFilters();
if (abiFilters != null && !abiFilters.isEmpty()) {
if (abiFilters.size() == 1) {
builder.addArgs("APP_ABI=" + abiFilters.iterator().next());
} else {
Joiner joiner = Joiner.on(',').skipNulls();
builder.addArgs("APP_ABI=" + joiner.join(abiFilters.iterator()));
}
} else {
builder.addArgs("APP_ABI=all");
}
if (ndk.getJobs() != null) {
builder.addArgs("-j" + ndk.getJobs());
}
ProcessOutputHandler handler = new LoggedProcessOutputHandler(getBuilder().getLogger());
getBuilder().executeProcess(builder.createProcess(), handler)
.rethrowFailure().assertNormalExitValue();
}
private boolean isNdkOptionUnset() {
// If none of the NDK options are set, then it is likely that NDK is not configured.
return (getModuleName() == null &&
getcFlags() == null &&
getLdLibs() == null &&
getAbiFilters() == null &&
getStl() == null);
}
public static class ConfigAction implements TaskConfigAction<NdkCompile> {
@NonNull private final VariantScope variantScope;
public ConfigAction(@NonNull VariantScope variantScope) {
this.variantScope = variantScope;
}
@NonNull
@Override
public String getName() {
return variantScope.getTaskName("compile", "Ndk");
}
@NonNull
@Override
public Class<NdkCompile> getType() {
return NdkCompile.class;
}
@Override
public void execute(@NonNull NdkCompile ndkCompile) {
final BaseVariantData variantData = variantScope.getVariantData();
ndkCompile.setAndroidBuilder(variantScope.getGlobalScope().getAndroidBuilder());
ndkCompile.setVariantName(variantData.getName());
ndkCompile.setNdkDirectory(
variantScope.getGlobalScope().getSdkHandler().getNdkFolder());
ndkCompile.setForTesting(variantData.getType().isForTesting());
ndkCompile.isUseDeprecatedNdkFlag =
variantScope
.getGlobalScope()
.getProjectOptions()
.get(BooleanOption.ENABLE_DEPRECATED_NDK);
ndkCompile.isDeprecatedNdkCompileLeaseExpired =
NdkLease.isDeprecatedNdkCompileLeaseExpired(
variantScope.getGlobalScope().getProjectOptions());
variantData.ndkCompileTask = ndkCompile;
final GradleVariantConfiguration variantConfig = variantData.getVariantConfiguration();
if (Boolean.TRUE
.equals(variantConfig.getMergedFlavor().getRenderscriptNdkModeEnabled())) {
ndkCompile.setNdkRenderScriptMode(true);
} else {
ndkCompile.setNdkRenderScriptMode(false);
}
final Callable<Collection<File>> callable =
TaskInputHelper.bypassFileCallable(
() -> {
Collection<File> sourceList = variantConfig.getJniSourceList();
if (Boolean.TRUE.equals(
variantConfig
.getMergedFlavor()
.getRenderscriptNdkModeEnabled())) {
sourceList.add(
variantData.renderscriptCompileTask
.getSourceOutputDir());
}
return sourceList;
});
ndkCompile.sourceFolders = variantScope.getGlobalScope().getProject().files(callable);
ndkCompile.setGeneratedMakefile(
new File(
variantScope.getGlobalScope().getIntermediatesDir(),
"ndk/"
+ variantData.getVariantConfiguration().getDirName()
+ "/Android.mk"));
ndkCompile.setNdkConfig(variantConfig.getNdkConfig());
ndkCompile.setDebuggable(variantConfig.getBuildType().isJniDebuggable());
ndkCompile.setObjFolder(
new File(
variantScope.getGlobalScope().getIntermediatesDir(),
"ndk/" + variantData.getVariantConfiguration().getDirName() + "/obj"));
Collection<File> ndkSoFolder = variantScope.getNdkSoFolder();
if (ndkSoFolder != null && !ndkSoFolder.isEmpty()) {
ndkCompile.setSoFolder(ndkSoFolder.iterator().next());
}
}
}
}