blob: 22ca80046204e989f7a723ee0e96cc4485c6851f [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.android.compiler.tools;
import com.android.SdkConstants;
import com.android.jarutils.DebugKeyProvider;
import com.android.jarutils.JavaResourceFilter;
import com.android.jarutils.SignedJarBuilder;
import com.android.prefs.AndroidLocation;
import com.android.sdklib.IAndroidTarget;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.containers.HashSet;
import com.intellij.util.text.DateFormatUtil;
import org.jetbrains.android.util.*;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static org.jetbrains.android.util.AndroidCompilerMessageKind.*;
/**
* @author yole
*/
public class AndroidApkBuilder {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.compiler.tools.AndroidApkBuilder");
@NonNls private static final String UNALIGNED_SUFFIX = ".unaligned";
@NonNls private static final String EXT_NATIVE_LIB = "so";
private AndroidApkBuilder() {
}
private static Map<AndroidCompilerMessageKind, List<String>> filterUsingKeystoreMessages(Map<AndroidCompilerMessageKind, List<String>> messages) {
List<String> infoMessages = messages.get(INFORMATION);
if (infoMessages == null) {
infoMessages = new ArrayList<String>();
messages.put(INFORMATION, infoMessages);
}
final List<String> errors = messages.get(ERROR);
for (Iterator<String> iterator = errors.iterator(); iterator.hasNext();) {
String s = iterator.next();
if (s.startsWith("Using keystore:")) {
// not actually an error
infoMessages.add(s);
iterator.remove();
}
}
return messages;
}
@SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"})
private static void collectDuplicateEntries(@NotNull String rootFile, @NotNull Set<String> entries, @NotNull Set<String> result)
throws IOException {
final JavaResourceFilter javaResourceFilter = new JavaResourceFilter();
FileInputStream fis = null;
ZipInputStream zis = null;
try {
fis = new FileInputStream(rootFile);
zis = new ZipInputStream(fis);
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory()) {
String name = entry.getName();
if (javaResourceFilter.checkEntry(name) && !entries.add(name)) {
result.add(name);
}
zis.closeEntry();
}
}
}
finally {
if (zis != null) {
zis.close();
}
if (fis != null) {
fis.close();
}
}
}
public static Map<AndroidCompilerMessageKind, List<String>> execute(@NotNull String resPackagePath,
@NotNull String dexPath,
@NotNull String[] resourceRoots,
@NotNull String[] externalJars,
@NotNull String[] nativeLibsFolders,
@NotNull Collection<AndroidNativeLibData> additionalNativeLibs,
@NotNull String finalApk,
boolean unsigned,
@NotNull String sdkPath,
@NotNull IAndroidTarget target,
@Nullable String customKeystorePath,
@NotNull Condition<File> resourceFilter) throws IOException {
final AndroidBuildTestingManager testingManager = AndroidBuildTestingManager.getTestingManager();
if (testingManager != null) {
testingManager.getCommandExecutor().log(StringUtil.join(new String[]{
"apk_builder",
resPackagePath,
dexPath,
AndroidBuildTestingManager.arrayToString(resourceRoots),
AndroidBuildTestingManager.arrayToString(externalJars),
AndroidBuildTestingManager.arrayToString(nativeLibsFolders),
additionalNativeLibs.toString(),
finalApk,
Boolean.toString(unsigned),
sdkPath,
customKeystorePath}, "\n"));
}
final Map<AndroidCompilerMessageKind, List<String>> map = new HashMap<AndroidCompilerMessageKind, List<String>>();
map.put(ERROR, new ArrayList<String>());
map.put(WARNING, new ArrayList<String>());
final File outputDir = new File(finalApk).getParentFile();
if (!outputDir.exists() && !outputDir.mkdirs()) {
map.get(ERROR).add("Cannot create directory " + outputDir.getPath());
return map;
}
File additionalLibsDir = null;
try {
if (additionalNativeLibs.size() > 0) {
additionalLibsDir = FileUtil.createTempDirectory("android_additional_libs", "tmp");
if (!copyNativeLibs(additionalNativeLibs, additionalLibsDir, map)) {
return map;
}
nativeLibsFolders = ArrayUtil.append(nativeLibsFolders, additionalLibsDir.getPath());
}
if (unsigned) {
return filterUsingKeystoreMessages(
finalPackage(dexPath, resourceRoots, externalJars, nativeLibsFolders, finalApk, resPackagePath, customKeystorePath, false,
resourceFilter));
}
final String zipAlignPath = AndroidCommonUtils.getZipAlign(sdkPath, target);
boolean withAlignment = new File(zipAlignPath).exists();
String unalignedApk = AndroidCommonUtils.addSuffixToFileName(finalApk, UNALIGNED_SUFFIX);
Map<AndroidCompilerMessageKind, List<String>> map2 = filterUsingKeystoreMessages(
finalPackage(dexPath, resourceRoots, externalJars, nativeLibsFolders, withAlignment ? unalignedApk : finalApk, resPackagePath,
customKeystorePath, true, resourceFilter));
map.putAll(map2);
if (withAlignment && map.get(ERROR).size() == 0) {
map2 = AndroidExecutionUtil.doExecute(zipAlignPath, "-f", "4", unalignedApk, finalApk);
map.putAll(map2);
}
return map;
}
finally {
if (additionalLibsDir != null) {
FileUtil.delete(additionalLibsDir);
}
}
}
private static boolean copyNativeLibs(@NotNull Collection<AndroidNativeLibData> libs,
@NotNull File targetDir,
@NotNull Map<AndroidCompilerMessageKind, List<String>> map) throws IOException {
for (AndroidNativeLibData lib : libs) {
final String path = lib.getPath();
final File srcFile = new File(path);
if (!srcFile.exists()) {
map.get(WARNING).add("File not found: " + FileUtil.toSystemDependentName(path) + ". The native library won't be placed into APK");
continue;
}
final File dstDir = new File(targetDir, lib.getArchitecture());
final File dstFile = new File(dstDir, lib.getTargetFileName());
if (dstFile.exists()) {
map.get(WARNING).add("Duplicate native library " + dstFile.getName() + "; " + dstFile.getPath() + " already exists");
continue;
}
if (!dstDir.exists() && !dstDir.mkdirs()) {
map.get(ERROR).add("Cannot create directory: " + FileUtil.toSystemDependentName(dstDir.getPath()));
continue;
}
FileUtil.copy(srcFile, dstFile);
}
return map.get(ERROR).size() == 0;
}
private static Map<AndroidCompilerMessageKind, List<String>> finalPackage(@NotNull String dexPath,
@NotNull String[] javaResourceRoots,
@NotNull String[] externalJars,
@NotNull String[] nativeLibsFolders,
@NotNull String outputApk,
@NotNull String apkPath,
@Nullable String customKeystorePath,
boolean signed,
@NotNull Condition<File> resourceFilter) {
final Map<AndroidCompilerMessageKind, List<String>> result = new HashMap<AndroidCompilerMessageKind, List<String>>();
result.put(ERROR, new ArrayList<String>());
result.put(INFORMATION, new ArrayList<String>());
result.put(WARNING, new ArrayList<String>());
FileOutputStream fos = null;
SignedJarBuilder builder = null;
try {
String keyStoreOsPath = customKeystorePath != null && customKeystorePath.length() > 0
? customKeystorePath
: DebugKeyProvider.getDefaultKeyStoreOsPath();
DebugKeyProvider provider = createDebugKeyProvider(result, keyStoreOsPath);
X509Certificate certificate = signed ? (X509Certificate)provider.getCertificate() : null;
if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
// generate a new one
File keyStoreFile = new File(keyStoreOsPath);
if (keyStoreFile.exists()) {
keyStoreFile.delete();
}
provider = createDebugKeyProvider(result, keyStoreOsPath);
certificate = (X509Certificate)provider.getCertificate();
}
if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
String date = DateFormatUtil.formatPrettyDateTime(certificate.getNotAfter());
result.get(ERROR).add(
("Debug certificate expired on " + date + ". Cannot regenerate it, please delete file \"" + keyStoreOsPath + "\" manually."));
return result;
}
PrivateKey key = provider.getDebugKey();
if (key == null) {
result.get(ERROR).add("Cannot create new key or keystore");
return result;
}
if (!new File(apkPath).exists()) {
result.get(ERROR).add("File " + apkPath + " not found. Try to rebuild project");
return result;
}
File dexEntryFile = new File(dexPath);
if (!dexEntryFile.exists()) {
result.get(ERROR).add("File " + dexEntryFile.getPath() + " not found. Try to rebuild project");
return result;
}
for (String externalJar : externalJars) {
if (new File(externalJar).isDirectory()) {
result.get(ERROR).add(externalJar + " is directory. Directory libraries are not supported");
}
}
if (result.get(ERROR).size() > 0) {
return result;
}
fos = new FileOutputStream(outputApk);
builder = new SafeSignedJarBuilder(fos, key, certificate, outputApk);
FileInputStream fis = new FileInputStream(apkPath);
try {
builder.writeZip(fis, null);
}
finally {
fis.close();
}
builder.writeFile(dexEntryFile, AndroidCommonUtils.CLASSES_FILE_NAME);
final HashSet<String> added = new HashSet<String>();
for (String resourceRootPath : javaResourceRoots) {
final HashSet<File> javaResources = new HashSet<File>();
final File resourceRoot = new File(resourceRootPath);
collectStandardJavaResources(resourceRoot, javaResources, resourceFilter);
writeStandardJavaResources(javaResources, resourceRoot, builder, added);
}
Set<String> duplicates = new HashSet<String>();
Set<String> entries = new HashSet<String>();
for (String externalJar : externalJars) {
collectDuplicateEntries(externalJar, entries, duplicates);
}
for (String duplicate : duplicates) {
result.get(WARNING).add("Duplicate entry " + duplicate + ". The file won't be added");
}
MyResourceFilter filter = new MyResourceFilter(duplicates);
for (String externalJar : externalJars) {
fis = new FileInputStream(externalJar);
try {
builder.writeZip(fis, filter);
}
finally {
fis.close();
}
}
final HashSet<String> nativeLibs = new HashSet<String>();
for (String nativeLibsFolderPath : nativeLibsFolders) {
final File nativeLibsFolder = new File(nativeLibsFolderPath);
final File[] children = nativeLibsFolder.listFiles();
if (children != null) {
for (File child : children) {
writeNativeLibraries(builder, nativeLibsFolder, child, signed, nativeLibs);
}
}
}
}
catch (IOException e) {
return addExceptionMessage(e, result);
}
catch (CertificateException e) {
return addExceptionMessage(e, result);
}
catch (DebugKeyProvider.KeytoolException e) {
return addExceptionMessage(e, result);
}
catch (AndroidLocation.AndroidLocationException e) {
return addExceptionMessage(e, result);
}
catch (NoSuchAlgorithmException e) {
return addExceptionMessage(e, result);
}
catch (UnrecoverableEntryException e) {
return addExceptionMessage(e, result);
}
catch (KeyStoreException e) {
return addExceptionMessage(e, result);
}
catch (GeneralSecurityException e) {
return addExceptionMessage(e, result);
}
finally {
if (builder != null) {
try {
builder.close();
}
catch (IOException e) {
addExceptionMessage(e, result);
}
catch (GeneralSecurityException e) {
addExceptionMessage(e, result);
}
}
if (fos != null) {
try {
fos.close();
}
catch (IOException ignored) {
}
}
}
return result;
}
private static DebugKeyProvider createDebugKeyProvider(final Map<AndroidCompilerMessageKind, List<String>> result, String path) throws
KeyStoreException,
NoSuchAlgorithmException,
CertificateException,
UnrecoverableEntryException,
IOException,
DebugKeyProvider.KeytoolException,
AndroidLocation.AndroidLocationException {
return new DebugKeyProvider(path, null, new DebugKeyProvider.IKeyGenOutput() {
public void err(String message) {
result.get(ERROR).add("Error during key creation: " + message);
}
public void out(String message) {
result.get(INFORMATION).add("Info message during key creation: " + message);
}
});
}
private static void writeNativeLibraries(SignedJarBuilder builder,
File nativeLibsFolder,
File child,
boolean debugBuild,
Set<String> added)
throws IOException {
ArrayList<File> list = new ArrayList<File>();
collectNativeLibraries(child, list, debugBuild);
for (File file : list) {
final String relativePath = FileUtil.getRelativePath(nativeLibsFolder, file);
String path = FileUtil.toSystemIndependentName(SdkConstants.FD_APK_NATIVE_LIBS + File.separator + relativePath);
if (added.add(path)) {
builder.writeFile(file, path);
LOG.info("Native lib file added to APK: " + file.getPath());
}
else {
LOG.info("Duplicate in APK: native lib file " + file.getPath() + " won't be added.");
}
}
}
private static Map<AndroidCompilerMessageKind, List<String>> addExceptionMessage(Exception e,
Map<AndroidCompilerMessageKind, List<String>> result) {
LOG.info(e);
String simpleExceptionName = e.getClass().getCanonicalName();
result.get(ERROR).add(simpleExceptionName + ": " + e.getMessage());
return result;
}
public static void collectNativeLibraries(@NotNull File file, @NotNull List<File> result, boolean debugBuild) {
if (!file.isDirectory()) {
// some users store jars and *.so libs in the same directory. Do not pack JARs to APKs "lib" folder!
if (FileUtilRt.extensionEquals(file.getName(), EXT_NATIVE_LIB) ||
(debugBuild && !(FileUtilRt.extensionEquals(file.getName(), "jar")))) {
result.add(file);
}
}
else if (JavaResourceFilter.checkFolderForPackaging(file.getName())) {
final File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
collectNativeLibraries(child, result, debugBuild);
}
}
}
}
public static void collectStandardJavaResources(@NotNull File folder,
@NotNull Collection<File> result,
@NotNull Condition<File> filter) {
final File[] children = folder.listFiles();
if (children != null) {
for (File child : children) {
if (child.exists()) {
if (child.isDirectory()) {
if (JavaResourceFilter.checkFolderForPackaging(child.getName()) && filter.value(child)) {
collectStandardJavaResources(child, result, filter);
}
}
else if (checkFileForPackaging(child) && filter.value(child)) {
result.add(child);
}
}
}
}
}
private static void writeStandardJavaResources(Collection<File> resources,
File sourceRoot,
SignedJarBuilder jarBuilder,
Set<String> added) throws IOException {
for (File child : resources) {
final String relativePath = FileUtil.getRelativePath(sourceRoot, child);
if (relativePath != null && !added.contains(relativePath)) {
jarBuilder.writeFile(child, FileUtil.toSystemIndependentName(relativePath));
added.add(relativePath);
}
}
}
public static boolean checkFileForPackaging(@NotNull File file) {
String fileName = FileUtil.getNameWithoutExtension(file);
if (fileName.length() > 0) {
final String extension = FileUtilRt.getExtension(file.getName());
if (SdkConstants.EXT_ANDROID_PACKAGE.equals(extension)) {
return false;
}
return JavaResourceFilter.checkFileForPackaging(fileName, extension);
}
return false;
}
private static class MyResourceFilter extends JavaResourceFilter {
private final Set<String> myExcludedEntries;
public MyResourceFilter(@NotNull Set<String> excludedEntries) {
myExcludedEntries = excludedEntries;
}
@Override
public boolean checkEntry(String name) {
if (myExcludedEntries.contains(name)) {
return false;
}
return super.checkEntry(name);
}
}
}