blob: aedd1fe1f82bf3bd275d08559057d4db8d828e85 [file] [log] [blame]
/*
* Copyright (C) 2016 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.build.gradle.internal.packaging;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.gradle.AndroidGradleOptions;
import com.android.build.gradle.internal.incremental.CapturingChangesApkCreator;
import com.android.build.gradle.internal.incremental.FolderBasedApkCreator;
import com.android.builder.errors.EvalIssueReporter;
import com.android.builder.internal.packaging.IncrementalPackager;
import com.android.builder.model.SigningConfig;
import com.android.builder.packaging.PackagerException;
import com.android.builder.packaging.PackagingUtils;
import com.android.ide.common.signing.CertificateInfo;
import com.android.ide.common.signing.KeystoreHelper;
import com.android.ide.common.signing.KeytoolException;
import com.android.tools.build.apkzlib.sign.SigningOptions;
import com.android.tools.build.apkzlib.zfile.ApkCreatorFactory;
import com.android.tools.build.apkzlib.zfile.NativeLibrariesPackagingMode;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import org.gradle.api.Project;
/**
* Factory class to create instances of {@link IncrementalPackager}. Since there are many options
* for {@link IncrementalPackager} and not all are always required, this makes building the packager
* easier.
*
* <p>While some parameters have sensible defaults, some parameters must be defined. See the
* {@link #build()} method for information on which parameters are mandatory.
*/
public class IncrementalPackagerBuilder {
/** Enums for all the supported output format. */
public enum ApkFormat {
/** Usual APK format. */
FILE {
@Override
ApkCreatorFactory factory(boolean keepTimestampsInApk, boolean debuggableBuild) {
return ApkCreatorFactories.fromProjectProperties(
keepTimestampsInApk, debuggableBuild);
}
},
FILE_WITH_LIST_OF_CHANGES {
@SuppressWarnings({"OResourceOpenedButNotSafelyClosed", "resource"})
@Override
ApkCreatorFactory factory(boolean keepTimestampsInApk, boolean debuggableBuild) {
ApkCreatorFactory apk =
ApkCreatorFactories.fromProjectProperties(
keepTimestampsInApk, debuggableBuild);
return creationData ->
new CapturingChangesApkCreator(creationData, apk.make(creationData));
}
},
/** Directory with a structure mimicking the APK format. */
DIRECTORY {
@SuppressWarnings({"OResourceOpenedButNotSafelyClosed", "resource"})
@Override
ApkCreatorFactory factory(boolean keepTimestampsInApk, boolean debuggableBuild) {
return creationData ->
new CapturingChangesApkCreator(
creationData, new FolderBasedApkCreator(creationData));
}
};
abstract ApkCreatorFactory factory(boolean keepTimestampsInApk, boolean debuggableBuild);
}
/**
* Type of build that invokes the instance of IncrementalPackagerBuilder
*
* <p>This information is provided as a hint for possible performance optimizations
*/
public enum BuildType {
UNKNOWN,
CLEAN,
INCREMENTAL
}
/** Builder for the data to create APK file. */
@NonNull
private ApkCreatorFactory.CreationData.Builder creationDataBuilder =
ApkCreatorFactory.CreationData.builder();
/** Desired format of the output. */
@NonNull private ApkFormat apkFormat;
/**
* How should native libraries be packaged. If not defined, it can be inferred if
* {@link #manifest} is defined.
*/
@Nullable
private NativeLibrariesPackagingMode nativeLibrariesPackagingMode;
/**
* The no-compress predicate: returns {@code true} for paths that should not be compressed. If
* not defined, but {@link #aaptOptionsNoCompress} and {@link #manifest} are both defined, it
* can be inferred.
*/
@Nullable private Predicate<String> noCompressPredicate;
/**
* The project.
*/
@Nullable
private Project project;
/** Whether the timestamps should be kept in the apk. */
@Nullable private Boolean keepTimestampsInApk;
/**
* Directory for intermediate contents.
*/
@Nullable
private File intermediateDir;
/**
* Is the build debuggable?
*/
private boolean debuggableBuild;
/**
* Is the build JNI-debuggable?
*/
private boolean jniDebuggableBuild;
/**
* ABI filters. Empty if none.
*/
@NonNull
private Set<String> abiFilters;
/**
* Manifest.
*/
@Nullable
private File manifest;
/** aapt options no compress config. */
@Nullable private Collection<String> aaptOptionsNoCompress;
@Nullable private EvalIssueReporter issueReporter;
@Nullable private BooleanSupplier canParseManifest;
@NonNull private BuildType buildType;
/** Creates a new builder. */
public IncrementalPackagerBuilder(@NonNull ApkFormat apkFormat, @NonNull BuildType buildType) {
abiFilters = new HashSet<>();
this.apkFormat = apkFormat;
this.buildType = buildType;
creationDataBuilder.setIncremental(buildType == BuildType.INCREMENTAL);
}
/** Creates a new builder. */
public IncrementalPackagerBuilder(@NonNull ApkFormat apkFormat) {
this(apkFormat, BuildType.UNKNOWN);
}
/**
* Sets the signing configuration information for the incremental packager.
*
* @param signingConfig the signing config; if {@code null} then the APK will not be signed
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withSigning(@Nullable SigningConfig signingConfig) {
return withSigning(signingConfig, 1);
}
/**
* Sets the signing configuration information for the incremental packager.
*
* @param signingConfig the signing config; if {@code null} then the APK will not be signed
* @param minSdk the minimum SDK
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withSigning(
@Nullable SigningConfig signingConfig, int minSdk) {
if (signingConfig == null) {
return this;
}
try {
String error =
"SigningConfig \""
+ signingConfig.getName()
+ "\" is missing required property \"%s\".";
CertificateInfo certificateInfo =
KeystoreHelper.getCertificateInfo(
signingConfig.getStoreType(),
Preconditions.checkNotNull(
signingConfig.getStoreFile(), error, "storeFile"),
Preconditions.checkNotNull(
signingConfig.getStorePassword(), error, "storePassword"),
Preconditions.checkNotNull(
signingConfig.getKeyPassword(), error, "keyPassword"),
Preconditions.checkNotNull(
signingConfig.getKeyAlias(), error, "keyAlias"));
// V1 signature is useless if minSdk is 24+
boolean enableV1Signing = minSdk < 24 && signingConfig.isV1SigningEnabled();
creationDataBuilder.setSigningOptions(
SigningOptions.builder()
.setKey(certificateInfo.getKey())
.setCertificates(certificateInfo.getCertificate())
.setV1SigningEnabled(enableV1Signing)
.setV2SigningEnabled(signingConfig.isV2SigningEnabled())
.setMinSdkVersion(minSdk)
.setValidation(computeValidation())
.build());
} catch (KeytoolException|FileNotFoundException e) {
throw new RuntimeException(e);
}
return this;
}
private SigningOptions.Validation computeValidation() {
switch (buildType) {
case INCREMENTAL:
return SigningOptions.Validation.ASSUME_VALID;
case CLEAN:
return SigningOptions.Validation.ASSUME_INVALID;
case UNKNOWN:
return SigningOptions.Validation.ALWAYS_VALIDATE;
default:
throw new RuntimeException(
"Unknown IncrementalPackagerBuilder build type " + buildType);
}
}
/**
* Sets the output file for the APK.
*
* @param f the output file
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withOutputFile(@NonNull File f) {
creationDataBuilder.setApkPath(f);
return this;
}
/**
* Sets the packaging mode for native libraries.
*
* @param packagingMode the packging mode
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withNativeLibraryPackagingMode(
@NonNull NativeLibrariesPackagingMode packagingMode) {
nativeLibrariesPackagingMode = packagingMode;
return this;
}
/**
* Sets the manifest. While the manifest itself is not used for packaging, information on
* the native libraries packaging mode can be inferred from the manifest.
*
* @param manifest the manifest
* @return {@code this} for use with fluent-style notation
*/
public IncrementalPackagerBuilder withManifest(@NonNull File manifest) {
this.manifest = manifest;
return this;
}
/**
* Sets the no-compress predicate. This predicate returns {@code true} for files that should
* not be compressed
*
* @param noCompressPredicate the predicate
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withNoCompressPredicate(
@NonNull Predicate<String> noCompressPredicate) {
this.noCompressPredicate = noCompressPredicate;
return this;
}
/**
* Sets the {@code aapt} options no compress predicate.
*
* <p>The no-compress predicate can be computed if this and the manifest (see {@link
* #withManifest(File)}) are both defined.
*/
@NonNull
public IncrementalPackagerBuilder withAaptOptionsNoCompress(
@Nullable Collection<String> aaptOptionsNoCompress) {
this.aaptOptionsNoCompress = aaptOptionsNoCompress;
return this;
}
/**
* Sets the project.
*
* @param project the project
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withProject(@NonNull Project project) {
this.project = project;
return this;
}
/**
* Sets whether the timestamps should be kept in the apk.
*
* @param keepTimestampsInApk whether the timestamps should be kept in the apk
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withKeepTimestampsInApk(boolean keepTimestampsInApk) {
this.keepTimestampsInApk = keepTimestampsInApk;
return this;
}
/**
* Sets the intermediate directory used to store information for incremental builds.
*
* @param intermediateDir the intermediate directory
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withIntermediateDir(@NonNull File intermediateDir) {
this.intermediateDir = intermediateDir;
return this;
}
/**
* Sets the created-by parameter.
*
* @param createdBy the optional value for created-by
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withCreatedBy(@Nullable String createdBy) {
creationDataBuilder.setCreatedBy(createdBy);
return this;
}
/**
* Sets whether the build is debuggable or not.
*
* @param debuggableBuild is the build debuggable?
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withDebuggableBuild(boolean debuggableBuild) {
this.debuggableBuild = debuggableBuild;
return this;
}
/**
* Sets whether the build is JNI-debuggable or not.
*
* @param jniDebuggableBuild is the build JNI-debuggable?
* @return {@code this} for use with fluent-style notation
*/
@NonNull
public IncrementalPackagerBuilder withJniDebuggableBuild(boolean jniDebuggableBuild) {
this.jniDebuggableBuild = jniDebuggableBuild;
return this;
}
/**
* Sets the set of accepted ABIs.
*
* @param acceptedAbis the accepted ABIs; if empty then all ABIs are accepted
* @return {@code this} for use with fluent-style notation
*/
public IncrementalPackagerBuilder withAcceptedAbis(@NonNull Set<String> acceptedAbis) {
this.abiFilters = ImmutableSet.copyOf(acceptedAbis);
return this;
}
/**
* Sets the issueReporter to report errors/warnings.
*
* @param issueReporter the EvalIssueReporter to use.
* @return {@code this} for use with fluent-style notation
*/
public IncrementalPackagerBuilder withIssueReporter(@NonNull EvalIssueReporter issueReporter) {
this.issueReporter = issueReporter;
return this;
}
/**
* Creates the packager, verifying that all the minimum data has been provided. The required
* information are:
*
* <ul>
* <li>{@link #withOutputFile(File)}
* <li>{@link #withProject(Project)}
* <li>{@link #withIntermediateDir(File)}
* </ul>
*
* @return the incremental packager
*/
@NonNull
public IncrementalPackager build() {
if (project != null) {
keepTimestampsInApk = AndroidGradleOptions.keepTimestampsInApk(project);
}
Preconditions.checkState(keepTimestampsInApk != null, "keepTimestampsInApk == null");
Preconditions.checkState(intermediateDir != null, "intermediateDir == null");
if (noCompressPredicate == null) {
if (manifest != null) {
noCompressPredicate =
PackagingUtils.getNoCompressPredicate(
aaptOptionsNoCompress, manifest, () -> true, issueReporter);
} else {
noCompressPredicate = path -> false;
}
}
if (nativeLibrariesPackagingMode == null) {
if (manifest != null) {
nativeLibrariesPackagingMode =
PackagingUtils.getNativeLibrariesLibrariesPackagingMode(
manifest, canParseManifest, issueReporter);
} else {
nativeLibrariesPackagingMode = NativeLibrariesPackagingMode.COMPRESSED;
}
}
creationDataBuilder
.setNativeLibrariesPackagingMode(nativeLibrariesPackagingMode)
.setNoCompressPredicate(noCompressPredicate::test);
try {
return new IncrementalPackager(
creationDataBuilder.build(),
intermediateDir,
apkFormat.factory(keepTimestampsInApk, debuggableBuild),
abiFilters,
jniDebuggableBuild);
} catch (PackagerException|IOException e) {
throw new RuntimeException(e);
}
}
}