blob: 00c0e9e2955400677a9c768d05393dd91e558686 [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.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);
}
}