| /* |
| * 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.android.SdkConstants.DOT_JAR; |
| import static com.android.builder.model.AndroidProject.FD_OUTPUTS; |
| import static com.android.utils.FileUtils.mkdirs; |
| import static com.android.utils.FileUtils.renameTo; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.build.api.transform.DirectoryInput; |
| import com.android.build.api.transform.Format; |
| import com.android.build.api.transform.JarInput; |
| import com.android.build.api.transform.QualifiedContent; |
| import com.android.build.api.transform.QualifiedContent.ContentType; |
| import com.android.build.api.transform.QualifiedContent.DefaultContentType; |
| import com.android.build.api.transform.QualifiedContent.Scope; |
| import com.android.build.api.transform.TransformException; |
| import com.android.build.api.transform.TransformInput; |
| import com.android.build.api.transform.TransformInvocation; |
| import com.android.build.api.transform.TransformOutputProvider; |
| import com.android.build.gradle.internal.pipeline.TransformManager; |
| import com.android.build.gradle.internal.scope.GlobalScope; |
| import com.android.build.gradle.internal.scope.VariantScope; |
| import com.android.build.gradle.internal.variant.LibraryVariantData; |
| import com.android.build.gradle.tasks.SimpleWorkQueue; |
| import com.android.builder.tasks.Job; |
| import com.android.builder.tasks.JobContext; |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| |
| import java.io.File; |
| import java.io.FilenameFilter; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Set; |
| |
| import proguard.ClassPath; |
| |
| /** |
| * ProGuard support as a transform |
| */ |
| public class ProGuardTransform extends BaseProguardAction { |
| |
| private final VariantScope variantScope; |
| private final boolean asJar; |
| |
| private final boolean isLibrary; |
| private final boolean isTest; |
| |
| private final File proguardOut; |
| |
| private final File printMapping; |
| private final File dump; |
| private final File printSeeds; |
| private final File printUsage; |
| private final ImmutableList<File> secondaryFileOutputs; |
| |
| private File testedMappingFile = null; |
| private org.gradle.api.artifacts.Configuration testMappingConfiguration = null; |
| |
| public ProGuardTransform( |
| @NonNull VariantScope variantScope, |
| boolean asJar) { |
| this.variantScope = variantScope; |
| |
| // TODO: Allow asJar to be true, once we make sure input jars have unique file names. |
| // There cannot be duplicate classes.jar inputs for example. This confuses ProGuard in |
| // "directory output" mode. |
| this.asJar = true; |
| |
| isLibrary = variantScope.getVariantData() instanceof LibraryVariantData; |
| isTest = variantScope.getTestedVariantData() != null; |
| |
| GlobalScope globalScope = variantScope.getGlobalScope(); |
| proguardOut = new File(Joiner.on(File.separatorChar).join( |
| String.valueOf(globalScope.getBuildDir()), |
| FD_OUTPUTS, |
| "mapping", |
| variantScope.getVariantConfiguration().getDirName())); |
| |
| printMapping = new File(proguardOut, "mapping.txt"); |
| dump = new File(proguardOut, "dump.txt"); |
| printSeeds = new File(proguardOut, "seeds.txt"); |
| printUsage = new File(proguardOut, "usage.txt"); |
| secondaryFileOutputs = ImmutableList.of(printMapping, dump, printSeeds, printUsage); |
| } |
| |
| @Nullable |
| public File getMappingFile() { |
| return printMapping; |
| } |
| |
| public void applyTestedMapping(@Nullable File testedMappingFile) { |
| this.testedMappingFile = testedMappingFile; |
| } |
| |
| public void applyTestedMapping( |
| @Nullable org.gradle.api.artifacts.Configuration testMappingConfiguration) { |
| this.testMappingConfiguration = testMappingConfiguration; |
| } |
| |
| @NonNull |
| @Override |
| public String getName() { |
| return "proguard"; |
| } |
| |
| @NonNull |
| @Override |
| public Set<ContentType> getInputTypes() { |
| return TransformManager.CONTENT_JARS; |
| } |
| |
| @NonNull |
| @Override |
| public Set<Scope> getScopes() { |
| if (isLibrary) { |
| return Sets.immutableEnumSet(Scope.PROJECT, Scope.PROJECT_LOCAL_DEPS); |
| } |
| |
| return TransformManager.SCOPE_FULL_PROJECT; |
| } |
| |
| @NonNull |
| @Override |
| public Set<Scope> getReferencedScopes() { |
| Set<Scope> set = Sets.newLinkedHashSetWithExpectedSize(5); |
| if (isLibrary) { |
| set.add(Scope.SUB_PROJECTS); |
| set.add(Scope.SUB_PROJECTS_LOCAL_DEPS); |
| set.add(Scope.EXTERNAL_LIBRARIES); |
| } |
| |
| if (isTest) { |
| set.add(Scope.TESTED_CODE); |
| } |
| |
| set.add(Scope.PROVIDED_ONLY); |
| |
| return Sets.immutableEnumSet(set); |
| } |
| |
| @NonNull |
| @Override |
| public Collection<File> getSecondaryFileInputs() { |
| final List<File> files = Lists.newArrayList(); |
| |
| // the mapping file. |
| File testedMappingFile = computeMappingFile(); |
| if (testedMappingFile != null) { |
| files.add(testedMappingFile); |
| } |
| |
| // the config files |
| files.addAll(getAllConfigurationFiles()); |
| |
| return files; |
| } |
| |
| @NonNull |
| @Override |
| public Collection<File> getSecondaryFileOutputs() { |
| return secondaryFileOutputs; |
| } |
| |
| @Override |
| public boolean isIncremental() { |
| return false; |
| } |
| |
| @Override |
| public void transform(final TransformInvocation invocation) throws TransformException { |
| // only run one minification at a time (across projects) |
| final Job<Void> job = new Job<Void>(getName(), |
| new com.android.builder.tasks.Task<Void>() { |
| @Override |
| public void run(@NonNull Job<Void> job, |
| @NonNull JobContext<Void> context) throws IOException { |
| doMinification( |
| invocation.getInputs(), |
| invocation.getReferencedInputs(), |
| invocation.getOutputProvider()); |
| } |
| }); |
| try { |
| SimpleWorkQueue.push(job); |
| |
| // wait for the task completion. |
| if (!job.awaitRethrowExceptions()) { |
| throw new RuntimeException("Job failed, see logs for details"); |
| } |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void doMinification( |
| @NonNull Collection<TransformInput> inputs, |
| @NonNull Collection<TransformInput> referencedInputs, |
| @Nullable TransformOutputProvider output) throws IOException { |
| checkNotNull(output, "Missing output object for transform " + getName()); |
| Set<ContentType> outputTypes = getOutputTypes(); |
| Set<Scope> scopes = getScopes(); |
| File outFile = output.getContentLocation("main", outputTypes, scopes, |
| asJar ? Format.JAR : Format.DIRECTORY); |
| if (asJar) { |
| mkdirs(outFile.getParentFile()); |
| } else { |
| mkdirs(outFile); |
| } |
| |
| try { |
| GlobalScope globalScope = variantScope.getGlobalScope(); |
| |
| if (isLibrary) { |
| keep("class **.R"); |
| keep("class **.R$*"); |
| } |
| |
| // set the mapping file if there is one. |
| File testedMappingFile = computeMappingFile(); |
| if (testedMappingFile != null) { |
| applyMapping(testedMappingFile); |
| } |
| |
| // --- InJars / LibraryJars --- |
| addInputsToConfiguration(inputs, false); |
| addInputsToConfiguration(referencedInputs, true); |
| |
| // libraryJars: the runtime jars, with all optional libraries. |
| for (File runtimeJar : globalScope.getAndroidBuilder().getBootClasspath(true)) { |
| libraryJar(runtimeJar); |
| } |
| |
| // --- Out files --- |
| outJar(outFile); |
| |
| // proguard doesn't verify that the seed/mapping/usage folders exist and will fail |
| // if they don't so create them. |
| mkdirs(proguardOut); |
| |
| for (File configFile : getAllConfigurationFiles()) { |
| applyConfigurationFile(configFile); |
| } |
| |
| configuration.printMapping = printMapping; |
| configuration.dump = dump; |
| configuration.printSeeds = printSeeds; |
| configuration.printUsage = printUsage; |
| |
| forceprocessing(); |
| runProguard(); |
| |
| if (!asJar) { |
| // if the output of proguard is a folder (rather than a single jar), the |
| // dependencies will be written as jar in the same folder output. |
| // So we move it to their normal location as new jar outputs. |
| File[] jars = outFile.listFiles(new FilenameFilter() { |
| @Override |
| public boolean accept(File file, String name) { |
| return name.endsWith(DOT_JAR); |
| } |
| }); |
| if (jars != null) { |
| for (File jarFile : jars) { |
| String jarFileName = jarFile.getName(); |
| File to = output.getContentLocation( |
| jarFileName.substring(0, jarFileName.length() - DOT_JAR.length()), |
| outputTypes, scopes, Format.JAR); |
| mkdirs(to.getParentFile()); |
| renameTo(jarFile, to); |
| } |
| } |
| } |
| |
| } catch (Exception e) { |
| if (e instanceof IOException) { |
| throw (IOException) e; |
| } |
| |
| throw new IOException(e); |
| } |
| } |
| |
| private void addInputsToConfiguration( |
| @NonNull Collection<TransformInput> inputs, |
| boolean referencedOnly) { |
| ClassPath classPath; |
| List<String> baseFilter; |
| |
| if (referencedOnly) { |
| classPath = configuration.libraryJars; |
| baseFilter = JAR_FILTER; |
| } else { |
| classPath = configuration.programJars; |
| baseFilter = null; |
| } |
| |
| for (TransformInput transformInput : inputs) { |
| for (JarInput jarInput : transformInput.getJarInputs()) { |
| handleQualifiedContent(classPath, jarInput, baseFilter); |
| } |
| |
| for (DirectoryInput directoryInput : transformInput.getDirectoryInputs()) { |
| handleQualifiedContent(classPath, directoryInput, baseFilter); |
| } |
| } |
| } |
| |
| private static void handleQualifiedContent( |
| @NonNull ClassPath classPath, |
| @NonNull QualifiedContent content, |
| @Nullable List<String> baseFilter) { |
| List<String> filter = baseFilter; |
| |
| if (!content.getContentTypes().contains(DefaultContentType.CLASSES)) { |
| // if the content is not meant to contain classes, we ignore them |
| // in case they are present. |
| ImmutableList.Builder<String> builder = ImmutableList.builder(); |
| if (filter != null) { |
| builder.addAll(filter); |
| } |
| builder.add("!**/*.class"); |
| filter = builder.build(); |
| } else if (!content.getContentTypes().contains(DefaultContentType.RESOURCES)) { |
| // if the content is not meant to contain resources, we ignore them |
| // in case they are present (by accepting only classes.) |
| filter = ImmutableList.of("**/*.class"); |
| } |
| |
| inputJar(classPath, content.getFile(), filter); |
| } |
| |
| @Nullable |
| private File computeMappingFile() { |
| if (testedMappingFile != null && testedMappingFile.isFile()) { |
| return testedMappingFile; |
| } else if (testMappingConfiguration != null && testMappingConfiguration.getSingleFile().isFile()) { |
| return testMappingConfiguration.getSingleFile(); |
| } |
| |
| return null; |
| } |
| } |