| package org.jetbrains.jps.android; |
| |
| import com.android.tools.idea.jps.AndroidTargetBuilder; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.Processor; |
| import com.intellij.util.containers.HashSet; |
| import org.jetbrains.android.compiler.tools.AndroidApkBuilder; |
| import org.jetbrains.android.util.AndroidCompilerMessageKind; |
| import org.jetbrains.android.util.AndroidNativeLibData; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.jps.ProjectPaths; |
| import org.jetbrains.jps.android.builder.AndroidDexBuildTarget; |
| import org.jetbrains.jps.android.builder.AndroidPackagingBuildTarget; |
| import org.jetbrains.jps.android.builder.AndroidResourcePackagingBuildTarget; |
| import org.jetbrains.jps.android.model.JpsAndroidModuleExtension; |
| import org.jetbrains.jps.builders.BuildOutputConsumer; |
| import org.jetbrains.jps.builders.BuildRootDescriptor; |
| import org.jetbrains.jps.builders.BuildTarget; |
| import org.jetbrains.jps.builders.DirtyFilesHolder; |
| import org.jetbrains.jps.builders.storage.BuildDataPaths; |
| import org.jetbrains.jps.incremental.CompileContext; |
| import org.jetbrains.jps.incremental.ProjectBuildException; |
| import org.jetbrains.jps.incremental.StopBuildException; |
| 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.incremental.storage.BuildDataManager; |
| import org.jetbrains.jps.model.JpsProject; |
| import org.jetbrains.jps.model.java.JpsJavaExtensionService; |
| import org.jetbrains.jps.model.java.compiler.JpsCompilerExcludes; |
| import org.jetbrains.jps.model.module.JpsModule; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.*; |
| |
| /** |
| * @author Eugene.Kudelevsky |
| */ |
| public class AndroidPackagingBuilder extends AndroidTargetBuilder<BuildRootDescriptor, AndroidPackagingBuildTarget> { |
| private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.android.AndroidPackagingBuilder"); |
| private static final String BUILDER_NAME = "Android Packager"; |
| |
| |
| public AndroidPackagingBuilder() { |
| super(Collections.singletonList(AndroidPackagingBuildTarget.MyTargetType.INSTANCE)); |
| } |
| |
| @NotNull |
| @Override |
| public String getPresentableName() { |
| return BUILDER_NAME; |
| } |
| |
| @Override |
| protected void buildTarget(@NotNull AndroidPackagingBuildTarget target, |
| @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidPackagingBuildTarget> holder, |
| @NotNull BuildOutputConsumer outputConsumer, |
| @NotNull CompileContext context) throws ProjectBuildException, IOException { |
| if (AndroidJpsUtil.isLightBuild(context)) { |
| return; |
| } |
| final boolean hasDirtyFiles = holder.hasDirtyFiles() || holder.hasRemovedFiles(); |
| |
| try { |
| if (!doPackaging(target, context, target.getModule(), hasDirtyFiles, outputConsumer)) { |
| throw new StopBuildException(); |
| } |
| } |
| catch (ProjectBuildException e) { |
| throw e; |
| } |
| catch (Exception e) { |
| AndroidJpsUtil.handleException(context, e, BUILDER_NAME, LOG); |
| } |
| } |
| |
| private static boolean doPackaging(@NotNull BuildTarget<?> target, |
| @NotNull CompileContext context, |
| @NotNull JpsModule module, |
| boolean hasDirtyFiles, |
| @NotNull BuildOutputConsumer outputConsumer) throws IOException { |
| final boolean release = AndroidJpsUtil.isReleaseBuild(context); |
| final BuildDataManager dataManager = context.getProjectDescriptor().dataManager; |
| |
| boolean success = true; |
| |
| final AndroidApkBuilderConfigStateStorage.Provider builderStateStoragetProvider = |
| new AndroidApkBuilderConfigStateStorage.Provider("apk_builder_config"); |
| final AndroidApkBuilderConfigStateStorage apkBuilderConfigStateStorage = |
| dataManager.getStorage(target, builderStateStoragetProvider); |
| |
| final AndroidPackagingStateStorage packagingStateStorage = |
| dataManager.getStorage(target, AndroidPackagingStateStorage.Provider.INSTANCE); |
| |
| try { |
| if (!doPackagingForModule(context, module, apkBuilderConfigStateStorage, packagingStateStorage, |
| release, hasDirtyFiles, outputConsumer)) { |
| success = false; |
| } |
| } |
| catch (IOException e) { |
| AndroidJpsUtil.reportExceptionError(context, null, e, BUILDER_NAME); |
| success = false; |
| } |
| return success; |
| } |
| |
| private static boolean doPackagingForModule(@NotNull CompileContext context, |
| @NotNull JpsModule module, |
| @NotNull AndroidApkBuilderConfigStateStorage apkBuilderConfigStateStorage, |
| @NotNull AndroidPackagingStateStorage packagingStateStorage, |
| boolean release, |
| boolean hasDirtyFiles, |
| @NotNull BuildOutputConsumer outputConsumer) throws IOException { |
| final JpsAndroidModuleExtension extension = AndroidJpsUtil.getExtension(module); |
| if (extension == null || extension.isLibrary()) { |
| return true; |
| } |
| |
| final String[] resourceRoots = AndroidJpsUtil.toPaths(AndroidJpsUtil.getJavaOutputRootsForModuleAndDependencies(module)); |
| Arrays.sort(resourceRoots); |
| |
| final File moduleOutputDir = ProjectPaths.getModuleOutputDir(module, false); |
| if (moduleOutputDir == null) { |
| context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, AndroidJpsBundle |
| .message("android.jps.errors.output.dir.not.specified", module.getName()))); |
| return false; |
| } |
| |
| final AndroidPlatform platform = AndroidJpsUtil.getAndroidPlatform(module, context, BUILDER_NAME); |
| if (platform == null) { |
| return false; |
| } |
| |
| final Set<String> externalJarsSet = new HashSet<String>(); |
| |
| for (String jarPath : AndroidJpsUtil.getExternalLibraries(context, module, platform)) { |
| if (new File(jarPath).exists()) { |
| externalJarsSet.add(jarPath); |
| } |
| } |
| final BuildDataPaths dataPaths = context.getProjectDescriptor().dataManager.getDataPaths(); |
| final File resPackage = AndroidResourcePackagingBuildTarget.getOutputFile(dataPaths, module); |
| final File classesDexFile = AndroidDexBuildTarget.getOutputFile(dataPaths, module); |
| |
| final String sdkPath = platform.getSdk().getHomePath(); |
| final String outputPath = AndroidJpsUtil.getApkPath(extension, moduleOutputDir); |
| |
| if (outputPath == null) { |
| context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, AndroidJpsBundle |
| .message("android.jps.errors.cannot.compute.output.apk", module.getName()))); |
| return false; |
| } |
| final String customKeyStorePath = FileUtil.toSystemDependentName(extension.getCustomDebugKeyStorePath()); |
| final String[] nativeLibDirs = AndroidPackagingBuildTarget.collectNativeLibsFolders(extension, true); |
| final String resPackagePath = resPackage.getPath(); |
| |
| final String classesDexFilePath = classesDexFile.getPath(); |
| final String[] externalJars = ArrayUtil.toStringArray(externalJarsSet); |
| Arrays.sort(externalJars); |
| |
| final List<AndroidNativeLibData> additionalNativeLibs = extension.getAdditionalNativeLibs(); |
| |
| final AndroidApkBuilderConfigState currentApkBuilderConfigState = |
| new AndroidApkBuilderConfigState(outputPath, customKeyStorePath, additionalNativeLibs); |
| |
| if (!hasDirtyFiles) { |
| final AndroidApkBuilderConfigState savedApkBuilderConfigState = apkBuilderConfigStateStorage.getState(module.getName()); |
| final AndroidPackagingStateStorage.MyState packagingState = packagingStateStorage.read(); |
| |
| if (currentApkBuilderConfigState.equalsTo(savedApkBuilderConfigState) && |
| packagingState != null && packagingState.isRelease() == release) { |
| return true; |
| } |
| } |
| context.processMessage(new ProgressMessage( |
| AndroidJpsBundle.message("android.jps.progress.packaging", AndroidJpsUtil.getApkName(module)))); |
| |
| final Map<AndroidCompilerMessageKind, List<String>> messages = AndroidApkBuilder |
| .execute(resPackagePath, classesDexFilePath, resourceRoots, externalJars, |
| nativeLibDirs, additionalNativeLibs, outputPath, release, sdkPath, platform.getTarget(), |
| customKeyStorePath, new MyExcludedSourcesFilter(context.getProjectDescriptor().getProject())); |
| |
| if (messages.get(AndroidCompilerMessageKind.ERROR).size() == 0) { |
| final List<String> srcFiles = new ArrayList<String>(); |
| srcFiles.add(resPackagePath); |
| srcFiles.add(classesDexFilePath); |
| |
| for (String resourceRoot : resourceRoots) { |
| FileUtil.processFilesRecursively(new File(resourceRoot), new Processor<File>() { |
| @Override |
| public boolean process(File file) { |
| if (file.isFile() && AndroidApkBuilder.checkFileForPackaging(file)) { |
| srcFiles.add(file.getPath()); |
| } |
| return true; |
| } |
| }); |
| } |
| Collections.addAll(srcFiles, externalJars); |
| |
| for (String nativeLibDir : nativeLibDirs) { |
| FileUtil.processFilesRecursively(new File(nativeLibDir), new Processor<File>() { |
| @Override |
| public boolean process(File file) { |
| if (file.isFile()) { |
| srcFiles.add(file.getPath()); |
| } |
| return true; |
| } |
| }); |
| } |
| outputConsumer.registerOutputFile(new File(outputPath), srcFiles); |
| } |
| AndroidJpsUtil.addMessages(context, messages, BUILDER_NAME, module.getName()); |
| final boolean success = messages.get(AndroidCompilerMessageKind.ERROR).isEmpty(); |
| |
| apkBuilderConfigStateStorage.update(module.getName(), success ? currentApkBuilderConfigState : null); |
| packagingStateStorage.saveState(new AndroidPackagingStateStorage.MyState(release)); |
| return success; |
| } |
| |
| private static class MyExcludedSourcesFilter implements Condition<File> { |
| private final JpsCompilerExcludes myExcludes; |
| |
| public MyExcludedSourcesFilter(@NotNull JpsProject project) { |
| myExcludes = JpsJavaExtensionService.getInstance().getOrCreateCompilerConfiguration(project).getCompilerExcludes(); |
| } |
| |
| @Override |
| public boolean value(File file) { |
| return !myExcludes.isExcluded(file); |
| } |
| } |
| } |