/* * 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.util; import com.android.SdkConstants; import com.android.builder.model.*; import com.android.ide.common.repository.GradleCoordinate; import com.android.sdklib.repository.FullRevision; import com.android.tools.idea.gradle.IdeaAndroidProject; import com.android.tools.idea.gradle.facet.AndroidGradleFacet; import com.android.tools.idea.gradle.project.ChooseGradleHomeDialog; import com.android.tools.idea.templates.TemplateManager; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.intellij.icons.AllIcons; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.ExtensionPoint; import com.intellij.openapi.externalSystem.model.ProjectSystemId; import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.options.Configurable; import com.intellij.openapi.options.ConfigurableEP; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.KeyValue; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; 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.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.tree.IElementType; import com.intellij.util.Processor; import com.intellij.util.SystemProperties; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.net.HttpConfigurable; import icons.AndroidIcons; import org.gradle.StartParameter; import org.gradle.wrapper.PathAssembler; import org.gradle.wrapper.WrapperConfiguration; import org.gradle.wrapper.WrapperExecutor; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.sdk.AndroidSdkData; import org.jetbrains.android.sdk.AndroidSdkUtils; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.gradle.service.GradleInstallationManager; import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings; import org.jetbrains.plugins.gradle.settings.GradleProjectSettings; import org.jetbrains.plugins.gradle.settings.GradleSettings; import org.jetbrains.plugins.gradle.util.GradleConstants; import org.jetbrains.plugins.groovy.lang.lexer.GroovyLexer; import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes; import javax.swing.*; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.android.SdkConstants.DOT_GRADLE; import static com.android.SdkConstants.FD_GRADLE_WRAPPER; import static com.android.SdkConstants.GRADLE_LATEST_VERSION; import static com.android.tools.idea.startup.AndroidStudioSpecificInitializer.GRADLE_DAEMON_TIMEOUT_MS; import static org.gradle.wrapper.WrapperExecutor.DISTRIBUTION_URL_PROPERTY; import static org.jetbrains.plugins.gradle.util.GradleUtil.getLastUsedGradleHome; /** * Utilities related to Gradle. */ public final class GradleUtil { @NonNls public static final String BUILD_DIR_DEFAULT_NAME = "build"; /** The name of the gradle wrapper executable associated with the current OS. */ @NonNls public static final String GRADLE_WRAPPER_EXECUTABLE_NAME = SystemInfo.isWindows ? SdkConstants.FN_GRADLE_WRAPPER_WIN : SdkConstants.FN_GRADLE_WRAPPER_UNIX; @NonNls private static final String GRADLE_EXECUTABLE_NAME = SystemInfo.isWindows ? SdkConstants.FN_GRADLE_WIN : SdkConstants.FN_GRADLE_UNIX; @NonNls public static final String GRADLEW_PROPERTIES_PATH = FileUtil.join(SdkConstants.FD_GRADLE_WRAPPER, SdkConstants.FN_GRADLE_WRAPPER_PROPERTIES); private static final Logger LOG = Logger.getInstance(GradleUtil.class); private static final Pattern GRADLE_JAR_NAME_PATTERN = Pattern.compile("gradle-(.*)-(.*)\\.jar"); private static final ProjectSystemId SYSTEM_ID = GradleConstants.SYSTEM_ID; /** * Finds characters that shouldn't be used in the Gradle path. * * I was unable to find any specification for Gradle paths. In my * experiments, Gradle only failed with slashes. This list may grow if * we find any other unsupported characters. */ public static final CharMatcher ILLEGAL_GRADLE_PATH_CHARS_MATCHER = CharMatcher.anyOf("\\/"); public static final Pattern GRADLE_DISTRIBUTION_URL_PATTERN = Pattern.compile(".*-([^-]+)-([^.]+).zip"); private GradleUtil() { } /** * This is temporary, until the model returns more outputs per artifact. * Deprecating since the model 0.13 provides multiple outputs per artifact if split apks are enabled. */ @Deprecated @NotNull public static AndroidArtifactOutput getOutput(@NotNull AndroidArtifact artifact) { Collection<AndroidArtifactOutput> outputs = artifact.getOutputs(); assert !outputs.isEmpty(); AndroidArtifactOutput output = ContainerUtil.getFirstItem(outputs); assert output != null; return output; } @NotNull public static Icon getModuleIcon(@NotNull Module module) { AndroidProject androidProject = getAndroidProject(module); if (androidProject != null) { return androidProject.isLibrary() ? AndroidIcons.LibraryModule : AndroidIcons.AppModule; } return Projects.isGradleProject(module.getProject()) ? AllIcons.Nodes.PpJdk : AllIcons.Nodes.Module; } @Nullable public static AndroidProject getAndroidProject(@NotNull Module module) { AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null) { IdeaAndroidProject androidProject = facet.getIdeaAndroidProject(); if (androidProject != null) { return androidProject.getDelegate(); } } return null; } /** * Returns the Gradle "logical" path (using colons as separators) if the given module represents a Gradle project or sub-project. * * @param module the given module. * @return the Gradle path for the given module, or {@code null} if the module does not represent a Gradle project or sub-project. */ @Nullable public static String getGradlePath(@NotNull Module module) { AndroidGradleFacet facet = AndroidGradleFacet.getInstance(module); return facet != null ? facet.getConfiguration().GRADLE_PROJECT_PATH : null; } /** * Returns the library dependencies in the given variant. This method checks dependencies in the "main" and "instrumentation tests" * artifacts. The dependency lookup is not transitive (only direct dependencies are returned.) * * @param variant the given variant. * @return the library dependencies in the given variant. */ @NotNull public static List<AndroidLibrary> getDirectLibraryDependencies(@NotNull Variant variant) { List<AndroidLibrary> libraries = Lists.newArrayList(); libraries.addAll(variant.getMainArtifact().getDependencies().getLibraries()); AndroidArtifact testArtifact = IdeaAndroidProject.findInstrumentationTestArtifact(variant); if (testArtifact != null) { libraries.addAll(testArtifact.getDependencies().getLibraries()); } return libraries; } @Nullable public static Module findModuleByGradlePath(@NotNull Project project, @NotNull String gradlePath) { ModuleManager moduleManager = ModuleManager.getInstance(project); for (Module module : moduleManager.getModules()) { AndroidGradleFacet gradleFacet = AndroidGradleFacet.getInstance(module); if (gradleFacet != null) { if (gradlePath.equals(gradleFacet.getConfiguration().GRADLE_PROJECT_PATH)) { return module; } } } return null; } @NotNull public static List<String> getPathSegments(@NotNull String gradlePath) { return Lists.newArrayList(Splitter.on(SdkConstants.GRADLE_PATH_SEPARATOR).omitEmptyStrings().split(gradlePath)); } @Nullable public static VirtualFile getGradleBuildFile(@NotNull Module module) { AndroidGradleFacet gradleFacet = AndroidGradleFacet.getInstance(module); if (gradleFacet != null && gradleFacet.getGradleProject() != null) { return gradleFacet.getGradleProject().getBuildFile(); } // At the time we're called, module.getModuleFile() may be null, but getModuleFilePath returns the path where it will be created. File moduleFilePath = new File(module.getModuleFilePath()); return getGradleBuildFile(moduleFilePath.getParentFile()); } @Nullable public static VirtualFile getGradleBuildFile(@NotNull File rootDir) { File gradleBuildFilePath = getGradleBuildFilePath(rootDir); return VfsUtil.findFileByIoFile(gradleBuildFilePath, true); } @NotNull public static File getGradleBuildFilePath(@NotNull File rootDir) { return new File(rootDir, SdkConstants.FN_BUILD_GRADLE); } @Nullable public static VirtualFile getGradleSettingsFile(@NotNull File rootDir) { File gradleSettingsFilePath = getGradleSettingsFilePath(rootDir); return VfsUtil.findFileByIoFile(gradleSettingsFilePath, true); } @NotNull public static File getGradleSettingsFilePath(@NotNull File rootDir) { return new File(rootDir, SdkConstants.FN_SETTINGS_GRADLE); } @NotNull public static File getGradleWrapperPropertiesFilePath(@NotNull File projectRootDir) { return new File(projectRootDir, GRADLEW_PROPERTIES_PATH); } /** * Updates the 'distributionUrl' in the given Gradle wrapper properties file. * * @param gradleVersion the Gradle version to update the property to. * @param propertiesFile the given Gradle wrapper properties file. * @return {@code true} if the property was updated, or {@code false} if no update was necessary because the property already had the * correct value. * @throws IOException if something goes wrong when saving the file. */ public static boolean updateGradleDistributionUrl(@NotNull String gradleVersion, @NotNull File propertiesFile) throws IOException { Properties properties = PropertiesUtil.getProperties(propertiesFile); String gradleDistributionUrl = getGradleDistributionUrl(gradleVersion, false); String property = properties.getProperty(DISTRIBUTION_URL_PROPERTY); if (property != null && (property.equals(gradleDistributionUrl) || property.equals(getGradleDistributionUrl(gradleVersion, true)))) { return false; } properties.setProperty(DISTRIBUTION_URL_PROPERTY, gradleDistributionUrl); PropertiesUtil.savePropertiesToFile(properties, propertiesFile, null); return true; } @Nullable public static String getGradleWrapperVersion(@NotNull File propertiesFile) throws IOException { Properties properties = PropertiesUtil.getProperties(propertiesFile); String url = properties.getProperty(DISTRIBUTION_URL_PROPERTY); if (url == null) { return null; } Matcher m = GRADLE_DISTRIBUTION_URL_PATTERN.matcher(url); if (m.matches()) { return m.group(1); } return null; } @NotNull private static String getGradleDistributionUrl(@NotNull String gradleVersion, boolean binOnly) { String suffix = binOnly ? "bin" : "all"; return String.format("http://services.gradle.org/distributions/gradle-%1$s-" + suffix + ".zip", gradleVersion); } @Nullable public static GradleExecutionSettings getGradleExecutionSettings(@NotNull Project project) { GradleProjectSettings projectSettings = getGradleProjectSettings(project); if (projectSettings == null) { String format = "Unable to obtain Gradle project settings for project '%1$s', located at '%2$s'"; String msg = String.format(format, project.getName(), FileUtil.toSystemDependentName(project.getBasePath())); LOG.info(msg); return null; } try { GradleExecutionSettings settings = ExternalSystemApiUtil.getExecutionSettings(project, projectSettings.getExternalProjectPath(), SYSTEM_ID); if (settings != null) { // By setting the Gradle daemon timeout to -1, we don't allow IDEA to set it to 1 minute. Gradle daemons need to be reused as // much as possible. The default timeout is 3 hours. settings.setRemoteProcessIdleTtlInMs(GRADLE_DAEMON_TIMEOUT_MS); } return settings; } catch (IllegalArgumentException e) { LOG.info("Failed to obtain Gradle execution settings", e); return null; } } @Nullable public static File findWrapperPropertiesFile(@NotNull Project project) { File baseDir = new File(project.getBasePath()); File wrapperPropertiesFile = getGradleWrapperPropertiesFilePath(baseDir); return wrapperPropertiesFile.isFile() ? wrapperPropertiesFile : null; } @Nullable public static GradleProjectSettings getGradleProjectSettings(@NotNull Project project) { GradleSettings settings = (GradleSettings)ExternalSystemApiUtil.getSettings(project, SYSTEM_ID); GradleSettings.MyState state = settings.getState(); assert state != null; Set<GradleProjectSettings> allProjectsSettings = state.getLinkedExternalProjectsSettings(); return getFirstNotNull(allProjectsSettings); } @Nullable private static GradleProjectSettings getFirstNotNull(@Nullable Set<GradleProjectSettings> allProjectSettings) { if (allProjectSettings != null) { for (GradleProjectSettings settings : allProjectSettings) { if (settings != null) { return settings; } } } return null; } @NotNull public static List<String> getGradleInvocationJvmArgs(@NotNull File projectDir, @Nullable BuildMode buildMode) { if (ExternalSystemApiUtil.isInProcessMode(SYSTEM_ID)) { List<String> args = Lists.newArrayList(); if (!AndroidGradleSettings.isAndroidSdkDirInLocalPropertiesFile(projectDir)) { AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk(); if (sdkData != null) { String arg = AndroidGradleSettings.createAndroidHomeJvmArg(sdkData.getLocation().getPath()); args.add(arg); } } List<KeyValue<String, String>> proxyProperties = HttpConfigurable.getJvmPropertiesList(false, null); for (KeyValue<String, String> proxyProperty : proxyProperties) { String arg = AndroidGradleSettings.createJvmArg(proxyProperty.getKey(), proxyProperty.getValue()); args.add(arg); } String arg = getGradleInvocationJvmArg(buildMode); if (arg != null) { args.add(arg); } return args; } return Collections.emptyList(); } @VisibleForTesting @Nullable static String getGradleInvocationJvmArg(@Nullable BuildMode buildMode) { if (BuildMode.ASSEMBLE_TRANSLATE == buildMode) { return AndroidGradleSettings.createJvmArg(GradleBuilds.ENABLE_TRANSLATION_JVM_ARG, true); } return null; } public static void stopAllGradleDaemons(boolean interactive) throws IOException { File gradleHome = findAnyGradleHome(interactive); if (gradleHome == null) { throw new FileNotFoundException("Unable to find path to Gradle home directory"); } File gradleExecutable = new File(gradleHome, "bin" + File.separatorChar + GRADLE_EXECUTABLE_NAME); if (!gradleExecutable.isFile()) { throw new FileNotFoundException("Unable to find Gradle executable: " + gradleExecutable.getPath()); } new ProcessBuilder(gradleExecutable.getPath(), "--stop").start(); } @Nullable public static File findAnyGradleHome(boolean interactive) { // Try cheapest option first: String lastUsedGradleHome = getLastUsedGradleHome(); if (!lastUsedGradleHome.isEmpty()) { File path = new File(lastUsedGradleHome); if (isValidGradleHome(path)) { return path; } } ProjectManager projectManager = ProjectManager.getInstance(); for (Project project : projectManager.getOpenProjects()) { File gradleHome = findGradleHome(project); if (gradleHome != null) { return gradleHome; } } if (interactive) { ChooseGradleHomeDialog chooseGradleHomeDialog = new ChooseGradleHomeDialog(); chooseGradleHomeDialog.setTitle("Choose Gradle Installation"); String description = "A Gradle installation is necessary to stop all daemons.\n" + "Please select the home directory of a Gradle installation, otherwise the project won't be closed."; chooseGradleHomeDialog.setDescription(description); if (!chooseGradleHomeDialog.showAndGet()) { return null; } String enteredPath = chooseGradleHomeDialog.getEnteredGradleHomePath(); File gradleHomePath = new File(enteredPath); if (isValidGradleHome(gradleHomePath)) { chooseGradleHomeDialog.storeLastUsedGradleHome(); return gradleHomePath; } } return null; } @Nullable private static File findGradleHome(@NotNull Project project) { GradleExecutionSettings settings = getGradleExecutionSettings(project); if (settings != null) { String gradleHome = settings.getGradleHome(); if (!Strings.isNullOrEmpty(gradleHome)) { File path = new File(gradleHome); if (isValidGradleHome(path)) { return path; } } } File wrapperPropertiesFile = findWrapperPropertiesFile(project); if (wrapperPropertiesFile != null) { WrapperExecutor wrapperExecutor = WrapperExecutor.forWrapperPropertiesFile(wrapperPropertiesFile, new StringBuilder()); WrapperConfiguration configuration = wrapperExecutor.getConfiguration(); File gradleHome = getGradleHome(project, configuration); if (gradleHome != null) { return gradleHome; } } return null; } @Nullable private static File getGradleHome(@NotNull Project project, @NotNull WrapperConfiguration configuration) { File systemHomePath = StartParameter.DEFAULT_GRADLE_USER_HOME; if ("PROJECT".equals(configuration.getDistributionBase())) { systemHomePath = new File(project.getBasePath(), SdkConstants.DOT_GRADLE); } if (!systemHomePath.isDirectory()) { return null; } PathAssembler.LocalDistribution localDistribution = new PathAssembler(systemHomePath).getDistribution(configuration); File distributionPath = localDistribution.getDistributionDir(); if (distributionPath != null) { File[] children = FileUtil.notNullize(distributionPath.listFiles()); for (File child : children) { if (child.isDirectory() && child.getName().startsWith("gradle-") && isValidGradleHome(child)) { return child; } } } return null; } private static boolean isValidGradleHome(@NotNull File path) { return path.isDirectory() && ServiceManager.getService(GradleInstallationManager.class).isGradleSdkHome(path); } /** * Convert a Gradle project name into a system dependent path relative to root project. Please note this is the default mapping from a * Gradle "logical" path to a physical path. Users can override this mapping in settings.gradle and this mapping may not always be * accurate. * <p/> * E.g. ":module" becomes "module" and ":directory:module" is converted to "directory/module" */ @NotNull public static String getDefaultPhysicalPathFromGradlePath(@NotNull String name) { List<String> segments = getPathSegments(name); return FileUtil.join(segments.toArray(new String[segments.size()])); } /** * Obtain default path for the Gradle subproject with the given name in the project. */ @NotNull public static File getDefaultSubprojectLocation(@NotNull VirtualFile project, @NotNull String gradlePath) { assert gradlePath.length() > 0; String relativePath = getDefaultPhysicalPathFromGradlePath(gradlePath); return new File(VfsUtilCore.virtualToIoFile(project), relativePath); } /** * Prefixes string with colon if there isn't one already there. */ @Nullable @Contract("null -> null;!null -> !null") public static String makeAbsolute(String string) { if (string == null) { return null; } else if (string.trim().length() == 0) { return ":"; } else if (!string.startsWith(":")) { return ":" + string.trim(); } else { return string.trim(); } } /** * Tests if the Gradle path is valid and return index of the offending * character or -1 if none. * <p/> */ public static int isValidGradlePath(@NotNull String gradlePath) { return ILLEGAL_GRADLE_PATH_CHARS_MATCHER.indexIn(gradlePath); } /** * Checks if the project already has a module with given Gradle path. */ public static boolean hasModule(@Nullable Project project, @NotNull String gradlePath, boolean checkProjectFolder) { if (project == null) { return false; } for (Module module : ModuleManager.getInstance(project).getModules()) { if (gradlePath.equals(getGradlePath(module))) { return true; } } if (checkProjectFolder) { File location = getDefaultSubprojectLocation(project.getBaseDir(), gradlePath); if (location.isFile()) { return true; } else if (location.isDirectory()) { File[] children = location.listFiles(); return children == null || children.length > 0; } else { return false; } } else { return false; } } public static void cleanUpPreferences(@NotNull ExtensionPoint<ConfigurableEP<Configurable>> preferences, @NotNull List<String> bundlesToRemove) { List<ConfigurableEP<Configurable>> nonStudioExtensions = Lists.newArrayList(); ConfigurableEP<Configurable>[] extensions = preferences.getExtensions(); for (ConfigurableEP<Configurable> extension : extensions) { if (bundlesToRemove.contains(extension.instanceClass)) { nonStudioExtensions.add(extension); } } for (ConfigurableEP<Configurable> toRemove : nonStudioExtensions) { preferences.unregisterExtension(toRemove); } } /** * Attempts to figure out the Gradle version of the given distribution. * * @param gradleHomePath the path of the directory containing the Gradle distribution. * @return the Gradle version of the given distribution, or {@code null} if it was not possible to obtain the version. */ @Nullable public static FullRevision getGradleVersion(@NotNull File gradleHomePath) { File libDirPath = new File(gradleHomePath, "lib"); for (File child : FileUtil.notNullize(libDirPath.listFiles())) { FullRevision version = getGradleVersionFromJar(child); if (version != null) { return version; } } return null; } @VisibleForTesting @Nullable static FullRevision getGradleVersionFromJar(@NotNull File libraryJarFile) { String fileName = libraryJarFile.getName(); Matcher matcher = GRADLE_JAR_NAME_PATTERN.matcher(fileName); if (matcher.matches()) { // Obtain the version of Gradle from a library name (e.g. "gradle-core-2.0.jar") String version = matcher.group(2); try { return FullRevision.parseRevision(version); } catch (NumberFormatException e) { LOG.warn(String.format("Unable to parse version '%1$s' (obtained from file '%2$s')", version, fileName)); } } return null; } /** * Creates the Gradle wrapper in the project at the given directory. * * @param projectDirPath the project's root directory. * @param gradleVersion the version of Gradle to use. If not specified, this method will use the latest supported version of Gradle. * @return {@code true} if the project already has the wrapper or the wrapper was successfully created; {@code false} if the wrapper was * not created (e.g. the template files for the wrapper were not found.) * @throws IOException any unexpected I/O error. * * @see com.android.SdkConstants#GRADLE_LATEST_VERSION */ public static boolean createGradleWrapper(@NotNull File projectDirPath, @Nullable String gradleVersion) throws IOException { File projectWrapperDirPath = new File(projectDirPath, FD_GRADLE_WRAPPER); if (!projectWrapperDirPath.isDirectory()) { File wrapperSrcDirPath = new File(TemplateManager.getTemplateRootFolder(), FD_GRADLE_WRAPPER); if (!wrapperSrcDirPath.exists()) { for (File root : TemplateManager.getExtraTemplateRootFolders()) { wrapperSrcDirPath = new File(root, FD_GRADLE_WRAPPER); if (wrapperSrcDirPath.exists()) { break; } else { wrapperSrcDirPath = null; } } } if (wrapperSrcDirPath == null) { return false; } FileUtil.copyDirContent(wrapperSrcDirPath, projectDirPath); } File wrapperPropertiesFile = getGradleWrapperPropertiesFilePath(projectDirPath); String version = gradleVersion != null ? gradleVersion : GRADLE_LATEST_VERSION; updateGradleDistributionUrl(version, wrapperPropertiesFile); return true; } @Nullable public static String getSupportedGradleVersion(@NotNull Project project) { FullRevision modelVersion = getResolvedAndroidGradleModelVersion(project); if (modelVersion != null) { return getSupportedGradleVersion(modelVersion); } return null; } @Nullable public static String getSupportedGradleVersion(@NotNull FullRevision modelVersion) { if (modelVersion.getMajor() == 0) { return modelVersion.getMinor() >= 13 ? SdkConstants.GRADLE_LATEST_VERSION : SdkConstants.GRADLE_MINIMUM_VERSION; } return null; } /** * Finds the version of the Android Gradle plug-in being used in the given project. * <p> * The version is returned as it is specified in build files if it does not use "+" notation. * </p> * <p> * If the version is using "+" notation for the "micro" portion, this method replaces the "+" with a zero. For example: "0.13.+" will be * returned as "0.13.0". In practice, the micro portion of the version is not used. * </p> * <p> * If the version in build files is "+" or uses "+" for the major or minor portions, this method will find the latest version in the local * Gradle cache. * </p> * * @param project the given project. * @return the version of the Android Gradle plug-in being used in the given project. (or an approximation.) */ @Nullable public static FullRevision getResolvedAndroidGradleModelVersion(@NotNull Project project) { VirtualFile baseDir = project.getBaseDir(); if (baseDir == null) { // This is default project. return null; } final Ref<FullRevision> modelVersionRef = new Ref<FullRevision>(); VfsUtil.processFileRecursivelyWithoutIgnored(baseDir, new Processor<VirtualFile>() { @Override public boolean process(VirtualFile virtualFile) { if (SdkConstants.FN_BUILD_GRADLE.equals(virtualFile.getName())) { File fileToCheck = VfsUtilCore.virtualToIoFile(virtualFile); try { String contents = FileUtil.loadFile(fileToCheck); FullRevision version = getResolvedAndroidGradleModelVersion(contents); if (version != null) { modelVersionRef.set(version); return false; // we found the model version. Stop. } } catch (IOException e) { LOG.warn("Failed to read contents of " + fileToCheck.getPath()); } } return true; } }); return modelVersionRef.get(); } @VisibleForTesting @Nullable static FullRevision getResolvedAndroidGradleModelVersion(@NotNull String fileContents) { GradleCoordinate found = null; GroovyLexer lexer = new GroovyLexer(); lexer.start(fileContents); while (lexer.getTokenType() != null) { IElementType type = lexer.getTokenType(); if (type == GroovyTokenTypes.mSTRING_LITERAL) { String text = StringUtil.unquoteString(lexer.getTokenText()); if (text.startsWith(SdkConstants.GRADLE_PLUGIN_NAME)) { found = GradleCoordinate.parseCoordinateString(text); if (found != null) { break; } } } lexer.advance(); } if (found != null) { String revision = getAndroidGradleModelVersion(found); if (StringUtil.isNotEmpty(revision)) { try { return FullRevision.parseRevision(revision); } catch (NumberFormatException ignored) { } } } return null; } @Nullable private static String getAndroidGradleModelVersion(@NotNull GradleCoordinate coordinate) { String revision = coordinate.getFullRevision(); if (StringUtil.isNotEmpty(revision)) { if (!coordinate.acceptsGreaterRevisions()) { return revision; } // For the Android plug-in we don't care about the micro version. Major and minor only matter. int major = coordinate.getMajorVersion(); int minor = coordinate.getMinorVersion(); if (coordinate.getMicroVersion() == -1 && major >= 0 && minor > 0) { return major + "." + minor + "." + 0; } } GradleCoordinate latest = findLatestVersionInGradleCache(coordinate, null); return latest != null ? latest.getFullRevision() : null; } @Nullable public static GradleCoordinate findLatestVersionInGradleCache(@NotNull GradleCoordinate original, @Nullable String filter) { List<GradleCoordinate> coordinates = Lists.newArrayList(); File gradleCache = new File(SystemProperties.getUserHome(), FileUtil.join(DOT_GRADLE, "caches")); if (gradleCache.exists()) { String groupId = original.getGroupId(); String artifactId = original.getArtifactId(); for (File moduleDir : FileUtil.notNullize(gradleCache.listFiles())) { if (!moduleDir.getName().startsWith("modules-") || !moduleDir.isDirectory()) { continue; } for (File metadataDir : FileUtil.notNullize(moduleDir.listFiles())) { if (!metadataDir.getName().startsWith("metadata-") || !metadataDir.isDirectory()) { continue; } File versionDir = new File(metadataDir, FileUtil.join("descriptors", groupId, artifactId)); if (!versionDir.isDirectory()) { continue; } for (File version : FileUtil.notNullize(versionDir.listFiles())) { String name = version.getName(); if ((filter == null || name.startsWith(filter)) && !name.isEmpty() && Character.isDigit(name.charAt(0))) { GradleCoordinate found = GradleCoordinate.parseCoordinateString(groupId + ":" + artifactId + ":" + name); if (found != null) { coordinates.add(found); } } } } } if (!coordinates.isEmpty()) { Collections.sort(coordinates, GradleCoordinate.COMPARE_PLUS_LOWER); return coordinates.get(coordinates.size() - 1); } } return null; } }