| /* |
| * 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; |
| } |
| } |