blob: d3df0584fbf71374cc1632799775fa09e9c01600 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.ide.eclipse.adt.internal.wizards.exportgradle;
import static com.android.SdkConstants.GRADLE_LATEST_VERSION;
import static com.android.SdkConstants.GRADLE_PLUGIN_LATEST_VERSION;
import static com.android.SdkConstants.GRADLE_PLUGIN_NAME;
import static com.android.tools.lint.checks.GradleDetector.APP_PLUGIN_ID;
import static com.android.tools.lint.checks.GradleDetector.LIB_PLUGIN_ID;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.io.IFolderWrapper;
import com.android.io.IAbstractFile;
import com.android.sdklib.io.FileOp;
import com.android.xml.AndroidManifest;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
/**
* Creates build.gradle and settings.gradle files for a set of projects.
* <p>
* Based on {@link org.eclipse.ant.internal.ui.datatransfer.BuildFileCreator}
*/
public class BuildFileCreator {
static final String BUILD_FILE = "build.gradle"; //$NON-NLS-1$
static final String SETTINGS_FILE = "settings.gradle"; //$NON-NLS-1$
private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$
private static final String GRADLE_WRAPPER_LOCATION =
"tools/templates/gradle/wrapper"; //$NON-NLS-1$
static final String PLUGIN_CLASSPATH =
"classpath '" + GRADLE_PLUGIN_NAME + GRADLE_PLUGIN_LATEST_VERSION + "'"; //$NON-NLS-1$
static final String MAVEN_REPOSITORY = "jcenter()"; //$NON-NLS-1$
private static final String[] GRADLE_WRAPPER_FILES = new String[] {
"gradlew", //$NON-NLS-1$
"gradlew.bat", //$NON-NLS-1$
"gradle/wrapper/gradle-wrapper.jar", //$NON-NLS-1$
"gradle/wrapper/gradle-wrapper.properties" //$NON-NLS-1$
};
private static final Comparator<IFile> FILE_COMPARATOR = new Comparator<IFile>() {
@Override
public int compare(IFile o1, IFile o2) {
return o1.toString().compareTo(o2.toString());
}
};
private final GradleModule mModule;
private final StringBuilder mBuildFile = new StringBuilder();
/**
* Create buildfile for the projects.
*
* @param shell parent instance for dialogs
* @return project names for which buildfiles were created
* @throws InterruptedException thrown when user cancels task
*/
public static void createBuildFiles(
@NonNull ProjectSetupBuilder builder,
@NonNull Shell shell,
@NonNull IProgressMonitor pm) {
File gradleLocation = new File(Sdk.getCurrent().getSdkOsLocation(), GRADLE_WRAPPER_LOCATION);
SubMonitor localmonitor = null;
try {
// See if we have a Gradle wrapper in the SDK templates directory. If so, we can copy
// it over.
boolean hasGradleWrapper = true;
for (File wrapperFile : getGradleWrapperFiles(gradleLocation)) {
if (!wrapperFile.exists()) {
hasGradleWrapper = false;
}
}
Collection<GradleModule> modules = builder.getModules();
boolean multiModules = modules.size() > 1;
// determine files to create/change
List<IFile> files = new ArrayList<IFile>();
// add the build.gradle file for all modules.
for (GradleModule module : modules) {
// build.gradle file
IFile file = module.getProject().getFile(BuildFileCreator.BUILD_FILE);
files.add(file);
}
// get the commonRoot for all modules. If only one module, this returns the path
// of the project.
IPath commonRoot = builder.getCommonRoot();
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
IPath workspaceLocation = workspaceRoot.getLocation();
IPath relativePath = commonRoot.makeRelativeTo(workspaceLocation);
// if makeRelativePath to returns the same path, then commonRoot is not in the
// workspace.
boolean rootInWorkspace = !relativePath.equals(commonRoot);
// we only care if the root is a workspace project. if it's the workspace folder itself,
// then the files won't be handled by the workspace.
rootInWorkspace = rootInWorkspace && relativePath.segmentCount() > 0;
File settingsFile = new File(commonRoot.toFile(), SETTINGS_FILE);
// more than one modules -> generate settings.gradle
if (multiModules && rootInWorkspace) {
// Locate the settings.gradle file and add it to the changed files list
IPath settingsGradle = Path.fromOSString(settingsFile.getAbsolutePath());
// different path, means commonRoot is inside the workspace, which means we have
// to add settings.gradle and wrapper files to the list of files to add.
IFile iFile = workspaceRoot.getFile(settingsGradle);
if (iFile != null) {
files.add(iFile);
}
}
// Gradle wrapper files
if (hasGradleWrapper && rootInWorkspace) {
// See if there already wrapper files there and only mark nonexistent ones for
// creation.
for (File wrapperFile : getGradleWrapperFiles(commonRoot.toFile())) {
if (!wrapperFile.exists()) {
IPath path = Path.fromOSString(wrapperFile.getAbsolutePath());
IFile file = workspaceRoot.getFile(path);
files.add(file);
}
}
}
ExportStatus status = new ExportStatus();
builder.setStatus(status);
// Trigger checkout of changed files
Set<IFile> confirmedFiles = validateEdit(files, status, shell);
if (status.hasError()) {
return;
}
// Now iterate over all the modules and generate the build files.
localmonitor = SubMonitor.convert(pm, ExportMessages.PageTitle,
confirmedFiles.size());
List<String> projectSettingsPath = Lists.newArrayList();
for (GradleModule currentModule : modules) {
IProject moduleProject = currentModule.getProject();
IFile file = moduleProject.getFile(BuildFileCreator.BUILD_FILE);
if (!confirmedFiles.contains(file)) {
continue;
}
localmonitor.setTaskName(NLS.bind(ExportMessages.FileStatusMessage,
moduleProject.getName()));
ProjectState projectState = Sdk.getProjectState(moduleProject);
BuildFileCreator instance = new BuildFileCreator(currentModule, shell);
if (projectState != null) {
// This is an Android project
if (!multiModules) {
instance.appendBuildScript();
}
instance.appendHeader(projectState.isLibrary());
instance.appendDependencies();
instance.startAndroidTask(projectState);
//instance.appendDefaultConfig();
instance.createAndroidSourceSets();
instance.finishAndroidTask();
} else {
// This is a plain Java project
instance.appendJavaHeader();
instance.createJavaSourceSets();
}
try {
// Write the build file
String buildfile = instance.mBuildFile.toString();
InputStream is =
new ByteArrayInputStream(buildfile.getBytes("UTF-8")); //$NON-NLS-1$
if (file.exists()) {
file.setContents(is, true, true, null);
} else {
file.create(is, true, null);
}
} catch (Exception e) {
status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE,
file.getLocation().toFile());
status.setErrorMessage(e.getMessage());
return;
}
if (localmonitor.isCanceled()) {
return;
}
localmonitor.worked(1);
// get the project path to add it to the settings.gradle.
projectSettingsPath.add(currentModule.getPath());
}
// write the settings file.
if (multiModules) {
try {
writeGradleSettingsFile(settingsFile, projectSettingsPath);
} catch (IOException e) {
status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, settingsFile);
status.setErrorMessage(e.getMessage());
return;
}
File mainBuildFile = new File(commonRoot.toFile(), BUILD_FILE);
try {
writeRootBuildGradle(mainBuildFile);
} catch (IOException e) {
status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, mainBuildFile);
status.setErrorMessage(e.getMessage());
return;
}
}
// finally write the wrapper
// TODO check we can based on where it is
if (hasGradleWrapper) {
copyGradleWrapper(gradleLocation, commonRoot.toFile(), status);
if (status.hasError()) {
return;
}
}
} finally {
if (localmonitor != null && !localmonitor.isCanceled()) {
localmonitor.done();
}
if (pm != null) {
pm.done();
}
}
}
/**
* @param GradleModule create buildfile for this project
* @param shell parent instance for dialogs
*/
private BuildFileCreator(GradleModule module, Shell shell) {
mModule = module;
}
/**
* Return the files that comprise the Gradle wrapper as a collection of {@link File} instances.
* @param root
* @return
*/
private static List<File> getGradleWrapperFiles(File root) {
List<File> files = new ArrayList<File>(GRADLE_WRAPPER_FILES.length);
for (String file : GRADLE_WRAPPER_FILES) {
files.add(new File(root, file));
}
return files;
}
/**
* Copy the Gradle wrapper files from one directory to another.
*/
private static void copyGradleWrapper(File from, File to, ExportStatus status) {
for (String file : GRADLE_WRAPPER_FILES) {
File dest = new File(to, file);
try {
File src = new File(from, file);
dest.getParentFile().mkdirs();
new FileOp().copyFile(src, dest);
if (src.getName().equals(GRADLE_PROPERTIES)) {
updateGradleDistributionUrl(GRADLE_LATEST_VERSION, dest);
}
dest.setExecutable(src.canExecute());
status.addFileStatus(ExportStatus.FileStatus.OK, dest);
} catch (IOException e) {
status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, dest);
return;
}
}
}
/**
* Outputs boilerplate buildscript information common to all Gradle build files.
*/
private void appendBuildScript() {
appendBuildScript(mBuildFile);
}
/**
* Outputs boilerplate header information common to all Gradle build files.
*/
private static void appendBuildScript(StringBuilder builder) {
builder.append("buildscript {\n"); //$NON-NLS-1$
builder.append(" repositories {\n"); //$NON-NLS-1$
builder.append(" " + MAVEN_REPOSITORY + "\n"); //$NON-NLS-1$
builder.append(" }\n"); //$NON-NLS-1$
builder.append(" dependencies {\n"); //$NON-NLS-1$
builder.append(" " + PLUGIN_CLASSPATH + "\n"); //$NON-NLS-1$
builder.append(" }\n"); //$NON-NLS-1$
builder.append("}\n"); //$NON-NLS-1$
}
/**
* Outputs boilerplate header information common to all Gradle build files.
*/
private void appendHeader(boolean isLibrary) {
if (isLibrary) {
mBuildFile.append("apply plugin: '").append(LIB_PLUGIN_ID).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$
} else {
mBuildFile.append("apply plugin: '").append(APP_PLUGIN_ID).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$
}
mBuildFile.append("\n"); //$NON-NLS-1$
}
/**
* Outputs a block which sets up library and project dependencies.
*/
private void appendDependencies() {
mBuildFile.append("dependencies {\n"); //$NON-NLS-1$
// first the local jars.
// TODO: Fix
mBuildFile.append(" compile fileTree(dir: 'libs', include: '*.jar')\n"); //$NON-NLS-1$
for (GradleModule dep : mModule.getDependencies()) {
mBuildFile.append(" compile project('" + dep.getPath() + "')\n"); //$NON-NLS-1$ //$NON-NLS-2$
}
mBuildFile.append("}\n"); //$NON-NLS-1$
mBuildFile.append("\n"); //$NON-NLS-1$
}
/**
* Outputs the beginning of an Android task in the build file.
*/
private void startAndroidTask(ProjectState projectState) {
int buildApi = projectState.getTarget().getVersion().getApiLevel();
String toolsVersion = projectState.getTarget().getBuildToolInfo().getRevision().toString();
mBuildFile.append("android {\n"); //$NON-NLS-1$
mBuildFile.append(" compileSdkVersion " + buildApi + "\n"); //$NON-NLS-1$
mBuildFile.append(" buildToolsVersion \"" + toolsVersion + "\"\n"); //$NON-NLS-1$
mBuildFile.append("\n"); //$NON-NLS-1$
try {
IJavaProject javaProject = BaseProjectHelper.getJavaProject(projectState.getProject());
// otherwise we check source compatibility
String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true);
if (JavaCore.VERSION_1_7.equals(source)) {
mBuildFile.append(
" compileOptions {\n" + //$NON-NLS-1$
" sourceCompatibility JavaVersion.VERSION_1_7\n" + //$NON-NLS-1$
" targetCompatibility JavaVersion.VERSION_1_7\n" + //$NON-NLS-1$
" }\n" + //$NON-NLS-1$
"\n"); //$NON-NLS-1$
}
} catch (CoreException e) {
// Ignore compliance level, go with default
}
}
/**
* Outputs a sourceSets block to the Android task that locates all of the various source
* subdirectories in the project.
*/
private void createAndroidSourceSets() {
IFolderWrapper projectFolder = new IFolderWrapper(mModule.getProject());
IAbstractFile mManifestFile = AndroidManifest.getManifest(projectFolder);
if (mManifestFile == null) {
return;
}
List<String> srcDirs = new ArrayList<String>();
for (IClasspathEntry entry : mModule.getJavaProject().readRawClasspath()) {
if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE ||
SdkConstants.FD_GEN_SOURCES.equals(entry.getPath().lastSegment())) {
continue;
}
IPath path = entry.getPath().removeFirstSegments(1);
srcDirs.add("'" + path.toOSString() + "'"); //$NON-NLS-1$
}
String srcPaths = Joiner.on(",").join(srcDirs);
mBuildFile.append(" sourceSets {\n"); //$NON-NLS-1$
mBuildFile.append(" main {\n"); //$NON-NLS-1$
mBuildFile.append(" manifest.srcFile '" + SdkConstants.FN_ANDROID_MANIFEST_XML + "'\n"); //$NON-NLS-1$
mBuildFile.append(" java.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
mBuildFile.append(" resources.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
mBuildFile.append(" aidl.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
mBuildFile.append(" renderscript.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
mBuildFile.append(" res.srcDirs = ['res']\n"); //$NON-NLS-1$
mBuildFile.append(" assets.srcDirs = ['assets']\n"); //$NON-NLS-1$
mBuildFile.append(" }\n"); //$NON-NLS-1$
mBuildFile.append("\n"); //$NON-NLS-1$
mBuildFile.append(" // Move the tests to tests/java, tests/res, etc...\n"); //$NON-NLS-1$
mBuildFile.append(" instrumentTest.setRoot('tests')\n"); //$NON-NLS-1$
if (srcDirs.contains("'src'")) {
mBuildFile.append("\n"); //$NON-NLS-1$
mBuildFile.append(" // Move the build types to build-types/<type>\n"); //$NON-NLS-1$
mBuildFile.append(" // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...\n"); //$NON-NLS-1$
mBuildFile.append(" // This moves them out of them default location under src/<type>/... which would\n"); //$NON-NLS-1$
mBuildFile.append(" // conflict with src/ being used by the main source set.\n"); //$NON-NLS-1$
mBuildFile.append(" // Adding new build types or product flavors should be accompanied\n"); //$NON-NLS-1$
mBuildFile.append(" // by a similar customization.\n"); //$NON-NLS-1$
mBuildFile.append(" debug.setRoot('build-types/debug')\n"); //$NON-NLS-1$
mBuildFile.append(" release.setRoot('build-types/release')\n"); //$NON-NLS-1$
}
mBuildFile.append(" }\n"); //$NON-NLS-1$
}
/**
* Outputs the completion of the Android task in the build file.
*/
private void finishAndroidTask() {
mBuildFile.append("}\n"); //$NON-NLS-1$
}
/**
* Outputs a boilerplate header for non-Android projects
*/
private void appendJavaHeader() {
mBuildFile.append("apply plugin: 'java'\n"); //$NON-NLS-1$
}
/**
* Outputs a sourceSets block for non-Android projects to locate the source directories.
*/
private void createJavaSourceSets() {
List<String> dirs = new ArrayList<String>();
for (IClasspathEntry entry : mModule.getJavaProject().readRawClasspath()) {
if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) {
continue;
}
IPath path = entry.getPath().removeFirstSegments(1);
dirs.add("'" + path.toOSString() + "'"); //$NON-NLS-1$
}
String srcPaths = Joiner.on(",").join(dirs);
mBuildFile.append("sourceSets {\n"); //$NON-NLS-1$
mBuildFile.append(" main.java.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
mBuildFile.append(" main.resources.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
mBuildFile.append(" test.java.srcDirs = ['tests/java']\n"); //$NON-NLS-1$
mBuildFile.append(" test.resources.srcDirs = ['tests/resources']\n"); //$NON-NLS-1$
mBuildFile.append("}\n"); //$NON-NLS-1$
}
/**
* Merges the new subproject dependencies into the settings.gradle file if it already exists,
* and creates one if it does not.
* @throws IOException
*/
private static void writeGradleSettingsFile(File settingsFile, List<String> projectPaths)
throws IOException {
StringBuilder contents = new StringBuilder();
for (String path : projectPaths) {
contents.append("include '").append(path).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$
}
Files.write(contents.toString(), settingsFile, Charsets.UTF_8);
}
private static void writeRootBuildGradle(File buildFile) throws IOException {
StringBuilder sb = new StringBuilder(
"// Top-level build file where you can add configuration options common to all sub-projects/modules.\n");
appendBuildScript(sb);
Files.write(sb.toString(), buildFile, Charsets.UTF_8);
}
/**
* Request write access to given files. Depending on the version control
* plug-in opens a confirm checkout dialog.
*
* @param shell
* parent instance for dialogs
* @return <code>IFile</code> objects for which user confirmed checkout
* @throws CoreException
* thrown if project is under version control, but not connected
*/
static Set<IFile> validateEdit(
@NonNull List<IFile> files,
@NonNull ExportStatus exportStatus,
@NonNull Shell shell) {
Set<IFile> confirmedFiles = new TreeSet<IFile>(FILE_COMPARATOR);
if (files.size() == 0) {
return confirmedFiles;
}
IStatus status = (files.get(0)).getWorkspace().validateEdit(
files.toArray(new IFile[files.size()]), shell);
if (status.isMultiStatus() && status.getChildren().length > 0) {
for (int i = 0; i < status.getChildren().length; i++) {
IStatus statusChild = status.getChildren()[i];
if (statusChild.isOK()) {
confirmedFiles.add(files.get(i));
} else {
exportStatus.addFileStatus(
ExportStatus.FileStatus.VCS_FAILURE,
files.get(i).getLocation().toFile());
}
}
} else if (status.isOK()) {
confirmedFiles.addAll(files);
}
if (status.getSeverity() == IStatus.ERROR) {
// not possible to checkout files: not connected to version
// control plugin or hijacked files and made read-only, so
// collect error messages provided by validator and re-throw
StringBuffer message = new StringBuffer(status.getPlugin() + ": " //$NON-NLS-1$
+ status.getMessage() + NEWLINE);
if (status.isMultiStatus()) {
for (int i = 0; i < status.getChildren().length; i++) {
IStatus statusChild = status.getChildren()[i];
message.append(statusChild.getMessage() + NEWLINE);
}
}
String s = message.toString();
exportStatus.setErrorMessage(s);
}
return confirmedFiles;
}
// -------------------------------------------------------------------------------
// Fix gradle wrapper version. This code is from GradleUtil in the Studio plugin:
// -------------------------------------------------------------------------------
private static final String GRADLE_PROPERTIES = "gradle-wrapper.properties";
private static final String GRADLEW_PROPERTIES_PATH =
"gradle" + File.separator + "wrapper" + File.separator + GRADLE_PROPERTIES;
private static final String GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME = "distributionUrl";
@NonNull
private static File getGradleWrapperPropertiesFilePath(@NonNull File projectRootDir) {
return new File(projectRootDir, GRADLEW_PROPERTIES_PATH);
}
@Nullable
public static File findWrapperPropertiesFile(@NonNull File projectRootDir) {
File wrapperPropertiesFile = getGradleWrapperPropertiesFilePath(projectRootDir);
return wrapperPropertiesFile.isFile() ? wrapperPropertiesFile : null;
}
private static boolean updateGradleDistributionUrl(
@NonNull String gradleVersion,
@NonNull File propertiesFile) throws IOException {
Properties properties = loadGradleWrapperProperties(propertiesFile);
String gradleDistributionUrl = getGradleDistributionUrl(gradleVersion, false);
String property = properties.getProperty(GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME);
if (property != null
&& (property.equals(gradleDistributionUrl) || property
.equals(getGradleDistributionUrl(gradleVersion, true)))) {
return false;
}
properties.setProperty(GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME, gradleDistributionUrl);
FileOutputStream out = null;
try {
out = new FileOutputStream(propertiesFile);
properties.store(out, null);
return true;
} finally {
Closeables.close(out, true);
}
}
@NonNull
private static Properties loadGradleWrapperProperties(@NonNull File propertiesFile)
throws IOException {
Properties properties = new Properties();
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(propertiesFile);
properties.load(fileInputStream);
return properties;
} finally {
Closeables.close(fileInputStream, true);
}
}
@NonNull
private static String getGradleDistributionUrl(@NonNull String gradleVersion,
boolean binOnly) {
String suffix = binOnly ? "bin" : "all";
return String.format("https://services.gradle.org/distributions/gradle-%1$s-" + suffix
+ ".zip", gradleVersion);
}
}