| /* |
| * Copyright (C) 2014 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.tasks; |
| |
| import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.ALL; |
| import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.MANIFEST; |
| import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH; |
| import static com.android.build.gradle.internal.scope.InternalArtifactType.MERGED_MANIFESTS; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.build.VariantOutput; |
| import com.android.build.gradle.internal.LoggerWrapper; |
| import com.android.build.gradle.internal.core.VariantConfiguration; |
| import com.android.build.gradle.internal.dsl.CoreBuildType; |
| import com.android.build.gradle.internal.dsl.CoreProductFlavor; |
| import com.android.build.gradle.internal.publishing.AndroidArtifacts; |
| import com.android.build.gradle.internal.scope.ApkData; |
| import com.android.build.gradle.internal.scope.BuildArtifactsHolder; |
| import com.android.build.gradle.internal.scope.BuildElements; |
| import com.android.build.gradle.internal.scope.BuildOutput; |
| import com.android.build.gradle.internal.scope.BuildOutputProperty; |
| import com.android.build.gradle.internal.scope.ExistingBuildElements; |
| import com.android.build.gradle.internal.scope.InternalArtifactType; |
| import com.android.build.gradle.internal.scope.OutputScope; |
| import com.android.build.gradle.internal.scope.VariantScope; |
| import com.android.build.gradle.internal.tasks.TaskInputHelper; |
| import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction; |
| import com.android.builder.internal.TestManifestGenerator; |
| import com.android.manifmerger.ManifestMerger2; |
| import com.android.manifmerger.ManifestProvider; |
| import com.android.manifmerger.ManifestSystemProperty; |
| import com.android.manifmerger.MergingReport; |
| import com.android.manifmerger.PlaceholderHandler; |
| import com.android.utils.FileUtils; |
| import com.android.utils.ILogger; |
| import com.google.common.base.Charsets; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.Files; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.function.Supplier; |
| import javax.inject.Inject; |
| import org.gradle.api.artifacts.ArtifactCollection; |
| import org.gradle.api.artifacts.result.ResolvedArtifactResult; |
| import org.gradle.api.file.FileCollection; |
| import org.gradle.api.model.ObjectFactory; |
| import org.gradle.api.tasks.Input; |
| import org.gradle.api.tasks.InputFile; |
| import org.gradle.api.tasks.InputFiles; |
| import org.gradle.api.tasks.Optional; |
| import org.gradle.api.tasks.TaskProvider; |
| |
| /** |
| * A task that processes the manifest for test modules and tests in androidTest. |
| * |
| * <p>For both test modules and tests in androidTest process is the same, expect |
| * for how the tested application id is extracted.</p> |
| * |
| * <p>Tests in androidTest get that info form the |
| * {@link VariantConfiguration#getTestedApplicationId()}, while the test modules get the info from |
| * the published intermediate manifest with type {@link AndroidArtifacts#TYPE_METADATA} |
| * of the tested app.</p> |
| */ |
| public abstract class ProcessTestManifest extends ManifestProcessorTask { |
| |
| @NonNull private FileCollection testTargetMetadata; |
| |
| @Nullable |
| private File testManifestFile; |
| |
| /** Whether there's just a single APK with both test and tested code. */ |
| private boolean onlyTestApk; |
| |
| private File tmpDir; |
| private Supplier<String> testApplicationId; |
| private Supplier<String> testedApplicationId; |
| private Supplier<String> minSdkVersion; |
| private Supplier<String> targetSdkVersion; |
| private Supplier<String> instrumentationRunner; |
| private Supplier<Boolean> handleProfiling; |
| private Supplier<Boolean> functionalTest; |
| private Supplier<Map<String, Object>> placeholdersValues; |
| |
| private ArtifactCollection manifests; |
| |
| private Supplier<String> testLabel; |
| |
| private OutputScope outputScope; |
| |
| @Inject |
| public ProcessTestManifest(ObjectFactory objectFactory) { |
| super(objectFactory); |
| } |
| |
| public OutputScope getOutputScope() { |
| return outputScope; |
| } |
| |
| @Override |
| protected void doFullTaskAction() throws IOException { |
| if (testedApplicationId == null && testTargetMetadata == null) { |
| throw new RuntimeException("testedApplicationId and testTargetMetadata are null"); |
| } |
| String testedApplicationId = this.getTestedApplicationId(); |
| if (!onlyTestApk && testTargetMetadata != null) { |
| BuildElements manifestOutputs = |
| ExistingBuildElements.from( |
| MERGED_MANIFESTS, testTargetMetadata.getSingleFile()); |
| |
| java.util.Optional<BuildOutput> mainSplit = |
| manifestOutputs |
| .stream() |
| .filter( |
| output -> |
| output.getApkData().getType() |
| != VariantOutput.OutputType.SPLIT) |
| .findFirst(); |
| |
| if (mainSplit.isPresent()) { |
| testedApplicationId = |
| mainSplit.get().getProperties().get(BuildOutputProperty.PACKAGE_ID); |
| } else { |
| throw new RuntimeException("cannot find main APK"); |
| } |
| } |
| // TODO : LOAD FROM APK_LIST... |
| List<ApkData> apkDatas = outputScope.getApkDatas(); |
| if (apkDatas.isEmpty()) { |
| throw new RuntimeException("No output defined for test module, please file a bug"); |
| } |
| if (apkDatas.size() > 1) { |
| throw new RuntimeException( |
| "Test modules only support a single split, this one defines" |
| + Joiner.on(",").join(apkDatas)); |
| } |
| ApkData mainApkData = apkDatas.get(0); |
| File manifestOutputFolder = |
| Strings.isNullOrEmpty(mainApkData.getDirName()) |
| ? getManifestOutputDirectory().get().getAsFile() |
| : getManifestOutputDirectory() |
| .get() |
| .file(mainApkData.getDirName()) |
| .getAsFile(); |
| |
| |
| FileUtils.mkdirs(manifestOutputFolder); |
| File manifestOutputFile = new File(manifestOutputFolder, SdkConstants.ANDROID_MANIFEST_XML); |
| |
| mergeManifestsForTestVariant( |
| getTestApplicationId(), |
| getMinSdkVersion(), |
| getTargetSdkVersion(), |
| testedApplicationId, |
| getInstrumentationRunner(), |
| getHandleProfiling(), |
| getFunctionalTest(), |
| getTestLabel(), |
| getTestManifestFile(), |
| computeProviders(), |
| getPlaceholdersValues(), |
| manifestOutputFile, |
| getTmpDir()); |
| |
| new BuildElements( |
| ImmutableList.of( |
| new BuildOutput(MERGED_MANIFESTS, mainApkData, manifestOutputFile))) |
| .save(getManifestOutputDirectory().get().getAsFile()); |
| } |
| |
| /** |
| * 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 testLabel the label for the tests |
| * @param testManifestFile optionally user provided AndroidManifest.xml for testing application |
| * @param manifestProviders the manifest providers |
| * @param manifestPlaceholders used placeholders in the manifest |
| * @param outManifest the output location for the merged manifest |
| * @param tmpDir temporary dir used for processing |
| */ |
| public void mergeManifestsForTestVariant( |
| @NonNull String testApplicationId, |
| @NonNull String minSdkVersion, |
| @NonNull String targetSdkVersion, |
| @NonNull String testedApplicationId, |
| @NonNull String instrumentationRunner, |
| @NonNull Boolean handleProfiling, |
| @NonNull Boolean functionalTest, |
| @Nullable String testLabel, |
| @Nullable File testManifestFile, |
| @NonNull List<? extends ManifestProvider> manifestProviders, |
| @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(manifestProviders, "manifestProviders cannot be null."); |
| checkNotNull(outManifest, "outManifestLocation cannot be null."); |
| |
| ILogger logger = new LoggerWrapper(getLogger()); |
| |
| // These temp files are only need in the middle of processing manifests; delete |
| // them when they're done. We're not relying on File#deleteOnExit for this |
| // since in the Gradle daemon for example that would leave the files around much |
| // longer than we want. |
| File tempFile1 = null; |
| File tempFile2 = null; |
| try { |
| FileUtils.mkdirs(tmpDir); |
| File generatedTestManifest = |
| manifestProviders.isEmpty() && testManifestFile == null |
| ? outManifest |
| : (tempFile1 = File.createTempFile("manifestMerger", ".xml", tmpDir)); |
| |
| // we are generating the manifest and if there is an existing one, |
| // it will be overlaid with the generated one |
| logger.verbose("Generating in %1$s", generatedTestManifest.getAbsolutePath()); |
| generateTestManifest( |
| testApplicationId, |
| minSdkVersion, |
| targetSdkVersion.equals("-1") ? null : targetSdkVersion, |
| testedApplicationId, |
| instrumentationRunner, |
| handleProfiling, |
| functionalTest, |
| generatedTestManifest); |
| |
| if (testManifestFile != null && testManifestFile.exists()) { |
| ManifestMerger2.Invoker invoker = |
| ManifestMerger2.newMerger( |
| testManifestFile, |
| logger, |
| ManifestMerger2.MergeType.APPLICATION) |
| .setPlaceHolderValues(manifestPlaceholders) |
| .setPlaceHolderValue( |
| PlaceholderHandler.INSTRUMENTATION_RUNNER, |
| instrumentationRunner) |
| .addLibraryManifest(generatedTestManifest); |
| |
| // we override these properties |
| invoker.setOverride(ManifestSystemProperty.PACKAGE, testApplicationId); |
| invoker.setOverride(ManifestSystemProperty.MIN_SDK_VERSION, minSdkVersion); |
| invoker.setOverride(ManifestSystemProperty.NAME, instrumentationRunner); |
| invoker.setOverride(ManifestSystemProperty.TARGET_PACKAGE, testedApplicationId); |
| invoker.setOverride( |
| ManifestSystemProperty.FUNCTIONAL_TEST, functionalTest.toString()); |
| invoker.setOverride( |
| ManifestSystemProperty.HANDLE_PROFILING, handleProfiling.toString()); |
| if (testLabel != null) { |
| invoker.setOverride(ManifestSystemProperty.LABEL, testLabel); |
| } |
| |
| if (!targetSdkVersion.equals("-1")) { |
| invoker.setOverride( |
| ManifestSystemProperty.TARGET_SDK_VERSION, targetSdkVersion); |
| } |
| |
| MergingReport mergingReport = invoker.merge(); |
| if (manifestProviders.isEmpty()) { |
| handleMergingResult(mergingReport, outManifest, logger); |
| } else { |
| tempFile2 = File.createTempFile("manifestMerger", ".xml", tmpDir); |
| handleMergingResult(mergingReport, tempFile2, logger); |
| generatedTestManifest = tempFile2; |
| } |
| } |
| |
| if (!manifestProviders.isEmpty()) { |
| MergingReport mergingReport = |
| ManifestMerger2.newMerger( |
| generatedTestManifest, |
| logger, |
| ManifestMerger2.MergeType.APPLICATION) |
| .withFeatures( |
| ManifestMerger2.Invoker.Feature.REMOVE_TOOLS_DECLARATIONS) |
| .setOverride(ManifestSystemProperty.PACKAGE, testApplicationId) |
| .addManifestProviders(manifestProviders) |
| .setPlaceHolderValues(manifestPlaceholders) |
| .merge(); |
| |
| handleMergingResult(mergingReport, outManifest, logger); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException("Unable to create the temporary file", e); |
| } catch (ManifestMerger2.MergeFailureException e) { |
| throw new RuntimeException("Manifest merging exception", e); |
| } finally { |
| try { |
| if (tempFile1 != null) { |
| FileUtils.delete(tempFile1); |
| } |
| if (tempFile2 != null) { |
| FileUtils.delete(tempFile2); |
| } |
| } catch (IOException e) { |
| // just log this, so we do not mask the initial exception if there is any |
| logger.error(e, "Unable to clean up the temporary files."); |
| } |
| } |
| } |
| |
| private void handleMergingResult( |
| @NonNull MergingReport mergingReport, @NonNull File outFile, @NonNull ILogger logger) |
| throws IOException { |
| outputMergeBlameContents(mergingReport, getMergeBlameFile().get().getAsFile()); |
| |
| switch (mergingReport.getResult()) { |
| case WARNING: |
| mergingReport.log(logger); |
| // fall through since these are just warnings. |
| case SUCCESS: |
| try { |
| String annotatedDocument = |
| mergingReport.getMergedDocument(MergingReport.MergedManifestKind.BLAME); |
| if (annotatedDocument != null) { |
| logger.verbose(annotatedDocument); |
| } else { |
| logger.verbose("No blaming records from manifest merger"); |
| } |
| } catch (Exception e) { |
| logger.error(e, "cannot print resulting xml"); |
| } |
| String finalMergedDocument = |
| mergingReport.getMergedDocument(MergingReport.MergedManifestKind.MERGED); |
| if (finalMergedDocument == null) { |
| throw new RuntimeException("No result from manifest merger"); |
| } |
| try { |
| Files.asCharSink(outFile, Charsets.UTF_8).write(finalMergedDocument); |
| } catch (IOException e) { |
| logger.error(e, "Cannot write resulting xml"); |
| throw new RuntimeException(e); |
| } |
| logger.verbose("Merged manifest saved to " + outFile); |
| break; |
| case ERROR: |
| mergingReport.log(logger); |
| 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); |
| } |
| } |
| |
| @Nullable |
| @Override |
| public File getAaptFriendlyManifestOutputFile() { |
| return null; |
| } |
| |
| @Nullable |
| @InputFile |
| @Optional |
| public File getTestManifestFile() { |
| return testManifestFile; |
| } |
| |
| public void setTestManifestFile(@Nullable File testManifestFile) { |
| this.testManifestFile = testManifestFile; |
| } |
| |
| public File getTmpDir() { |
| return tmpDir; |
| } |
| |
| public void setTmpDir(File tmpDir) { |
| this.tmpDir = tmpDir; |
| } |
| |
| @Input |
| public String getTestApplicationId() { |
| return testApplicationId.get(); |
| } |
| @Input |
| @Optional |
| public String getTestedApplicationId() { |
| return testedApplicationId.get(); |
| } |
| |
| @Input |
| public String getMinSdkVersion() { |
| return minSdkVersion.get(); |
| } |
| @Input |
| public String getTargetSdkVersion() { |
| return targetSdkVersion.get(); |
| } |
| |
| @Input |
| public String getInstrumentationRunner() { |
| return instrumentationRunner.get(); |
| } |
| |
| @Input |
| public Boolean getHandleProfiling() { |
| return handleProfiling.get(); |
| } |
| |
| @Input |
| public Boolean getFunctionalTest() { |
| return functionalTest.get(); |
| } |
| |
| @Input |
| @Optional |
| public String getTestLabel() { |
| return testLabel.get(); |
| } |
| |
| @Input |
| public Map<String, Object> getPlaceholdersValues() { |
| return placeholdersValues.get(); |
| } |
| |
| @InputFiles |
| @Optional |
| public FileCollection getTestTargetMetadata() { |
| return testTargetMetadata; |
| } |
| |
| /** |
| * Compute the final list of providers based on the manifest file collection. |
| * @return the list of providers. |
| */ |
| public List<ManifestProvider> computeProviders() { |
| final Set<ResolvedArtifactResult> artifacts = manifests.getArtifacts(); |
| List<ManifestProvider> providers = Lists.newArrayListWithCapacity(artifacts.size()); |
| |
| for (ResolvedArtifactResult artifact : artifacts) { |
| providers.add( |
| new ProcessApplicationManifest.CreationAction.ManifestProviderImpl( |
| artifact.getFile(), |
| ProcessApplicationManifest.getArtifactName(artifact))); |
| } |
| |
| return providers; |
| } |
| |
| @InputFiles |
| public FileCollection getManifests() { |
| return manifests.getArtifactFiles(); |
| } |
| |
| public static class CreationAction extends VariantTaskCreationAction<ProcessTestManifest> { |
| |
| @NonNull private final FileCollection testTargetMetadata; |
| |
| public CreationAction( |
| @NonNull VariantScope scope, @NonNull FileCollection testTargetMetadata) { |
| super(scope); |
| this.testTargetMetadata = testTargetMetadata; |
| } |
| |
| @NonNull |
| @Override |
| public String getName() { |
| return getVariantScope().getTaskName("process", "Manifest"); |
| } |
| |
| @NonNull |
| @Override |
| public Class<ProcessTestManifest> getType() { |
| return ProcessTestManifest.class; |
| } |
| |
| @Override |
| public void preConfigure(@NonNull String taskName) { |
| super.preConfigure(taskName); |
| getVariantScope() |
| .getArtifacts() |
| .republish( |
| InternalArtifactType.MERGED_MANIFESTS, |
| InternalArtifactType.MANIFEST_METADATA); |
| } |
| |
| @Override |
| public void handleProvider( |
| @NonNull TaskProvider<? extends ProcessTestManifest> taskProvider) { |
| super.handleProvider(taskProvider); |
| getVariantScope().getTaskContainer().setProcessManifestTask(taskProvider); |
| |
| BuildArtifactsHolder artifacts = getVariantScope().getArtifacts(); |
| artifacts.producesDir( |
| InternalArtifactType.MERGED_MANIFESTS, |
| BuildArtifactsHolder.OperationType.INITIAL, |
| taskProvider, |
| ManifestProcessorTask::getManifestOutputDirectory, |
| ""); |
| |
| artifacts.producesFile( |
| InternalArtifactType.MANIFEST_MERGE_BLAME_FILE, |
| BuildArtifactsHolder.OperationType.INITIAL, |
| taskProvider, |
| ProcessTestManifest::getMergeBlameFile, |
| "manifest-merger-blame-" |
| + getVariantScope().getVariantConfiguration().getBaseName() |
| + "-report.txt"); |
| } |
| |
| @Override |
| public void configure(@NonNull final ProcessTestManifest task) { |
| super.configure(task); |
| |
| getVariantScope() |
| .getArtifacts() |
| .setTaskInputToFinalProduct( |
| InternalArtifactType.CHECK_MANIFEST_RESULT, |
| task.getCheckManifestResult()); |
| |
| final VariantConfiguration<CoreBuildType, CoreProductFlavor, CoreProductFlavor> config = |
| getVariantScope().getVariantConfiguration(); |
| |
| task.setTestManifestFile(config.getMainManifest()); |
| task.outputScope = getVariantScope().getOutputScope(); |
| |
| task.setTmpDir( |
| FileUtils.join( |
| getVariantScope().getGlobalScope().getIntermediatesDir(), |
| "tmp", |
| "manifest", |
| getVariantScope().getDirName())); |
| |
| task.minSdkVersion = |
| TaskInputHelper.memoize(() -> config.getMinSdkVersion().getApiString()); |
| |
| task.targetSdkVersion = |
| TaskInputHelper.memoize(() -> config.getTargetSdkVersion().getApiString()); |
| |
| task.testTargetMetadata = testTargetMetadata; |
| task.testApplicationId = TaskInputHelper.memoize(config::getTestApplicationId); |
| |
| // will only be used if testTargetMetadata is null. |
| task.testedApplicationId = TaskInputHelper.memoize(config::getTestedApplicationId); |
| |
| VariantConfiguration testedConfig = config.getTestedConfig(); |
| task.onlyTestApk = testedConfig != null && testedConfig.getType().isAar(); |
| |
| task.instrumentationRunner = TaskInputHelper.memoize(config::getInstrumentationRunner); |
| task.handleProfiling = TaskInputHelper.memoize(config::getHandleProfiling); |
| task.functionalTest = TaskInputHelper.memoize(config::getFunctionalTest); |
| task.testLabel = TaskInputHelper.memoize(config::getTestLabel); |
| |
| task.manifests = |
| getVariantScope().getArtifactCollection(RUNTIME_CLASSPATH, ALL, MANIFEST); |
| |
| task.placeholdersValues = TaskInputHelper.memoize(config::getManifestPlaceholders); |
| } |
| } |
| } |