blob: c2957897219bebd24a4ede159e721e75e05aa18c [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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 org.jetbrains.jps.android;
import com.android.sdklib.BuildToolInfo;
import com.android.tools.idea.jps.AndroidTargetBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Processor;
import com.intellij.util.containers.HashMap;
import com.intellij.util.containers.HashSet;
import com.intellij.util.execution.ParametersListUtil;
import org.jetbrains.android.compiler.tools.AndroidDxRunner;
import org.jetbrains.android.util.AndroidBuildTestingManager;
import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.android.util.AndroidCompilerMessageKind;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.android.builder.AndroidDexBuildTarget;
import org.jetbrains.jps.android.builder.AndroidPreDexBuildTarget;
import org.jetbrains.jps.android.model.JpsAndroidDexCompilerConfiguration;
import org.jetbrains.jps.android.model.JpsAndroidExtensionService;
import org.jetbrains.jps.android.model.JpsAndroidModuleExtension;
import org.jetbrains.jps.android.model.JpsAndroidSdkProperties;
import org.jetbrains.jps.builders.BuildOutputConsumer;
import org.jetbrains.jps.builders.BuildRootDescriptor;
import org.jetbrains.jps.builders.DirtyFilesHolder;
import org.jetbrains.jps.cmdline.ClasspathBootstrap;
import org.jetbrains.jps.incremental.*;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.incremental.messages.ProgressMessage;
import org.jetbrains.jps.model.JpsProject;
import org.jetbrains.jps.model.JpsSimpleElement;
import org.jetbrains.jps.model.java.JpsJavaSdkType;
import org.jetbrains.jps.model.library.JpsLibrary;
import org.jetbrains.jps.model.library.sdk.JpsSdk;
import org.jetbrains.jps.model.module.JpsModule;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidDexBuilder extends AndroidTargetBuilder<BuildRootDescriptor, AndroidDexBuildTarget> {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.android.AndroidDexBuilder");
@NonNls private static final String DEX_BUILDER_NAME = "Android Dex";
@NonNls private static final String PRO_GUARD_BUILDER_NAME = "ProGuard";
public AndroidDexBuilder() {
super(Collections.singletonList(AndroidDexBuildTarget.MyTargetType.INSTANCE));
}
@Override
protected void buildTarget(@NotNull final AndroidDexBuildTarget buildTarget,
@NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidDexBuildTarget> holder,
@NotNull BuildOutputConsumer outputConsumer,
@NotNull CompileContext context) throws ProjectBuildException, IOException {
assert !AndroidJpsUtil.isLightBuild(context);
try {
if (!doDexBuild(buildTarget, context, holder.hasDirtyFiles() || holder.hasRemovedFiles(), outputConsumer)) {
throw new StopBuildException();
}
}
catch (ProjectBuildException e) {
throw e;
}
catch (Exception e) {
AndroidJpsUtil.handleException(context, e, DEX_BUILDER_NAME, LOG);
}
}
private static boolean isPredexingInScope(@NotNull CompileContext context) {
final JpsProject project = context.getProjectDescriptor().getProject();
return context.getScope().isAffected(new AndroidPreDexBuildTarget(project));
}
private static boolean doDexBuild(@NotNull AndroidDexBuildTarget target,
@NotNull CompileContext context,
boolean hasDirtyFiles,
@NotNull BuildOutputConsumer outputConsumer) throws IOException {
final JpsModule module = target.getModule();
final JpsAndroidModuleExtension extension = AndroidJpsUtil.getExtension(module);
assert extension != null;
assert !extension.isLibrary();
final AndroidPlatform platform = AndroidJpsUtil.getAndroidPlatform(module, context, DEX_BUILDER_NAME);
if (platform == null) {
return false;
}
File dexOutputDir = AndroidJpsUtil.getDirectoryForIntermediateArtifacts(context, module);
dexOutputDir = AndroidJpsUtil.createDirIfNotExist(dexOutputDir, context, DEX_BUILDER_NAME);
if (dexOutputDir == null) {
return false;
}
final ProGuardOptions proGuardOptions = AndroidJpsUtil.getProGuardConfigIfShouldRun(context, extension);
if (proGuardOptions != null) {
if (proGuardOptions.getCfgFiles() == null) {
context.processMessage(new CompilerMessage(DEX_BUILDER_NAME, BuildMessage.Kind.ERROR,
AndroidJpsBundle
.message("android.jps.errors.cannot.find.proguard.cfg", module.getName())));
return false;
}
}
final File proguardCfgOutputFile = new File(dexOutputDir, AndroidCommonUtils.PROGUARD_CFG_OUTPUT_FILE_NAME);
final AndroidProGuardStateStorage proGuardOptionsStorage =
context.getProjectDescriptor().dataManager.getStorage(target, AndroidProGuardOptionsStorageProvider.INSTANCE);
final AndroidProGuardStateStorage.MyState oldProGuardState = proGuardOptionsStorage.read();
final Set<String> fileSet;
AndroidProGuardStateStorage.MyState newProGuardState = null;
try {
if (proGuardOptions != null) {
final List<String> proguardCfgFilePathsList = new ArrayList<String>();
for (File file : proGuardOptions.getCfgFiles()) {
proguardCfgFilePathsList.add(file.getAbsolutePath());
}
proguardCfgFilePathsList.add(proguardCfgOutputFile.getPath());
final String[] proguardCfgFilePaths = ArrayUtil.toStringArray(proguardCfgFilePathsList);
final String outputJarPath =
FileUtil.toSystemDependentName(dexOutputDir.getPath() + '/' + AndroidCommonUtils.PROGUARD_OUTPUT_JAR_NAME);
final Pair<Boolean, AndroidProGuardStateStorage.MyState> pair = runProguardIfNecessary(
extension, target, platform, context, outputJarPath, proguardCfgFilePaths,
hasDirtyFiles, oldProGuardState);
if (pair == null) {
// error reported
return false;
}
if (!pair.getFirst()) {
// nothing changed
return true;
}
newProGuardState = pair.getSecond();
assert newProGuardState != null;
fileSet = Collections.singleton(outputJarPath);
}
else {
if (!hasDirtyFiles && oldProGuardState == null) {
return true;
}
final List<BuildRootDescriptor> roots = context.getProjectDescriptor().getBuildRootIndex().getTargetRoots(target, context);
fileSet = new HashSet<String>();
final boolean predexingEnabled = extension.isPreDexingEnabled() && isPredexingInScope(context);
for (BuildRootDescriptor root : roots) {
final File rootFile = root.getRootFile();
if (!rootFile.exists()) {
continue;
}
if (root instanceof AndroidDexBuildTarget.MyClassesDirBuildRootDescriptor) {
final AndroidDexBuildTarget.ClassesDirType type =
((AndroidDexBuildTarget.MyClassesDirBuildRootDescriptor)root).getClassesDirType();
if (type == AndroidDexBuildTarget.ClassesDirType.JAVA) {
fileSet.add(rootFile.getPath());
}
else if (type == AndroidDexBuildTarget.ClassesDirType.ANDROID_APP) {
AndroidJpsUtil.addSubdirectories(rootFile, fileSet);
}
}
else if (root instanceof AndroidDexBuildTarget.MyJarBuildRootDescriptor) {
if (((AndroidDexBuildTarget.MyJarBuildRootDescriptor)root).isPreDexed() == predexingEnabled) {
fileSet.add(rootFile.getPath());
}
}
}
}
final boolean success;
if (fileSet.size() > 0) {
final String[] files = new String[fileSet.size()];
int i = 0;
for (String filePath : fileSet) {
files[i++] = FileUtil.toSystemDependentName(filePath);
}
context.processMessage(new ProgressMessage(AndroidJpsBundle.message("android.jps.progress.dex", module.getName())));
Arrays.sort(files);
success = runDex(platform, dexOutputDir.getPath(), files, context, module, outputConsumer);
}
else {
success = true;
}
if (success) {
proGuardOptionsStorage.update(newProGuardState);
}
return success;
}
catch (IOException e) {
AndroidJpsUtil.reportExceptionError(context, null, e, DEX_BUILDER_NAME);
return false;
}
}
@NotNull
@Override
public String getPresentableName() {
return DEX_BUILDER_NAME;
}
private static boolean runDex(@NotNull AndroidPlatform platform,
@NotNull String outputDir,
@NotNull String[] compileTargets,
@NotNull CompileContext context,
@NotNull JpsModule module,
@NotNull BuildOutputConsumer outputConsumer) throws IOException {
final String outFilePath = outputDir + File.separatorChar + AndroidCommonUtils.CLASSES_FILE_NAME;
return runDex(platform, outFilePath, compileTargets, context, module.getProject(), outputConsumer,
DEX_BUILDER_NAME, module.getName());
}
public static boolean runDex(@NotNull AndroidPlatform platform,
@NotNull String outFilePath,
@NotNull String[] compileTargets,
@NotNull CompileContext context,
@NotNull JpsProject project, @NotNull BuildOutputConsumer outputConsumer,
@NotNull String builderName,
@NotNull String srcTargetName) throws IOException {
BuildToolInfo buildToolInfo = platform.getTarget().getBuildToolInfo();
if (buildToolInfo == null) {
return false;
}
final String dxJarPath = FileUtil.toSystemDependentName(buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR));
final AndroidBuildTestingManager testingManager = AndroidBuildTestingManager.getTestingManager();
final File dxJar = new File(dxJarPath);
if (testingManager == null && !dxJar.isFile()) {
context.processMessage(
new CompilerMessage(builderName, BuildMessage.Kind.ERROR, AndroidJpsBundle.message("android.jps.cannot.find.file", dxJarPath)));
return false;
}
final List<String> programParamList = new ArrayList<String>();
programParamList.add(dxJarPath);
programParamList.add(outFilePath);
final JpsAndroidDexCompilerConfiguration configuration =
JpsAndroidExtensionService.getInstance().getDexCompilerConfiguration(project);
final List<String> vmOptions;
if (configuration != null) {
vmOptions = new ArrayList<String>();
vmOptions.addAll(ParametersListUtil.parse(configuration.getVmOptions()));
if (!AndroidCommonUtils.hasXmxParam(vmOptions)) {
vmOptions.add("-Xmx" + configuration.getMaxHeapSize() + "M");
}
programParamList.addAll(Arrays.asList("--optimize", Boolean.toString(configuration.isOptimize())));
if (configuration.isForceJumbo()) {
programParamList.addAll(Arrays.asList("--forceJumbo", Boolean.TRUE.toString()));
}
if (configuration.isCoreLibrary()) {
programParamList.add("--coreLibrary");
}
}
else {
vmOptions = Collections.singletonList("-Xmx1024M");
}
programParamList.addAll(Arrays.asList(compileTargets));
programParamList.add("--exclude");
final List<String> classPath = new ArrayList<String>();
classPath.add(ClasspathBootstrap.getResourcePath(AndroidDxRunner.class));
classPath.add(ClasspathBootstrap.getResourcePath(FileUtilRt.class));
final File outFile = new File(outFilePath);
if (outFile.exists() && !outFile.delete()) {
context.processMessage(new CompilerMessage(builderName, BuildMessage.Kind.WARNING,
AndroidJpsBundle.message("android.jps.cannot.delete.file", outFilePath)));
}
final String javaExecutable = getJavaExecutable(platform, context, builderName);
if (javaExecutable == null) {
return false;
}
final List<String> commandLine = ExternalProcessUtil
.buildJavaCommandLine(javaExecutable, AndroidDxRunner.class.getName(),
Collections.<String>emptyList(), classPath, vmOptions, programParamList);
LOG.info(AndroidCommonUtils.command2string(commandLine));
final String[] commands = ArrayUtil.toStringArray(commandLine);
final Process process;
if (testingManager != null) {
process = testingManager.getCommandExecutor().createProcess(
commands, Collections.<String, String>emptyMap());
}
else {
process = Runtime.getRuntime().exec(commands);
}
final HashMap<AndroidCompilerMessageKind, List<String>> messages = new HashMap<AndroidCompilerMessageKind, List<String>>(3);
messages.put(AndroidCompilerMessageKind.ERROR, new ArrayList<String>());
messages.put(AndroidCompilerMessageKind.WARNING, new ArrayList<String>());
messages.put(AndroidCompilerMessageKind.INFORMATION, new ArrayList<String>());
AndroidCommonUtils.handleDexCompilationResult(process, outFilePath, messages);
AndroidJpsUtil.addMessages(context, messages, builderName, srcTargetName);
final boolean success = messages.get(AndroidCompilerMessageKind.ERROR).size() == 0;
if (success) {
final List<String> srcFiles = new ArrayList<String>();
for (String compileTargetPath : compileTargets) {
final File compileTarget = new File(compileTargetPath);
if (compileTarget.isFile()) {
srcFiles.add(compileTargetPath);
}
else if(compileTarget.isDirectory()) {
AndroidJpsUtil.processClassFilesAndJarsRecursively(compileTarget, new Processor<File>() {
@Override
public boolean process(File file) {
if (file.isFile()) {
srcFiles.add(file.getPath());
}
return true;
}
});
}
}
outputConsumer.registerOutputFile(outFile, srcFiles);
}
return success;
}
@Nullable
private static String getJavaExecutable(@NotNull AndroidPlatform platform, @NotNull CompileContext context, @NotNull String builderName) {
final JpsSdk<JpsSimpleElement<JpsAndroidSdkProperties>> sdk = platform.getSdk();
final String jdkName = sdk.getSdkProperties().getData().getJdkName();
final JpsLibrary javaSdk = context.getProjectDescriptor().getModel().getGlobal().getLibraryCollection().findLibrary(jdkName);
if (javaSdk == null || !javaSdk.getType().equals(JpsJavaSdkType.INSTANCE)) {
context.processMessage(new CompilerMessage(builderName, BuildMessage.Kind.ERROR,
AndroidJpsBundle.message("android.jps.errors.java.sdk.not.specified", jdkName)));
return null;
}
return JpsJavaSdkType.getJavaExecutable((JpsSdk<?>)javaSdk.getProperties());
}
private static Pair<Boolean, AndroidProGuardStateStorage.MyState>
runProguardIfNecessary(@NotNull JpsAndroidModuleExtension extension,
@NotNull AndroidDexBuildTarget target,
@NotNull AndroidPlatform platform,
@NotNull CompileContext context,
@NotNull String outputJarPath,
@NotNull String[] proguardCfgPaths,
boolean hasDirtyFiles,
@Nullable AndroidProGuardStateStorage.MyState oldState)
throws IOException {
final JpsModule module = extension.getModule();
final File[] proguardCfgFiles = new File[proguardCfgPaths.length];
for (int i = 0; i < proguardCfgFiles.length; i++) {
proguardCfgFiles[i] = new File(proguardCfgPaths[i]);
if (!proguardCfgFiles[i].exists()) {
context.processMessage(new CompilerMessage(PRO_GUARD_BUILDER_NAME, BuildMessage.Kind.ERROR,
AndroidJpsBundle.message("android.jps.cannot.find.file", proguardCfgPaths[i])));
return null;
}
}
final File mainContentRoot = AndroidJpsUtil.getMainContentRoot(extension);
if (mainContentRoot == null) {
context.processMessage(new CompilerMessage(PRO_GUARD_BUILDER_NAME, BuildMessage.Kind.ERROR, AndroidJpsBundle
.message("android.jps.errors.main.content.root.not.found", module.getName())));
return null;
}
final String javaExecutable = getJavaExecutable(platform, context, PRO_GUARD_BUILDER_NAME);
if (javaExecutable == null) {
return null;
}
final File proguardLogsDir = extension.getProguardLogsDir();
final File logsDir;
if (proguardLogsDir != null) {
logsDir = proguardLogsDir;
}
else {
logsDir = new File(mainContentRoot.getPath() + '/' + AndroidCommonUtils.DIRECTORY_FOR_LOGS_NAME);
}
final AndroidProGuardStateStorage.MyState newState = new AndroidProGuardStateStorage.MyState(
proguardCfgFiles);
if (!hasDirtyFiles && newState.equals(oldState)) {
return Pair.create(false, null);
}
final List<String> classesDirs = new ArrayList<String>();
final List<String> libClassesDirs = new ArrayList<String>();
final List<String> externalJars = new ArrayList<String>();
final List<String> providedJars = new ArrayList<String>();
final List<BuildRootDescriptor> roots = context.getProjectDescriptor().getBuildRootIndex().getTargetRoots(target, context);
for (BuildRootDescriptor root : roots) {
final File rootFile = root.getRootFile();
if (!rootFile.exists()) {
continue;
}
if (root instanceof AndroidDexBuildTarget.MyClassesDirBuildRootDescriptor) {
final AndroidDexBuildTarget.ClassesDirType type =
((AndroidDexBuildTarget.MyClassesDirBuildRootDescriptor)root).getClassesDirType();
if (type == AndroidDexBuildTarget.ClassesDirType.JAVA ||
type == AndroidDexBuildTarget.ClassesDirType.ANDROID_APP) {
AndroidJpsUtil.addSubdirectories(rootFile, classesDirs);
}
else {
AndroidJpsUtil.addSubdirectories(rootFile, libClassesDirs);
}
}
else if (root instanceof AndroidDexBuildTarget.MyJarBuildRootDescriptor) {
final AndroidDexBuildTarget.MyJarBuildRootDescriptor jarRoot =
(AndroidDexBuildTarget.MyJarBuildRootDescriptor)root;
if (!jarRoot.isLibPackage() && !jarRoot.isPreDexed()) {
externalJars.add(rootFile.getPath());
}
}
else if (root instanceof AndroidDexBuildTarget.MyProvidedJarBuildRootDescriptor) {
providedJars.add(rootFile.getPath());
}
}
final String[] classFilesDirOsPaths = ArrayUtil.toStringArray(classesDirs);
final String[] libClassFilesDirOsPaths = ArrayUtil.toStringArray(libClassesDirs);
final String[] externalJarOsPaths = ArrayUtil.toStringArray(externalJars);
final String[] providedJarOsPaths = ArrayUtil.toStringArray(providedJars);
final String inputJarOsPath = AndroidCommonUtils.buildTempInputJar(classFilesDirOsPaths, libClassFilesDirOsPaths);
final AndroidBuildTestingManager testingManager = AndroidBuildTestingManager.getTestingManager();
if (testingManager != null) {
testingManager.getCommandExecutor().checkJarContent("proguard_input_jar", inputJarOsPath);
}
if (!logsDir.exists()) {
if (!logsDir.mkdirs()) {
context.processMessage(new CompilerMessage(
PRO_GUARD_BUILDER_NAME, BuildMessage.Kind.ERROR,
AndroidJpsBundle.message("android.jps.cannot.create.directory", FileUtil.toSystemDependentName(logsDir.getPath()))));
return null;
}
}
final JpsAndroidDexCompilerConfiguration configuration =
JpsAndroidExtensionService.getInstance().getDexCompilerConfiguration(module.getProject());
String proguardVmOptions = configuration != null ? configuration.getProguardVmOptions() : null;
if (proguardVmOptions == null) {
proguardVmOptions = "";
}
context.processMessage(new ProgressMessage(AndroidJpsBundle.message("android.jps.progress.proguard", module.getName())));
final Map<AndroidCompilerMessageKind, List<String>> messages =
AndroidCommonUtils.launchProguard(platform.getTarget(), platform.getSdkToolsRevision(), platform.getSdk().getHomePath(),
javaExecutable, proguardVmOptions, proguardCfgPaths, inputJarOsPath, externalJarOsPaths,
providedJarOsPaths, outputJarPath, logsDir.getPath());
AndroidJpsUtil.addMessages(context, messages, PRO_GUARD_BUILDER_NAME, module.getName());
return messages.get(AndroidCompilerMessageKind.ERROR).isEmpty()
? Pair.create(true, newState) : null;
}
}