Java tutorial
/* * 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 com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.build.api.transform.DirectoryInput; import com.android.build.api.transform.JarInput; import com.android.build.api.transform.QualifiedContent; import com.android.build.api.transform.Status; import com.android.build.api.transform.Transform; 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.gradle.internal.incremental.InstantRunBuildMode; import com.android.build.gradle.internal.scope.InstantRunVariantScope; import com.android.builder.model.OptionalCompilationStep; import com.android.build.gradle.internal.LoggerWrapper; import com.android.build.gradle.internal.incremental.InstantRunVerifierStatus; import com.android.build.gradle.internal.incremental.InstantRunBuildContext; import com.android.build.gradle.internal.incremental.InstantRunVerifier; import com.android.build.gradle.internal.incremental.InstantRunVerifier.ClassBytesJarEntryProvider; import com.android.build.gradle.internal.pipeline.TransformManager; import com.google.wireless.android.sdk.stats.AndroidStudioStats; import com.google.wireless.android.sdk.stats.AndroidStudioStats.GradleBuildProfileSpan.ExecutionType; import com.android.builder.profile.Recorder; import com.android.builder.profile.ThreadRecorder; import com.android.utils.FileUtils; import com.android.utils.ILogger; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.io.Files; import org.gradle.api.logging.Logging; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * No-op transform that verifies that changes between 2 versions of the same class are supported * by the InstantRun implementation. * * To verify class changes, this transform will save all .class files in a private directory * see {@link InstantRunVariantScope#getIncrementalVerifierDir}. * * When new classes are compiled, this transform will receive an incremental notification and will * compare the new versions to the ones saved in the private directory. The result of this * verification process will be encapsulated in an instance of {@link VerificationResult} and stored * in the VariantScope. */ public class InstantRunVerifierTransform extends Transform { private static final ILogger LOGGER = new LoggerWrapper(Logging.getLogger(InstantRunVerifierTransform.class)); private final InstantRunVariantScope variantScope; private final File outputDir; /** * Object that encapsulates the result of the verification process. */ private static class VerificationResult { @Nullable private final InstantRunVerifierStatus changes; @VisibleForTesting VerificationResult(@Nullable InstantRunVerifierStatus changes) { this.changes = changes; } /** * Returns true if the verification process has determined that the new class' version is * compatible with hot swap technology, false otherwise. * @return true if the new class can be hot swapped, false otherwise. */ public boolean isCompatible() { return changes == null; } } public InstantRunVerifierTransform(InstantRunVariantScope variantScope) { this.variantScope = variantScope; this.outputDir = variantScope.getIncrementalVerifierDir(); } @Override public void transform(@NonNull TransformInvocation invocation) throws IOException, TransformException, InterruptedException { if (invocation.getReferencedInputs().isEmpty()) { throw new RuntimeException("Empty list of referenced inputs"); } try { variantScope.getInstantRunBuildContext().startRecording(InstantRunBuildContext.TaskType.VERIFIER); doTransform(invocation.getReferencedInputs(), invocation.isIncremental()); } finally { variantScope.getInstantRunBuildContext().stopRecording(InstantRunBuildContext.TaskType.VERIFIER); } } private void doTransform(@NonNull Collection<TransformInput> inputs, boolean isIncremental) throws IOException, TransformException, InterruptedException { if (!isIncremental && outputDir.exists()) { FileUtils.cleanOutputDir(outputDir); } else { FileUtils.mkdirs(outputDir); } Optional<InstantRunVerifierStatus> currentVerifierStatus = variantScope.getInstantRunBuildContext() .getVerifierResult(); InstantRunVerifierStatus resultSoFar = currentVerifierStatus.orElse(InstantRunVerifierStatus.COMPATIBLE); for (TransformInput transformInput : inputs) { resultSoFar = processFolderInputs(resultSoFar, isIncremental, transformInput); resultSoFar = processJarInputs(resultSoFar, transformInput); } // if we are being asked to produce the RESTART artifacts, there is no need to set the // verifier result, however the transform needed to run to backup the .class files. if (!variantScope.getGlobalScope().isActive(OptionalCompilationStep.RESTART_ONLY)) { variantScope.getInstantRunBuildContext().setVerifierResult(resultSoFar); } } @NonNull private InstantRunVerifierStatus processFolderInputs(@NonNull InstantRunVerifierStatus verificationResult, boolean isIncremental, @NonNull TransformInput transformInput) throws IOException { for (DirectoryInput directoryInput : transformInput.getDirectoryInputs()) { File inputDir = directoryInput.getFile(); if (!isIncremental) { // non incremental mode, we need to traverse the folder for (File file : Files.fileTreeTraverser().breadthFirstTraversal(inputDir)) { if (file.isDirectory()) { continue; } copyFile(file, getOutputFile(inputDir, file, outputDir)); } continue; } for (Map.Entry<File, Status> changedFile : directoryInput.getChangedFiles().entrySet()) { File inputFile = changedFile.getKey(); if (inputFile.isDirectory()) { continue; } File lastIterationFile = getOutputFile(inputDir, inputFile, outputDir); switch (changedFile.getValue()) { case REMOVED: // remove the backup file. if (lastIterationFile.exists() && !lastIterationFile.delete()) { // it's not a big deal if the file cannot be deleted, hopefully // no code is still referencing it, yet we should notify. LOGGER.warning("Cannot delete %1$s file", lastIterationFile); } break; case ADDED: // new file, save it for next iteration. copyFile(inputFile, lastIterationFile); verificationResult = InstantRunVerifierStatus.CLASS_ADDED; break; case CHANGED: // a new version of the class has been compiled, we should compare // it with the one saved during the last iteration on the file, but only // if we have not failed any verification so far. if (verificationResult == InstantRunVerifierStatus.COMPATIBLE) { if (lastIterationFile.exists()) { verificationResult = runVerifier(inputFile.getName(), new InstantRunVerifier.ClassBytesFileProvider(lastIterationFile), new InstantRunVerifier.ClassBytesFileProvider(inputFile)); LOGGER.verbose("%1$s : verifier result : %2$s", inputFile.getName(), verificationResult); } else { verificationResult = InstantRunVerifierStatus.INSTANT_RUN_FAILURE; LOGGER.verbose("Changed file %1$s not found in verifier backup", inputFile.getAbsolutePath()); } } // always copy the new file over to our private backup directory for the // next iteration verification. copyFile(inputFile, lastIterationFile); break; case NOTCHANGED: break; default: throw new IllegalArgumentException("Unhandled DirectoryInput status " + changedFile.getValue()); } } } return verificationResult; } @NonNull private InstantRunVerifierStatus processJarInputs(@NonNull InstantRunVerifierStatus resultSoFar, @NonNull TransformInput transformInput) throws IOException { // can jarInput have colliding names ? for (JarInput jarInput : transformInput.getJarInputs()) { File backupJar = new File(outputDir, jarInput.getName()); switch (jarInput.getStatus()) { case REMOVED: if (backupJar.exists() && !backupJar.delete()) { // it's not a big deal if the file cannot be deleted, hopefully // no code is still referencing it, yet we should notify LOGGER.warning("Cannot delete %1$s file", backupJar); } break; case CHANGED: // get a Map of the back up jar entries indexed by name. if (resultSoFar == InstantRunVerifierStatus.COMPATIBLE) { if (backupJar.exists()) { if (backupJar.isDirectory()) { LOGGER.warning("Unexpected backup folder at %s while processing %s", backupJar.getAbsolutePath(), jarInput.getFile()); try { FileUtils.deleteDirectoryContents(backupJar); } catch (IOException e) { LOGGER.warning( String.format("Cannot delete %s : %s", backupJar.getAbsolutePath(), e)); } if (!backupJar.delete()) { LOGGER.warning("Cannot delete " + backupJar.getAbsolutePath()); } resultSoFar = InstantRunVerifierStatus.INSTANT_RUN_FAILURE; } else { try (JarFile backupJarFile = new JarFile(backupJar)) { try (JarFile jarFile = new JarFile(jarInput.getFile())) { resultSoFar = processChangedJar(backupJarFile, jarFile); } } } } } // fall through ADDED case. case ADDED: if (!jarInput.getFile().exists() || jarInput.getFile().isDirectory()) { LOGGER.warning(String.format( "Please file a bug : VerifierTransform expected a file" + " at:\n %s \nbut the file does not exist or is a directory", jarInput.getFile())); resultSoFar = InstantRunVerifierStatus.INSTANT_RUN_FAILURE; } copyFile(jarInput.getFile(), backupJar); break; case NOTCHANGED: break; default: throw new IllegalArgumentException("Unhandled JarInput status " + jarInput.getStatus()); } } return resultSoFar; } @NonNull private InstantRunVerifierStatus processChangedJar(JarFile backupJar, JarFile newJar) throws IOException { Map<String, JarEntry> backupEntries = new HashMap<>(); Enumeration<JarEntry> backupJarEntries = backupJar.entries(); while (backupJarEntries.hasMoreElements()) { JarEntry jarEntry = backupJarEntries.nextElement(); backupEntries.put(jarEntry.getName(), jarEntry); } // go through the jar file, entry by entry. Enumeration<JarEntry> jarEntries = newJar.entries(); while (jarEntries.hasMoreElements()) { JarEntry jarEntry = jarEntries.nextElement(); if (jarEntry.getName().endsWith(".class")) { JarEntry backupEntry = backupEntries.get(jarEntry.getName()); if (backupEntry != null) { InstantRunVerifierStatus verificationResult = runVerifier( newJar.getName() + ":" + jarEntry.getName(), new ClassBytesJarEntryProvider(backupJar, backupEntry), new ClassBytesJarEntryProvider(newJar, jarEntry)); if (verificationResult != InstantRunVerifierStatus.COMPATIBLE) { return verificationResult; } } } } return InstantRunVerifierStatus.COMPATIBLE; } @VisibleForTesting @NonNull protected InstantRunVerifierStatus runVerifier(String name, @NonNull final InstantRunVerifier.ClassBytesProvider originalClass, @NonNull final InstantRunVerifier.ClassBytesProvider updatedClass) throws IOException { if (!name.endsWith(SdkConstants.DOT_CLASS)) { return InstantRunVerifierStatus.COMPATIBLE; } InstantRunVerifierStatus status = ThreadRecorder.get().record(ExecutionType.TASK_FILE_VERIFICATION, variantScope.getGlobalScope().getProject().getPath(), variantScope.getFullVariantName(), new Recorder.Block<InstantRunVerifierStatus>() { @Override @NonNull public InstantRunVerifierStatus call() throws Exception { return InstantRunVerifier.run(originalClass, updatedClass); } }); // TODO: re-add approximation of target. if (status == null) { LOGGER.warning("No verifier result provided for %1$s", name); return InstantRunVerifierStatus.NOT_RUN; } return status; } @VisibleForTesting protected void copyFile(File inputFile, File outputFile) { try { Files.createParentDirs(outputFile); Files.copy(inputFile, outputFile); } catch (IOException e) { LOGGER.error(e, "Cannot copy %1$s to back up folder, build will continue but " + "next time this file is modified will result in a cold swap.", inputFile.getAbsolutePath()); } } @NonNull @Override public String getName() { return "instantRunVerifier"; } @NonNull @Override public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; } @NonNull @Override public Set<QualifiedContent.ContentType> getOutputTypes() { return TransformManager.CONTENT_CLASS; } @NonNull @Override public Set<QualifiedContent.Scope> getScopes() { return ImmutableSet.of(); } @NonNull @Override public Set<QualifiedContent.Scope> getReferencedScopes() { return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.SUB_PROJECTS); } @NonNull @Override public Collection<File> getSecondaryDirectoryOutputs() { return ImmutableList.of(outputDir); } @Override public boolean isIncremental() { return true; } /** * Return the expected output {@link File} for an input file located in the transform input * directory. The output file will have a similar relative path than the input file (to the * input dir) inside the output directory. * * @param inputDir the input directory containing the input file * @param inputFile the input file within the input directory * @param outputDir the output directory * @return the output file within the output directory with the right relative path. */ protected static File getOutputFile(File inputDir, File inputFile, File outputDir) { String relativePath = FileUtils.relativePossiblyNonExistingPath(inputFile, inputDir); return new File(outputDir, relativePath); } }