blob: c7c6987b48a82ecb4f555e4fb449587864b90e5e [file] [log] [blame]
/*
* Copyright (C) 2015 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.transforms;
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.api.transform.Format;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.QualifiedContent.ContentType;
import com.android.build.api.transform.QualifiedContent.Scope;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.dsl.PackagingOptions;
import com.android.build.gradle.internal.packaging.PackagingFileAction;
import com.android.build.gradle.internal.packaging.ParsedPackagingOptions;
import com.android.build.gradle.internal.pipeline.ExtendedContentType;
import com.android.build.gradle.internal.pipeline.IncrementalFileMergerTransformUtils;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.builder.files.FileCacheByPath;
import com.android.builder.merge.DelegateIncrementalFileMergerOutput;
import com.android.builder.merge.FilterIncrementalFileMergerInput;
import com.android.builder.merge.IncrementalFileMerger;
import com.android.builder.merge.IncrementalFileMergerInput;
import com.android.builder.merge.IncrementalFileMergerOutput;
import com.android.builder.merge.IncrementalFileMergerOutputs;
import com.android.builder.merge.IncrementalFileMergerState;
import com.android.builder.merge.MergeOutputWriters;
import com.android.builder.merge.RenameIncrementalFileMergerInput;
import com.android.builder.merge.StreamMergeAlgorithm;
import com.android.builder.merge.StreamMergeAlgorithms;
import com.android.utils.FileUtils;
import com.android.utils.ImmutableCollectors;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Transform to merge all the Java resources.
*
* Based on the value of {@link #getInputTypes()} this will either process native libraries
* or java resources. While native libraries inside jars are technically java resources, they
* must be handled separately.
*/
public class MergeJavaResourcesTransform extends Transform {
private static final Pattern JAR_ABI_PATTERN = Pattern.compile("lib/([^/]+)/[^/]+");
private static final Pattern ABI_FILENAME_PATTERN = Pattern.compile(".*\\.so");
@NonNull private final PackagingOptions packagingOptions;
@NonNull
private final String name;
@NonNull private final Set<? super Scope> mergeScopes;
@NonNull
private final Set<ContentType> mergedType;
@NonNull
private final File intermediateDir;
private final Predicate<String> acceptedPathsPredicate;
@NonNull private final File cacheDir;
public MergeJavaResourcesTransform(
@NonNull PackagingOptions packagingOptions,
@NonNull Set<? super Scope> mergeScopes,
@NonNull ContentType mergedType,
@NonNull String name,
@NonNull VariantScope variantScope) {
this.packagingOptions = packagingOptions;
this.name = name;
this.mergeScopes = ImmutableSet.copyOf(mergeScopes);
this.mergedType = ImmutableSet.of(mergedType);
this.intermediateDir = variantScope.getIncrementalDir(
variantScope.getFullVariantName() + "-" + name);
cacheDir = new File(intermediateDir, "zip-cache");
if (mergedType == QualifiedContent.DefaultContentType.RESOURCES) {
acceptedPathsPredicate =
path -> !path.endsWith(SdkConstants.DOT_CLASS)
&& !path.endsWith(SdkConstants.DOT_NATIVE_LIBS);
} else if (mergedType == ExtendedContentType.NATIVE_LIBS) {
acceptedPathsPredicate =
path -> {
Matcher m = JAR_ABI_PATTERN.matcher(path);
// if the ABI is accepted, check the 3rd segment
if (m.matches()) {
// remove the beginning of the path (lib/<abi>/)
String filename = path.substring(5 + m.group(1).length());
// and check the filename
return ABI_FILENAME_PATTERN.matcher(filename).matches() ||
SdkConstants.FN_GDBSERVER.equals(filename) ||
SdkConstants.FN_GDB_SETUP.equals(filename);
}
return false;
};
} else {
throw new UnsupportedOperationException(
"mergedType param must be RESOURCES or NATIVE_LIBS");
}
}
@NonNull
@Override
public String getName() {
return name;
}
@NonNull
@Override
public Set<ContentType> getInputTypes() {
return mergedType;
}
@NonNull
@Override
public Set<? super Scope> getScopes() {
return mergeScopes;
}
@NonNull
@Override
public Collection<File> getSecondaryDirectoryOutputs() {
return ImmutableList.of(cacheDir);
}
@NonNull
@Override
public Map<String, Object> getParameterInputs() {
return ImmutableMap.of(
"exclude", packagingOptions.getExcludes(),
"pickFirst", packagingOptions.getPickFirsts(),
"merge", packagingOptions.getMerges());
}
@Override
public boolean isIncremental() {
return true;
}
/**
* Obtains the file where incremental state is saved.
*
* @return the file, may not exist
*/
@NonNull
private File incrementalStateFile() {
return new File(intermediateDir, "merge-state");
}
/**
* Loads the incremental state.
*
* @return {@code null} if the state is not defined
* @throws IOException failed to load the incremental state
*/
@Nullable
private IncrementalFileMergerState loadMergeState() throws IOException {
File incrementalFile = incrementalStateFile();
if (!incrementalFile.isFile()) {
return null;
}
try (ObjectInputStream i = new ObjectInputStream(new FileInputStream(incrementalFile))) {
return (IncrementalFileMergerState) i.readObject();
} catch (ClassNotFoundException e) {
throw new IOException(e);
}
}
/**
* Save the incremental merge state.
*
* @param state the state
* @throws IOException failed to save the state
*/
private void saveMergeState(@NonNull IncrementalFileMergerState state) throws IOException {
File incrementalFile = incrementalStateFile();
FileUtils.mkdirs(incrementalFile.getParentFile());
try (ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(incrementalFile))) {
o.writeObject(state);
}
}
@Override
public void transform(@NonNull TransformInvocation invocation)
throws IOException, TransformException {
FileUtils.mkdirs(cacheDir);
FileCacheByPath zipCache = new FileCacheByPath(cacheDir);
TransformOutputProvider outputProvider = invocation.getOutputProvider();
checkNotNull(outputProvider, "Missing output object for transform " + getName());
ParsedPackagingOptions packagingOptions = new ParsedPackagingOptions(this.packagingOptions);
boolean full = false;
IncrementalFileMergerState state = loadMergeState();
if (state == null || !invocation.isIncremental()) {
/*
* This is a full build.
*/
state = new IncrementalFileMergerState();
outputProvider.deleteAll();
full = true;
}
List<Runnable> cacheUpdates = new ArrayList<>();
Map<IncrementalFileMergerInput, QualifiedContent> contentMap = new HashMap<>();
List<IncrementalFileMergerInput> inputs =
new ArrayList<>(
IncrementalFileMergerTransformUtils.toInput(
invocation,
zipCache,
cacheUpdates,
full,
contentMap));
/*
* In an ideal world, we could just send the inputs to the file merger. However, in the
* real world we live in, things are more complicated :)
*
* We need to:
*
* 1. We need to bring inputs that refer to the project scope before the other inputs.
* 2. Prefix libraries that come from directories with "lib/".
* 3. Filter all inputs to remove anything not accepted by acceptedPathsPredicate neither
* by packagingOptions.
*/
// Sort inputs to move project scopes to the start.
inputs.sort((i0, i1) -> {
int v0 = contentMap.get(i0).getScopes().contains(Scope.PROJECT)? 0 : 1;
int v1 = contentMap.get(i1).getScopes().contains(Scope.PROJECT)? 0 : 1;
return v0 - v1;
});
// Prefix libraries with "lib/" if we're doing libraries.
assert mergedType.size() == 1;
ContentType mergedType = this.mergedType.iterator().next();
if (mergedType == ExtendedContentType.NATIVE_LIBS) {
inputs =
inputs.stream()
.map(
i -> {
QualifiedContent qc = contentMap.get(i);
if (qc.getFile().isDirectory()) {
i =
new RenameIncrementalFileMergerInput(
i,
s -> "lib/" + s,
s -> s.substring("lib/".length()));
contentMap.put(i, qc);
}
return i;
})
.collect(Collectors.toList());
}
// Filter inputs.
Predicate<String> inputFilter =
acceptedPathsPredicate.and(
path -> packagingOptions.getAction(path) != PackagingFileAction.EXCLUDE);
inputs = inputs.stream()
.map(i -> {
IncrementalFileMergerInput i2 =
new FilterIncrementalFileMergerInput(i, inputFilter);
contentMap.put(i2, contentMap.get(i));
return i2;
})
.collect(Collectors.toList());
/*
* Create the algorithm used by the merge transform. This algorithm decides on which
* algorithm to delegate to depending on the packaging option of the path. By default it
* requires just one file (no merging).
*/
StreamMergeAlgorithm mergeTransformAlgorithm = StreamMergeAlgorithms.select(path -> {
PackagingFileAction packagingAction = packagingOptions.getAction(path);
switch (packagingAction) {
case EXCLUDE:
// Should have been excluded from the input.
throw new AssertionError();
case PICK_FIRST:
return StreamMergeAlgorithms.pickFirst();
case MERGE:
return StreamMergeAlgorithms.concat();
case NONE:
return StreamMergeAlgorithms.acceptOnlyOne();
default:
throw new AssertionError();
}
});
/*
* Create an output that uses the algorithm. This is not the final output because,
* unfortunately, we still have the complexity of the project scope overriding other scopes
* to solve.
*
* When resources inside a jar file are extracted to a directory, the results may not be
* expected on Windows if the file names end with "." (bug 65337573), or if there is an
* uppercase/lowercase conflict. To work around this issue, we copy these resources to a
* jar file.
*/
IncrementalFileMergerOutput baseOutput;
if (mergedType == QualifiedContent.DefaultContentType.RESOURCES) {
File outputLocation =
outputProvider.getContentLocation(
"resources", getOutputTypes(), getScopes(), Format.JAR);
baseOutput =
IncrementalFileMergerOutputs.fromAlgorithmAndWriter(
mergeTransformAlgorithm, MergeOutputWriters.toZip(outputLocation));
} else {
File outputLocation =
outputProvider.getContentLocation(
"resources", getOutputTypes(), getScopes(), Format.DIRECTORY);
baseOutput =
IncrementalFileMergerOutputs.fromAlgorithmAndWriter(
mergeTransformAlgorithm,
MergeOutputWriters.toDirectory(outputLocation));
}
/*
* We need a custom output to handle the case in which the same path appears in multiple
* inputs and the action is NONE, but only one input is actually PROJECT. In this specific
* case we will ignore all other inputs.
*/
Set<IncrementalFileMergerInput> projectInputs =
contentMap.keySet().stream()
.filter(i -> contentMap.get(i).getScopes().contains(Scope.PROJECT))
.collect(Collectors.toSet());
IncrementalFileMergerOutput output = new DelegateIncrementalFileMergerOutput(baseOutput) {
@Override
public void create(
@NonNull String path,
@NonNull List<IncrementalFileMergerInput> inputs) {
super.create(path, filter(path, inputs));
}
@Override
public void update(
@NonNull String path,
@NonNull List<String> prevInputNames,
@NonNull List<IncrementalFileMergerInput> inputs) {
super.update(path, prevInputNames, filter(path, inputs));
}
@Override
public void remove(@NonNull String path) {
super.remove(path);
}
@NonNull
private ImmutableList<IncrementalFileMergerInput> filter(
@NonNull String path,
@NonNull List<IncrementalFileMergerInput> inputs) {
PackagingFileAction packagingAction = packagingOptions.getAction(path);
if (packagingAction == PackagingFileAction.NONE
&& inputs.stream().anyMatch(projectInputs::contains)) {
inputs = inputs.stream()
.filter(projectInputs::contains)
.collect(ImmutableCollectors.toImmutableList());
}
return ImmutableList.copyOf(inputs);
}
};
state = IncrementalFileMerger.merge(ImmutableList.copyOf(inputs), output, state);
saveMergeState(state);
cacheUpdates.forEach(Runnable::run);
}
}