blob: 85bb8c6f03d2a430e9f4edc65c752a3f43cc82be [file] [log] [blame]
/*
* Copyright (C) 2012 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;
import static com.android.builder.model.AndroidProject.FD_INTERMEDIATES;
import static com.google.common.base.Preconditions.checkState;
import static java.io.File.separator;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.build.gradle.internal.ApiObjectFactory;
import com.android.build.gradle.internal.BadPluginException;
import com.android.build.gradle.internal.DependencyManager;
import com.android.build.gradle.internal.ExecutionConfigurationUtil;
import com.android.build.gradle.internal.ExtraModelInfo;
import com.android.build.gradle.internal.LibraryCache;
import com.android.build.gradle.internal.LoggerWrapper;
import com.android.build.gradle.internal.NativeLibraryFactoryImpl;
import com.android.build.gradle.internal.NdkHandler;
import com.android.build.gradle.internal.SdkHandler;
import com.android.build.gradle.internal.TaskContainerAdaptor;
import com.android.build.gradle.internal.TaskManager;
import com.android.build.gradle.internal.VariantManager;
import com.android.build.gradle.internal.coverage.JacocoPlugin;
import com.android.build.gradle.internal.dsl.BuildType;
import com.android.build.gradle.internal.dsl.BuildTypeFactory;
import com.android.build.gradle.internal.dsl.ProductFlavor;
import com.android.build.gradle.internal.dsl.ProductFlavorFactory;
import com.android.build.gradle.internal.dsl.SigningConfig;
import com.android.build.gradle.internal.dsl.SigningConfigFactory;
import com.android.build.gradle.internal.model.ModelBuilder;
import com.android.build.gradle.internal.process.GradleJavaProcessExecutor;
import com.android.build.gradle.internal.process.GradleProcessExecutor;
import com.android.build.gradle.internal.profile.RecordingBuildListener;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.build.gradle.internal.variant.VariantFactory;
import com.android.build.gradle.tasks.JillTask;
import com.android.build.gradle.tasks.PreDex;
import com.android.builder.Version;
import com.android.builder.core.AndroidBuilder;
import com.android.builder.core.BuilderConstants;
import com.android.builder.internal.compiler.JackConversionCache;
import com.android.builder.internal.compiler.PreDexCache;
import com.android.builder.profile.ExecutionType;
import com.android.builder.profile.ProcessRecorderFactory;
import com.android.builder.profile.Recorder;
import com.android.builder.profile.ThreadRecorder;
import com.android.builder.sdk.TargetInfo;
import com.android.ide.common.internal.ExecutorSingleton;
import com.android.utils.ILogger;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.gradle.BuildListener;
import org.gradle.BuildResult;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
import org.gradle.api.execution.TaskExecutionGraph;
import org.gradle.api.execution.TaskExecutionGraphListener;
import org.gradle.api.initialization.Settings;
import org.gradle.api.invocation.Gradle;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.tasks.StopExecutionException;
import org.gradle.internal.reflect.Instantiator;
import org.gradle.tooling.BuildException;
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
/**
* Base class for all Android plugins
*/
public abstract class BasePlugin {
private static final String GRADLE_MIN_VERSION = "2.2";
public static final Pattern GRADLE_ACCEPTABLE_VERSIONS = Pattern.compile("2\\.[2-9].*");
private static final String GRADLE_VERSION_CHECK_OVERRIDE_PROPERTY =
"com.android.build.gradle.overrideVersionCheck";
private static final String SKIP_PATH_CHECK_PROPERTY =
"com.android.build.gradle.overridePathCheck";
/** default retirement age in days since its inception date for RC or beta versions. */
private static final int DEFAULT_RETIREMENT_AGE_FOR_NON_RELEASE_IN_DAYS = 40;
protected BaseExtension extension;
protected VariantManager variantManager;
protected TaskManager taskManager;
protected Project project;
protected SdkHandler sdkHandler;
private NdkHandler ndkHandler;
protected AndroidBuilder androidBuilder;
protected Instantiator instantiator;
protected VariantFactory variantFactory;
private ToolingModelBuilderRegistry registry;
private JacocoPlugin jacocoPlugin;
private LoggerWrapper loggerWrapper;
private ExtraModelInfo extraModelInfo;
private String creator;
private boolean hasCreatedTasks = false;
protected BasePlugin(Instantiator instantiator, ToolingModelBuilderRegistry registry) {
this.instantiator = instantiator;
this.registry = registry;
creator = "Android Gradle " + Version.ANDROID_GRADLE_PLUGIN_VERSION;
verifyRetirementAge();
ModelBuilder.clearCaches();
}
/**
* Verify that this plugin execution is within its public time range.
*/
private void verifyRetirementAge() {
Manifest manifest;
URLClassLoader cl = (URLClassLoader) getClass().getClassLoader();
try {
URL url = cl.findResource("META-INF/MANIFEST.MF");
manifest = new Manifest(url.openStream());
} catch (IOException ignore) {
return;
}
String inceptionDateAttr = manifest.getMainAttributes().getValue("Inception-Date");
// when running in unit tests, etc... the manifest entries are absent.
if (inceptionDateAttr == null) {
return;
}
List<String> items = ImmutableList.copyOf(Splitter.on(':').split(inceptionDateAttr));
GregorianCalendar inceptionDate = new GregorianCalendar(Integer.parseInt(items.get(0)),
Integer.parseInt(items.get(1)), Integer.parseInt(items.get(2)));
int retirementAgeInDays =
getRetirementAgeInDays(manifest.getMainAttributes().getValue("Plugin-Version"));
if (retirementAgeInDays == -1) {
return;
}
Calendar now = GregorianCalendar.getInstance();
long nowTimestamp = now.getTimeInMillis();
long inceptionTimestamp = inceptionDate.getTimeInMillis();
long days = TimeUnit.DAYS.convert(nowTimestamp - inceptionTimestamp, TimeUnit.MILLISECONDS);
if (days > retirementAgeInDays) {
// this plugin is too old.
String dailyOverride = System.getenv("ANDROID_DAILY_OVERRIDE");
final MessageDigest crypt;
try {
crypt = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
return;
}
crypt.reset();
// encode the day, not the current time.
try {
crypt.update(String.format("%1$s:%2$s:%3$s",
now.get(Calendar.YEAR),
now.get(Calendar.MONTH),
now.get(Calendar.DATE)).getBytes("utf8"));
} catch (UnsupportedEncodingException e) {
return;
}
String overrideValue = new BigInteger(1, crypt.digest()).toString(16);
if (dailyOverride == null) {
String message = "Plugin is too old, please update to a more recent version, or " +
"set ANDROID_DAILY_OVERRIDE environment variable to \"" + overrideValue + '"';
System.err.println(message);
throw new RuntimeException(message);
} else {
if (!dailyOverride.equals(overrideValue)) {
String message = "Plugin is too old and ANDROID_DAILY_OVERRIDE value is " +
"also outdated, please use new value :\"" + overrideValue + '"';
System.err.println(message);
throw new RuntimeException(message);
}
}
}
}
private static int getRetirementAgeInDays(@Nullable String version) {
if (version == null || version.contains("rc") || version.contains("beta")
|| version.contains("alpha")) {
return DEFAULT_RETIREMENT_AGE_FOR_NON_RELEASE_IN_DAYS;
}
return -1;
}
protected abstract Class<? extends BaseExtension> getExtensionClass();
protected abstract VariantFactory createVariantFactory();
protected abstract TaskManager createTaskManager(
Project project,
AndroidBuilder androidBuilder,
AndroidConfig extension,
SdkHandler sdkHandler,
DependencyManager dependencyManager,
ToolingModelBuilderRegistry toolingRegistry);
/**
* Return whether this plugin creates Android library. Should be overridden if true.
*/
protected boolean isLibrary() {
return false;
}
@VisibleForTesting
VariantManager getVariantManager() {
return variantManager;
}
protected ILogger getLogger() {
if (loggerWrapper == null) {
loggerWrapper = new LoggerWrapper(project.getLogger());
}
return loggerWrapper;
}
protected void apply(Project project) throws IOException {
this.project = project;
ExecutionConfigurationUtil.setThreadPoolSize(project);
checkPathForErrors();
checkModulesForErrors();
List<Recorder.Property> propertyList = Lists.newArrayList(
new Recorder.Property("plugin_version", Version.ANDROID_GRADLE_PLUGIN_VERSION),
new Recorder.Property("next_gen_plugin", "false"),
new Recorder.Property("gradle_version", project.getGradle().getGradleVersion())
);
String benchmarkName = AndroidGradleOptions.getBenchmarkName(project);
if (benchmarkName != null) {
propertyList.add(new Recorder.Property("benchmark_name", benchmarkName));
}
String benchmarkMode = AndroidGradleOptions.getBenchmarkMode(project);
if (benchmarkMode != null) {
propertyList.add(new Recorder.Property("benchmark_mode", benchmarkMode));
}
ProcessRecorderFactory.initialize(
getLogger(),
project.getRootProject().file("profiler" + System.currentTimeMillis() + ".json"),
propertyList);
project.getGradle().addListener(new RecordingBuildListener(ThreadRecorder.get()));
ThreadRecorder.get().record(ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE,
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
configureProject();
return null;
}
}, new Recorder.Property("project", project.getName()));
ThreadRecorder.get().record(ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSTION_CREATION,
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
createExtension();
return null;
}
}, new Recorder.Property("project", project.getName()));
ThreadRecorder.get().record(ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION,
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
createTasks();
return null;
}
}, new Recorder.Property("project", project.getName()));
}
protected void configureProject() {
checkGradleVersion();
extraModelInfo = new ExtraModelInfo(project, isLibrary());
sdkHandler = new SdkHandler(project, getLogger());
androidBuilder = new AndroidBuilder(
project == project.getRootProject() ? project.getName() : project.getPath(),
creator,
new GradleProcessExecutor(project),
new GradleJavaProcessExecutor(project),
extraModelInfo,
getLogger(),
isVerbose());
project.getPlugins().apply(JavaBasePlugin.class);
jacocoPlugin = project.getPlugins().apply(JacocoPlugin.class);
project.getTasks().getByName("assemble").setDescription(
"Assembles all variants of all applications and secondary packages.");
// call back on execution. This is called after the whole build is done (not
// after the current project is done).
// This is will be called for each (android) projects though, so this should support
// being called 2+ times.
project.getGradle().addBuildListener(new BuildListener() {
@Override
public void buildStarted(Gradle gradle) { }
@Override
public void settingsEvaluated(Settings settings) { }
@Override
public void projectsLoaded(Gradle gradle) { }
@Override
public void projectsEvaluated(Gradle gradle) { }
@Override
public void buildFinished(BuildResult buildResult) {
ExecutorSingleton.shutdown();
sdkHandler.unload();
ThreadRecorder.get().record(ExecutionType.BASE_PLUGIN_BUILD_FINISHED,
new Recorder.Block() {
@Override
public Void call() throws Exception {
PreDexCache.getCache().clear(
new File(project.getRootProject().getBuildDir(),
FD_INTERMEDIATES + "/dex-cache/cache.xml"),
getLogger());
JackConversionCache.getCache().clear(
new File(project.getRootProject().getBuildDir(),
FD_INTERMEDIATES + "/jack-cache/cache.xml"),
getLogger());
LibraryCache.getCache().unload();
return null;
}
}, new Recorder.Property("project", project.getName()));
try {
ProcessRecorderFactory.shutdown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
project.getGradle().getTaskGraph().addTaskExecutionGraphListener(
new TaskExecutionGraphListener() {
@Override
public void graphPopulated(TaskExecutionGraph taskGraph) {
for (Task task : taskGraph.getAllTasks()) {
if (task instanceof PreDex) {
PreDexCache.getCache().load(
new File(project.getRootProject().getBuildDir(),
FD_INTERMEDIATES + "/dex-cache/cache.xml"));
break;
} else if (task instanceof JillTask) {
JackConversionCache.getCache().load(
new File(project.getRootProject().getBuildDir(),
FD_INTERMEDIATES + "/jack-cache/cache.xml"));
break;
}
}
}
});
}
private void createExtension() {
final NamedDomainObjectContainer<BuildType> buildTypeContainer = project.container(
BuildType.class,
new BuildTypeFactory(instantiator, project, project.getLogger()));
final NamedDomainObjectContainer<ProductFlavor> productFlavorContainer = project.container(
ProductFlavor.class,
new ProductFlavorFactory(instantiator, project, project.getLogger()));
final NamedDomainObjectContainer<SigningConfig> signingConfigContainer = project.container(
SigningConfig.class,
new SigningConfigFactory(instantiator));
extension = project.getExtensions().create("android", getExtensionClass(),
project, instantiator, androidBuilder, sdkHandler,
buildTypeContainer, productFlavorContainer, signingConfigContainer,
extraModelInfo, isLibrary());
// create the default mapping configuration.
project.getConfigurations().create("default-mapping")
.setDescription("Configuration for default mapping artifacts.");
project.getConfigurations().create("default-metadata")
.setDescription("Metadata for the produced APKs.");
DependencyManager dependencyManager = new DependencyManager(project, extraModelInfo);
taskManager = createTaskManager(
project,
androidBuilder,
extension,
sdkHandler,
dependencyManager,
registry);
variantFactory = createVariantFactory();
variantManager = new VariantManager(
project,
androidBuilder,
extension,
variantFactory,
taskManager,
instantiator);
ndkHandler = new NdkHandler(
project.getRootDir(),
null, /* compileSkdVersion, this will be set in afterEvaluate */
"gcc",
"" /*toolchainVersion*/);
// Register a builder for the custom tooling model
ModelBuilder modelBuilder = new ModelBuilder(
androidBuilder,
variantManager,
taskManager,
extension,
extraModelInfo,
ndkHandler,
new NativeLibraryFactoryImpl(ndkHandler),
isLibrary());
registry.register(modelBuilder);
// map the whenObjectAdded callbacks on the containers.
signingConfigContainer.whenObjectAdded(new Action<SigningConfig>() {
@Override
public void execute(SigningConfig signingConfig) {
variantManager.addSigningConfig(signingConfig);
}
});
buildTypeContainer.whenObjectAdded(new Action<BuildType>() {
@Override
public void execute(BuildType buildType) {
SigningConfig signingConfig = signingConfigContainer.findByName(BuilderConstants.DEBUG);
buildType.init(signingConfig);
variantManager.addBuildType(buildType);
}
});
productFlavorContainer.whenObjectAdded(new Action<ProductFlavor>() {
@Override
public void execute(ProductFlavor productFlavor) {
variantManager.addProductFlavor(productFlavor);
}
});
// map whenObjectRemoved on the containers to throw an exception.
signingConfigContainer.whenObjectRemoved(
new UnsupportedAction("Removing signingConfigs is not supported."));
buildTypeContainer.whenObjectRemoved(
new UnsupportedAction("Removing build types is not supported."));
productFlavorContainer.whenObjectRemoved(
new UnsupportedAction("Removing product flavors is not supported."));
// create default Objects, signingConfig first as its used by the BuildTypes.
variantFactory.createDefaultComponents(
buildTypeContainer, productFlavorContainer, signingConfigContainer);
}
private static class UnsupportedAction implements Action<Object> {
private final String message;
UnsupportedAction(String message) {
this.message = message;
}
@Override
public void execute(Object o) {
throw new UnsupportedOperationException(message);
}
}
private void createTasks() {
ThreadRecorder.get().record(ExecutionType.TASK_MANAGER_CREATE_TASKS,
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
taskManager.createTasksBeforeEvaluate(
new TaskContainerAdaptor(project.getTasks()));
return null;
}
},
new Recorder.Property("project", project.getName()));
project.afterEvaluate(new Action<Project>() {
@Override
public void execute(Project project) {
ThreadRecorder.get().record(ExecutionType.BASE_PLUGIN_CREATE_ANDROID_TASKS,
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
createAndroidTasks(false);
return null;
}
},
new Recorder.Property("project", project.getName()));
}
});
}
private void checkGradleVersion() {
if (!GRADLE_ACCEPTABLE_VERSIONS.matcher(project.getGradle().getGradleVersion()).matches()) {
boolean allowNonMatching = Boolean.getBoolean(GRADLE_VERSION_CHECK_OVERRIDE_PROPERTY);
File file = new File("gradle" + separator + "wrapper" + separator +
"gradle-wrapper.properties");
String errorMessage = String.format(
"Gradle version %s is required. Current version is %s. " +
"If using the gradle wrapper, try editing the distributionUrl in %s " +
"to gradle-%s-all.zip",
GRADLE_MIN_VERSION, project.getGradle().getGradleVersion(), file.getAbsolutePath(),
GRADLE_MIN_VERSION);
if (allowNonMatching) {
getLogger().warning(errorMessage);
getLogger().warning("As %s is set, continuing anyways.",
GRADLE_VERSION_CHECK_OVERRIDE_PROPERTY);
} else {
throw new BuildException(errorMessage, null);
}
}
}
@VisibleForTesting
final void createAndroidTasks(boolean force) {
// Make sure unit tests set the required fields.
checkState(extension.getBuildToolsRevision() != null, "buildToolsVersion is not specified.");
checkState(extension.getCompileSdkVersion() != null, "compileSdkVersion is not specified.");
ndkHandler.setCompileSdkVersion(extension.getCompileSdkVersion());
// get current plugins and look for the default Java plugin.
if (project.getPlugins().hasPlugin(JavaPlugin.class)) {
throw new BadPluginException(
"The 'java' plugin has been applied, but it is not compatible with the Android plugins.");
}
ensureTargetSetup();
// don't do anything if the project was not initialized.
// Unless TEST_SDK_DIR is set in which case this is unit tests and we don't return.
// This is because project don't get evaluated in the unit test setup.
// See AppPluginDslTest
if (!force
&& (!project.getState().getExecuted() || project.getState().getFailure()!= null)
&& SdkHandler.sTestSdkFolder == null) {
return;
}
if (hasCreatedTasks) {
return;
}
hasCreatedTasks = true;
extension.disableWrite();
ThreadRecorder.get().record(
ExecutionType.GENERAL_CONFIG,
Recorder.EmptyBlock,
new Recorder.Property("build_tools_version",
extension.getBuildToolsRevision().toString()));
// setup SDK repositories.
for (final File file : sdkHandler.getSdkLoader().getRepositories()) {
project.getRepositories().maven(new Action<MavenArtifactRepository>() {
@Override
public void execute(MavenArtifactRepository mavenArtifactRepository) {
mavenArtifactRepository.setUrl(file.toURI());
}
});
}
taskManager.createMockableJarTask();
ThreadRecorder.get().record(ExecutionType.VARIANT_MANAGER_CREATE_ANDROID_TASKS,
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
variantManager.createAndroidTasks();
ApiObjectFactory apiObjectFactory = new ApiObjectFactory(
androidBuilder, extension, variantFactory, instantiator);
for (BaseVariantData variantData : variantManager.getVariantDataList()) {
apiObjectFactory.create(variantData);
}
return null;
}
}, new Recorder.Property("project", project.getName()));
}
private boolean isVerbose() {
return project.getLogger().isEnabled(LogLevel.INFO);
}
private void ensureTargetSetup() {
// check if the target has been set.
TargetInfo targetInfo = androidBuilder.getTargetInfo();
if (targetInfo == null) {
if (extension.getCompileOptions() == null) {
throw new GradleException("Calling getBootClasspath before compileSdkVersion");
}
sdkHandler.initTarget(
extension.getCompileSdkVersion(),
extension.getBuildToolsRevision(),
extension.getLibraryRequests(),
androidBuilder);
}
}
/**
* Check the sub-projects structure :
* So far, checks that 2 modules do not have the same identification (group+name).
*/
private void checkModulesForErrors() {
Project rootProject = project.getRootProject();
Map<String, Project> subProjectsById = new HashMap<String, Project>();
for (Project subProject : rootProject.getAllprojects()) {
String id = subProject.getGroup().toString() + ":" + subProject.getName();
if (subProjectsById.containsKey(id)) {
String message = String.format(
"Your project contains 2 or more modules with the same " +
"identification %1$s\n" +
"at \"%2$s\" and \"%3$s\".\n" +
"You must use different identification (either name or group) for " +
"each modules.",
id,
subProjectsById.get(id).getPath(),
subProject.getPath() );
throw new StopExecutionException(message);
} else {
subProjectsById.put(id, subProject);
}
}
}
private void checkPathForErrors() {
// See if the user disabled the check:
if (Boolean.getBoolean(SKIP_PATH_CHECK_PROPERTY)) {
return;
}
if (project.hasProperty(SKIP_PATH_CHECK_PROPERTY)
&& project.property(SKIP_PATH_CHECK_PROPERTY) instanceof String
&& Boolean.valueOf((String) project.property(SKIP_PATH_CHECK_PROPERTY))) {
return;
}
// See if we're on Windows:
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
return;
}
// See if the path contains non-ASCII characters.
if (CharMatcher.ASCII.matchesAllOf(project.getRootDir().getAbsolutePath())) {
return;
}
String message = "Your project path contains non-ASCII characters. This will most likely " +
"cause the build to fail on Windows. Please move your project to a different " +
"directory. See http://b.android.com/95744 for details. " +
"This warning can be disabled by using the command line flag -D" +
SKIP_PATH_CHECK_PROPERTY + "=true, or adding the line " +
SKIP_PATH_CHECK_PROPERTY + "=true' to gradle.properties file " +
"in the project directory.";
throw new StopExecutionException(message);
}
}