/* * Copyright (C) 2014 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.sdklib.repository.FullRevision; import com.android.tools.idea.gradle.util.GradleUtil; import com.android.tools.idea.gradle.util.PropertiesUtil; import com.google.common.annotations.VisibleForTesting; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.tree.IElementType; import com.intellij.util.Processor; import org.gradle.wrapper.WrapperExecutor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.gradle.settings.DistributionType; import org.jetbrains.plugins.gradle.settings.GradleProjectSettings; import org.jetbrains.plugins.groovy.lang.lexer.GroovyLexer; import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes; import java.io.File; import java.io.IOException; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; final class PreSyncChecks { private static final Pattern GRADLE_DISTRIBUTION_URL_PATTERN = Pattern.compile("http://services\\.gradle\\.org/distributions/gradle-(.+)-(.+)\\.zip"); private static final Logger LOG = Logger.getInstance(PreSyncChecks.class); private static final String GRADLE_SYNC_MSG_TITLE = "Gradle Sync"; private PreSyncChecks() { } static boolean canSync(@NotNull Project project) { VirtualFile baseDir = project.getBaseDir(); if (baseDir == null) { // Unlikely to happen because it would mean this is the default project. return true; } try { if (hasEmptySettingsFile(project) && isMultiModuleProject(project)) { String msg = "The project seems to have more than one module (or sub-project) " + "but the file 'settings.gradle' does not specify any of them.\n\n" + "Click 'OK' to continue, but the sync operation may not finish due to a possible bug in Gradle."; int answer = Messages.showOkCancelDialog(project, msg, GRADLE_SYNC_MSG_TITLE, Messages.getQuestionIcon()); return answer == Messages.OK; } } catch (IOException e) { // Failed to read settings.gradle, ask user if she would like to continue. String msg = "Failed to read contents of settings.gradle file: " + e.getMessage() + ".\n" + "Would you like to continue? (Project sync may never stop if the file is empty.)"; int answer = Messages.showYesNoDialog(project, msg, GRADLE_SYNC_MSG_TITLE, Messages.getErrorIcon()); return answer == Messages.YES; } if (!ApplicationManager.getApplication().isUnitTestMode()) { // Don't check Gradle settings in unit tests. They should be set up properly. FullRevision modelVersion = GradleUtil.getResolvedAndroidGradleModelVersion(project); ensureCorrectGradleSettings(project, modelVersion); } return true; } @VisibleForTesting static boolean hasEmptySettingsFile(@NotNull Project project) throws IOException { File settingsFile = new File(project.getBasePath(), SdkConstants.FN_SETTINGS_GRADLE); if (!settingsFile.isFile()) { return false; } String text = FileUtil.loadFile(settingsFile); if (StringUtil.isEmptyOrSpaces(text)) { // empty file (maybe with spaces only) return true; } GroovyLexer lexer = new GroovyLexer(); lexer.start(text); while (lexer.getTokenType() != null) { IElementType type = lexer.getTokenType(); if (type == GroovyTokenTypes.mIDENT && "include".equals(lexer.getTokenText())) { // most likely this is a module (e.g. include ":app") return false; } lexer.advance(); } return true; } private static boolean isMultiModuleProject(@NotNull Project project) { VirtualFile baseDir = project.getBaseDir(); if (baseDir == null) { // At this point this should not happen. We already perform this check before getting here. return false; } final AtomicInteger buildFileCounter = new AtomicInteger(); VfsUtil.processFileRecursivelyWithoutIgnored(baseDir, new Processor<VirtualFile>() { @Override public boolean process(VirtualFile virtualFile) { if (SdkConstants.FN_BUILD_GRADLE.equals(virtualFile.getName())) { int count = buildFileCounter.addAndGet(1); if (count > 1) { return false; // We know this is multi-module project. Stop. } } return true; } }); return buildFileCounter.get() > 1; } private static void ensureCorrectGradleSettings(@NotNull Project project, @Nullable FullRevision modelVersion) { if (modelVersion == null || createWrapperIfNecessary(project, modelVersion)) { return; } GradleProjectSettings gradleSettings = GradleUtil.getGradleProjectSettings(project); File wrapperPropertiesFile = GradleUtil.findWrapperPropertiesFile(project); DistributionType distributionType = gradleSettings != null ? gradleSettings.getDistributionType() : null; boolean usingWrapper = (distributionType == null || distributionType == DistributionType.DEFAULT_WRAPPED) && wrapperPropertiesFile != null; if (usingWrapper) { attemptToUpdateGradleVersionInWrapper(wrapperPropertiesFile, modelVersion, project); } else if (distributionType == DistributionType.LOCAL) { attemptToUseSupportedLocalGradle(modelVersion, gradleSettings, project); } } // Returns true if wrapper was created or sync should continue immediately after executing this method. private static boolean createWrapperIfNecessary(@NotNull Project project, @Nullable FullRevision modelVersion) { GradleProjectSettings gradleSettings = GradleUtil.getGradleProjectSettings(project); if (gradleSettings == null) { // Unlikely to happen. When we get to this point we already created GradleProjectSettings. return true; } File wrapperPropertiesFile = GradleUtil.findWrapperPropertiesFile(project); if (wrapperPropertiesFile == null) { String gradleVersion = null; if (modelVersion != null) { gradleVersion = GradleUtil.getSupportedGradleVersion(modelVersion); } DistributionType distributionType = gradleSettings.getDistributionType(); boolean createWrapper = false; if (distributionType == null) { String msg = "Gradle settings for this project are not configured yet.\n\n" + "Would you like the project to use the Gradle wrapper?\n" + "(The wrapper will automatically download the latest supported Gradle version).\n\n" + "Click 'OK' to use the Gradle wrapper, or 'Cancel' to manually set the path of a local Gradle distribution."; int answer = Messages.showOkCancelDialog(project, msg, GRADLE_SYNC_MSG_TITLE, Messages.getQuestionIcon()); createWrapper = answer == Messages.OK; } else if (distributionType == DistributionType.DEFAULT_WRAPPED) { createWrapper = true; } if (createWrapper) { File projectDirPath = new File(project.getBasePath()); // attempt to delete the whole gradle wrapper folder. File gradleDirPath = new File(projectDirPath, SdkConstants.FD_GRADLE); if (!FileUtil.delete(gradleDirPath)) { // deletion failed. Let sync continue. return true; } try { GradleUtil.createGradleWrapper(projectDirPath, gradleVersion); if (distributionType == null) { gradleSettings.setDistributionType(DistributionType.DEFAULT_WRAPPED); } return true; } catch (IOException e) { LOG.info("Failed to create Gradle wrapper for project '" + project.getName() + "'", e); } } else if (distributionType == null) { ChooseGradleHomeDialog dialog = new ChooseGradleHomeDialog(gradleVersion); if (dialog.showAndGet()) { String enteredGradleHomePath = dialog.getEnteredGradleHomePath(); gradleSettings.setGradleHome(enteredGradleHomePath); gradleSettings.setDistributionType(DistributionType.LOCAL); return true; } } } return false; } private static void attemptToUpdateGradleVersionInWrapper(@NotNull final File wrapperPropertiesFile, @NotNull FullRevision modelVersion, @NotNull Project project) { if (modelVersion.getMajor() == 0 && modelVersion.getMinor() <=12) { // Do not perform this check for plug-in 0.12. It supports many versions of Gradle. // Let sync fail if using an unsupported Gradle versions. return; } Properties wrapperProperties = null; try { wrapperProperties = PropertiesUtil.getProperties(wrapperPropertiesFile); } catch (IOException e) { LOG.warn("Failed to read file " + wrapperPropertiesFile.getPath()); } if (wrapperProperties == null) { // There is a wrapper, but the Gradle version could not be read. Continue with sync. return; } String url = wrapperProperties.getProperty(WrapperExecutor.DISTRIBUTION_URL_PROPERTY); Matcher matcher = GRADLE_DISTRIBUTION_URL_PATTERN.matcher(url); if (!matcher.matches()) { // Could not get URL of Gradle distribution. Continue with sync. return; } String gradleVersion = matcher.group(1); FullRevision gradleRevision = FullRevision.parseRevision(gradleVersion); if (!isSupportedGradleVersion(modelVersion, gradleRevision)) { String newGradleVersion = GradleUtil.getSupportedGradleVersion(modelVersion); assert newGradleVersion != null; String msg = "Version " + modelVersion + " of the Android Gradle plug-in requires Gradle " + newGradleVersion + " or newer.\n\n" + "Click 'OK' to automatically update the Gradle version in the Gradle wrapper and continue."; Messages.showMessageDialog(project, msg, GRADLE_SYNC_MSG_TITLE, Messages.getQuestionIcon()); try { GradleUtil.updateGradleDistributionUrl(newGradleVersion, wrapperPropertiesFile); } catch (IOException e) { LOG.warn("Failed to update Gradle wrapper file to Gradle version " + newGradleVersion); } } } private static void attemptToUseSupportedLocalGradle(@NotNull FullRevision modelVersion, @NotNull GradleProjectSettings gradleSettings, @NotNull Project project) { String gradleHome = gradleSettings.getGradleHome(); FullRevision gradleVersion = null; boolean askToSwitchToWrapper = false; if (StringUtil.isEmpty(gradleHome)) { // Unable to obtain the path of the Gradle local installation. Continue with sync. askToSwitchToWrapper = true; } else { File gradleHomePath = new File(gradleHome); gradleVersion = GradleUtil.getGradleVersion(gradleHomePath); if (gradleVersion == null) { askToSwitchToWrapper = true; } } if (!askToSwitchToWrapper) { askToSwitchToWrapper = !isSupportedGradleVersion(modelVersion, gradleVersion); } if (askToSwitchToWrapper) { String newGradleVersion = GradleUtil.getSupportedGradleVersion(modelVersion); String msg = "Version " + modelVersion + " of the Android Gradle plug-in requires Gradle " + newGradleVersion + " or newer.\n" + "A local Gradle distribution was not found, or was not properly set in the IDE.\n\n" + "Would you like your project to use the Gradle wrapper instead?\n" + "(The wrapper will automatically download the latest supported Gradle version).\n\n" + "Click 'OK' to use the Gradle wrapper, or 'Cancel' to manually set the path of a local Gradle distribution."; int answer = Messages.showOkCancelDialog(project, msg, GRADLE_SYNC_MSG_TITLE, Messages.getQuestionIcon()); if (answer == Messages.OK) { try { File projectDirPath = new File(project.getBasePath()); GradleUtil.createGradleWrapper(projectDirPath, newGradleVersion); gradleSettings.setDistributionType(DistributionType.DEFAULT_WRAPPED); } catch (IOException e) { LOG.warn("Failed to update Gradle wrapper file to Gradle version " + newGradleVersion, e); } return; } ChooseGradleHomeDialog dialog = new ChooseGradleHomeDialog(newGradleVersion); dialog.setTitle(String.format("Please select the location of a Gradle distribution version %1$s or newer", newGradleVersion)); if (dialog.showAndGet()) { String enteredGradleHomePath = dialog.getEnteredGradleHomePath(); gradleSettings.setGradleHome(enteredGradleHomePath); } } } private static boolean isSupportedGradleVersion(@NotNull FullRevision modelVersion, @NotNull FullRevision gradleVersion) { String supported = GradleUtil.getSupportedGradleVersion(modelVersion); if (supported != null) { return gradleVersion.compareTo(FullRevision.parseRevision(supported)) == 0; } return false; } }