blob: d395e1e4670ba05cd9a4d3c682d9e64017cca208 [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.SdkConstants;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.Variant;
import com.android.sdklib.repository.FullRevision;
import com.android.tools.idea.gradle.AndroidProjectKeys;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.IdeaGradleProject;
import com.android.tools.idea.gradle.IdeaJavaProject;
import com.android.tools.idea.gradle.util.AndroidGradleSettings;
import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.sdk.DefaultSdks;
import com.android.tools.idea.startup.AndroidStudioSpecificInitializer;
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.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.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.externalSystem.util.Order;
import com.intellij.openapi.util.KeyValue;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import org.gradle.tooling.model.gradle.GradleScript;
import org.gradle.tooling.model.idea.IdeaModule;
import org.gradle.util.GradleVersion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.model.ModuleExtendedModel;
import org.jetbrains.plugins.gradle.service.project.AbstractProjectImportErrorHandler;
import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import java.io.File;
import java.io.IOException;
import java.util.*;
import static com.android.SdkConstants.GRADLE_MINIMUM_VERSION;
import static com.android.tools.idea.gradle.util.GradleBuilds.BUILD_SRC_FOLDER_NAME;
/**
* Imports Android-Gradle projects into IDEA.
*/
@Order(ExternalSystemConstants.UNORDERED)
public class AndroidGradleProjectResolver extends AbstractProjectResolverExtension {
/**
* These String constants are being used in {@link com.android.tools.idea.gradle.service.notification.GradleNotificationExtension} to add
* "quick-fix"/"help" hyperlinks to error messages. Given that the contract between the consumer and producer of error messages is pretty
* loose, please do not use these constants, to prevent any unexpected side effects during project sync.
*/
@NotNull public static final String UNSUPPORTED_MODEL_VERSION_ERROR_PREFIX =
"The project is using an unsupported version of the Android Gradle plug-in";
@NotNull public static final String UNABLE_TO_FIND_BUILD_FOLDER_ERROR_PREFIX = "Unable to find 'build folder for project";
@NotNull public static final String READ_MIGRATION_GUIDE_MSG = "Please read the migration guide";
@NotNull private final ProjectImportErrorHandler myErrorHandler;
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 && !GradleModelVersionCheck.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) {
GradleScript buildScript = null;
try {
buildScript = gradleModule.getGradleProject().getBuildScript();
} catch (UnsupportedOperationException ignore) {}
if (buildScript == null || !inAndroidGradleProject(gradleModule)) {
nextResolver.populateModuleContentRoots(gradleModule, ideModule);
return;
}
File moduleFilePath = new File(FileUtil.toSystemDependentName(ideModule.getData().getModuleFilePath()));
File moduleRootDirPath = moduleFilePath.getParentFile();
AndroidProject androidProject = resolverCtx.getExtraProject(gradleModule, AndroidProject.class);
if (androidProject != null) {
Variant selectedVariant = getVariantToSelect(androidProject);
IdeaAndroidProject ideaAndroidProject =
new IdeaAndroidProject(gradleModule.getName(), moduleRootDirPath, androidProject, selectedVariant.getName());
ideModule.createChild(AndroidProjectKeys.IDE_ANDROID_PROJECT, ideaAndroidProject);
}
File gradleSettingsFile = new File(moduleRootDirPath, SdkConstants.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);
return;
}
File buildFilePath = buildScript.getSourceFile();
IdeaGradleProject gradleProject = IdeaGradleProject.newIdeaGradleProject(gradleModule.getName(),
gradleModule.getGradleProject(), buildFilePath);
ideModule.createChild(AndroidProjectKeys.IDE_GRADLE_PROJECT, gradleProject);
if (androidProject == null) {
// This is a Java lib module.
createJavaProject(gradleModule, ideModule);
}
}
private void createJavaProject(@NotNull IdeaModule gradleModule, @NotNull DataNode<ModuleData> ideModule) {
ModuleExtendedModel model = resolverCtx.getExtraProject(gradleModule, ModuleExtendedModel.class);
IdeaJavaProject javaProject = new IdeaJavaProject(gradleModule, model);
ideModule.createChild(AndroidProjectKeys.IDE_JAVA_PROJECT, javaProject);
}
private void populateContentRootsForProjectModule(@NotNull IdeaModule gradleModule, @NotNull DataNode<ModuleData> ideModule) {
// We need to warn users that we were not able to undo the exclusion of the top-level build folder. The IDE will not work properly.
String msg = UNABLE_TO_FIND_BUILD_FOLDER_ERROR_PREFIX + String.format(" '%1$s'.\n", gradleModule.getProject().getName());
msg +=
"The IDE will not find references to the project's dependencies, and, as a result, basic functionality will not work properly.";
throw new IllegalArgumentException(msg);
}
@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()) && AndroidStudioSpecificInitializer.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 (AndroidStudioSpecificInitializer.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.
SdkSync.syncIdeAndProjectAndroidHomes(localProperties);
}
}
@Override
@NotNull
public List<KeyValue<String, String>> getExtraJvmArgs() {
if (ExternalSystemApiUtil.isInProcessMode(GradleConstants.SYSTEM_ID)) {
List<KeyValue<String, String>> args = Lists.newArrayList();
if (!AndroidStudioSpecificInitializer.isAndroidStudio()) {
LocalProperties localProperties = getLocalProperties();
if (localProperties.getAndroidSdkPath() == null) {
File androidHomePath = DefaultSdks.getDefaultAndroidHome();
// 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(AndroidGradleSettings.ANDROID_HOME_JVM_ARG, androidHomePath.getPath()));
}
}
}
return args;
}
return Collections.emptyList();
}
@NotNull
@Override
public List<String> getExtraCommandLineArgs() {
List<String> args = Lists.newArrayList();
args.add(AndroidGradleSettings.createProjectProperty(AndroidProject.PROPERTY_BUILD_MODEL_ONLY, true));
args.add(AndroidGradleSettings.createProjectProperty(AndroidProject.PROPERTY_INVOKED_FROM_IDE, true));
return args;
}
@NotNull
private LocalProperties getLocalProperties() {
File projectDir = new File(FileUtil.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 = ExceptionUtil.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)) {
GradleVersion supported = getGradleSupportedVersion();
return new ExternalSystemException(getUnsupportedGradleVersionErrorMsg(supported));
}
}
}
ExternalSystemException userFriendlyError = myErrorHandler.getUserFriendlyError(error, projectPath, buildFilePath);
return userFriendlyError != null ? userFriendlyError : nextResolver.getUserFriendlyError(error, projectPath, buildFilePath);
}
@NotNull
private static GradleVersion getGradleSupportedVersion() {
return GradleVersion.version(GRADLE_MINIMUM_VERSION);
}
@NotNull
private static String getUnsupportedModelVersionErrorMsg(@Nullable FullRevision modelVersion) {
StringBuilder builder = new StringBuilder();
builder.append(UNSUPPORTED_MODEL_VERSION_ERROR_PREFIX);
if (modelVersion != null) {
builder.append(String.format(" (%1$s)", modelVersion.toString()));
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.");
}
}
return builder.toString();
}
@NotNull
private static String getUnsupportedGradleVersionErrorMsg(@NotNull GradleVersion supportedVersion) {
String version = supportedVersion.getVersion();
return String.format("The project is using an unsupported version of Gradle. Please use version %1$s.\n", version) +
AbstractProjectImportErrorHandler.FIX_GRADLE_VERSION;
}
@NotNull
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);
Collections.sort(sortedVariants, new Comparator<Variant>() {
@Override
public int compare(Variant o1, Variant o2) {
return o1.getName().compareTo(o2.getName());
}
});
return sortedVariants.get(0);
}
@Override
public void enhanceRemoteProcessing(@NotNull SimpleJavaParameters parameters) {
final List<String> classPath = ContainerUtilRt.newArrayList();
// Android module jars
ContainerUtil.addIfNotNull(PathUtil.getJarPathForClass(getClass()), classPath);
// Android sdklib jar
ContainerUtil.addIfNotNull(PathUtil.getJarPathForClass(FullRevision.class), classPath);
// Android common jar
ContainerUtil.addIfNotNull(PathUtil.getJarPathForClass(AndroidGradleSettings.class), classPath);
// Android gradle model jar
ContainerUtil.addIfNotNull(PathUtil.getJarPathForClass(AndroidProject.class), classPath);
parameters.getClassPath().addAll(classPath);
}
}