| /* |
| * 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.eclipse; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.ide.common.repository.GradleCoordinate; |
| import com.android.sdklib.repository.FullRevision; |
| import com.android.utils.SdkUtils; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.io.Files; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.*; |
| |
| /** Records information about the import to be presented to the user: |
| * <ul> |
| * <li>List of files *not* migrated</li> |
| * <li>Explain that the files were moved into the canonical gradle directory |
| * structure and explain what it is</li> |
| * <li>A summary of the file changes (files moved from where to where)</li> |
| * <li>Tips for things to do next (e.g. create signing configs, flavors, etc</li> |
| * <li>Warning if manifest merger was not enabled before AND there are libraries |
| * without empty manifests</li> |
| * <li>Warning if I've replaced a .jar with a dependency of unknown version</li> |
| * <li>TODO: End with a section of migration tips for Eclipse users (e.g. to not look |
| * for the Problems view, how to use Eclipse key bindings, etc.</li> |
| * </ul> |
| */ |
| public class ImportSummary { |
| static final String MSG_HEADER = "" |
| + "ECLIPSE ANDROID PROJECT IMPORT SUMMARY\n" |
| + "======================================\n"; |
| |
| static final String MSG_MANIFEST = "\n" |
| + "Manifest Merging:\n" |
| + "-----------------\n" |
| + "Your project uses libraries that provide manifests, and your Eclipse\n" |
| + "project did not explicitly turn on manifest merging. In Android Gradle\n" |
| + "projects, manifests are always merged (meaning that contents from your\n" |
| + "libraries' manifests will be merged into the app manifest. If you had\n" |
| + "manually copied contents from library manifests into your app manifest\n" |
| + "you may need to remove these for the app to build correctly.\n"; |
| |
| static final String MSG_UNHANDLED = "\n" |
| + "Ignored Files:\n" |
| + "--------------\n" |
| + "The following files were *not* copied into the new Gradle project; you\n" |
| + "should evaluate whether these are still needed in your project and if\n" |
| + "so manually move them:\n\n"; |
| |
| static final String MSG_REPLACED_JARS = "\n" |
| + "Replaced Jars with Dependencies:\n" |
| + "--------------------------------\n" |
| + "The importer recognized the following .jar files as third party\n" |
| + "libraries and replaced them with Gradle dependencies instead. This has\n" |
| + "the advantage that more explicit version information is known, and the\n" |
| + "libraries can be updated automatically. However, it is possible that\n" |
| + "the .jar file in your project was of an older version than the\n" |
| + "dependency we picked, which could render the project not compileable.\n" |
| + "You can disable the jar replacement in the import wizard and try again:\n\n"; |
| |
| static final String MSG_REPLACED_LIBS = "\n" |
| + "Replaced Libraries with Dependencies:\n" |
| + "-------------------------------------\n" |
| + "The importer recognized the following library projects as third party\n" |
| + "libraries and replaced them with Gradle dependencies instead. This has\n" |
| + "the advantage that more explicit version information is known, and the\n" |
| + "libraries can be updated automatically. However, it is possible that\n" |
| + "the source files in your project were of an older version than the\n" |
| + "dependency we picked, which could render the project not compileable.\n" |
| + "You can disable the library replacement in the import wizard and try\n" |
| + "again:\n\n"; |
| |
| static final String MSG_FOOTER = "\n" |
| + "Next Steps:\n" |
| + "-----------\n" |
| + "You can now build the project. The Gradle project needs network\n" |
| + "connectivity to download dependencies.\n" |
| + "\n" |
| + "Bugs:\n" |
| + "-----\n" |
| + "If for some reason your project does not build, and you determine that\n" |
| + "it is due to a bug or limitation of the Eclipse to Gradle importer,\n" |
| + "please file a bug at http://b.android.com with category\n" |
| + "Component-Tools.\n" |
| + "\n" |
| + "(This import summary is for your information only, and can be deleted\n" |
| + "after import once you are satisfied with the results.)\n"; |
| |
| static final String MSG_FOLDER_STRUCTURE = "\n" |
| + "Moved Files:\n" |
| + "------------\n" |
| + "Android Gradle projects use a different directory structure than ADT\n" |
| + "Eclipse projects. Here's how the projects were restructured:\n\n"; |
| |
| static final String MSG_MISSING_REPO_1 = "\n" |
| + "Missing Android Support Repository:\n" |
| + "-----------------------------------\n" |
| + "Some useful libraries, such as the Android Support Library, are\n" |
| + "installed from a special Maven repository, which should be installed\n" |
| + "via the SDK manager.\n" |
| + "\n" |
| + "It looks like this library is missing from your SDK installation at:\n"; |
| |
| static final String MSG_MISSING_REPO_2 = "\n" |
| + "To install it, open the SDK manager, and in the Extras category,\n" |
| + "select \"Android Support Repository\". You may also want to install the\n" |
| + "\"Google Repository\" if you want to use libraries like Google Play\n" |
| + "Services.\n"; |
| |
| static final String MSG_MISSING_GOOGLE_REPOSITORY_1 = "\n" |
| + "Missing Google Repository:\n" |
| + "--------------------------\n" |
| + "The Google Play Services library is installed from a special Maven\n" |
| + "Repository, which should be installed via the SDK manager.\n" |
| + "\n" |
| + "It looks like this library is missing from your SDK installation at:\n"; |
| |
| static final String MSG_MISSING_GOOGLE_REPOSITORY_2 = "\n" |
| + "To install it, open the SDK manager, and in the Extras category,\n" |
| + "select \"Google Repository\".\n"; |
| |
| static final String MSG_BUILD_TOOLS_VERSION = "\n" |
| + "Old Build Tools:\n" |
| + "----------------\n" |
| + "The version of the build tools installed with your SDK is old. It\n" |
| + "should be at least version 19.0.1 to work well with the Gradle build\n" |
| + "system. To update it, open the Android SDK Manager, and install the\n" |
| + "highest available version of Tools > Android SDK Build-tools.\n"; |
| |
| static final String MSG_GUESSED_VERSIONS = "\n" |
| + "Potentially Missing Dependency:\n" |
| + "-------------------------------\n" |
| + "When we replaced the following .jar files with a Gradle dependency, we\n" |
| + "inferred the dependency version number from the filename. This\n" |
| + "specific version may not actually be available from the repository.\n" |
| + "If you get a build error stating that the dependency is missing, edit\n" |
| + "the version number to for example \"+\" to pick up the latest version\n" |
| + "instead. (This may require you to update your code if the library APIs\n" |
| + "have changed.)\n\n"; |
| |
| static final String MSG_USER_HOME_PROGUARD = "\n" |
| + "Ignored Per-User ProGuard Configuration File:\n" |
| + "---------------------------------------------\n" |
| + "The ProGuard configuration in the imported project pointed to a\n" |
| + "ProGuard rule file in the current user's home directory. This is not\n" |
| + "supported from the Android Gradle build system (which emphasizes\n" |
| + "repeatable builds). If you want to share ProGuard rules between\n" |
| + "projects, use relative paths (from the project location) instead.\n"; |
| |
| static final String MSG_RISKY_PROJECT_LOCATION = "\n" |
| + "Risky Project Location:\n" |
| + "-----------------------\n" |
| + "The tools *should* handle project locations in any directory. However,\n" |
| + "due to bugs, placing projects in directories containing spaces in the\n" |
| + "path, or characters like \", ' and &, have had issues. We're working to\n" |
| + "eliminate these bugs, but to save yourself headaches you may want to\n" |
| + "move your project to a location where this is not a problem.\n"; |
| |
| private final GradleImport myImporter; |
| private File myDestDir; |
| private boolean myManifestsMayDiffer; |
| private Map<String, List<String>> myNotMigrated = Maps.newHashMap(); |
| private Map<ImportModule, Map<File, File>> myMoved = Maps.newHashMap(); |
| private Map<File, GradleCoordinate> myJarDependencies = Maps.newHashMap(); |
| private Map<String, List<GradleCoordinate>> myLibDependencies = Maps.newHashMap(); |
| private List<String> myGuessedDependencyVersions = Lists.newArrayList(); |
| private File myLastGuessedJar; |
| private List<String> myIgnoredUserHomeProGuardFiles = Lists.newArrayList(); |
| private boolean myHasRiskyPathChars; |
| private boolean myWrapErrorMessages = true; |
| |
| ImportSummary(@NonNull GradleImport importer) { |
| myImporter = importer; |
| } |
| |
| private static boolean isRiskyPathChar(char c) { |
| return (c == ' ' || c == '\'' || c == '"' || c == '&'); |
| } |
| |
| /** |
| * Writes the summary to the given file. The file should be in a directory which |
| * has already been created by the caller. |
| */ |
| public void write(@NonNull File file) throws IOException { |
| String summary = createSummary(); |
| assert file.getParentFile().exists(); |
| Files.write(summary, file, Charsets.UTF_8); |
| } |
| |
| public void setDestDir(File destDir) { |
| myDestDir = destDir; |
| |
| myHasRiskyPathChars = false; |
| String path = destDir.getPath(); |
| for (int i = 0, n = path.length(); i < n; i++) { |
| char c = path.charAt(i); |
| if (isRiskyPathChar(c)) { |
| myHasRiskyPathChars = true; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| void setWrapErrorMessages(boolean wrap) { |
| myWrapErrorMessages = wrap; |
| } |
| |
| public void reportManifestsMayDiffer() { |
| myManifestsMayDiffer = true; |
| } |
| |
| public void reportReplacedJar(@NonNull File jar, @NonNull GradleCoordinate dependency) { |
| myJarDependencies.put(jar, dependency); |
| if (jar.equals(myLastGuessedJar)) { |
| boolean replaced = myGuessedDependencyVersions.remove(jar.getName()); |
| if (replaced) { |
| myGuessedDependencyVersions.add(jar.getName() + " => version " + |
| dependency.getFullRevision() + " in " + dependency.toString()); |
| } |
| myLastGuessedJar = null; |
| } |
| } |
| |
| public void reportReplacedLib(@NonNull String module, @NonNull List<GradleCoordinate> dependencies) { |
| myLibDependencies.put(module, dependencies); |
| } |
| |
| public void reportGuessedVersion(@NonNull File jar) { |
| myGuessedDependencyVersions.add(jar.getName()); |
| myLastGuessedJar = jar; |
| } |
| |
| public void reportIgnoredUserHomeProGuardFile(@NonNull String relativePath) { |
| myIgnoredUserHomeProGuardFiles.add(relativePath); |
| } |
| |
| public void reportMoved(@NonNull ImportModule module, @NonNull File from, @NonNull File to) { |
| Map<File, File> map = myMoved.get(module); |
| if (map == null) { |
| map = new LinkedHashMap<File, File>(); // preserve insert order |
| myMoved.put(module, map); |
| } |
| map.put(from, to); |
| } |
| |
| /** |
| * Reports an ignored relative path. (We use a path string rather than a file since |
| * we want to include a trailing file separator on directories; these relative paths are |
| * not interpreted in any way other than to display in the report.) |
| */ |
| public void reportIgnored(@NonNull String module, @NonNull String path) { |
| List<String> list = myNotMigrated.get(module); |
| if (list == null) { |
| list = Lists.newArrayList(); |
| myNotMigrated.put(module, list); |
| } |
| list.add(path); |
| } |
| |
| /** |
| * Provides the summary |
| */ |
| @NonNull |
| public String createSummary() { |
| StringBuilder sb = new StringBuilder(2000); |
| sb.append(MSG_HEADER); |
| |
| List<String> problems = Lists.newArrayList(); |
| problems.addAll(myImporter.getErrors()); |
| problems.addAll(myImporter.getWarnings()); |
| if (!problems.isEmpty()) { |
| sb.append("\n"); |
| for (String warning : problems) { |
| sb.append(" * "); |
| if (myWrapErrorMessages) { |
| sb.append(SdkUtils.wrap(warning, 80, " ")); |
| } |
| else { |
| sb.append(warning); |
| } |
| sb.append("\n"); |
| } |
| } |
| |
| if (myHasRiskyPathChars) { |
| sb.append(MSG_RISKY_PROJECT_LOCATION); |
| String path = myDestDir.getPath(); |
| sb.append(path).append("\n"); |
| for (int i = 0, n = path.length(); i < n; i++) { |
| char c = path.charAt(i); |
| sb.append(isRiskyPathChar(c) ? '-' : ' '); |
| } |
| sb.append("\n"); |
| } |
| |
| if (myManifestsMayDiffer) { |
| sb.append(MSG_MANIFEST); |
| } |
| |
| if (!myNotMigrated.isEmpty()) { |
| sb.append(MSG_UNHANDLED); |
| List<String> modules = Lists.newArrayList(myNotMigrated.keySet()); |
| Collections.sort(modules); |
| for (String module : modules) { |
| if (modules.size() > 1) { |
| sb.append("From ").append(module).append(":\n"); |
| } |
| List<String> sorted = new ArrayList<String>(myNotMigrated.get(module)); |
| Collections.sort(sorted); |
| for (String path : sorted) { |
| sb.append("* ").append(path).append("\n"); |
| } |
| } |
| } |
| |
| if (!myJarDependencies.isEmpty()) { |
| sb.append(MSG_REPLACED_JARS); |
| // TODO: Also add note here about switching to AAR's potentially also creating |
| // compilation errors because it now enforces that app min sdk version is >= library |
| // min sdk version, and suggesting that they re-run import with replaceJars=false |
| // if this leads to problems. |
| List<File> files = Lists.newArrayList(myJarDependencies.keySet()); |
| Collections.sort(files); |
| for (File file : files) { |
| String jar = file.getName(); |
| GradleCoordinate dependency = myJarDependencies.get(file); |
| sb.append(jar).append(" => ").append(dependency).append("\n"); |
| } |
| } |
| |
| if (!myGuessedDependencyVersions.isEmpty()) { |
| sb.append(MSG_GUESSED_VERSIONS); |
| Collections.sort(myGuessedDependencyVersions); |
| for (String replaced : myGuessedDependencyVersions) { |
| sb.append(replaced).append("\n"); |
| } |
| } |
| |
| if (!myLibDependencies.isEmpty()) { |
| sb.append(MSG_REPLACED_LIBS); |
| List<String> modules = Lists.newArrayList(myLibDependencies.keySet()); |
| Collections.sort(modules); |
| for (String module : modules) { |
| List<GradleCoordinate> dependencies = myLibDependencies.get(module); |
| if (dependencies.size() == 1) { |
| sb.append(module).append(" => ").append(dependencies).append("\n"); |
| } |
| else { |
| sb.append(module).append(" =>\n"); |
| for (GradleCoordinate dependency : dependencies) { |
| sb.append(" ").append(dependency).append("\n"); |
| } |
| } |
| } |
| } |
| |
| if (!myMoved.isEmpty()) { |
| sb.append(MSG_FOLDER_STRUCTURE); |
| List<ImportModule> modules = Lists.newArrayList(myMoved.keySet()); |
| Collections.sort(modules); |
| for (ImportModule module : modules) { |
| if (modules.size() > 1) { |
| sb.append("In ").append(module.getOriginalName()).append(":\n"); |
| } |
| Map<File, File> map = myMoved.get(module); |
| List<File> sorted = new ArrayList<File>(map.keySet()); |
| Collections.sort(sorted); |
| for (File from : sorted) { |
| sb.append("* "); |
| File to = map.get(from); |
| assert to != null : from; |
| |
| File fromRelative = null; |
| File toRelative = null; |
| try { |
| fromRelative = module.computeProjectRelativePath(from); |
| if (myDestDir != null) { |
| toRelative = GradleImport.computeRelativePath(myDestDir.getCanonicalFile(), to); |
| } |
| } |
| catch (IOException ioe) { |
| // pass; use full path |
| } |
| if (fromRelative == null) { |
| fromRelative = from; |
| } |
| if (toRelative == null) { |
| toRelative = to; |
| } |
| sb.append(fromRelative.getPath()); |
| if (from.isDirectory()) { |
| sb.append(File.separator); |
| } |
| sb.append(" => "); |
| sb.append(toRelative.getPath()); |
| if (to.isDirectory()) { |
| sb.append(File.separator); |
| } |
| sb.append("\n"); |
| } |
| } |
| } |
| |
| if (myImporter.needSupportRepository() && myImporter.isMissingSupportRepository()) { |
| sb.append(MSG_MISSING_REPO_1); |
| sb.append(myImporter.getSdkLocation()).append("\n"); |
| sb.append(MSG_MISSING_REPO_2); |
| } |
| |
| if (myImporter.needGoogleRepository() && myImporter.isMissingGoogleRepository()) { |
| sb.append(MSG_MISSING_GOOGLE_REPOSITORY_1); |
| sb.append(myImporter.getSdkLocation()).append("\n"); |
| sb.append(MSG_MISSING_GOOGLE_REPOSITORY_2); |
| } |
| |
| if (FullRevision.parseRevision(myImporter.getBuildToolsVersion()).getMajor() < 19) { |
| sb.append(MSG_BUILD_TOOLS_VERSION); |
| } |
| |
| if (!myIgnoredUserHomeProGuardFiles.isEmpty()) { |
| sb.append(MSG_USER_HOME_PROGUARD); |
| Collections.sort(myIgnoredUserHomeProGuardFiles); |
| for (String path : myIgnoredUserHomeProGuardFiles) { |
| sb.append(path).append("\n"); |
| } |
| } |
| |
| sb.append(MSG_FOOTER); |
| |
| // TODO: Add further suggestions: |
| // - Consider removing uses-sdk elements and versionName/Code from manifest (such that it's |
| // only in the Gradle file) |
| // - Mention that we switched over to compileSdkVersion and buildToolsVersion 19 (to pick |
| // up on necessary gradle support). If the tools relied on building with older APIs, |
| // be aware of changes. (Mention API lint (gradlew lint) to prevent accidental API |
| // usage.) |
| |
| return sb.toString().replace("\n", GradleImport.NL); |
| } |
| } |