blob: 0ee1b89040f24cec16d3e9a1a7cbb46a7c22dc7c [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.tools.idea.gradle.project;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.Variant;
import com.android.sdklib.repository.FullRevision;
import com.android.tools.idea.gradle.*;
import com.android.tools.idea.gradle.util.AndroidGradleSettings;
import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.sdk.IdeSdks;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.execution.configurations.SimpleJavaParameters;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.externalSystem.util.Order;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.KeyValue;
import com.intellij.util.containers.ContainerUtil;
import org.gradle.tooling.model.GradleProject;
import org.gradle.tooling.model.gradle.GradleScript;
import org.gradle.tooling.model.idea.IdeaModule;
import org.jetbrains.android.AndroidPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.model.BuildScriptClasspathModel;
import org.jetbrains.plugins.gradle.model.ModuleExtendedModel;
import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension;
import java.io.File;
import java.io.IOException;
import java.util.*;
import static com.android.SdkConstants.FN_SETTINGS_GRADLE;
import static com.android.SdkConstants.GRADLE_PLUGIN_RECOMMENDED_VERSION;
import static com.android.builder.model.AndroidProject.*;
import static com.android.tools.idea.gradle.AndroidProjectKeys.*;
import static com.android.tools.idea.gradle.IdeaGradleProject.newIdeaGradleProject;
import static com.android.tools.idea.gradle.IdeaJavaProject.newJavaProject;
import static com.android.tools.idea.gradle.project.GradleModelVersionCheck.isSupportedVersion;
import static com.android.tools.idea.gradle.project.SdkSync.syncIdeAndProjectAndroidSdks;
import static com.android.tools.idea.gradle.service.notification.errors.UnsupportedModelVersionErrorHandler.READ_MIGRATION_GUIDE_MSG;
import static com.android.tools.idea.gradle.service.notification.errors.UnsupportedModelVersionErrorHandler.UNSUPPORTED_MODEL_VERSION_ERROR_PREFIX;
import static com.android.tools.idea.gradle.service.notification.hyperlink.SyncProjectWithExtraCommandLineOptionsHyperlink.EXTRA_GRADLE_COMMAND_LINE_OPTIONS_KEY;
import static com.android.tools.idea.gradle.util.AndroidGradleSettings.ANDROID_HOME_JVM_ARG;
import static com.android.tools.idea.gradle.util.AndroidGradleSettings.createProjectProperty;
import static com.android.tools.idea.gradle.util.GradleBuilds.BUILD_SRC_FOLDER_NAME;
import static com.android.tools.idea.gradle.util.GradleUtil.GRADLE_SYSTEM_ID;
import static com.android.tools.idea.gradle.util.GradleUtil.addLocalMavenRepoInitScriptCommandLineOption;
import static com.android.tools.idea.startup.AndroidStudioSpecificInitializer.isAndroidStudio;
import static com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil.isInProcessMode;
import static com.intellij.openapi.util.io.FileUtil.filesEqual;
import static com.intellij.openapi.util.io.FileUtil.toSystemDependentName;
import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
import static com.intellij.util.ArrayUtil.toStringArray;
import static com.intellij.util.ExceptionUtil.getRootCause;
import static com.intellij.util.PathUtil.getJarPathForClass;
import static com.intellij.util.containers.ContainerUtil.addIfNotNull;
import static java.util.Collections.sort;
import static org.jetbrains.android.AndroidPlugin.isGuiTestingMode;
/**
* Imports Android-Gradle projects into IDEA.
*/
@Order(ExternalSystemConstants.UNORDERED)
public class AndroidGradleProjectResolver extends AbstractProjectResolverExtension {
/** Default test artifact selected when importing a project. */
private static final String DEFAULT_TEST_ARTIFACT = AndroidProject.ARTIFACT_ANDROID_TEST;
@NotNull private final ProjectImportErrorHandler myErrorHandler;
@SuppressWarnings("UnusedDeclaration")
public AndroidGradleProjectResolver() {
this(new ProjectImportErrorHandler());
}
@VisibleForTesting
AndroidGradleProjectResolver(@NotNull ProjectImportErrorHandler errorHandler) {
myErrorHandler = errorHandler;
}
@NotNull
@Override
public ModuleData createModule(@NotNull IdeaModule gradleModule, @NotNull ProjectData projectData) {
AndroidProject androidProject = resolverCtx.getExtraProject(gradleModule, AndroidProject.class);
if (androidProject != null && !isSupportedVersion(androidProject)) {
String msg = getUnsupportedModelVersionErrorMsg(GradleModelVersionCheck.getModelVersion(androidProject));
throw new IllegalStateException(msg);
}
return nextResolver.createModule(gradleModule, projectData);
}
@Override
public void populateModuleContentRoots(@NotNull IdeaModule gradleModule, @NotNull DataNode<ModuleData> ideModule) {
ImportedModule importedModule = new ImportedModule(gradleModule);
ideModule.createChild(AndroidProjectKeys.IMPORTED_MODULE, importedModule);
GradleProject gradleProject = gradleModule.getGradleProject();
GradleScript buildScript = null;
try {
buildScript = gradleProject.getBuildScript();
} catch (UnsupportedOperationException ignore) {}
if (buildScript == null || !inAndroidGradleProject(gradleModule)) {
nextResolver.populateModuleContentRoots(gradleModule, ideModule);
return;
}
File moduleFilePath = new File(toSystemDependentName(ideModule.getData().getModuleFilePath()));
File moduleRootDirPath = moduleFilePath.getParentFile();
AndroidProject androidProject = resolverCtx.getExtraProject(gradleModule, AndroidProject.class);
boolean androidProjectWithoutVariants = false;
if (androidProject != null) {
Variant selectedVariant = getVariantToSelect(androidProject);
if (selectedVariant == null) {
// If an Android project does not have variants, it would be impossible to build. This is a possible but invalid use case.
// For now we are going to treat this case as a Java library module, because everywhere in the IDE (e.g. run configurations,
// editors, test support, variants tool window, project building, etc.) we have the assumption that there is at least one variant
// per Android project, and changing that in the code base is too risky, for very little benefit.
// See https://code.google.com/p/android/issues/detail?id=170722
androidProjectWithoutVariants = true;
}
else {
IdeaAndroidProject androidModel = new IdeaAndroidProject(GRADLE_SYSTEM_ID, gradleModule.getName(), moduleRootDirPath,
androidProject, selectedVariant.getName(), DEFAULT_TEST_ARTIFACT);
ideModule.createChild(IDE_ANDROID_PROJECT, androidModel);
}
}
File gradleSettingsFile = new File(moduleRootDirPath, FN_SETTINGS_GRADLE);
if (gradleSettingsFile.isFile() && androidProject == null) {
// This is just a root folder for a group of Gradle projects. We don't set an IdeaGradleProject so the JPS builder won't try to
// compile it using Gradle. We still need to create the module to display files inside it.
createJavaProject(gradleModule, ideModule, false);
return;
}
BuildScriptClasspathModel buildScriptModel = resolverCtx.getExtraProject(BuildScriptClasspathModel.class);
String gradleVersion = buildScriptModel != null ? buildScriptModel.getGradleVersion() : null;
File buildFilePath = buildScript.getSourceFile();
IdeaGradleProject ideaGradleProject = newIdeaGradleProject(gradleModule.getName(), gradleProject, buildFilePath, gradleVersion);
ideModule.createChild(IDE_GRADLE_PROJECT, ideaGradleProject);
if (androidProject == null || androidProjectWithoutVariants) {
// This is a Java lib module.
createJavaProject(gradleModule, ideModule, androidProjectWithoutVariants);
}
}
private void createJavaProject(@NotNull IdeaModule gradleModule,
@NotNull DataNode<ModuleData> ideModule,
boolean androidProjectWithoutVariants) {
ModuleExtendedModel model = resolverCtx.getExtraProject(gradleModule, ModuleExtendedModel.class);
IdeaJavaProject javaProject = newJavaProject(gradleModule, model, androidProjectWithoutVariants);
ideModule.createChild(IDE_JAVA_PROJECT, javaProject);
}
@Override
public void populateModuleCompileOutputSettings(@NotNull IdeaModule gradleModule, @NotNull DataNode<ModuleData> ideModule) {
if (!inAndroidGradleProject(gradleModule)) {
nextResolver.populateModuleCompileOutputSettings(gradleModule, ideModule);
}
}
@Override
public void populateModuleDependencies(@NotNull IdeaModule gradleModule,
@NotNull DataNode<ModuleData> ideModule,
@NotNull DataNode<ProjectData> ideProject) {
if (!inAndroidGradleProject(gradleModule)) {
// For plain Java projects (non-Gradle) we let the framework populate dependencies
nextResolver.populateModuleDependencies(gradleModule, ideModule, ideProject);
}
}
// Indicates it is an "Android" project if at least one module has an AndroidProject.
private boolean inAndroidGradleProject(@NotNull IdeaModule gradleModule) {
if (!resolverCtx.findModulesWithModel(AndroidProject.class).isEmpty()) {
return true;
}
if (BUILD_SRC_FOLDER_NAME.equals(gradleModule.getGradleProject().getName()) && isAndroidStudio()) {
// For now, we will "buildSrc" to be considered part of an Android project. We need changes in IDEA to make this distinction better.
// Currently, when processing "buildSrc" we don't have access to the rest of modules in the project, making it impossible to tell
// if the project has at least one Android module.
return true;
}
return false;
}
@Override
@NotNull
public Set<Class> getExtraProjectModelClasses() {
return Sets.<Class>newHashSet(AndroidProject.class);
}
@Override
public void preImportCheck() {
if (isGuiTestingMode()) {
// We use this task in GUI tests to simulate errors coming from Gradle project sync.
Application application = ApplicationManager.getApplication();
Runnable task = application.getUserData(AndroidPlugin.EXECUTE_BEFORE_PROJECT_SYNC_TASK_IN_GUI_TEST_KEY);
if (task != null) {
application.putUserData(AndroidPlugin.EXECUTE_BEFORE_PROJECT_SYNC_TASK_IN_GUI_TEST_KEY, null);
task.run();
}
}
if (isAndroidStudio()) {
LocalProperties localProperties = getLocalProperties();
// Ensure that Android Studio and the project (local.properties) point to the same Android SDK home. If they are not the same, we'll
// ask the user to choose one and updates either the IDE's default SDK or project's SDK based on the user's choice.
syncIdeAndProjectAndroidSdks(localProperties);
}
}
@Override
@NotNull
public List<KeyValue<String, String>> getExtraJvmArgs() {
if (isInProcessMode(GRADLE_SYSTEM_ID)) {
List<KeyValue<String, String>> args = Lists.newArrayList();
if (!isAndroidStudio()) {
LocalProperties localProperties = getLocalProperties();
if (localProperties.getAndroidSdkPath() == null) {
File androidHomePath = IdeSdks.getAndroidSdkPath();
// In Android Studio, the Android SDK home path will never be null. It may be null when running in IDEA.
if (androidHomePath != null) {
args.add(KeyValue.create(ANDROID_HOME_JVM_ARG, androidHomePath.getPath()));
}
}
}
return args;
}
return Collections.emptyList();
}
@NotNull
@Override
public List<String> getExtraCommandLineArgs() {
List<String> args = Lists.newArrayList();
Project project = findProject();
if (project != null) {
String[] commandLineOptions = project.getUserData(EXTRA_GRADLE_COMMAND_LINE_OPTIONS_KEY);
if (commandLineOptions != null) {
project.putUserData(EXTRA_GRADLE_COMMAND_LINE_OPTIONS_KEY, null);
Collections.addAll(args, commandLineOptions);
}
}
args.add(createProjectProperty(PROPERTY_BUILD_MODEL_ONLY, true));
args.add(createProjectProperty(PROPERTY_BUILD_MODEL_ONLY_ADVANCED, true));
args.add(createProjectProperty(PROPERTY_INVOKED_FROM_IDE, true));
if (isGuiTestingMode()) {
// We store the command line args, the GUI test will later on verify that the correct values were passed to the sync process.
ApplicationManager.getApplication().putUserData(AndroidPlugin.GRADLE_SYNC_COMMAND_LINE_OPTIONS_KEY, toStringArray(args));
}
addLocalMavenRepoInitScriptCommandLineOption(args);
return args;
}
@Nullable
private Project findProject() {
String projectDir = resolverCtx.getProjectPath();
if (isNotEmpty(projectDir)) {
File projectDirPath = new File(toSystemDependentName(projectDir));
Project[] projects = ProjectManager.getInstance().getOpenProjects();
for (Project project : projects) {
String basePath = project.getBasePath();
if (basePath != null) {
File currentPath = new File(basePath);
if (filesEqual(projectDirPath, currentPath)) {
return project;
}
}
}
}
return null;
}
@NotNull
private LocalProperties getLocalProperties() {
File projectDir = new File(toSystemDependentName(resolverCtx.getProjectPath()));
try {
return new LocalProperties(projectDir);
}
catch (IOException e) {
String msg = String.format("Unable to read local.properties file in project '%1$s'", projectDir.getPath());
throw new ExternalSystemException(msg, e);
}
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored") // Studio complains that the exceptions created by this method are never thrown.
@NotNull
@Override
public ExternalSystemException getUserFriendlyError(@NotNull Throwable error,
@NotNull String projectPath,
@Nullable String buildFilePath) {
String msg = error.getMessage();
if (msg != null && !msg.contains(UNSUPPORTED_MODEL_VERSION_ERROR_PREFIX)) {
Throwable rootCause = getRootCause(error);
if (rootCause instanceof ClassNotFoundException) {
msg = rootCause.getMessage();
// Project is using an old version of Gradle (and most likely an old version of the plug-in.)
if ("org.gradle.api.artifacts.result.ResolvedComponentResult".equals(msg) ||
"org.gradle.api.artifacts.result.ResolvedModuleVersionResult".equals(msg)) {
return new ExternalSystemException("The project is using an unsupported version of Gradle.");
}
}
}
ExternalSystemException userFriendlyError = myErrorHandler.getUserFriendlyError(error, projectPath, buildFilePath);
return userFriendlyError != null ? userFriendlyError : nextResolver.getUserFriendlyError(error, projectPath, buildFilePath);
}
@NotNull
private static String getUnsupportedModelVersionErrorMsg(@Nullable FullRevision modelVersion) {
StringBuilder builder = new StringBuilder();
builder.append(UNSUPPORTED_MODEL_VERSION_ERROR_PREFIX);
String recommendedVersion = String.format("The recommended version is %1$s.", GRADLE_PLUGIN_RECOMMENDED_VERSION);
if (modelVersion != null) {
builder.append(String.format(" (%1$s).", modelVersion.toString()))
.append(" ")
.append(recommendedVersion);
if (modelVersion.getMajor() == 0 && modelVersion.getMinor() <= 8) {
builder.append("\n\nStarting with version 0.9.0 incompatible changes were introduced in the build language.\n")
.append(READ_MIGRATION_GUIDE_MSG)
.append(" to learn how to update your project.");
}
}
else {
builder.append(". ")
.append(recommendedVersion);
}
return builder.toString();
}
@Nullable
private static Variant getVariantToSelect(@NotNull AndroidProject androidProject) {
Collection<Variant> variants = androidProject.getVariants();
if (variants.size() == 1) {
Variant variant = ContainerUtil.getFirstItem(variants);
assert variant != null;
return variant;
}
// look for "debug" variant. This is just a little convenience for the user that has not created any additional flavors/build types.
// trying to match something else may add more complexity for little gain.
for (Variant variant : variants) {
if ("debug".equals(variant.getName())) {
return variant;
}
}
List<Variant> sortedVariants = Lists.newArrayList(variants);
sort(sortedVariants, new Comparator<Variant>() {
@Override
public int compare(Variant o1, Variant o2) {
return o1.getName().compareTo(o2.getName());
}
});
return sortedVariants.isEmpty() ? null : sortedVariants.get(0);
}
@Override
public void enhanceRemoteProcessing(@NotNull SimpleJavaParameters parameters) {
List<String> classPath = Lists.newArrayList();
// Android module jars
addIfNotNull(getJarPathForClass(getClass()), classPath);
// Android sdklib jar
addIfNotNull(getJarPathForClass(FullRevision.class), classPath);
// Android common jar
addIfNotNull(getJarPathForClass(AndroidGradleSettings.class), classPath);
// Android gradle model jar
addIfNotNull(getJarPathForClass(AndroidProject.class), classPath);
parameters.getClassPath().addAll(classPath);
}
}