|
| 1 | +/* |
| 2 | + Copyright (C) 2020 Electronic Arts Inc. All rights reserved. |
| 3 | +
|
| 4 | + Redistribution and use in source and binary forms, with or without |
| 5 | + modification, are permitted provided that the following conditions |
| 6 | + are met: |
| 7 | +
|
| 8 | + 1. Redistributions of source code must retain the above copyright |
| 9 | + notice, this list of conditions and the following disclaimer. |
| 10 | + 2. Redistributions in binary form must reproduce the above copyright |
| 11 | + notice, this list of conditions and the following disclaimer in the |
| 12 | + documentation and/or other materials provided with the distribution. |
| 13 | + 3. Neither the name of Electronic Arts, Inc. ("EA") nor the names of |
| 14 | + its contributors may be used to endorse or promote products derived |
| 15 | + from this software without specific prior written permission. |
| 16 | +
|
| 17 | + THIS SOFTWARE IS PROVIDED BY ELECTRONIC ARTS AND ITS CONTRIBUTORS "AS IS" AND ANY |
| 18 | + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 19 | + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 20 | + DISCLAIMED. IN NO EVENT SHALL ELECTRONIC ARTS OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| 21 | + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| 22 | + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| 23 | + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| 24 | + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 25 | + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| 26 | + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 27 | + */ |
| 28 | + |
| 29 | +package com.ea.async.gradle.plugin; |
| 30 | + |
| 31 | +import com.ea.async.instrumentation.Transformer; |
| 32 | + |
| 33 | +import org.gradle.api.Plugin; |
| 34 | +import org.gradle.api.Project; |
| 35 | +import org.gradle.api.tasks.compile.JavaCompile; |
| 36 | + |
| 37 | +import java.io.File; |
| 38 | +import java.io.FileInputStream; |
| 39 | +import java.io.IOException; |
| 40 | +import java.net.MalformedURLException; |
| 41 | +import java.net.URL; |
| 42 | +import java.net.URLClassLoader; |
| 43 | +import java.nio.file.Files; |
| 44 | +import java.nio.file.Paths; |
| 45 | +import java.util.ArrayList; |
| 46 | +import java.util.List; |
| 47 | +import java.util.Objects; |
| 48 | + |
| 49 | +/** |
| 50 | + * A plugin that allows easily integrating EA Async instrumentation into the Gradle build process. |
| 51 | + */ |
| 52 | +public class AsyncPlugin implements Plugin<Project> |
| 53 | +{ |
| 54 | + |
| 55 | + /** |
| 56 | + * Applies the plugin to the project. Prepares Java compilation tasks to be followed up on by instrumentation. |
| 57 | + * @param project the Gradle project to which the plugin was applied |
| 58 | + */ |
| 59 | + @Override |
| 60 | + public void apply(final Project project) |
| 61 | + { |
| 62 | + project.getTasks().withType(JavaCompile.class, task -> |
| 63 | + { |
| 64 | + // This will be called for every JavaCompile task, whether already existing or added dynamically later |
| 65 | + task.doLast("EA Async instrumentation", t -> instrumentCompileResults(task)); |
| 66 | + }); |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Rewrites compiled classes output by a Java compilation task. |
| 71 | + * Called for each compilation task after it finishes. |
| 72 | + * @param task the Java compilation task whose output to instrument |
| 73 | + */ |
| 74 | + private void instrumentCompileResults(final JavaCompile task) |
| 75 | + { |
| 76 | + // Set up the Transformer to explode on failure |
| 77 | + final Transformer asyncTransformer = new Transformer(); |
| 78 | + asyncTransformer.setErrorListener(err -> |
| 79 | + { |
| 80 | + throw new RuntimeException("Failed to instrument the output of " + task.toString() + ": " + err); |
| 81 | + }); |
| 82 | + // Create a classloader that can see the same CompletionStage subclasses as the compiler |
| 83 | + final List<URL> classpathUrls = new ArrayList<>(); |
| 84 | + try |
| 85 | + { |
| 86 | + for (File f : task.getClasspath().plus(task.getOutputs().getFiles()).getFiles()) |
| 87 | + { |
| 88 | + classpathUrls.add(f.toURI().toURL()); |
| 89 | + } |
| 90 | + } |
| 91 | + catch (MalformedURLException e) |
| 92 | + { |
| 93 | + throw new RuntimeException("Bad URL when preparing instrumentation of " + task.toString(), e); |
| 94 | + } |
| 95 | + final ClassLoader classLoader = new URLClassLoader(classpathUrls.toArray(new URL[0])); |
| 96 | + // Rewrite the class files in the output directory |
| 97 | + instrumentFile(task.getDestinationDir(), task, asyncTransformer, classLoader); |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Recursively instruments the specified file system entry and its descendants. |
| 102 | + * @param fsEntry a File in the output directory of a task |
| 103 | + * @param task the JavaCompile task whose output is being instrumented |
| 104 | + * @param asyncTransformer the transformer used to instrument the classes |
| 105 | + * @param classLoader the classloader to supply to the transformer |
| 106 | + */ |
| 107 | + private void instrumentFile(final File fsEntry, final JavaCompile task, |
| 108 | + final Transformer asyncTransformer, final ClassLoader classLoader) |
| 109 | + { |
| 110 | + if (fsEntry.isDirectory()) |
| 111 | + { |
| 112 | + // Recurse into subdirectories and files |
| 113 | + for (File subentry : Objects.requireNonNull(fsEntry.listFiles())) { |
| 114 | + instrumentFile(subentry, task, asyncTransformer, classLoader); |
| 115 | + } |
| 116 | + } |
| 117 | + else if (fsEntry.isFile() && fsEntry.getName().endsWith(".class")) |
| 118 | + { |
| 119 | + // Instrument a class |
| 120 | + byte[] instrumentedClass; |
| 121 | + try (FileInputStream fis = new FileInputStream(fsEntry)) |
| 122 | + { |
| 123 | + instrumentedClass = asyncTransformer.instrument(classLoader, fis); |
| 124 | + } |
| 125 | + catch (IOException e) |
| 126 | + { |
| 127 | + throw new RuntimeException("Failed to instrument '" + fsEntry.getPath() + "' from " + task.toString(), e); |
| 128 | + } |
| 129 | + if (instrumentedClass != null) |
| 130 | + { |
| 131 | + // Transformer#instrument returns null if no changes were needed |
| 132 | + try |
| 133 | + { |
| 134 | + Files.write(Paths.get(fsEntry.getAbsolutePath()), instrumentedClass); |
| 135 | + } |
| 136 | + catch (IOException e) |
| 137 | + { |
| 138 | + throw new RuntimeException("Failed to write '" + fsEntry.getPath() + "'", e); |
| 139 | + } |
| 140 | + } |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | +} |
0 commit comments