/*
* Copyright (C) 2012 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.core;
import static com.android.SdkConstants.DOT_DEX;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FD_RES_XML;
import static com.android.builder.core.BuilderConstants.ANDROID_WEAR;
import static com.android.builder.core.BuilderConstants.ANDROID_WEAR_MICRO_APK;
import static com.android.manifmerger.ManifestMerger2.Invoker;
import static com.android.manifmerger.ManifestMerger2.SystemProperty;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.compiling.DependencyFileProcessor;
import com.android.builder.core.BuildToolsServiceLoader.BuildToolServiceLoader;
import com.android.builder.dependency.ManifestDependency;
import com.android.builder.dependency.SymbolFileProvider;
import com.android.builder.internal.ClassFieldImpl;
import com.android.builder.internal.SymbolLoader;
import com.android.builder.internal.SymbolWriter;
import com.android.builder.internal.TestManifestGenerator;
import com.android.builder.internal.compiler.AidlProcessor;
import com.android.builder.internal.compiler.JackConversionCache;
import com.android.builder.internal.compiler.LeafFolderGatherer;
import com.android.builder.internal.compiler.PreDexCache;
import com.android.builder.internal.compiler.RenderScriptProcessor;
import com.android.builder.internal.compiler.SourceSearcher;
import com.android.builder.internal.incremental.DependencyData;
import com.android.builder.internal.packaging.Packager;
import com.android.builder.model.ClassField;
import com.android.builder.model.PackagingOptions;
import com.android.builder.model.SigningConfig;
import com.android.builder.model.SyncIssue;
import com.android.builder.packaging.DuplicateFileException;
import com.android.builder.packaging.PackagerException;
import com.android.builder.packaging.SealedPackageException;
import com.android.builder.packaging.SigningException;
import com.android.builder.sdk.SdkInfo;
import com.android.builder.sdk.TargetInfo;
import com.android.builder.signing.SignedJarBuilder;
import com.android.ide.common.internal.AaptCruncher;
import com.android.ide.common.internal.LoggedErrorException;
import com.android.ide.common.internal.PngCruncher;
import com.android.ide.common.process.CachedProcessOutputHandler;
import com.android.ide.common.process.JavaProcessExecutor;
import com.android.ide.common.process.JavaProcessInfo;
import com.android.ide.common.process.ProcessException;
import com.android.ide.common.process.ProcessExecutor;
import com.android.ide.common.process.ProcessInfo;
import com.android.ide.common.process.ProcessInfoBuilder;
import com.android.ide.common.process.ProcessOutputHandler;
import com.android.ide.common.process.ProcessResult;
import com.android.ide.common.signing.CertificateInfo;
import com.android.ide.common.signing.KeystoreHelper;
import com.android.ide.common.signing.KeytoolException;
import com.android.jack.api.ConfigNotSupportedException;
import com.android.jack.api.JackProvider;
import com.android.jack.api.v01.Api01CompilationTask;
import com.android.jack.api.v01.Api01Config;
import com.android.jack.api.v01.CompilationException;
import com.android.jack.api.v01.ConfigurationException;
import com.android.jack.api.v01.MultiDexKind;
import com.android.jack.api.v01.ReporterKind;
import com.android.jack.api.v01.UnrecoverableException;
import com.android.jill.api.JillProvider;
import com.android.jill.api.v01.Api01TranslationTask;
import com.android.jill.api.v01.TranslationException;
import com.android.manifmerger.ManifestMerger2;
import com.android.manifmerger.MergingReport;
import com.android.manifmerger.PlaceholderEncoder;
import com.android.manifmerger.PlaceholderHandler;
import com.android.manifmerger.XmlDocument;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.IAndroidTarget.OptionalLibrary;
import com.android.sdklib.repository.FullRevision;
import com.android.utils.ILogger;
import com.android.utils.Pair;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* This is the main builder class. It is given all the data to process the build (such as
* {@link DefaultProductFlavor}s, {@link DefaultBuildType} and dependencies) and use them when doing specific
* build steps.
*
* To use:
* create a builder with {@link #AndroidBuilder(String, String, ProcessExecutor, JavaProcessExecutor, ErrorReporter, ILogger, boolean)}
*
* then build steps can be done with
* {@link #mergeManifests(File, List, List, String, int, String, String, String, Integer, String, String, ManifestMerger2.MergeType, Map, File)}
* {@link #processTestManifest(String, String, String, String, String, Boolean, Boolean, File, List, Map, File, File)}
* {@link #processResources(AaptPackageProcessBuilder, boolean, ProcessOutputHandler)}
* {@link #compileAllAidlFiles(List, File, File, List, DependencyFileProcessor, ProcessOutputHandler)}
* {@link #convertByteCode(Collection, Collection, File, boolean, File, DexOptions, List, File, boolean, boolean, ProcessOutputHandler)}
* {@link #packageApk(String, File, Collection, Collection, String, Collection, File, Set, boolean, SigningConfig, PackagingOptions, SignedJarBuilder.IZipEntryFilter, String)}
*
* Java compilation is not handled but the builder provides the bootclasspath with
* {@link #getBootClasspath()}.
*/
public class AndroidBuilder {
private static final FullRevision MIN_BUILD_TOOLS_REV = new FullRevision(19, 1, 0);
private static final DependencyFileProcessor sNoOpDependencyFileProcessor = new DependencyFileProcessor() {
@Override
public DependencyData processFile(@NonNull File dependencyFile) {
return null;
}
};
@NonNull
private final String mProjectId;
@NonNull
private final ILogger mLogger;
@NonNull
private final ProcessExecutor mProcessExecutor;
@NonNull
private final JavaProcessExecutor mJavaProcessExecutor;
@NonNull
private final ErrorReporter mErrorReporter;
private final boolean mVerboseExec;
@Nullable private String mCreatedBy;
private SdkInfo mSdkInfo;
private TargetInfo mTargetInfo;
private List<File> mBootClasspath;
@NonNull
private List<LibraryRequest> mLibraryRequests = ImmutableList.of();
/**
* Creates an AndroidBuilder.
* <p/>
* <var>verboseExec</var> is needed on top of the ILogger due to remote exec tools not being
* able to output info and verbose messages separately.
*
* @param createdBy the createdBy String for the apk manifest.
* @param logger the Logger
* @param verboseExec whether external tools are launched in verbose mode
*/
public AndroidBuilder(
@NonNull String projectId,
@Nullable String createdBy,
@NonNull ProcessExecutor processExecutor,
@NonNull JavaProcessExecutor javaProcessExecutor,
@NonNull ErrorReporter errorReporter,
@NonNull ILogger logger,
boolean verboseExec) {
mProjectId = checkNotNull(projectId);
mCreatedBy = createdBy;
mProcessExecutor = checkNotNull(processExecutor);
mJavaProcessExecutor = checkNotNull(javaProcessExecutor);
mErrorReporter = checkNotNull(errorReporter);
mLogger = checkNotNull(logger);
mVerboseExec = verboseExec;
}
/**
* Sets the SdkInfo and the targetInfo on the builder. This is required to actually
* build (some of the steps).
*
* @param sdkInfo the SdkInfo
* @param targetInfo the TargetInfo
*
* @see com.android.builder.sdk.SdkLoader
*/
public void setTargetInfo(
@NonNull SdkInfo sdkInfo,
@NonNull TargetInfo targetInfo,
@NonNull Collection<LibraryRequest> libraryRequests) {
mSdkInfo = sdkInfo;
mTargetInfo = targetInfo;
if (mTargetInfo.getBuildTools().getRevision().compareTo(MIN_BUILD_TOOLS_REV) < 0) {
throw new IllegalArgumentException(String.format(
"The SDK Build Tools revision (%1$s) is too low for project '%2$s'. Minimum required is %3$s",
mTargetInfo.getBuildTools().getRevision(), mProjectId, MIN_BUILD_TOOLS_REV));
}
mLibraryRequests = ImmutableList.copyOf(libraryRequests);
}
/**
* Returns the SdkInfo, if set.
*/
@Nullable
public SdkInfo getSdkInfo() {
return mSdkInfo;
}
/**
* Returns the TargetInfo, if set.
*/
@Nullable
public TargetInfo getTargetInfo() {
return mTargetInfo;
}
@NonNull
public ILogger getLogger() {
return mLogger;
}
@NonNull
public ErrorReporter getErrorReporter() {
return mErrorReporter;
}
/**
* Returns the compilation target, if set.
*/
@Nullable
public IAndroidTarget getTarget() {
checkState(mTargetInfo != null,
"Cannot call getTarget() before setTargetInfo() is called.");
return mTargetInfo.getTarget();
}
/**
* Returns whether the compilation target is a preview.
*/
public boolean isPreviewTarget() {
checkState(mTargetInfo != null,
"Cannot call isTargetAPreview() before setTargetInfo() is called.");
return mTargetInfo.getTarget().getVersion().isPreview();
}
public String getTargetCodename() {
checkState(mTargetInfo != null,
"Cannot call getTargetCodename() before setTargetInfo() is called.");
return mTargetInfo.getTarget().getVersion().getCodename();
}
@NonNull
public File getDxJar() {
checkState(mTargetInfo != null,
"Cannot call getDxJar() before setTargetInfo() is called.");
return new File(mTargetInfo.getBuildTools().getPath(BuildToolInfo.PathId.DX_JAR));
}
/**
* Helper method to get the boot classpath to be used during compilation.
*/
@NonNull
public List<File> getBootClasspath() {
if (mBootClasspath == null) {
checkState(mTargetInfo != null,
"Cannot call getBootClasspath() before setTargetInfo() is called.");
List<File> classpath = Lists.newArrayList();
IAndroidTarget target = mTargetInfo.getTarget();
for (String p : target.getBootClasspath()) {
classpath.add(new File(p));
}
List<LibraryRequest> requestedLibs = Lists.newArrayList(mLibraryRequests);
// add additional libraries if any
List<OptionalLibrary> libs = target.getAdditionalLibraries();
for (OptionalLibrary lib : libs) {
// add it always for now
classpath.add(lib.getJar());
// remove from list of requested if match
LibraryRequest requestedLib = findMatchingLib(lib.getName(), requestedLibs);
if (requestedLib != null) {
requestedLibs.remove(requestedLib);
}
}
// add optional libraries if needed.
List<OptionalLibrary> optionalLibraries = target.getOptionalLibraries();
for (OptionalLibrary lib : optionalLibraries) {
// search if requested
LibraryRequest requestedLib = findMatchingLib(lib.getName(), requestedLibs);
if (requestedLib != null) {
// add to classpath
classpath.add(lib.getJar());
// remove from requested list.
requestedLibs.remove(requestedLib);
}
}
// look for not found requested libraries.
for (LibraryRequest library : requestedLibs) {
mErrorReporter.handleSyncError(
library.getName(),
SyncIssue.TYPE_OPTIONAL_LIB_NOT_FOUND,
"Unable to find optional library: " + library.getName());
}
// add annotations.jar if needed.
if (target.getVersion().getApiLevel() <= 15) {
classpath.add(mSdkInfo.getAnnotationsJar());
}
mBootClasspath = ImmutableList.copyOf(classpath);
}
return mBootClasspath;
}
@Nullable
private static LibraryRequest findMatchingLib(@NonNull String name, @NonNull List<LibraryRequest> libraries) {
for (LibraryRequest library : libraries) {
if (name.equals(library.getName())) {
return library;
}
}
return null;
}
/**
* Helper method to get the boot classpath to be used during compilation.
*/
@NonNull
public List<String> getBootClasspathAsStrings() {
List<File> classpath = getBootClasspath();
// convert to Strings.
List<String> results = Lists.newArrayListWithCapacity(classpath.size());
for (File f : classpath) {
results.add(f.getAbsolutePath());
}
return results;
}
/**
* Returns the jar file for the renderscript mode.
*
* This may return null if the SDK has not been loaded yet.
*
* @return the jar file, or null.
*
* @see #setTargetInfo(SdkInfo, TargetInfo, Collection)
*/
@Nullable
public File getRenderScriptSupportJar() {
if (mTargetInfo != null) {
return RenderScriptProcessor.getSupportJar(
mTargetInfo.getBuildTools().getLocation().getAbsolutePath());
}
return null;
}
/**
* Returns the compile classpath for this config. If the config tests a library, this
* will include the classpath of the tested config.
*
* If the SDK was loaded, this may include the renderscript support jar.
*
* @return a non null, but possibly empty set.
*/
@NonNull
public Set<File> getCompileClasspath(@NonNull VariantConfiguration<?,?,?> variantConfiguration) {
Set<File> compileClasspath = variantConfiguration.getCompileClasspath();
if (variantConfiguration.getRenderscriptSupportModeEnabled()) {
File renderScriptSupportJar = getRenderScriptSupportJar();
Set<File> fullJars = Sets.newHashSetWithExpectedSize(compileClasspath.size() + 1);
fullJars.addAll(compileClasspath);
if (renderScriptSupportJar != null) {
fullJars.add(renderScriptSupportJar);
}
compileClasspath = fullJars;
}
return compileClasspath;
}
/**
* Returns the list of packaged jars for this config. If the config tests a library, this
* will include the jars of the tested config
*
* If the SDK was loaded, this may include the renderscript support jar.
*
* @return a non null, but possibly empty list.
*/
@NonNull
public Set<File> getPackagedJars(@NonNull VariantConfiguration<?,?,?> variantConfiguration) {
Set<File> packagedJars = Sets.newHashSet(variantConfiguration.getPackagedJars());
if (variantConfiguration.getRenderscriptSupportModeEnabled()) {
File renderScriptSupportJar = getRenderScriptSupportJar();
if (renderScriptSupportJar != null) {
packagedJars.add(renderScriptSupportJar);
}
}
return packagedJars;
}
/**
* Returns the native lib folder for the renderscript mode.
*
* This may return null if the SDK has not been loaded yet.
*
* @return the folder, or null.
*
* @see #setTargetInfo(SdkInfo, TargetInfo, Collection)
*/
@Nullable
public File getSupportNativeLibFolder() {
if (mTargetInfo != null) {
return RenderScriptProcessor.getSupportNativeLibFolder(
mTargetInfo.getBuildTools().getLocation().getAbsolutePath());
}
return null;
}
/**
* Returns an {@link PngCruncher} using aapt underneath
* @return an PngCruncher object
*/
@NonNull
public PngCruncher getAaptCruncher(ProcessOutputHandler processOutputHandler) {
checkState(mTargetInfo != null,
"Cannot call getAaptCruncher() before setTargetInfo() is called.");
return new AaptCruncher(
mTargetInfo.getBuildTools().getPath(BuildToolInfo.PathId.AAPT),
mProcessExecutor,
processOutputHandler);
}
@NonNull
public ProcessExecutor getProcessExecutor() {
return mProcessExecutor;
}
@NonNull
public ProcessResult executeProcess(@NonNull ProcessInfo processInfo,
@NonNull ProcessOutputHandler handler) {
return mProcessExecutor.execute(processInfo, handler);
}
@NonNull
public static ClassField createClassField(@NonNull String type, @NonNull String name, @NonNull String value) {
return new ClassFieldImpl(type, name, value);
}
/**
* Invoke the Manifest Merger version 2.
*/
public void mergeManifests(
@NonNull File mainManifest,
@NonNull List<File> manifestOverlays,
@NonNull List<? extends ManifestDependency> libraries,
String packageOverride,
int versionCode,
String versionName,
@Nullable String minSdkVersion,
@Nullable String targetSdkVersion,
@Nullable Integer maxSdkVersion,
@NonNull String outManifestLocation,
@Nullable String outAaptSafeManifestLocation,
ManifestMerger2.MergeType mergeType,
Map<String, String> placeHolders,
@Nullable File reportFile) {
try {
Invoker manifestMergerInvoker =
ManifestMerger2.newMerger(mainManifest, mLogger, mergeType)
.setPlaceHolderValues(placeHolders)
.addFlavorAndBuildTypeManifests(
manifestOverlays.toArray(new File[manifestOverlays.size()]))
.addLibraryManifests(collectLibraries(libraries))
.setMergeReportFile(reportFile);
if (mergeType == ManifestMerger2.MergeType.APPLICATION) {
manifestMergerInvoker.withFeatures(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS);
}
setInjectableValues(manifestMergerInvoker,
packageOverride, versionCode, versionName,
minSdkVersion, targetSdkVersion, maxSdkVersion);
MergingReport mergingReport = manifestMergerInvoker.merge();
mLogger.info("Merging result:" + mergingReport.getResult());
switch (mergingReport.getResult()) {
case WARNING:
mergingReport.log(mLogger);
// fall through since these are just warnings.
case SUCCESS:
XmlDocument xmlDocument = mergingReport.getMergedDocument().get();
try {
String annotatedDocument = mergingReport.getActions().blame(xmlDocument);
mLogger.verbose(annotatedDocument);
} catch (Exception e) {
mLogger.error(e, "cannot print resulting xml");
}
save(xmlDocument, new File(outManifestLocation));
if (outAaptSafeManifestLocation != null) {
new PlaceholderEncoder().visit(xmlDocument);
save(xmlDocument, new File(outAaptSafeManifestLocation));
}
mLogger.info("Merged manifest saved to " + outManifestLocation);
break;
case ERROR:
mergingReport.log(mLogger);
throw new RuntimeException(mergingReport.getReportString());
default:
throw new RuntimeException("Unhandled result type : "
+ mergingReport.getResult());
}
} catch (ManifestMerger2.MergeFailureException e) {
// TODO: unacceptable.
throw new RuntimeException(e);
}
}
/**
* Sets the {@link com.android.manifmerger.ManifestMerger2.SystemProperty} that can be injected
* in the manifest file.
*/
private static void setInjectableValues(
ManifestMerger2.Invoker<?> invoker,
String packageOverride,
int versionCode,
String versionName,
@Nullable String minSdkVersion,
@Nullable String targetSdkVersion,
@Nullable Integer maxSdkVersion) {
if (!Strings.isNullOrEmpty(packageOverride)) {
invoker.setOverride(SystemProperty.PACKAGE, packageOverride);
}
if (versionCode > 0) {
invoker.setOverride(SystemProperty.VERSION_CODE,
String.valueOf(versionCode));
}
if (!Strings.isNullOrEmpty(versionName)) {
invoker.setOverride(SystemProperty.VERSION_NAME, versionName);
}
if (!Strings.isNullOrEmpty(minSdkVersion)) {
invoker.setOverride(SystemProperty.MIN_SDK_VERSION, minSdkVersion);
}
if (!Strings.isNullOrEmpty(targetSdkVersion)) {
invoker.setOverride(SystemProperty.TARGET_SDK_VERSION, targetSdkVersion);
}
if (maxSdkVersion != null) {
invoker.setOverride(SystemProperty.MAX_SDK_VERSION, maxSdkVersion.toString());
}
}
/**
* Saves the {@link com.android.manifmerger.XmlDocument} to a file in UTF-8 encoding.
* @param xmlDocument xml document to save.
* @param out file to save to.
*/
private void save(XmlDocument xmlDocument, File out) {
try {
Files.write(xmlDocument.prettyPrint(), out, Charsets.UTF_8);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
/**
* Collect the list of libraries' manifest files.
* @param libraries declared dependencies
* @return a list of files and names for the libraries' manifest files.
*/
private static ImmutableList<Pair<String, File>> collectLibraries(
List<? extends ManifestDependency> libraries) {
ImmutableList.Builder<Pair<String, File>> manifestFiles = ImmutableList.builder();
if (libraries != null) {
collectLibraries(libraries, manifestFiles);
}
return manifestFiles.build();
}
/**
* recursively calculate the list of libraries to merge the manifests files from.
* @param libraries the dependencies
* @param manifestFiles list of files and names identifiers for the libraries' manifest files.
*/
private static void collectLibraries(List<? extends ManifestDependency> libraries,
ImmutableList.Builder<Pair<String, File>> manifestFiles) {
for (ManifestDependency library : libraries) {
manifestFiles.add(Pair.of(library.getName(), library.getManifest()));
List<? extends ManifestDependency> manifestDependencies = library
.getManifestDependencies();
if (!manifestDependencies.isEmpty()) {
collectLibraries(manifestDependencies, manifestFiles);
}
}
}
/**
* Creates the manifest for a test variant
*
* @param testApplicationId the application id of the test application
* @param minSdkVersion the minSdkVersion of the test application
* @param targetSdkVersion the targetSdkVersion of the test application
* @param testedApplicationId the application id of the tested application
* @param instrumentationRunner the name of the instrumentation runner
* @param handleProfiling whether or not the Instrumentation object will turn profiling on and off
* @param functionalTest whether or not the Instrumentation class should run as a functional test
* @param testManifestFile optionally user provided AndroidManifest.xml for testing application
* @param libraries the library dependency graph
* @param outManifest the output location for the merged manifest
*
* @see VariantConfiguration#getApplicationId()
* @see VariantConfiguration#getTestedConfig()
* @see VariantConfiguration#getMinSdkVersion()
* @see VariantConfiguration#getTestedApplicationId()
* @see VariantConfiguration#getInstrumentationRunner()
* @see VariantConfiguration#getHandleProfiling()
* @see VariantConfiguration#getFunctionalTest()
* @see VariantConfiguration#getDirectLibraries()
*/
public void processTestManifest(
@NonNull String testApplicationId,
@Nullable String minSdkVersion,
@Nullable String targetSdkVersion,
@NonNull String testedApplicationId,
@NonNull String instrumentationRunner,
@NonNull Boolean handleProfiling,
@NonNull Boolean functionalTest,
@Nullable File testManifestFile,
@NonNull List<? extends ManifestDependency> libraries,
@NonNull Map<String, Object> manifestPlaceholders,
@NonNull File outManifest,
@NonNull File tmpDir) {
checkNotNull(testApplicationId, "testApplicationId cannot be null.");
checkNotNull(testedApplicationId, "testedApplicationId cannot be null.");
checkNotNull(instrumentationRunner, "instrumentationRunner cannot be null.");
checkNotNull(handleProfiling, "handleProfiling cannot be null.");
checkNotNull(functionalTest, "functionalTest cannot be null.");
checkNotNull(libraries, "libraries cannot be null.");
checkNotNull(outManifest, "outManifestLocation cannot be null.");
try {
tmpDir.mkdirs();
File generatedTestManifest = libraries.isEmpty() && testManifestFile == null
? outManifest : File.createTempFile("manifestMerger", ".xml", tmpDir);
mLogger.verbose("Generating in %1$s", generatedTestManifest.getAbsolutePath());
generateTestManifest(
testApplicationId,
minSdkVersion,
targetSdkVersion.equals("-1") ? null : targetSdkVersion,
testedApplicationId,
instrumentationRunner,
handleProfiling,
functionalTest,
generatedTestManifest);
if (testManifestFile != null) {
File mergedTestManifest = File.createTempFile("manifestMerger", ".xml", tmpDir);
mLogger.verbose("Merging user supplied manifest in %1$s",
generatedTestManifest.getAbsolutePath());
Invoker invoker = ManifestMerger2.newMerger(
testManifestFile, mLogger, ManifestMerger2.MergeType.APPLICATION)
.setOverride(SystemProperty.PACKAGE, testApplicationId)
.setPlaceHolderValues(manifestPlaceholders)
.setPlaceHolderValue(PlaceholderHandler.INSTRUMENTATION_RUNNER,
instrumentationRunner)
.addLibraryManifests(generatedTestManifest);
if (minSdkVersion != null) {
invoker.setOverride(SystemProperty.MIN_SDK_VERSION, minSdkVersion);
}
if (!targetSdkVersion.equals("-1")) {
invoker.setOverride(SystemProperty.TARGET_SDK_VERSION, targetSdkVersion);
}
MergingReport mergingReport = invoker.merge();
if (libraries.isEmpty()) {
handleMergingResult(mergingReport, outManifest);
} else {
handleMergingResult(mergingReport, mergedTestManifest);
generatedTestManifest = mergedTestManifest;
}
}
if (!libraries.isEmpty()) {
MergingReport mergingReport = ManifestMerger2.newMerger(
generatedTestManifest, mLogger, ManifestMerger2.MergeType.APPLICATION)
.withFeatures(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)
.setOverride(SystemProperty.PACKAGE, testApplicationId)
.addLibraryManifests(collectLibraries(libraries))
.setPlaceHolderValues(manifestPlaceholders)
.merge();
handleMergingResult(mergingReport, outManifest);
}
} catch(Exception e) {
throw new RuntimeException(e);
}
}
private void handleMergingResult(@NonNull MergingReport mergingReport, @NonNull File outFile) {
switch (mergingReport.getResult()) {
case WARNING:
mergingReport.log(mLogger);
// fall through since these are just warnings.
case SUCCESS:
XmlDocument xmlDocument = mergingReport.getMergedDocument().get();
try {
String annotatedDocument = mergingReport.getActions().blame(xmlDocument);
mLogger.verbose(annotatedDocument);
} catch (Exception e) {
mLogger.error(e, "cannot print resulting xml");
}
save(xmlDocument, outFile);
mLogger.info("Merged manifest saved to " + outFile);
break;
case ERROR:
mergingReport.log(mLogger);
throw new RuntimeException(mergingReport.getReportString());
default:
throw new RuntimeException("Unhandled result type : "
+ mergingReport.getResult());
}
}
private static void generateTestManifest(
@NonNull String testApplicationId,
@Nullable String minSdkVersion,
@Nullable String targetSdkVersion,
@NonNull String testedApplicationId,
@NonNull String instrumentationRunner,
@NonNull Boolean handleProfiling,
@NonNull Boolean functionalTest,
@NonNull File outManifestLocation) {
TestManifestGenerator generator = new TestManifestGenerator(
outManifestLocation,
testApplicationId,
minSdkVersion,
targetSdkVersion,
testedApplicationId,
instrumentationRunner,
handleProfiling,
functionalTest);
try {
generator.generate();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Process the resources and generate R.java and/or the packaged resources.
*
* @param aaptCommand aapt command invocation parameters.
* @param enforceUniquePackageName if true method will fail if some libraries share the same
* package name
*
* @throws IOException
* @throws InterruptedException
* @throws ProcessException
*/
public void processResources(
@NonNull AaptPackageProcessBuilder aaptCommand,
boolean enforceUniquePackageName,
@NonNull ProcessOutputHandler processOutputHandler)
throws IOException, InterruptedException, ProcessException {
checkState(mTargetInfo != null,
"Cannot call processResources() before setTargetInfo() is called.");
// launch aapt: create the command line
ProcessInfo processInfo = aaptCommand.build(
mTargetInfo.getBuildTools(), mTargetInfo.getTarget(), mLogger);
ProcessResult result = mProcessExecutor.execute(processInfo, processOutputHandler);
result.rethrowFailure().assertNormalExitValue();
// now if the project has libraries, R needs to be created for each libraries,
// but only if the current project is not a library.
if (aaptCommand.getSourceOutputDir() != null
&& aaptCommand.getType() != VariantType.LIBRARY
&& !aaptCommand.getLibraries().isEmpty()) {
SymbolLoader fullSymbolValues = null;
// First pass processing the libraries, collecting them by packageName,
// and ignoring the ones that have the same package name as the application
// (since that R class was already created).
String appPackageName = aaptCommand.getPackageForR();
if (appPackageName == null) {
appPackageName = VariantConfiguration.getManifestPackage(aaptCommand.getManifestFile());
}
// list of all the symbol loaders per package names.
Multimap<String, SymbolLoader> libMap = ArrayListMultimap.create();
for (SymbolFileProvider lib : aaptCommand.getLibraries()) {
if (lib.isOptional()) {
continue;
}
String packageName = VariantConfiguration.getManifestPackage(lib.getManifest());
if (appPackageName == null) {
continue;
}
if (appPackageName.equals(packageName)) {
if (enforceUniquePackageName) {
String msg = String.format(
"Error: A library uses the same package as this project: %s",
packageName);
throw new RuntimeException(msg);
}
// ignore libraries that have the same package name as the app
continue;
}
File rFile = lib.getSymbolFile();
// if the library has no resource, this file won't exist.
if (rFile.isFile()) {
// load the full values if that's not already been done.
// Doing it lazily allow us to support the case where there's no
// resources anywhere.
if (fullSymbolValues == null) {
fullSymbolValues = new SymbolLoader(new File(aaptCommand.getSymbolOutputDir(), "R.txt"),
mLogger);
fullSymbolValues.load();
}
SymbolLoader libSymbols = new SymbolLoader(rFile, mLogger);
libSymbols.load();
// store these symbols by associating them with the package name.
libMap.put(packageName, libSymbols);
}
}
// now loop on all the package name, merge all the symbols to write, and write them
for (String packageName : libMap.keySet()) {
Collection<SymbolLoader> symbols = libMap.get(packageName);
if (enforceUniquePackageName && symbols.size() > 1) {
String msg = String.format(
"Error: more than one library with package name '%s'\n" +
"You can temporarily disable this error with android.enforceUniquePackageName=false\n" +
"However, this is temporary and will be enforced in 1.0", packageName);
throw new RuntimeException(msg);
}
SymbolWriter writer = new SymbolWriter(aaptCommand.getSourceOutputDir(), packageName,
fullSymbolValues);
for (SymbolLoader symbolLoader : symbols) {
writer.addSymbolsToWrite(symbolLoader);
}
writer.write();
}
}
}
public void generateApkData(
@NonNull File apkFile,
@NonNull File outResFolder,
@NonNull String mainPkgName,
@NonNull String resName) throws ProcessException, IOException {
// need to run aapt to get apk information
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
String aapt = buildToolInfo.getPath(BuildToolInfo.PathId.AAPT);
if (aapt == null) {
throw new IllegalStateException(
"Unable to get aapt location from Build Tools " + buildToolInfo.getRevision());
}
ApkInfoParser parser = new ApkInfoParser(new File(aapt), mProcessExecutor);
ApkInfoParser.ApkInfo apkInfo = parser.parseApk(apkFile);
if (!apkInfo.getPackageName().equals(mainPkgName)) {
throw new RuntimeException("The main and the micro apps do not have the same package name.");
}
String content = String.format(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<wearableApp package=\"%1$s\">\n" +
" <versionCode>%2$s</versionCode>\n" +
" <versionName>%3$s</versionName>\n" +
" <rawPathResId>%4$s</rawPathResId>\n" +
"</wearableApp>",
apkInfo.getPackageName(),
apkInfo.getVersionCode(),
apkInfo.getVersionName(),
resName);
// xml folder
File resXmlFile = new File(outResFolder, FD_RES_XML);
resXmlFile.mkdirs();
Files.write(content,
new File(resXmlFile, ANDROID_WEAR_MICRO_APK + DOT_XML),
Charsets.UTF_8);
}
public static void generateApkDataEntryInManifest(
int minSdkVersion,
int targetSdkVersion,
@NonNull File manifestFile)
throws InterruptedException, LoggedErrorException, IOException {
StringBuilder content = new StringBuilder();
content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
.append("<manifest package=\"\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n")
.append(" <uses-sdk android:minSdkVersion=\"")
.append(minSdkVersion).append("\"");
if (targetSdkVersion != -1) {
content.append(" android:targetSdkVersion=\"").append(targetSdkVersion).append("\"");
}
content.append("/>\n");
content.append(" <application>\n")
.append(" <meta-data android:name=\"" + ANDROID_WEAR + "\"\n")
.append(" android:resource=\"@xml/" + ANDROID_WEAR_MICRO_APK)
.append("\" />\n")
.append(" </application>\n")
.append("</manifest>\n");
Files.write(content, manifestFile, Charsets.UTF_8);
}
/**
* Compiles all the aidl files found in the given source folders.
*
* @param sourceFolders all the source folders to find files to compile
* @param sourceOutputDir the output dir in which to generate the source code
* @param importFolders import folders
* @param dependencyFileProcessor the dependencyFileProcessor to record the dependencies
* of the compilation.
* @throws IOException
* @throws InterruptedException
* @throws LoggedErrorException
*/
public void compileAllAidlFiles(@NonNull List<File> sourceFolders,
@NonNull File sourceOutputDir,
@Nullable File parcelableOutputDir,
@NonNull List<File> importFolders,
@Nullable DependencyFileProcessor dependencyFileProcessor,
@NonNull ProcessOutputHandler processOutputHandler)
throws IOException, InterruptedException, LoggedErrorException, ProcessException {
checkNotNull(sourceFolders, "sourceFolders cannot be null.");
checkNotNull(sourceOutputDir, "sourceOutputDir cannot be null.");
checkNotNull(importFolders, "importFolders cannot be null.");
checkState(mTargetInfo != null,
"Cannot call compileAllAidlFiles() before setTargetInfo() is called.");
IAndroidTarget target = mTargetInfo.getTarget();
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
String aidl = buildToolInfo.getPath(BuildToolInfo.PathId.AIDL);
if (aidl == null || !new File(aidl).isFile()) {
throw new IllegalStateException("aidl is missing");
}
List<File> fullImportList = Lists.newArrayListWithCapacity(
sourceFolders.size() + importFolders.size());
fullImportList.addAll(sourceFolders);
fullImportList.addAll(importFolders);
AidlProcessor processor = new AidlProcessor(
aidl,
target.getPath(IAndroidTarget.ANDROID_AIDL),
fullImportList,
sourceOutputDir,
parcelableOutputDir,
dependencyFileProcessor != null ?
dependencyFileProcessor : sNoOpDependencyFileProcessor,
mProcessExecutor,
processOutputHandler);
SourceSearcher searcher = new SourceSearcher(sourceFolders, "aidl");
searcher.setUseExecutor(true);
searcher.search(processor);
}
/**
* Compiles the given aidl file.
*
* @param aidlFile the AIDL file to compile
* @param sourceOutputDir the output dir in which to generate the source code
* @param importFolders all the import folders, including the source folders.
* @param dependencyFileProcessor the dependencyFileProcessor to record the dependencies
* of the compilation.
* @throws IOException
* @throws InterruptedException
* @throws LoggedErrorException
*/
public void compileAidlFile(@NonNull File sourceFolder,
@NonNull File aidlFile,
@NonNull File sourceOutputDir,
@Nullable File parcelableOutputDir,
@NonNull List<File> importFolders,
@Nullable DependencyFileProcessor dependencyFileProcessor,
@NonNull ProcessOutputHandler processOutputHandler)
throws IOException, InterruptedException, LoggedErrorException, ProcessException {
checkNotNull(aidlFile, "aidlFile cannot be null.");
checkNotNull(sourceOutputDir, "sourceOutputDir cannot be null.");
checkNotNull(importFolders, "importFolders cannot be null.");
checkState(mTargetInfo != null,
"Cannot call compileAidlFile() before setTargetInfo() is called.");
IAndroidTarget target = mTargetInfo.getTarget();
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
String aidl = buildToolInfo.getPath(BuildToolInfo.PathId.AIDL);
if (aidl == null || !new File(aidl).isFile()) {
throw new IllegalStateException("aidl is missing");
}
AidlProcessor processor = new AidlProcessor(
aidl,
target.getPath(IAndroidTarget.ANDROID_AIDL),
importFolders,
sourceOutputDir,
parcelableOutputDir,
dependencyFileProcessor != null ?
dependencyFileProcessor : sNoOpDependencyFileProcessor,
mProcessExecutor,
processOutputHandler);
processor.processFile(sourceFolder, aidlFile);
}
/**
* Compiles all the renderscript files found in the given source folders.
*
* Right now this is the only way to compile them as the renderscript compiler requires all
* renderscript files to be passed for all compilation.
*
* Therefore whenever a renderscript file or header changes, all must be recompiled.
*
* @param sourceFolders all the source folders to find files to compile
* @param importFolders all the import folders.
* @param sourceOutputDir the output dir in which to generate the source code
* @param resOutputDir the output dir in which to generate the bitcode file
* @param targetApi the target api
* @param debugBuild whether the build is debug
* @param optimLevel the optimization level
* @param ndkMode
* @param supportMode support mode flag to generate .so files.
* @param abiFilters ABI filters in case of support mode
*
* @throws IOException
* @throws InterruptedException
* @throws LoggedErrorException
*/
public void compileAllRenderscriptFiles(@NonNull List<File> sourceFolders,
@NonNull List<File> importFolders,
@NonNull File sourceOutputDir,
@NonNull File resOutputDir,
@NonNull File objOutputDir,
@NonNull File libOutputDir,
int targetApi,
boolean debugBuild,
int optimLevel,
boolean ndkMode,
boolean supportMode,
@Nullable Set<String> abiFilters,
@NonNull ProcessOutputHandler processOutputHandler)
throws InterruptedException, ProcessException, LoggedErrorException, IOException {
checkNotNull(sourceFolders, "sourceFolders cannot be null.");
checkNotNull(importFolders, "importFolders cannot be null.");
checkNotNull(sourceOutputDir, "sourceOutputDir cannot be null.");
checkNotNull(resOutputDir, "resOutputDir cannot be null.");
checkState(mTargetInfo != null,
"Cannot call compileAllRenderscriptFiles() before setTargetInfo() is called.");
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
String renderscript = buildToolInfo.getPath(BuildToolInfo.PathId.LLVM_RS_CC);
if (renderscript == null || !new File(renderscript).isFile()) {
throw new IllegalStateException("llvm-rs-cc is missing");
}
RenderScriptProcessor processor = new RenderScriptProcessor(
sourceFolders,
importFolders,
sourceOutputDir,
resOutputDir,
objOutputDir,
libOutputDir,
buildToolInfo,
targetApi,
debugBuild,
optimLevel,
ndkMode,
supportMode,
abiFilters);
processor.build(mProcessExecutor, processOutputHandler);
}
/**
* Computes and returns the leaf folders based on a given file extension.
*
* This looks through all the given root import folders, and recursively search for leaf
* folders containing files matching the given extensions. All the leaf folders are gathered
* and returned in the list.
*
* @param extension the extension to search for.
* @param importFolders an array of list of root folders.
* @return a list of leaf folder, never null.
*/
@NonNull
public List<File> getLeafFolders(@NonNull String extension, List<File>... importFolders) {
List<File> results = Lists.newArrayList();
if (importFolders != null) {
for (List<File> folders : importFolders) {
SourceSearcher searcher = new SourceSearcher(folders, extension);
searcher.setUseExecutor(false);
LeafFolderGatherer processor = new LeafFolderGatherer();
try {
searcher.search(processor);
} catch (InterruptedException e) {
// wont happen as we're not using the executor, and our processor
// doesn't throw those.
} catch (IOException e) {
// wont happen as we're not using the executor, and our processor
// doesn't throw those.
} catch (LoggedErrorException e) {
// wont happen as we're not using the executor, and our processor
// doesn't throw those.
} catch (ProcessException e) {
// wont happen as we're not using the executor, and our processor
// doesn't throw those.
}
results.addAll(processor.getFolders());
}
}
return results;
}
/**
* Converts the bytecode to Dalvik format
* @param inputs the input files
* @param preDexedLibraries the list of pre-dexed libraries
* @param outDexFolder the location of the output folder
* @param dexOptions dex options
* @param additionalParameters list of additional parameters to give to dx
* @param incremental true if it should attempt incremental dex if applicable
*
* @throws IOException
* @throws InterruptedException
* @throws ProcessException
*/
public void convertByteCode(
@NonNull Collection<File> inputs,
@NonNull Collection<File> preDexedLibraries,
@NonNull File outDexFolder,
boolean multidex,
@Nullable File mainDexList,
@NonNull DexOptions dexOptions,
@Nullable List<String> additionalParameters,
@NonNull File tmpFolder,
boolean incremental,
boolean optimize,
@NonNull ProcessOutputHandler processOutputHandler)
throws IOException, InterruptedException, ProcessException {
checkNotNull(inputs, "inputs cannot be null.");
checkNotNull(preDexedLibraries, "preDexedLibraries cannot be null.");
checkNotNull(outDexFolder, "outDexFolder cannot be null.");
checkNotNull(dexOptions, "dexOptions cannot be null.");
checkNotNull(tmpFolder, "tmpFolder cannot be null");
checkArgument(outDexFolder.isDirectory(), "outDexFolder must be a folder");
checkArgument(tmpFolder.isDirectory(), "tmpFolder must be a folder");
checkState(mTargetInfo != null,
"Cannot call convertByteCode() before setTargetInfo() is called.");
ImmutableList.Builder<File> verifiedInputs = ImmutableList.builder();
for (File input : inputs) {
if (checkLibraryClassesJar(input)) {
verifiedInputs.add(input);
}
}
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
DexProcessBuilder builder = new DexProcessBuilder(outDexFolder);
builder.setVerbose(mVerboseExec)
.setIncremental(incremental)
.setNoOptimize(!optimize)
.setMultiDex(multidex)
.setMainDexList(mainDexList)
.addInputs(preDexedLibraries)
.addInputs(verifiedInputs.build());
if (additionalParameters != null) {
builder.additionalParameters(additionalParameters);
}
JavaProcessInfo javaProcessInfo = builder.build(buildToolInfo, dexOptions);
ProcessResult result = mJavaProcessExecutor.execute(javaProcessInfo, processOutputHandler);
result.rethrowFailure().assertNormalExitValue();
}
public Set<String> createMainDexList(
@NonNull File allClassesJarFile,
@NonNull File jarOfRoots) throws ProcessException {
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
ProcessInfoBuilder builder = new ProcessInfoBuilder();
String dx = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
if (dx == null || !new File(dx).isFile()) {
throw new IllegalStateException("dx.jar is missing");
}
builder.setClasspath(dx);
builder.setMain("com.android.multidex.ClassReferenceListBuilder");
builder.addArgs(jarOfRoots.getAbsolutePath());
builder.addArgs(allClassesJarFile.getAbsolutePath());
CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler();
mJavaProcessExecutor.execute(builder.createJavaProcess(), processOutputHandler)
.rethrowFailure()
.assertNormalExitValue();
String content = processOutputHandler.getProcessOutput().getStandardOutputAsString();
return Sets.newHashSet(Splitter.on('\n').split(content));
}
/**
* Converts the bytecode to Dalvik format
* @param inputFile the input file
* @param outFile the output file or folder if multi-dex is enabled.
* @param multiDex whether multidex is enabled.
* @param dexOptions dex options
*
* @throws IOException
* @throws InterruptedException
* @throws ProcessException
*/
public void preDexLibrary(
@NonNull File inputFile,
@NonNull File outFile,
boolean multiDex,
@NonNull DexOptions dexOptions,
@NonNull ProcessOutputHandler processOutputHandler)
throws IOException, InterruptedException, ProcessException {
checkState(mTargetInfo != null,
"Cannot call preDexLibrary() before setTargetInfo() is called.");
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
PreDexCache.getCache().preDexLibrary(
inputFile,
outFile,
multiDex,
dexOptions,
buildToolInfo,
mVerboseExec,
mJavaProcessExecutor,
processOutputHandler);
}
/**
* Converts the bytecode to Dalvik format
*
* @param inputFile the input file
* @param outFile the output file or folder if multi-dex is enabled.
* @param multiDex whether multidex is enabled.
* @param dexOptions the dex options
* @param buildToolInfo the build tools info
* @param verbose verbose flag
* @param processExecutor the java process executor
* @return the list of generated files.
* @throws ProcessException
*/
@NonNull
public static ImmutableList<File> preDexLibrary(
@NonNull File inputFile,
@NonNull File outFile,
boolean multiDex,
@NonNull DexOptions dexOptions,
@NonNull BuildToolInfo buildToolInfo,
boolean verbose,
@NonNull JavaProcessExecutor processExecutor,
@NonNull ProcessOutputHandler processOutputHandler)
throws ProcessException {
checkNotNull(inputFile, "inputFile cannot be null.");
checkNotNull(outFile, "outFile cannot be null.");
checkNotNull(dexOptions, "dexOptions cannot be null.");
try {
if (!checkLibraryClassesJar(inputFile)) {
return ImmutableList.of();
}
} catch(IOException e) {
throw new RuntimeException("Exception while checking library jar", e);
}
DexProcessBuilder builder = new DexProcessBuilder(outFile);
builder.setVerbose(verbose)
.setMultiDex(multiDex)
.addInput(inputFile);
JavaProcessInfo javaProcessInfo = builder.build(buildToolInfo, dexOptions);
ProcessResult result = processExecutor.execute(javaProcessInfo, processOutputHandler);
result.rethrowFailure().assertNormalExitValue();
if (multiDex) {
File[] files = outFile.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String name) {
return name.endsWith(DOT_DEX);
}
});
if (files == null || files.length == 0) {
throw new RuntimeException("No dex files created at " + outFile.getAbsolutePath());
}
return ImmutableList.copyOf(files);
} else {
return ImmutableList.of(outFile);
}
}
/**
* Returns true if the library (jar or folder) contains class files, false otherwise.
*/
private static boolean checkLibraryClassesJar(@NonNull File input) throws IOException {
if (!input.exists()) {
return false;
}
if (input.isDirectory()) {
return checkFolder(input);
}
ZipFile zipFile = null;
try {
zipFile = new ZipFile(input);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while(entries.hasMoreElements()) {
if (entries.nextElement().getName().endsWith(".class")) {
return true;
}
}
return false;
} finally {
if (zipFile != null) {
zipFile.close();
}
}
}
/**
* Returns true if this folder or one of its subfolder contains a class file, false otherwise.
*/
private static boolean checkFolder(@NonNull File folder) {
File[] subFolders = folder.listFiles();
if (subFolders != null) {
for (File childFolder : subFolders) {
if (childFolder.isFile() && childFolder.getName().endsWith(".class")) {
return true;
}
if (childFolder.isDirectory()) {
// if childFolder returns false, continue search otherwise return success.
if (checkFolder(childFolder)) {
return true;
}
}
}
}
return false;
}
/**
* Converts java source code into android byte codes using the jack integration APIs.
* Jack will run in memory.
*/
public boolean convertByteCodeUsingJackApis(
@NonNull File dexOutputFolder,
@NonNull File jackOutputFile,
@NonNull Collection<File> classpath,
@NonNull Collection<File> packagedLibraries,
@NonNull Collection<File> sourceFiles,
@Nullable Collection<File> proguardFiles,
@Nullable File mappingFile,
@NonNull Collection<File> jarJarRulesFiles,
@Nullable File incrementalDir,
@Nullable File javaResourcesFolder,
boolean multiDex,
int minSdkVersion) {
BuildToolServiceLoader buildToolServiceLoader
= BuildToolsServiceLoader.INSTANCE.forVersion(mTargetInfo.getBuildTools());
Api01CompilationTask compilationTask = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
Optional<JackProvider> jackProvider = buildToolServiceLoader
.getSingleService(getLogger(), BuildToolsServiceLoader.JACK);
if (jackProvider.isPresent()) {
Api01Config config;
// Get configuration object
try {
config = jackProvider.get().createConfig(Api01Config.class);
config.setClasspath(new ArrayList<File>(classpath));
config.setOutputDexDir(dexOutputFolder);
config.setOutputJackFile(jackOutputFile);
config.setImportedJackLibraryFiles(new ArrayList<File>(packagedLibraries));
if (proguardFiles != null) {
config.setProguardConfigFiles(new ArrayList<File>(proguardFiles));
}
if (!jarJarRulesFiles.isEmpty()) {
config.setJarJarConfigFiles(ImmutableList.copyOf(jarJarRulesFiles));
}
if (multiDex) {
if (minSdkVersion < BuildToolInfo.SDK_LEVEL_FOR_MULTIDEX_NATIVE_SUPPORT) {
config.setMultiDexKind(MultiDexKind.LEGACY);
} else {
config.setMultiDexKind(MultiDexKind.NATIVE);
}
}
config.setSourceEntries(new ArrayList<File>(sourceFiles));
if (mappingFile != null) {
config.setProperty("jack.obfuscation.mapping.dump", "true");
config.setObfuscationMappingOutputFile(mappingFile);
}
config.setProperty("jack.import.resource.policy", "keep-first");
config.setReporter(ReporterKind.DEFAULT, outputStream);
// set the incremental dir if set and either already exists or can be created.
if (incrementalDir != null) {
if (!incrementalDir.exists() && !incrementalDir.mkdirs()) {
mLogger.warning("Cannot create %1$s directory, "
+ "jack incremental support disabled", incrementalDir);
}
if (incrementalDir.exists()) {
config.setIncrementalDir(incrementalDir);
}
}
if (javaResourcesFolder != null) {
ArrayList<File> folders = Lists.newArrayListWithExpectedSize(3);
folders.add(javaResourcesFolder);
config.setResourceDirs(folders);
}
compilationTask = config.getTask();
} catch (ConfigNotSupportedException e1) {
mLogger.warning("Jack APIs v01 not supported");
} catch (ConfigurationException e) {
mLogger.error(e,
"Jack APIs v01 configuration failed, reverting to native process");
}
}
if (compilationTask == null) {
return false;
}
// Run the compilation
try {
compilationTask.run();
mLogger.info(outputStream.toString());
return true;
} catch (CompilationException e) {
mLogger.error(e, outputStream.toString());
} catch (UnrecoverableException e) {
mLogger.error(e,
"Something out of Jack control has happened: " + e.getMessage());
} catch (ConfigurationException e) {
mLogger.error(e, outputStream.toString());
}
} catch (ClassNotFoundException e) {
getLogger().warning("Cannot load Jack APIs v01 " + e.getMessage());
getLogger().warning("Reverting to native process invocation");
}
return false;
}
public void convertByteCodeWithJack(
@NonNull File dexOutputFolder,
@NonNull File jackOutputFile,
@NonNull String classpath,
@NonNull Collection<File> packagedLibraries,
@NonNull File ecjOptionFile,
@Nullable Collection<File> proguardFiles,
@Nullable File mappingFile,
@NonNull Collection<File> jarJarRuleFiles,
boolean multiDex,
int minSdkVersion,
boolean debugLog,
String javaMaxHeapSize,
@NonNull ProcessOutputHandler processOutputHandler) throws ProcessException {
JackProcessBuilder builder = new JackProcessBuilder();
builder.setDebugLog(debugLog)
.setVerbose(mVerboseExec)
.setJavaMaxHeapSize(javaMaxHeapSize)
.setClasspath(classpath)
.setDexOutputFolder(dexOutputFolder)
.setJackOutputFile(jackOutputFile)
.addImportFiles(packagedLibraries)
.setEcjOptionFile(ecjOptionFile);
if (proguardFiles != null) {
builder.addProguardFiles(proguardFiles).setMappingFile(mappingFile);
}
if (multiDex) {
builder.setMultiDex(true).setMinSdkVersion(minSdkVersion);
}
if (jarJarRuleFiles != null) {
builder.setJarJarRuleFiles(jarJarRuleFiles);
}
mJavaProcessExecutor.execute(
builder.build(mTargetInfo.getBuildTools()), processOutputHandler)
.rethrowFailure().assertNormalExitValue();
}
/**
* Converts the bytecode of a library to the jack format
* @param inputFile the input file
* @param outFile the location of the output classes.dex file
* @param dexOptions dex options
*
* @throws ProcessException
* @throws IOException
* @throws InterruptedException
*/
public void convertLibraryToJack(
@NonNull File inputFile,
@NonNull File outFile,
@NonNull DexOptions dexOptions,
@NonNull ProcessOutputHandler processOutputHandler)
throws ProcessException, IOException, InterruptedException {
checkState(mTargetInfo != null,
"Cannot call preJackLibrary() before setTargetInfo() is called.");
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
JackConversionCache.getCache().convertLibrary(
inputFile,
outFile,
dexOptions,
buildToolInfo,
mVerboseExec,
mJavaProcessExecutor,
processOutputHandler,
mLogger);
}
public static List<File> convertLibaryToJackUsingApis(
@NonNull File inputFile,
@NonNull File outFile,
@NonNull DexOptions dexOptions,
@NonNull BuildToolInfo buildToolInfo,
boolean verbose,
@NonNull JavaProcessExecutor processExecutor,
@NonNull ProcessOutputHandler processOutputHandler,
@NonNull ILogger logger) throws ProcessException {
BuildToolServiceLoader buildToolServiceLoader = BuildToolsServiceLoader.INSTANCE
.forVersion(buildToolInfo);
if (System.getenv("USE_JACK_API") != null) {
try {
Optional<JillProvider> jillProviderOptional = buildToolServiceLoader
.getSingleService(logger, BuildToolsServiceLoader.JILL);
if (jillProviderOptional.isPresent()) {
com.android.jill.api.v01.Api01Config config =
jillProviderOptional.get().createConfig(
com.android.jill.api.v01.Api01Config.class);
config.setInputJavaBinaryFile(inputFile);
config.setOutputJackFile(outFile);
config.setVerbose(verbose);
Api01TranslationTask translationTask = config.getTask();
translationTask.run();
return ImmutableList.of(outFile);
}
} catch (ClassNotFoundException e) {
logger.warning("Cannot find the jill tool in the classpath, reverting to native");
} catch (com.android.jill.api.ConfigNotSupportedException e) {
logger.warning(e.getMessage() + ", reverting to native");
} catch (com.android.jill.api.v01.ConfigurationException e) {
logger.warning(e.getMessage() + ", reverting to native");
} catch (TranslationException e) {
logger.error(e, "In process translation failed, reverting to native, file a bug");
}
}
return convertLibraryToJack(inputFile, outFile, dexOptions, buildToolInfo, verbose,
processExecutor, processOutputHandler, logger);
}
public static List<File> convertLibraryToJack(
@NonNull File inputFile,
@NonNull File outFile,
@NonNull DexOptions dexOptions,
@NonNull BuildToolInfo buildToolInfo,
boolean verbose,
@NonNull JavaProcessExecutor processExecutor,
@NonNull ProcessOutputHandler processOutputHandler,
@NonNull ILogger logger)
throws ProcessException {
checkNotNull(inputFile, "inputFile cannot be null.");
checkNotNull(outFile, "outFile cannot be null.");
checkNotNull(dexOptions, "dexOptions cannot be null.");
// launch dx: create the command line
ProcessInfoBuilder builder = new ProcessInfoBuilder();
String jill = buildToolInfo.getPath(BuildToolInfo.PathId.JILL);
if (jill == null || !new File(jill).isFile()) {
throw new IllegalStateException("jill.jar is missing");
}
builder.setClasspath(jill);
builder.setMain("com.android.jill.Main");
if (dexOptions.getJavaMaxHeapSize() != null) {
builder.addJvmArg("-Xmx" + dexOptions.getJavaMaxHeapSize());
}
builder.addArgs(inputFile.getAbsolutePath());
builder.addArgs("--output");
builder.addArgs(outFile.getAbsolutePath());
if (verbose) {
builder.addArgs("--verbose");
}
logger.verbose(builder.toString());
JavaProcessInfo javaProcessInfo = builder.createJavaProcess();
ProcessResult result = processExecutor.execute(javaProcessInfo, processOutputHandler);
result.rethrowFailure().assertNormalExitValue();
return Collections.singletonList(outFile);
}
/**
* Packages the apk.
*
* @param androidResPkgLocation the location of the packaged resource file
* @param dexFolder the folder with the dex file.
* @param dexedLibraries optional collection of additional dex files to put in the apk.
* @param packagedJars the jars that are packaged (libraries + jar dependencies)
* @param javaResourcesLocation the processed Java resource folder
* @param jniLibsFolders the folders containing jni shared libraries
* @param mergingFolder folder to contain files that are being merged
* @param abiFilters optional ABI filter
* @param jniDebugBuild whether the app should include jni debug data
* @param signingConfig the signing configuration
* @param packagingOptions the packaging options
* @param outApkLocation location of the APK.
* @throws DuplicateFileException
* @throws FileNotFoundException if the store location was not found
* @throws KeytoolException
* @throws PackagerException
* @throws SigningException when the key cannot be read from the keystore
*
* @see VariantConfiguration#getPackagedJars()
*/
public void packageApk(
@NonNull String androidResPkgLocation,
@Nullable File dexFolder,
@NonNull Collection<File> dexedLibraries,
@NonNull Collection<File> packagedJars,
@Nullable String javaResourcesLocation,
@Nullable Collection<File> jniLibsFolders,
@NonNull File mergingFolder,
@Nullable Set<String> abiFilters,
boolean jniDebugBuild,
@Nullable SigningConfig signingConfig,
@Nullable PackagingOptions packagingOptions,
@Nullable SignedJarBuilder.IZipEntryFilter packagingOptionsFilter,
@NonNull String outApkLocation)
throws DuplicateFileException, FileNotFoundException,
KeytoolException, PackagerException, SigningException {
checkNotNull(androidResPkgLocation, "androidResPkgLocation cannot be null.");
checkNotNull(outApkLocation, "outApkLocation cannot be null.");
CertificateInfo certificateInfo = null;
if (signingConfig != null && signingConfig.isSigningReady()) {
//noinspection ConstantConditions - isSigningReady() called above.
certificateInfo = KeystoreHelper.getCertificateInfo(signingConfig.getStoreType(),
signingConfig.getStoreFile(), signingConfig.getStorePassword(),
signingConfig.getKeyPassword(), signingConfig.getKeyAlias());
if (certificateInfo == null) {
throw new SigningException("Failed to read key from keystore");
}
}
try {
Packager packager = new Packager(
outApkLocation, androidResPkgLocation, mergingFolder,
certificateInfo, mCreatedBy, packagingOptions, packagingOptionsFilter, mLogger);
// add dex folder to the apk root.
if (dexFolder != null) {
if (!dexFolder.isDirectory()) {
throw new IllegalArgumentException("dexFolder must be a directory");
}
packager.addDexFiles(dexFolder, dexedLibraries);
}
packager.setJniDebugMode(jniDebugBuild);
if (javaResourcesLocation != null && !packagedJars.isEmpty()) {
throw new PackagerException("javaResourcesLocation and packagedJars both provided");
}
if (javaResourcesLocation != null || !packagedJars.isEmpty()) {
packager.addResources(javaResourcesLocation != null
? new File(javaResourcesLocation) : Iterables.getOnlyElement(packagedJars));
}
// also add resources from library projects and jars
if (jniLibsFolders != null) {
for (File jniFolder : jniLibsFolders) {
if (jniFolder.isDirectory()) {
packager.addNativeLibraries(jniFolder, abiFilters);
}
}
}
packager.sealApk();
} catch (SealedPackageException e) {
// shouldn't happen since we control the package from start to end.
throw new RuntimeException(e);
}
}
/**
* Signs a single jar file using the passed {@link SigningConfig}.
* @param in the jar file to sign.
* @param signingConfig the signing configuration
* @param out the file path for the signed jar.
* @throws IOException
* @throws KeytoolException
* @throws SigningException
* @throws NoSuchAlgorithmException
* @throws SignedJarBuilder.IZipEntryFilter.ZipAbortException
* @throws com.android.builder.signing.SigningException
*/
public void signApk(File in, SigningConfig signingConfig, File out)
throws IOException, KeytoolException, SigningException, NoSuchAlgorithmException,
SignedJarBuilder.IZipEntryFilter.ZipAbortException,
com.android.builder.signing.SigningException {
CertificateInfo certificateInfo = null;
if (signingConfig != null && signingConfig.isSigningReady()) {
//noinspection ConstantConditions - isSigningReady() called above.
certificateInfo = KeystoreHelper.getCertificateInfo(signingConfig.getStoreType(),
signingConfig.getStoreFile(), signingConfig.getStorePassword(),
signingConfig.getKeyPassword(), signingConfig.getKeyAlias());
if (certificateInfo == null) {
throw new SigningException("Failed to read key from keystore");
}
}
SignedJarBuilder signedJarBuilder = new SignedJarBuilder(
new FileOutputStream(out),
certificateInfo != null ? certificateInfo.getKey() : null,
certificateInfo != null ? certificateInfo.getCertificate() : null,
Packager.getLocalVersion(), mCreatedBy);
signedJarBuilder.writeZip(new FileInputStream(in));
signedJarBuilder.close();
}
}