| /* |
| * 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.android.util; |
| |
| import com.android.SdkConstants; |
| import com.android.jarutils.SignedJarBuilder; |
| import com.android.resources.ResourceFolderType; |
| import com.android.resources.ResourceType; |
| import com.android.sdklib.BuildToolInfo; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.sdklib.internal.project.ProjectProperties; |
| import com.google.common.base.Charsets; |
| import com.google.common.io.Files; |
| import com.intellij.execution.process.BaseOSProcessHandler; |
| import com.intellij.execution.process.ProcessAdapter; |
| import com.intellij.execution.process.ProcessEvent; |
| import com.intellij.execution.process.ProcessOutputTypes; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.Key; |
| 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.containers.HashMap; |
| import com.intellij.util.execution.ParametersListUtil; |
| import org.jetbrains.android.AndroidCommonBundle; |
| import org.jetbrains.android.sdk.MessageBuildingSdkLog; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.io.*; |
| import java.security.*; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.security.cert.X509Certificate; |
| import java.util.*; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarOutputStream; |
| import java.util.regex.Pattern; |
| |
| /** |
| * @author Eugene.Kudelevsky |
| */ |
| public class AndroidCommonUtils { |
| @NonNls public static final String PROGUARD_CFG_FILE_NAME = "proguard-project.txt"; |
| public static final String SDK_HOME_MACRO = "%MODULE_SDK_HOME%"; |
| public static final String PROGUARD_SYSTEM_CFG_FILE_URL = |
| "file://" + SDK_HOME_MACRO + "/tools/proguard/proguard-android.txt"; |
| private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.util.AndroidCommonUtils"); |
| |
| @NonNls public static final String MANIFEST_JAVA_FILE_NAME = "Manifest.java"; |
| @NonNls public static final String R_JAVA_FILENAME = "R.java"; |
| @NonNls public static final String CLASSES_JAR_FILE_NAME = "classes.jar"; |
| @NonNls public static final String AAR_DEPS_JAR_FILE_NAME = "aar_deps.jar"; |
| @NonNls public static final String CLASSES_FILE_NAME = "classes.dex"; |
| private static final Pattern WARNING_PATTERN = Pattern.compile(".*warning.*"); |
| private static final Pattern ERROR_PATTERN = Pattern.compile(".*error.*"); |
| private static final Pattern EXCEPTION_PATTERN = Pattern.compile(".*exception.*"); |
| |
| public static final Pattern R_PATTERN = Pattern.compile("R(\\$.*)?\\.class"); |
| private static final Pattern MANIFEST_PATTERN = Pattern.compile("Manifest(\\$.*)?\\.class"); |
| private static final String BUILD_CONFIG_CLASS_NAME = "BuildConfig.class"; |
| |
| public static final Pattern COMPILER_MESSAGE_PATTERN = Pattern.compile("(.+):(\\d+):.+"); |
| |
| @NonNls public static final String PNG_EXTENSION = "png"; |
| private static final String[] DRAWABLE_EXTENSIONS = new String[]{PNG_EXTENSION, "jpg", "gif"}; |
| |
| @NonNls public static final String RELEASE_BUILD_OPTION = "RELEASE_BUILD_KEY"; |
| @NonNls public static final String PROGUARD_CFG_PATHS_OPTION = "ANDROID_PROGUARD_CFG_PATHS"; |
| @NonNls public static final String DIRECTORY_FOR_LOGS_NAME = "proguard_logs"; |
| @NonNls public static final String PROGUARD_OUTPUT_JAR_NAME = "obfuscated_sources.jar"; |
| @NonNls public static final String SYSTEM_PROGUARD_CFG_FILE_NAME = "proguard-android.txt"; |
| @NonNls private static final String PROGUARD_HOME_ENV_VARIABLE = "PROGUARD_HOME"; |
| public static final ResourceType[] ID_PROVIDING_RESOURCE_TYPES = new ResourceType[] { |
| ResourceType.LAYOUT, ResourceType.MENU, ResourceType.DRAWABLE, ResourceType.XML, ResourceType.TRANSITION |
| }; |
| |
| @NonNls public static final String INCLUDE_ASSETS_FROM_LIBRARIES_ELEMENT_NAME = "includeAssetsFromLibraries"; |
| @NonNls public static final String ADDITIONAL_NATIVE_LIBS_ELEMENT = "additionalNativeLibs"; |
| @NonNls public static final String ITEM_ELEMENT = "item"; |
| @NonNls public static final String ARCHITECTURE_ATTRIBUTE = "architecture"; |
| @NonNls public static final String URL_ATTRIBUTE = "url"; |
| @NonNls public static final String TARGET_FILE_NAME_ATTRIBUTE = "targetFileName"; |
| |
| private static final String[] TEST_CONFIGURATION_TYPE_IDS = |
| {"JUnit", "TestNG", "ScalaTestRunConfiguration", "SpecsRunConfiguration", "Specs2RunConfiguration"}; |
| @NonNls public static final String ANNOTATIONS_JAR_RELATIVE_PATH = "/tools/support/annotations.jar"; |
| |
| @NonNls public static final String PACKAGE_MANIFEST_ATTRIBUTE = "package"; |
| |
| @NonNls public static final String ANDROID_FINAL_PACKAGE_FOR_ARTIFACT_SUFFIX = ".afp"; |
| @NonNls public static final String PROGUARD_CFG_OUTPUT_FILE_NAME = "proguard.txt"; |
| |
| @NonNls public static final String MANIFEST_MERGING_BUILD_TARGET_TYPE_ID = "android-manifest-merging"; |
| @NonNls public static final String AAR_DEPS_BUILD_TARGET_TYPE_ID = "android-aar-deps"; |
| @NonNls public static final String DEX_BUILD_TARGET_TYPE_ID = "android-dex"; |
| @NonNls public static final String PRE_DEX_BUILD_TARGET_TYPE_ID = "android-pre-dex"; |
| @NonNls public static final String PACKAGING_BUILD_TARGET_TYPE_ID = "android-packaging"; |
| @NonNls public static final String RESOURCE_CACHING_BUILD_TARGET_ID = "android-resource-caching"; |
| @NonNls public static final String RESOURCE_PACKAGING_BUILD_TARGET_ID = "android-resource-packaging"; |
| @NonNls public static final String LIBRARY_PACKAGING_BUILD_TARGET_ID = "android-library-packaging"; |
| @NonNls public static final String AUTOGENERATED_JAVA_FILE_HEADER = "/*___Generated_by_IDEA___*/"; |
| |
| /** Android Test Run Configuration Type Id, defined here so as to be accessible to both JPS and Android plugin. */ |
| @NonNls public static final String ANDROID_TEST_RUN_CONFIGURATION_TYPE = "AndroidTestRunConfigurationType"; |
| |
| private AndroidCommonUtils() { |
| } |
| |
| public static boolean isTestConfiguration(@NotNull String typeId) { |
| return ArrayUtil.find(TEST_CONFIGURATION_TYPE_IDS, typeId) >= 0; |
| } |
| |
| public static boolean isInstrumentationTestConfiguration(@NotNull String typeId) { |
| return ANDROID_TEST_RUN_CONFIGURATION_TYPE.equals(typeId); |
| } |
| |
| public static String command2string(@NotNull Collection<String> command) { |
| final StringBuilder builder = new StringBuilder(); |
| for (Iterator<String> it = command.iterator(); it.hasNext(); ) { |
| String s = it.next(); |
| builder.append('['); |
| builder.append(s); |
| builder.append(']'); |
| if (it.hasNext()) { |
| builder.append(' '); |
| } |
| } |
| return builder.toString(); |
| } |
| |
| public static void moveAllFiles(@NotNull File from, @NotNull File to, @NotNull Collection<File> newFiles) throws IOException { |
| if (from.isFile()) { |
| FileUtil.rename(from, to); |
| newFiles.add(to); |
| } |
| else { |
| final File[] children = from.listFiles(); |
| |
| if (children != null) { |
| for (File child : children) { |
| moveAllFiles(child, new File(to, child.getName()), newFiles); |
| } |
| } |
| } |
| } |
| |
| public static void handleDexCompilationResult(@NotNull Process process, |
| @NotNull String outputFilePath, |
| @NotNull final Map<AndroidCompilerMessageKind, List<String>> messages) { |
| final BaseOSProcessHandler handler = new BaseOSProcessHandler(process, null, null); |
| handler.addProcessListener(new ProcessAdapter() { |
| private AndroidCompilerMessageKind myCategory = null; |
| |
| @Override |
| public void onTextAvailable(ProcessEvent event, Key outputType) { |
| String[] msgs = event.getText().split("\\n"); |
| for (String msg : msgs) { |
| msg = msg.trim(); |
| String msglc = msg.toLowerCase(); |
| if (outputType == ProcessOutputTypes.STDERR) { |
| if (WARNING_PATTERN.matcher(msglc).matches()) { |
| myCategory = AndroidCompilerMessageKind.WARNING; |
| } |
| if (ERROR_PATTERN.matcher(msglc).matches() || EXCEPTION_PATTERN.matcher(msglc).matches() || myCategory == null) { |
| myCategory = AndroidCompilerMessageKind.ERROR; |
| } |
| messages.get(myCategory).add(msg); |
| } |
| else if (outputType == ProcessOutputTypes.STDOUT) { |
| if (!msglc.startsWith("processing")) { |
| messages.get(AndroidCompilerMessageKind.INFORMATION).add(msg); |
| } |
| } |
| |
| LOG.debug(msg); |
| } |
| } |
| }); |
| |
| handler.startNotify(); |
| handler.waitFor(); |
| |
| final List<String> errors = messages.get(AndroidCompilerMessageKind.ERROR); |
| |
| if (new File(outputFilePath).isFile()) { |
| // if compilation finished correctly, show all errors as warnings |
| messages.get(AndroidCompilerMessageKind.WARNING).addAll(errors); |
| errors.clear(); |
| } |
| else if (errors.size() == 0) { |
| errors.add("Cannot create classes.dex file"); |
| } |
| } |
| |
| @NotNull |
| public static List<String> packClassFilesIntoJar(@NotNull String[] firstPackageDirPaths, |
| @NotNull String[] libFirstPackageDirPaths, |
| @NotNull File jarFile) throws IOException { |
| final List<Pair<File, String>> files = new ArrayList<Pair<File, String>>(); |
| for (String path : firstPackageDirPaths) { |
| final File firstPackageDir = new File(path); |
| if (firstPackageDir.exists()) { |
| packClassFilesIntoJar(firstPackageDir, firstPackageDir.getParentFile(), true, files); |
| } |
| } |
| |
| for (String path : libFirstPackageDirPaths) { |
| final File firstPackageDir = new File(path); |
| if (firstPackageDir.exists()) { |
| packClassFilesIntoJar(firstPackageDir, firstPackageDir.getParentFile(), false, files); |
| } |
| } |
| |
| if (files.size() > 0) { |
| final JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile)); |
| try { |
| for (Pair<File, String> pair : files) { |
| packIntoJar(jos, pair.getFirst(), pair.getSecond()); |
| } |
| } |
| finally { |
| jos.close(); |
| } |
| } |
| else if (jarFile.isFile()) { |
| if (!jarFile.delete()) { |
| throw new IOException("Cannot delete file " + FileUtil.toSystemDependentName(jarFile.getPath())); |
| } |
| } |
| final List<String> srcFiles = new ArrayList<String>(); |
| |
| for (Pair<File, String> pair : files) { |
| srcFiles.add(pair.getFirst().getPath()); |
| } |
| return srcFiles; |
| } |
| |
| private static void packClassFilesIntoJar(@NotNull File file, |
| @NotNull File rootDirectory, |
| boolean packRAndManifestClasses, |
| @NotNull List<Pair<File, String>> files) |
| throws IOException { |
| |
| if (file.isDirectory()) { |
| final File[] children = file.listFiles(); |
| |
| if (children != null) { |
| for (File child : children) { |
| packClassFilesIntoJar(child, rootDirectory, packRAndManifestClasses, files); |
| } |
| } |
| } |
| else if (file.isFile()) { |
| if (!FileUtilRt.extensionEquals(file.getName(), "class")) { |
| return; |
| } |
| |
| if (!packRAndManifestClasses && |
| (R_PATTERN.matcher(file.getName()).matches() || |
| MANIFEST_PATTERN.matcher(file.getName()).matches() || |
| BUILD_CONFIG_CLASS_NAME.equals(file.getName()))) { |
| return; |
| } |
| |
| final String rootPath = rootDirectory.getAbsolutePath(); |
| |
| String path = file.getAbsolutePath(); |
| path = FileUtil.toSystemIndependentName(path.substring(rootPath.length())); |
| if (path.charAt(0) == '/') { |
| path = path.substring(1); |
| } |
| |
| files.add(Pair.create(file, path)); |
| } |
| } |
| |
| public static void packIntoJar(@NotNull JarOutputStream jar, @NotNull File file, @NotNull String path) throws IOException { |
| final JarEntry entry = new JarEntry(path); |
| entry.setTime(file.lastModified()); |
| jar.putNextEntry(entry); |
| |
| BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); |
| try { |
| final byte[] buffer = new byte[1024]; |
| int count; |
| while ((count = bis.read(buffer)) != -1) { |
| jar.write(buffer, 0, count); |
| } |
| jar.closeEntry(); |
| } |
| finally { |
| bis.close(); |
| } |
| } |
| |
| @Nullable |
| public static String getResourceTypeByDirName(@NotNull String name) { |
| ResourceFolderType folderType = ResourceFolderType.getFolderType(name); |
| return folderType != null ? folderType.getName() : null; |
| } |
| |
| @NotNull |
| public static String getResourceName(@NotNull String resourceType, @NotNull String fileName) { |
| final String s = FileUtil.getNameWithoutExtension(fileName); |
| |
| return resourceType.equals("drawable") && |
| s.endsWith(".9") && |
| FileUtilRt.extensionEquals(fileName, PNG_EXTENSION) |
| ? s.substring(0, s.length() - 2) |
| : s; |
| } |
| |
| @NotNull |
| public static String getResourceTypeByTagName(@NotNull String tagName) { |
| if (tagName.equals("declare-styleable")) { |
| tagName = "styleable"; |
| } |
| else if (tagName.endsWith("-array")) { |
| tagName = "array"; |
| } |
| return tagName; |
| } |
| |
| @NotNull |
| public static Map<AndroidCompilerMessageKind, List<String>> launchProguard(@NotNull IAndroidTarget target, |
| int sdkToolsRevision, |
| @NotNull String sdkOsPath, |
| @NotNull String javaExecutablePath, |
| @NotNull String proguardVmOptions, |
| @NotNull String[] proguardConfigFileOsPaths, |
| @NotNull String inputJarOsPath, |
| @NotNull String[] externalJarOsPaths, |
| @NotNull String[] providedJarOsPaths, |
| @NotNull String outputJarFileOsPath, |
| @Nullable String logDirOutputOsPath) throws IOException { |
| final List<String> commands = new ArrayList<String>(); |
| commands.add(javaExecutablePath); |
| |
| if (proguardVmOptions.length() > 0) { |
| commands.addAll(ParametersListUtil.parse(proguardVmOptions)); |
| } |
| commands.add("-jar"); |
| final String proguardHome = getProguardHomeDirOsPath(sdkOsPath); |
| final String proguardJarOsPath = proguardHome + File.separator + "lib" + File.separator + "proguard.jar"; |
| commands.add(proguardJarOsPath); |
| |
| if (isIncludingInProguardSupported(sdkToolsRevision)) { |
| for (String proguardConfigFileOsPath : proguardConfigFileOsPaths) { |
| commands.add("-include"); |
| commands.add(quotePath(proguardConfigFileOsPath)); |
| } |
| } |
| else { |
| commands.add("@" + quotePath(proguardConfigFileOsPaths[0])); |
| } |
| |
| commands.add("-injars"); |
| |
| StringBuilder builder = new StringBuilder(quotePath(inputJarOsPath)); |
| |
| for (String jarFile : externalJarOsPaths) { |
| builder.append(File.pathSeparatorChar); |
| builder.append(quotePath(jarFile)); |
| } |
| commands.add(builder.toString()); |
| |
| commands.add("-outjars"); |
| commands.add(quotePath(outputJarFileOsPath)); |
| |
| commands.add("-libraryjars"); |
| |
| builder = new StringBuilder(quotePath(target.getPath(IAndroidTarget.ANDROID_JAR))); |
| |
| List<IAndroidTarget.OptionalLibrary> libraries = target.getAdditionalLibraries(); |
| for (IAndroidTarget.OptionalLibrary lib : libraries) { |
| builder.append(File.pathSeparatorChar); |
| builder.append(quotePath(lib.getJar().getAbsolutePath())); |
| } |
| for (String path : providedJarOsPaths) { |
| builder.append(File.pathSeparatorChar); |
| builder.append(quotePath(path)); |
| } |
| commands.add(builder.toString()); |
| |
| if (logDirOutputOsPath != null) { |
| commands.add("-dump"); |
| commands.add(quotePath(new File(logDirOutputOsPath, "dump.txt").getAbsolutePath())); |
| |
| commands.add("-printseeds"); |
| commands.add(quotePath(new File(logDirOutputOsPath, "seeds.txt").getAbsolutePath())); |
| |
| commands.add("-printusage"); |
| commands.add(quotePath(new File(logDirOutputOsPath, "usage.txt").getAbsolutePath())); |
| |
| commands.add("-printmapping"); |
| commands.add(quotePath(new File(logDirOutputOsPath, "mapping.txt").getAbsolutePath())); |
| } |
| |
| LOG.info(command2string(commands)); |
| final Map<String, String> home = System.getenv().containsKey(PROGUARD_HOME_ENV_VARIABLE) |
| ? Collections.<String, String>emptyMap() |
| : Collections.singletonMap(PROGUARD_HOME_ENV_VARIABLE, proguardHome); |
| return AndroidExecutionUtil.doExecute(ArrayUtil.toStringArray(commands), home); |
| } |
| |
| @NotNull |
| public static String getProguardHomeDirOsPath(@NotNull String sdkOsPath) { |
| return sdkOsPath + File.separator + SdkConstants.FD_TOOLS + File.separator + SdkConstants.FD_PROGUARD; |
| } |
| |
| @NotNull |
| public static String getProguardHomeDirPath(@NotNull String sdkOsPath) { |
| return FileUtil.toSystemIndependentName(getProguardHomeDirOsPath(sdkOsPath)); |
| } |
| |
| public static String getProguardSystemCfgPath(@NotNull String sdkOsPath) { |
| return getProguardHomeDirPath(sdkOsPath) + '/' + SYSTEM_PROGUARD_CFG_FILE_NAME; |
| } |
| |
| private static String quotePath(String path) { |
| if (path.indexOf(' ') != -1) { |
| path = '\'' + path + '\''; |
| } |
| return path; |
| } |
| |
| public static String buildTempInputJar(@NotNull String[] classFilesDirOsPaths, @NotNull String[] libClassFilesDirOsPaths) |
| throws IOException { |
| final File inputJar = FileUtil.createTempFile("proguard_input", ".jar"); |
| |
| packClassFilesIntoJar(classFilesDirOsPaths, libClassFilesDirOsPaths, inputJar); |
| |
| return FileUtil.toSystemDependentName(inputJar.getPath()); |
| } |
| |
| public static String toolPath(@NotNull String toolFileName) { |
| return SdkConstants.OS_SDK_TOOLS_FOLDER + toolFileName; |
| } |
| |
| public static String platformToolPath(@NotNull String toolFileName) { |
| return SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER + toolFileName; |
| } |
| |
| public static boolean isIncludingInProguardSupported(int sdkToolsRevision) { |
| return sdkToolsRevision == -1 || sdkToolsRevision >= 17; |
| } |
| |
| public static int parsePackageRevision(@NotNull String sdkDirOsPath, @NotNull String packageDirName) { |
| final File propFile = |
| new File(sdkDirOsPath + File.separatorChar + packageDirName + File.separatorChar + SdkConstants.FN_SOURCE_PROP); |
| int revisionNumber = -1; |
| if (propFile.exists() && propFile.isFile()) { |
| final Map<String, String> map = |
| ProjectProperties.parsePropertyFile(new BufferingFileWrapper(propFile), new MessageBuildingSdkLog()); |
| if (map == null) { |
| return -1; |
| } |
| String revision = map.get("Pkg.Revision"); |
| |
| if (revision != null) { |
| final int dot = revision.indexOf('.'); |
| if (dot > 0) { |
| revision = revision.substring(0, dot); |
| } |
| |
| try { |
| revisionNumber = Integer.parseInt(revision); |
| } |
| catch (NumberFormatException e) { |
| LOG.debug(e); |
| } |
| } |
| } |
| return revisionNumber > 0 ? revisionNumber : -1; |
| } |
| |
| @NotNull |
| public static String readFile(@NotNull File file) throws IOException { |
| return Files.toString(file, Charsets.UTF_8); |
| } |
| |
| public static boolean contains2Identifiers(String packageName) { |
| return packageName.split("\\.").length >= 2; |
| } |
| |
| public static boolean directoriesContainSameContent(@NotNull File dir1, @NotNull File dir2, @Nullable FileFilter filter) |
| throws IOException { |
| if (dir1.exists() != dir2.exists()) { |
| return false; |
| } |
| |
| final File[] children1 = getFilteredChildren(dir1, filter); |
| final File[] children2 = getFilteredChildren(dir2, filter); |
| |
| if (children1 == null || children2 == null) { |
| return children1 == children2; |
| } |
| |
| if (children1.length != children2.length) { |
| return false; |
| } |
| |
| for (int i = 0; i < children1.length; i++) { |
| final File child1 = children1[i]; |
| final File child2 = children2[i]; |
| |
| if (!Comparing.equal(child1.getName(), child2.getName())) { |
| return false; |
| } |
| |
| final boolean childDir = child1.isDirectory(); |
| if (childDir != child2.isDirectory()) { |
| return false; |
| } |
| |
| if (childDir) { |
| if (!directoriesContainSameContent(child1, child2, filter)) { |
| return false; |
| } |
| } |
| else { |
| final String content1 = readFile(child1); |
| final String content2 = readFile(child2); |
| |
| if (!Comparing.equal(content1, content2)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| @Nullable |
| private static File[] getFilteredChildren(@NotNull File dir, @Nullable FileFilter filter) { |
| final File[] children = dir.listFiles(); |
| if (children == null || children.length == 0 || filter == null) { |
| return children; |
| } |
| |
| final List<File> result = new ArrayList<File>(); |
| for (File child : children) { |
| if (child.isDirectory() || filter.accept(child)) { |
| result.add(child); |
| } |
| } |
| return result.toArray(new File[result.size()]); |
| } |
| |
| @NotNull |
| public static String addSuffixToFileName(@NotNull String path, @NotNull String suffix) { |
| final int dot = path.lastIndexOf('.'); |
| if (dot < 0) { |
| return path + suffix; |
| } |
| final String a = path.substring(0, dot); |
| final String b = path.substring(dot); |
| return a + suffix + b; |
| } |
| |
| public static void signApk(@NotNull File srcApk, |
| @NotNull File destFile, |
| @NotNull PrivateKey privateKey, |
| @NotNull X509Certificate certificate) |
| throws IOException, GeneralSecurityException { |
| FileOutputStream fos = new FileOutputStream(destFile); |
| SignedJarBuilder builder = new SafeSignedJarBuilder(fos, privateKey, certificate, destFile.getPath()); |
| FileInputStream fis = new FileInputStream(srcApk); |
| try { |
| builder.writeZip(fis, null); |
| builder.close(); |
| } |
| finally { |
| try { |
| fis.close(); |
| } |
| catch (IOException ignored) { |
| } |
| finally { |
| try { |
| fos.close(); |
| } |
| catch (IOException ignored) { |
| } |
| } |
| } |
| } |
| |
| @NotNull |
| public static String getStackTrace(@NotNull Throwable t) { |
| final StringWriter stringWriter = new StringWriter(); |
| final PrintWriter writer = new PrintWriter(stringWriter); |
| try { |
| t.printStackTrace(writer); |
| return stringWriter.toString(); |
| } |
| finally { |
| writer.close(); |
| } |
| } |
| |
| public static boolean hasXmxParam(@NotNull List<String> parameters) { |
| for (String param : parameters) { |
| if (param.startsWith("-Xmx")) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Nullable |
| public static String executeZipAlign(@NotNull String zipAlignPath, @NotNull File source, @NotNull File destination) { |
| final ProcessBuilder processBuilder = new ProcessBuilder( |
| zipAlignPath, "-f", "4", source.getAbsolutePath(), destination.getAbsolutePath()); |
| |
| BaseOSProcessHandler handler; |
| try { |
| handler = new BaseOSProcessHandler(processBuilder.start(), "", null); |
| } |
| catch (IOException e) { |
| return e.getMessage(); |
| } |
| final StringBuilder builder = new StringBuilder(); |
| handler.addProcessListener(new ProcessAdapter() { |
| @Override |
| public void onTextAvailable(ProcessEvent event, Key outputType) { |
| builder.append(event.getText()); |
| } |
| }); |
| handler.startNotify(); |
| handler.waitFor(); |
| int exitCode = handler.getProcess().exitValue(); |
| return exitCode != 0 ? builder.toString() : null; |
| } |
| |
| @NotNull |
| public static Map<AndroidCompilerMessageKind, List<String>> buildArtifact(@NotNull String artifactName, |
| @NotNull String messagePrefix, |
| @NotNull String sdkLocation, |
| @NotNull IAndroidTarget target, |
| @Nullable String artifactFilePath, |
| @NotNull String keyStorePath, |
| @Nullable String keyAlias, |
| @Nullable String keyStorePassword, |
| @Nullable String keyPassword) |
| throws GeneralSecurityException, IOException { |
| final Map<AndroidCompilerMessageKind, List<String>> messages = new HashMap<AndroidCompilerMessageKind, List<String>>(); |
| messages.put(AndroidCompilerMessageKind.ERROR, new ArrayList<String>()); |
| messages.put(AndroidCompilerMessageKind.WARNING, new ArrayList<String>()); |
| messages.put(AndroidCompilerMessageKind.INFORMATION, new ArrayList<String>()); |
| |
| final Pair<PrivateKey, X509Certificate> pair = getPrivateKeyAndCertificate(messagePrefix, messages, keyAlias, keyStorePath, |
| keyStorePassword, keyPassword); |
| if (pair == null) { |
| return messages; |
| } |
| final String prefix = "Cannot sign artifact " + artifactName + ": "; |
| |
| if (artifactFilePath == null) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(prefix + "output path is not specified"); |
| return messages; |
| } |
| |
| final File artifactFile = new File(artifactFilePath); |
| if (!artifactFile.exists()) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(prefix + "file " + artifactFilePath + " hasn't been generated"); |
| return messages; |
| } |
| final String zipAlignPath = getZipAlign(sdkLocation, target); |
| final boolean runZipAlign = new File(zipAlignPath).isFile(); |
| |
| File tmpDir = null; |
| try { |
| tmpDir = FileUtil.createTempDirectory("android_artifact", "tmp"); |
| final File tmpArtifact = new File(tmpDir, "tmpArtifact.apk"); |
| |
| signApk(artifactFile, tmpArtifact, pair.getFirst(), pair.getSecond()); |
| |
| if (!FileUtil.delete(artifactFile)) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add("Cannot delete file " + artifactFile.getPath()); |
| return messages; |
| } |
| |
| if (runZipAlign) { |
| final String errorMessage = executeZipAlign(zipAlignPath, tmpArtifact, artifactFile); |
| if (errorMessage != null) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(messagePrefix + "zip-align: " + errorMessage); |
| return messages; |
| } |
| } |
| else { |
| messages.get(AndroidCompilerMessageKind.WARNING).add(messagePrefix + AndroidCommonBundle.message( |
| "android.artifact.building.cannot.find.zip.align.error")); |
| FileUtil.copy(tmpArtifact, artifactFile); |
| } |
| } |
| finally { |
| if (tmpDir != null) { |
| FileUtil.delete(tmpDir); |
| } |
| } |
| return messages; |
| } |
| |
| @Nullable |
| private static Pair<PrivateKey, X509Certificate> getPrivateKeyAndCertificate(@NotNull String errorPrefix, |
| @NotNull Map<AndroidCompilerMessageKind, List<String>> messages, |
| @Nullable String keyAlias, |
| @Nullable String keyStoreFilePath, |
| @Nullable String keyStorePasswordStr, |
| @Nullable String keyPasswordStr) |
| throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableEntryException { |
| |
| if (keyStoreFilePath == null || keyStoreFilePath.length() == 0) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(errorPrefix + "Key store file is not specified"); |
| return null; |
| } |
| if (keyStorePasswordStr == null) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(errorPrefix + "Key store password is not specified"); |
| return null; |
| } |
| if (keyAlias == null || keyAlias.length() == 0) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(errorPrefix + "Key alias is not specified"); |
| return null; |
| } |
| if (keyPasswordStr == null) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(errorPrefix + "Key password is not specified"); |
| return null; |
| } |
| final File keyStoreFile = new File(keyStoreFilePath); |
| final char[] keystorePassword = keyStorePasswordStr.toCharArray(); |
| final char[] plainKeyPassword = keyPasswordStr.toCharArray(); |
| |
| final KeyStore keyStore; |
| InputStream is = null; |
| try { |
| //noinspection IOResourceOpenedButNotSafelyClosed |
| is = new FileInputStream(keyStoreFile); |
| keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
| keyStore.load(is, keystorePassword); |
| |
| final KeyStore.PrivateKeyEntry entry = |
| (KeyStore.PrivateKeyEntry)keyStore.getEntry(keyAlias, new KeyStore.PasswordProtection(plainKeyPassword)); |
| if (entry == null) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(errorPrefix + AndroidCommonBundle.message( |
| "android.artifact.building.cannot.find.key.error", keyAlias)); |
| return null; |
| } |
| |
| final PrivateKey privateKey = entry.getPrivateKey(); |
| final Certificate certificate = entry.getCertificate(); |
| if (privateKey == null || certificate == null) { |
| messages.get(AndroidCompilerMessageKind.ERROR).add(errorPrefix + AndroidCommonBundle.message( |
| "android.artifact.building.cannot.find.key.error", keyAlias)); |
| return null; |
| } |
| return Pair.create(privateKey, (X509Certificate)certificate); |
| } |
| finally { |
| if (is != null) { |
| try { |
| is.close(); |
| } |
| catch (IOException e) { |
| LOG.info(e); |
| } |
| } |
| } |
| } |
| |
| @NotNull |
| public static String getZipAlign(@NotNull String sdkPath, @NotNull IAndroidTarget target) { |
| final BuildToolInfo buildToolInfo = target.getBuildToolInfo(); |
| |
| if (buildToolInfo != null) { |
| String path = null; |
| try { |
| path = buildToolInfo.getPath(BuildToolInfo.PathId.ZIP_ALIGN); |
| } |
| catch (Throwable ignored) { |
| } |
| if (path != null && new File(path).exists()) { |
| return path; |
| } |
| } |
| return sdkPath + File.separatorChar + toolPath(SdkConstants.FN_ZIPALIGN); |
| } |
| } |