com.android.builder.internal.packaging.OldPackager Maven / Gradle / Ivy
Show all versions of builder Show documentation
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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 com.android.builder.internal.packaging;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.FN_APK_CLASSES_DEX;
import static com.android.SdkConstants.FN_APK_CLASSES_N_DEX;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.files.FileModificationType;
import com.android.builder.files.RelativeFile;
import com.android.builder.packaging.ApkCreator;
import com.android.builder.packaging.ApkCreatorFactory;
import com.android.builder.packaging.DuplicateFileException;
import com.android.builder.packaging.PackagerException;
import com.android.builder.packaging.ZipAbortException;
import com.android.builder.packaging.ZipEntryFilter;
import com.android.builder.signing.SignedJarApkCreatorFactory;
import com.android.utils.ILogger;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.io.Closer;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/**
* This is the old implementation of the {@link IncrementalPackager} class maintained to allow
* using the old packaging code in case the new one is not usable in some scenario that was not
* tested. This class will eventually be removed in a future version (along with all other old
* packaging code).
*
* Previous documentation:
*
*
Class making the final app package.
* The inputs are:
* - packaged resources (output of aapt)
* - code file (output of dx)
* - Java resources coming from the project, its libraries, and its jar files
* - Native libraries from the project or its library.
*
* @deprecated the {@link IncrementalPackager} should be used from now one that allows both full
* and incremental packaging.
*/
@Deprecated
public final class OldPackager implements Closeable {
/**
* Filter to detect duplicate entries
*
*/
private final class DuplicateZipFilter implements ZipEntryFilter {
private File mInputFile;
void reset(File inputFile) {
mInputFile = inputFile;
}
@Override
public boolean checkEntry(String archivePath) throws ZipAbortException {
mLogger.verbose("=> %s", archivePath);
File duplicate = checkFileForDuplicate(archivePath);
if (duplicate != null) {
// we have a duplicate but it might be the same source file, in this case,
// we just ignore the duplicate, and of course, we don't add it again.
File potentialDuplicate = new File(mInputFile, archivePath);
if (!duplicate.getAbsolutePath().equals(potentialDuplicate.getAbsolutePath())) {
throw new DuplicateFileException(archivePath, duplicate, mInputFile);
}
return false;
} else {
mAddedFiles.put(archivePath, mInputFile);
}
return true;
}
}
/**
* A filter to filter out binary files like .class
*/
private static final class NoJavaClassZipFilter implements ZipEntryFilter {
@NonNull
private final ZipEntryFilter parentFilter;
private NoJavaClassZipFilter(@NonNull ZipEntryFilter parentFilter) {
this.parentFilter = parentFilter;
}
@Override
public boolean checkEntry(String archivePath) throws ZipAbortException {
return parentFilter.checkEntry(archivePath) && !archivePath.endsWith(DOT_CLASS);
}
}
/**
* APK creator. {@code null} if not open.
*/
@Nullable
private ApkCreator mApkCreator;
private final ILogger mLogger;
private final DuplicateZipFilter mNoDuplicateFilter = new DuplicateZipFilter();
private final NoJavaClassZipFilter mNoJavaClassZipFilter = new NoJavaClassZipFilter(
mNoDuplicateFilter);
private final HashMap mAddedFiles = new HashMap();
/**
* Creates a new instance.
*
* This creates a new builder that will create the specified output file.
*
* @param creationData APK creation data
* @param resLocation location of the zip with the resources, if any
* @param logger the logger
* @throws PackagerException failed to create the initial APK
* @throws IOException failed to create the APK
*/
public OldPackager(@NonNull ApkCreatorFactory.CreationData creationData,
@Nullable String resLocation,
@NonNull ILogger logger) throws PackagerException, IOException {
checkOutputFile(creationData.getApkPath());
Closer closer = Closer.create();
try {
checkOutputFile(creationData.getApkPath());
File resFile = null;
if (resLocation != null) {
resFile = new File(resLocation);
checkInputFile(resFile);
}
mLogger = logger;
ApkCreatorFactory factory = new SignedJarApkCreatorFactory();
mApkCreator = factory.make(creationData);
mLogger.verbose("Packaging %s", creationData.getApkPath().getName());
// add the resources
if (resFile != null) {
addZipFile(resFile);
}
} catch (Throwable e) {
closer.register(mApkCreator);
mApkCreator = null;
throw closer.rethrow(e, PackagerException.class);
} finally {
closer.close();
}
}
public void addDexFiles(@NonNull Set dexFolders) throws PackagerException, IOException {
Preconditions.checkNotNull(mApkCreator, "mApkCreator == null");
// If there is a single folder that's either no multi-dex or pre-21 multidex (where
// dx has merged them all into 2+ dex files).
// IF there are 2+ folders then we are directly adding the pre-dexing output.
if (dexFolders.size() == 1 ) {
File[] dexFiles = Iterables.getOnlyElement(dexFolders).listFiles(
new FilenameFilter() {
@Override
public boolean accept(File file, String name) {
return name.endsWith(SdkConstants.DOT_DEX);
}
});
if (dexFiles != null) {
for (File dexFile : dexFiles) {
addFile(dexFile, dexFile.getName());
}
}
} else {
// in 21+ mode we can simply include all the dex files, and rename them as we
// go so that their indices are contiguous.
int dexIndex = 1;
for (File folderEntry : dexFolders) {
dexIndex = addContentOfDexFolder(folderEntry, dexIndex);
}
}
}
private int addContentOfDexFolder(@NonNull File dexFolder, int dexIndex)
throws PackagerException, IOException {
File[] dexFiles = dexFolder.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String name) {
return name.endsWith(SdkConstants.DOT_DEX);
}
});
if (dexFiles != null) {
for (File dexFile : dexFiles) {
addFile(dexFile,
dexIndex == 1 ?
FN_APK_CLASSES_DEX :
String.format(FN_APK_CLASSES_N_DEX, dexIndex));
dexIndex++;
}
}
return dexIndex;
}
/**
* Adds a file to the APK at a given path.
*
* @param file the file to add
* @param archivePath the path of the file inside the APK archive.
* @throws PackagerException if an error occurred
* @throws IOException if an error occurred
*/
public void addFile(File file, String archivePath) throws PackagerException, IOException {
Preconditions.checkState(mApkCreator != null, "mApkCreator == null");
doAddFile(file, archivePath);
}
/**
* Adds the content from a zip file.
* All file keep the same path inside the archive.
*
* @param zipFile the zip File.
* @throws PackagerException if an error occurred
* @throws IOException if an error occurred
*/
void addZipFile(File zipFile) throws PackagerException, IOException {
Preconditions.checkState(mApkCreator != null, "mApkCreator == null");
mLogger.verbose("%s:", zipFile);
// reset the filter with this input.
mNoDuplicateFilter.reset(zipFile);
/*
* Predicate does not allow exceptions so we mask the ZipAbortException inside a
* RuntimeException.
*/
try {
// ask the builder to add the content of the file.
mApkCreator.writeZip(zipFile, null, input -> {
try {
return !mNoDuplicateFilter.checkEntry(input);
} catch (ZipAbortException e) {
throw new RuntimeException(e);
}
});
} catch (RuntimeException e) {
if (e.getCause() instanceof ZipAbortException) {
throw (ZipAbortException) e.getCause();
}
throw e;
}
}
/**
* Incrementally updates a resource in the packaging. The resource can be added or removed,
* depending on the change made to the file.
*
* @param file the file to update
* @param modificationType the type of file modification
* @throws PackagerException failed to update the package
*/
public void updateResource(@NonNull RelativeFile file,
@NonNull FileModificationType modificationType) throws PackagerException {
if (modificationType == FileModificationType.NEW
|| modificationType == FileModificationType.CHANGED) {
doAddFile(file.getFile(), file.getOsIndependentRelativePath());
} else {
throw new UnsupportedOperationException("Cannot remove a file from archive.");
}
}
/**
* Incrementally updates resources in the packaging. The resources can be added or removed,
* depending on the changes made to the file. Updating an archive file as modified will update
* the entries, but will not remove archive entries tht are no longer in the archive.
*
* @param file the archive file (zip)
* @param modificationType the type of file modification
* @param isIgnored the filter to apply to the contents of the archive; the filter is applied
* before processing: filtered out files are exactly the same as inexistent files; the filter
* applies to the path stored in the zip
* @throws PackagerException failed to update the package
*/
public void updateResourceArchive(@NonNull File file,
@NonNull FileModificationType modificationType,
@NonNull final Predicate isIgnored) throws PackagerException {
Preconditions.checkNotNull(mApkCreator, "mApkCreator == null");
if (modificationType == FileModificationType.NEW
|| modificationType == FileModificationType.CHANGED) {
try {
Closer closer = Closer.create();
try {
/*
* Note that ZipAbortException has to be masked because it is not allowed in
* the Predicate interface.
*/
Predicate newIsIgnored = input -> {
try {
if (!mNoJavaClassZipFilter.checkEntry(input)) {
return true;
}
} catch (ZipAbortException e) {
throw new RuntimeException(e);
}
return isIgnored.apply(input);
};
mApkCreator.writeZip(file, null, newIsIgnored::apply);
} catch (Throwable t) {
throw closer.rethrow(t, ZipAbortException.class);
} finally {
closer.close();
}
} catch (IOException e) {
throw new PackagerException(e);
}
}
}
private void doAddFile(
@NonNull File file,
@NonNull String archivePath) throws PackagerException {
Preconditions.checkNotNull(mApkCreator, "mApkCreator == null");
mAddedFiles.put(archivePath, file);
try {
mApkCreator.writeFile(file, archivePath);
} catch (IOException e) {
throw new PackagerException(e);
}
}
/**
* Checks if the given path in the APK archive has not already been used and if it has been,
* then returns a {@link File} object for the source of the duplicate
* @param archivePath the archive path to test.
* @return A File object of either a file at the same location or an archive that contains a
* file that was put at the same location.
*/
private File checkFileForDuplicate(String archivePath) {
return mAddedFiles.get(archivePath);
}
/**
* Checks an output {@link File} object.
* This checks the following:
* - the file is not an existing directory.
* - if the file exists, that it can be modified.
* - if it doesn't exists, that a new file can be created.
* @param file the File to check
* @throws PackagerException If the check fails
*/
private static void checkOutputFile(File file) throws PackagerException {
if (file.isDirectory()) {
throw new PackagerException("%s is a directory!", file);
}
if (file.exists()) { // will be a file in this case.
if (!file.canWrite()) {
throw new PackagerException("Cannot write %s", file);
}
} else {
try {
if (!file.createNewFile()) {
throw new PackagerException("Failed to create %s", file);
}
/*
* We succeeded at creating the file. Now, delete it because a zero-byte file is
* not a valid APK and some ApkCreator implementations (e.g., the ZFile one)
* complain if open on top of an invalid zip file.
*/
if (!file.delete()) {
throw new PackagerException("Failed to delete newly created %s", file);
}
} catch (IOException e) {
throw new PackagerException(
"Failed to create '%1$ss': %2$s", file, e.getMessage());
}
}
}
/**
* Checks an input {@link File} object.
* This checks the following:
* - the file is not an existing directory.
* - that the file exists and can be read.
* @param file the File to check
* @throws FileNotFoundException if the file is not here.
* @throws PackagerException If the file is a folder or a file that cannot be read.
*/
private static void checkInputFile(File file) throws FileNotFoundException, PackagerException {
if (file.isDirectory()) {
throw new PackagerException("%s is a directory!", file);
}
if (file.exists()) {
if (!file.canRead()) {
throw new PackagerException("Cannot read %s", file);
}
} else {
throw new FileNotFoundException(String.format("%s does not exist", file));
}
}
public static String getLocalVersion() {
Class clazz = IncrementalPackager.class;
String className = clazz.getSimpleName() + ".class";
String classPath = clazz.getResource(className).toString();
if (!classPath.startsWith("jar")) {
// Class not from JAR, unlikely
return null;
}
try {
String manifestPath = classPath.substring(0, classPath.lastIndexOf('!') + 1) +
"/META-INF/MANIFEST.MF";
URLConnection jarConnection = new URL(manifestPath).openConnection();
jarConnection.setUseCaches(false);
InputStream jarInputStream = jarConnection.getInputStream();
Attributes attr = new Manifest(jarInputStream).getMainAttributes();
jarInputStream.close();
return attr.getValue("Builder-Version");
} catch (MalformedURLException ignored) {
} catch (IOException ignored) {
}
return null;
}
@Override
public void close() throws IOException {
if (mApkCreator == null) {
return;
}
ApkCreator builder = mApkCreator;
mApkCreator = null;
builder.close();
}
}