diff --git a/retrolambda-maven-plugin/src/main/java/net/orfjackal/retrolambda/maven/ProcessClassesMojo.java b/retrolambda-maven-plugin/src/main/java/net/orfjackal/retrolambda/maven/ProcessClassesMojo.java index a945a78e..483efeb6 100644 --- a/retrolambda-maven-plugin/src/main/java/net/orfjackal/retrolambda/maven/ProcessClassesMojo.java +++ b/retrolambda-maven-plugin/src/main/java/net/orfjackal/retrolambda/maven/ProcessClassesMojo.java @@ -6,14 +6,18 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; +import java.io.*; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.*; import org.apache.maven.plugins.annotations.*; import org.apache.maven.project.MavenProject; import org.apache.maven.toolchain.*; -import java.io.*; -import java.util.*; import static org.twdata.maven.mojoexecutor.MojoExecutor.*; @@ -63,6 +67,16 @@ abstract class ProcessClassesMojo extends AbstractMojo { */ @Parameter(defaultValue = "1.7", property = "retrolambdaTarget", required = true) public String target; + + /** + * Should we start new process or perform the retrolambdafication in the + * same VM as Maven runs in (which has to be 1.8 then)? If the VM is + * forked, it uses -javaagent argument to intercept class definitions. + * When we run in the same process, we hook into the class generation + * by internal "lambda dumping" API. + */ + @Parameter(defaultValue = "false") + public boolean fork; protected abstract File getInputDir(); @@ -79,7 +93,11 @@ public void execute() throws MojoExecutionException { retrieveRetrolambdaJar(version); getLog().info("Processing classes with Retrolambda"); - processClasses(); + if (fork) { + processClassesWithAgent(); + } else { + processClasses(); + } } String getJavaCommand() { @@ -130,7 +148,7 @@ private void retrieveRetrolambdaJar(String version) throws MojoExecutionExceptio executionEnvironment(project, session, pluginManager)); } - private void processClasses() throws MojoExecutionException { + private void processClassesWithAgent() throws MojoExecutionException { String retrolambdaJar = getRetrolambdaJarPath(); executeMojo( plugin(groupId("org.apache.maven.plugins"), @@ -155,6 +173,33 @@ private void processClasses() throws MojoExecutionException { element("arg", attribute("value", retrolambdaJar))))), executionEnvironment(project, session, pluginManager)); } + + private void processClasses() throws MojoExecutionException { + String retrolambdaJar = getRetrolambdaJarPath(); + File jar = new File(retrolambdaJar); + + try { + StringBuilder sb = new StringBuilder(); + sb.append(getInputDir()); + for (Artifact a : project.getArtifacts()) { + if (a.getFile() != null) { + sb.append(File.pathSeparator); + sb.append(a.getFile()); + } + } + + URLClassLoader url = new URLClassLoader(new URL[] { jar.toURI().toURL() }); + Class mainClass = Class.forName("net.orfjackal.retrolambda.Main", true, url); + System.setProperty("retrolambda.bytecodeVersion", "" + targetBytecodeVersions.get(target)); + System.setProperty("retrolambda.inputDir", getInputDir().getAbsolutePath()); + System.setProperty("retrolambda.outputDir", getOutputDir().getAbsolutePath()); + System.setProperty("retrolambda.classpath", sb.toString()); + Method main = mainClass.getMethod("main", String[].class); + main.invoke(null, (Object) new String[0]); + } catch (Exception ex) { + throw new MojoExecutionException("Cannot initialize classloader for " + retrolambdaJar, ex); + } + } private String getRetrolambdaJarPath() { return getRetrolambdaJarDir() + "/" + getRetrolambdaJarName(); diff --git a/retrolambda/src/main/java/net/orfjackal/retrolambda/Config.java b/retrolambda/src/main/java/net/orfjackal/retrolambda/Config.java index 7d074475..502716da 100644 --- a/retrolambda/src/main/java/net/orfjackal/retrolambda/Config.java +++ b/retrolambda/src/main/java/net/orfjackal/retrolambda/Config.java @@ -43,7 +43,7 @@ public Config(Properties p) { } public boolean isFullyConfigured() { - return hasAllRequiredProperties() && PreMain.isAgentLoaded(); + return hasAllRequiredProperties(); } private boolean hasAllRequiredProperties() { diff --git a/retrolambda/src/main/java/net/orfjackal/retrolambda/LambdaClassDumper.java b/retrolambda/src/main/java/net/orfjackal/retrolambda/LambdaClassDumper.java new file mode 100644 index 00000000..0c203dc4 --- /dev/null +++ b/retrolambda/src/main/java/net/orfjackal/retrolambda/LambdaClassDumper.java @@ -0,0 +1,440 @@ +// Copyright © 2013-2014 Esko Luontola +// This software is released under the Apache License 2.0. +// The license text is at http://www.apache.org/licenses/LICENSE-2.0 + +package net.orfjackal.retrolambda; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +final class LambdaClassDumper { + + private final Path outputDir; + private final int targetVersion; + + public LambdaClassDumper(Path outputDir, int targetVersion) { + this.outputDir = outputDir; + this.targetVersion = targetVersion; + } + + private Field field; + void registerDumper() { + try { + Class dumper = Class.forName("java.lang.invoke.ProxyClassesDumper"); + Constructor cnstr = dumper.getDeclaredConstructor(Path.class); + cnstr.setAccessible(true); + Class mf = Class.forName("java.lang.invoke.InnerClassLambdaMetafactory"); + field = mf.getDeclaredField("dumper"); + Field m = field.getClass().getDeclaredField("modifiers"); + m.setAccessible(true); + int mod = m.getInt(field); + m.setInt(field, mod & ~Modifier.FINAL); + field.setAccessible(true); + + Path p = new VirtualPath(""); + field.set(null, cnstr.newInstance(p)); + } catch (Exception ex) { + throw new IllegalStateException("Cannot initialize dumper", ex); + } + } + + void unregisterDumper() { + if (field != null) { + try { + field.set(null, null); + } catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + } + + private void reifyLambdaClass(String className, byte[] classfileBuffer) { + try { + System.out.println("Saving lambda class: " + className); + byte[] backportedBytecode = LambdaClassBackporter.transform(classfileBuffer, targetVersion); + Path savePath = outputDir.resolve(className + ".class"); + Files.createDirectories(savePath.getParent()); + Files.write(savePath, backportedBytecode); + + } catch (Throwable t) { + // print to stdout to keep in sync with other log output + System.out.println("ERROR: Failed to backport lambda class: " + className); + t.printStackTrace(System.out); + } + } + + private final class VirtualProvider extends FileSystemProvider { + + @Override + public String getScheme() { + throw new IllegalStateException(); + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + throw new IllegalStateException(); + } + + @Override + public FileSystem getFileSystem(URI uri) { + throw new IllegalStateException(); + } + + @Override + public Path getPath(URI uri) { + throw new IllegalStateException(); + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + return new ClassChannel(path); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + throw new IllegalStateException(); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + } + + @Override + public void delete(Path path) throws IOException { + throw new IllegalStateException(); + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + throw new IllegalStateException(); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + throw new IllegalStateException(); + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + throw new IllegalStateException(); + } + + @Override + public boolean isHidden(Path path) throws IOException { + throw new IllegalStateException(); + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + throw new IllegalStateException(); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + throw new IllegalStateException(); + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + throw new IllegalStateException(); + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) throws IOException { + throw new IllegalStateException(); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + throw new IllegalStateException(); + } + + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + throw new IllegalStateException(); + } + + } + + private final class VirtualFS extends FileSystem { + + @Override + public FileSystemProvider provider() { + return new VirtualProvider(); + } + + @Override + public void close() throws IOException { + throw new IllegalStateException(); + } + + @Override + public boolean isOpen() { + throw new IllegalStateException(); + } + + @Override + public boolean isReadOnly() { + throw new IllegalStateException(); + } + + @Override + public String getSeparator() { + throw new IllegalStateException(); + } + + @Override + public Iterable getRootDirectories() { + throw new IllegalStateException(); + } + + @Override + public Iterable getFileStores() { + throw new IllegalStateException(); + } + + @Override + public Set supportedFileAttributeViews() { + throw new IllegalStateException(); + } + + @Override + public Path getPath(String first, String... more) { + throw new IllegalStateException(); + } + + @Override + public PathMatcher getPathMatcher(String syntaxAndPattern) { + throw new IllegalStateException(); + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + throw new IllegalStateException(); + } + + @Override + public WatchService newWatchService() throws IOException { + throw new IllegalStateException(); + } + + } + + private final class VirtualPath implements Path { + private final String path; + + public VirtualPath(String path) { + this.path = path; + } + + @Override + public FileSystem getFileSystem() { + return new VirtualFS(); + } + + @Override + public boolean isAbsolute() { + throw new IllegalStateException(); + } + + @Override + public Path getRoot() { + throw new IllegalStateException(); + } + + @Override + public Path getFileName() { + throw new IllegalStateException(); + } + + @Override + public Path getParent() { + return this; + } + + @Override + public int getNameCount() { + throw new IllegalStateException(); + } + + @Override + public Path getName(int index) { + throw new IllegalStateException(); + } + + @Override + public Path subpath(int beginIndex, int endIndex) { + throw new IllegalStateException(); + } + + @Override + public boolean startsWith(Path other) { + throw new IllegalStateException(); + } + + @Override + public boolean startsWith(String other) { + throw new IllegalStateException(); + } + + @Override + public boolean endsWith(Path other) { + throw new IllegalStateException(); + } + + @Override + public boolean endsWith(String other) { + throw new IllegalStateException(); + } + + @Override + public Path normalize() { + throw new IllegalStateException(); + } + + @Override + public Path resolve(Path other) { + throw new IllegalStateException(); + } + + @Override + public Path resolve(String other) { + assert path.isEmpty(); + return new VirtualPath(other); + } + + @Override + public Path resolveSibling(Path other) { + throw new IllegalStateException(); + } + + @Override + public Path resolveSibling(String other) { + throw new IllegalStateException(); + } + + @Override + public Path relativize(Path other) { + throw new IllegalStateException(); + } + + @Override + public URI toUri() { + throw new IllegalStateException(); + } + + @Override + public Path toAbsolutePath() { + throw new IllegalStateException(); + } + + @Override + public Path toRealPath(LinkOption... options) throws IOException { + throw new IllegalStateException(); + } + + @Override + public File toFile() { + throw new IllegalStateException(); + } + + @Override + public WatchKey register(WatchService watcher, WatchEvent.Kind[] events, WatchEvent.Modifier... modifiers) throws IOException { + throw new IllegalStateException(); + } + + @Override + public WatchKey register(WatchService watcher, WatchEvent.Kind... events) throws IOException { + throw new IllegalStateException(); + } + + @Override + public Iterator iterator() { + throw new IllegalStateException(); + } + + @Override + public int compareTo(Path other) { + throw new IllegalStateException(); + } + + @Override + public String toString() { + return path; + } + } + + private final class ClassChannel implements SeekableByteChannel { + private final Path path; + private final ByteArrayOutputStream os; + private final WritableByteChannel ch; + + public ClassChannel(Path path) { + this.path = path; + this.os = new ByteArrayOutputStream(); + this.ch = Channels.newChannel(os); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + throw new IOException(); + } + + @Override + public int write(ByteBuffer src) throws IOException { + return ch.write(src); + } + + @Override + public long position() throws IOException { + throw new IOException(); + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + throw new IOException(); + } + + @Override + public long size() throws IOException { + throw new IOException(); + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + throw new IOException(); + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void close() throws IOException { + String className = path.toString(); + className = className.substring(0, className.length() - 6); + if (LambdaReifier.isLambdaClassToReify(className)) { + reifyLambdaClass(className, os.toByteArray()); + } + } + } // end of ClassCastException +} diff --git a/retrolambda/src/main/java/net/orfjackal/retrolambda/Main.java b/retrolambda/src/main/java/net/orfjackal/retrolambda/Main.java index feba36e5..87e21bb9 100644 --- a/retrolambda/src/main/java/net/orfjackal/retrolambda/Main.java +++ b/retrolambda/src/main/java/net/orfjackal/retrolambda/Main.java @@ -43,7 +43,15 @@ public static void main(String[] args) { visitFiles(inputDir, includedFiles, new BytecodeTransformingFileVisitor(inputDir, outputDir) { @Override protected byte[] transform(byte[] bytecode) { - return LambdaUsageBackporter.transform(bytecode, bytecodeVersion); + if (PreMain.isAgentLoaded()) { + return LambdaUsageBackporter.transform(bytecode, bytecodeVersion); + } else { + final LambdaClassDumper trans = new LambdaClassDumper(outputDir, bytecodeVersion); + trans.registerDumper(); + byte[] ret = LambdaUsageBackporter.transform(bytecode, bytecodeVersion); + trans.unregisterDumper(); + return ret; + } } }); } catch (Throwable t) {