![JAR search and dependency download from the Maven repository](/logo.png)
io.github.oliviercailloux.javagrade.bytecode.Compiler Maven / Gradle / Ivy
The newest version!
package io.github.oliviercailloux.javagrade.bytecode;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import static io.github.oliviercailloux.jaris.exceptions.Unchecker.URI_UNCHECKER;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.MoreCollectors;
import com.google.common.io.ByteSource;
import com.google.common.io.MoreFiles;
import com.google.common.io.Resources;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.Resource;
import io.github.classgraph.ScanResult;
import io.github.oliviercailloux.jaris.exceptions.CheckedStream;
import io.github.oliviercailloux.utils.Utils;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import org.eclipse.jdt.core.compiler.batch.BatchCompiler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* In the below list, “checked” means that I checked everything there and extracted the links to
* build this list, except that I didn’t check details of short posts such as including only two
* classes, which are too short to bother.
*
* - https://github.com/kite-sdk/kite/tree/master/kite-morphlines/kite-morphlines-core/src/main/java/org/kitesdk/morphline/scriptengine/java
* old but seems correct quality code; though bug in method header which inverts parameters. Kite is
* a set of libraries, tools, examples, and documentation focused on making it easier to build
* systems on top of the Hadoop ecosystem.
* - http://stackoverflow.com/a/33963977 (an answer to q 12173294, link below): extends
* http://javapracs.blogspot.com/2011/06/dynamic-in-memory-compilation-using.html by Rekha Kumari,
* provides multiple classes compilation. MOST PROMISING?
* - http://pfmiles.github.io/blog/dynamic-java/ in chinese, could be serious? (cited by Seems
* original.
* - http://www.soulmachine.me/blog/2015/07/22/compile-and-run-java-source-code-in-memory/
* references Kite but doesn’t say how it differs from Kite. Seems like copied from Kite, basically.
* TO EXPLORE
* - https://github.com/trung/InMemoryJavaCompiler (not maintained since end-2017, two years ago)
* TO EXPLORE LAST?
* - JANINO https://janino-compiler.github.io/ implements a Java compiler, does not reuse
* - https://github.com/OpenHFT/Java-Runtime-Compiler by Peter Lawrey (most maintained, but
* doesn’t seem OO)
* - https://stackoverflow.com/questions/3447359/how-to-provide-an-interface-to-javacompiler-when-compiling-a-source-file-dynamic
* checked
* - https://stackoverflow.com/questions/12173294/compile-code-fully-in-memory-with-javax-tools-javacompiler
* →
* - https://stackoverflow.com/questions/21544446/how-do-you-dynamically-compile-and-load-external-java-classes?noredirect=1&lq=1
* checked
* - https://stackoverflow.com/questions/31599427/how-to-compile-and-run-java-source-code-in-memory?noredirect=1&lq=1
* checked
* - https://stackoverflow.com/questions/2946338/how-do-i-programmatically-compile-and-instantiate-a-java-class?noredirect=1&lq=1
* checked
* - https://stackoverflow.com/questions/274474/how-do-i-use-jdk6-toolprovider-and-javacompiler-with-the-context-classloader?noredirect=1&lq=1
* checked
* - https://stackoverflow.com/questions/2315719/using-javax-tools-toolprovider-from-a-custom-classloader?noredirect=1&lq=1
* advanced discussion, perhaps worth reading? Though the bug report seems mostly unrelated and
* might be just a misunderstanding (according to the reply).
* - https://stackoverflow.com/questions/37822818/how-do-i-use-dependencies-only-available-in-memory-with-javax-tools-javacompiler?noredirect=1&lq=1
* checked
*
*
* @author Olivier Cailloux
*
*/
public class Compiler {
@SuppressWarnings("unused")
private static final Logger LOGGER = LoggerFactory.getLogger(Compiler.class);
public static Compiler tolerant(List cp, Path destDir) {
return new Compiler(cp, destDir, true);
}
public static Compiler intolerant(List cp, Path destDir) {
return new Compiler(cp, destDir, false);
}
public static ImmutableList>
compile(Collection classPath, Path destDir, Collection sources) {
/*
* Compiler throws if asked to compile no source (even though the doc seems to allow it).
*/
if (!sources.iterator().hasNext()) {
return ImmutableList.of();
}
return compile(classPath, sources, Optional.of(destDir));
}
private static ImmutableList>
compile(Collection classPath, Collection sources, Optional destDir) {
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
final boolean compiled;
final DiagnosticCollector diagnosticCollector = new DiagnosticCollector<>();
/*
* API about diagnostics is unclear, but from the source code of JavacTool, it seems like the
* second one (in the #getTask arguments) overrides the first one (in the getStandardFileManager
* arguments).
*/
try (StandardJavaFileManager fileManager =
compiler.getStandardFileManager(diagnosticCollector, null, null)) {
/*
* Have to set explicitly the annotation processor path, otherwise, the initialization of
* annotation processing (involving #getClassLoader) uses the class path, which fails if the
* paths do not refer to File instances, because JavacFileManager#getClassLoader(Location
* location) calls getLocation, which tries to return File instances, instead of
* getLocationAsPaths(Location location). See CompilerTests#testBugJdk(). I got an email on
* the 10th of March, 2021, stating that the incident is fixed in https://jdk.java.net/16/. I
* have not checked.
* https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/share/classes/com/sun/tools/
* javac/file/JavacFileManager.java#L744
*/
fileManager.setLocationFromPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH,
ImmutableList.of());
fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, classPath);
if (destDir.isPresent()) {
fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT,
ImmutableSet.of(destDir.get()));
}
final Iterable extends JavaFileObject> srcToCompileObjs =
fileManager.getJavaFileObjectsFromPaths(sources);
final StringWriter compilationOutputReceiver = new StringWriter();
compiled = compiler.getTask(compilationOutputReceiver, fileManager, diagnosticCollector,
ImmutableList.of(), null, srcToCompileObjs).call();
final String compilationOutput = compilationOutputReceiver.toString();
if (!compilationOutput.isEmpty()) {
throw new UnsupportedOperationException();
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
final List> diagnostics =
diagnosticCollector.getDiagnostics();
LOGGER.debug("Compiled and got: {}.", diagnostics);
verify(compiled == diagnostics.isEmpty());
if (compiled) {
// SourceScanner.scan()
/*
* Seems impossible to check that the file gets created, because seems impossible to
* (elegantly) obtain the package name or output exact location of the file.
*/
}
return ImmutableList.copyOf(diagnostics);
}
/**
* Note that this will fail if the properties file is in a resource, not in a reachable file.
*
* @throws IOException
*
* @see Eclipse
*/
public static CompilationResult eclipseCompile(List classPath, Set targets)
throws IOException {
return eclipseCompile(classPath, targets, true);
}
public static CompilationResult eclipseCompile(List classPath, Set targets,
boolean useStrictWarnings) throws IOException {
return eclipseCompile(classPath, targets, useStrictWarnings, Optional.empty());
}
/**
* TODO org.eclipse.jdt.internal.compiler.batch.Main#performCompilation.
*
* @throws IOException
*
*/
public static CompilationResult eclipseCompile(List classPath, Set targets,
boolean useStrictWarnings, Optional destination) throws IOException {
// TODO what if targets is empty?
checkArgument(targets.stream().allMatch(Files::exists));
final StringWriter out = new StringWriter();
final StringWriter err = new StringWriter();
final ImmutableList.Builder builder = ImmutableList.builder();
{
builder.add("--release", "17");
}
if (useStrictWarnings) {
/* Could instead use options such as "-warn:+allDeadCode,allDeprecation…". */
final URL propertiesUrl = Compiler.class.getResource("Eclipse-prefs.epf");
checkState(propertiesUrl != null);
Resources.asByteSource(propertiesUrl);
URI propertiesUri = URI_UNCHECKER.getUsing(() -> propertiesUrl.toURI());
// ScanResult scan = new ClassGraph().scan();
// LOGGER.info("Scan: {}.", scan.getAllResources().size());
// ImmutableSet filtered = scan.getAllResources().stream()
// .filter(r -> r.toString().contains("bytecode") && r.toString().contains(".epf"))
// .collect(ImmutableSet.toImmutableSet());
// Resource found = filtered.stream().collect(MoreCollectors.onlyElement());
// URI propertiesUri = found.getURI();
LOGGER.info("Found: {}.", propertiesUri);
verify(propertiesUri != null);
LOGGER.info("Using properties file: {}.", propertiesUri);
ByteSource propertiesByteSource = Resources.asByteSource(propertiesUrl);
/* Need to copy in order to extract content possibly embedded within this jar. */
Path propertiesPath = Utils.getTempUniqueDirectory("props");
propertiesByteSource.copyTo(MoreFiles.asByteSink(propertiesPath));
// try {
// Path p = Path.of(propertiesUri);
// } catch (FileSystemNotFoundException ex) {
// try (FileSystem fs = FileSystems.newFileSystem(propertiesUri, ImmutableMap.of())) {
// Path propertiesPath = fs.provider().getPath(propertiesUri);
// verify(Files.exists(propertiesPath));
// verify(propertiesPath.getFileSystem().provider().getScheme().equals("file"));
// builder.add("-properties", propertiesPath.toString());
// }
// }
// Path propertiesPath = Path.of(propertiesUri);
verify(Files.exists(propertiesPath));
verify(propertiesPath.getFileSystem().provider().getScheme().equals("file"));
builder.add("-properties", propertiesPath.toString());
}
checkArgument(
classPath.stream().allMatch(p -> p.getFileSystem().provider().getScheme().equals("file")));
{
builder.add("-classpath",
classPath.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator)));
}
if (destination.isPresent()) {
final Path destinationPath = destination.get();
checkArgument(destinationPath.getFileSystem().provider().getScheme().equals("file"));
builder.add("-d", destinationPath.toString());
}
final boolean targetsAreFiles =
targets.stream().allMatch(p -> p.getFileSystem().provider().getScheme().equals("file"));
final ImmutableSet effectiveTargets;
final Optional toDelete;
if (targetsAreFiles) {
effectiveTargets = ImmutableSet.copyOf(targets);
toDelete = Optional.empty();
} else {
final Path newStartPath = Files.createTempDirectory("sources");
toDelete = Optional.of(newStartPath);
final ImmutableMap corrPaths =
Maps.toMap(targets, p -> newStartPath.resolve(p.relativize(p.getRoot()).toString()));
LOGGER.debug("Corr: {}.", corrPaths.values());
CheckedStream.wrapping(targets.stream())
.peek(p -> Files.createDirectories(corrPaths.get(p).getParent()))
.forEach(p -> Files.copy(p, corrPaths.get(p)));
effectiveTargets = corrPaths.values().stream().collect(ImmutableSet.toImmutableSet());
}
{
effectiveTargets.stream().map(Path::toString).forEach(builder::add);
}
final ImmutableList args = builder.build();
final boolean compiled = BatchCompiler.compile(args.toArray(new String[args.size()]),
new PrintWriter(out), new PrintWriter(err), null);
LOGGER.debug("Compiled with output: {}, error: {}.", out, err);
if (toDelete.isPresent()) {
MoreFiles.deleteRecursively(toDelete.get());
}
return new CompilationResult(compiled, out.toString(), err.toString());
}
public static CompilationResult eclipseCompileUsingOurClasspath(Set targets,
Path destinationDir) throws IOException {
final List classpath = new ClassGraph().getClasspathFiles();
final ImmutableSet classpathPaths =
classpath.stream().map(File::toPath).collect(ImmutableSet.toImmutableSet());
return eclipseCompile(classpathPaths.asList(), targets, true, Optional.of(destinationDir));
}
public static class CompilationResultExt {
public static CompilationResultExt given(boolean compiled, String out, String err, int s, Path compiledDir, Set javaPaths) {
return new CompilationResultExt(compiled, out, err, s, compiledDir, javaPaths);
}
public static CompilationResultExt compiled(String out, int s, Path compiledDir, Set javaPaths) {
return new CompilationResultExt(true, out, "", s, compiledDir, javaPaths);
}
public static CompilationResultExt failed(String out, String err, int s, Path compiledDir, Set javaPaths) {
return new CompilationResultExt(false, out, err, s, compiledDir, javaPaths);
}
public boolean compiled;
public String out;
public String err;
public int nbSuppressWarnings;
public Path compiledDir;
public final ImmutableSet javaPaths;
private CompilationResultExt(boolean compiled, String out, String err, int s, Path compiledDir, Set javaPaths) {
this.compiled = compiled;
this.out = out;
this.err = err;
this.nbSuppressWarnings = s;
checkArgument(compiled == (countErrors() == 0), err);
this.compiledDir = compiledDir;
this.javaPaths = ImmutableSet.copyOf(javaPaths);
}
public int countWarnings() {
/* See https://github.com/google/guava/issues/877 */
return Splitter.on("WARNING ").splitToStream(err).mapToInt(e -> 1).sum() - 1;
}
public int countErrors() {
return Splitter.on("ERROR ").splitToStream(err).mapToInt(e -> 1).sum() - 1;
}
public int nbWarningsTot() {
return countWarnings() + nbSuppressWarnings;
}
}
public static class CompilationResult {
public static CompilationResult given(boolean compiled, String out, String err) {
return new CompilationResult(compiled, out, err);
}
public static CompilationResult compiled(String out) {
return new CompilationResult(true, out, "");
}
public static CompilationResult failed(String out, String err) {
return new CompilationResult(false, out, err);
}
public boolean compiled;
public String out;
public String err;
private CompilationResult(boolean compiled, String out, String err) {
super();
this.compiled = compiled;
this.out = out;
this.err = err;
checkArgument(compiled == (countErrors() == 0), err);
}
public int countWarnings() {
/* See https://github.com/google/guava/issues/877 */
return Splitter.on("WARNING ").splitToStream(err).mapToInt(e -> 1).sum() - 1;
}
public int countErrors() {
return Splitter.on("ERROR ").splitToStream(err).mapToInt(e -> 1).sum() - 1;
}
}
private boolean tolerateFailure;
private ImmutableList cp;
private Path destDir;
private Compiler(List cp, Path destDir, boolean tolerateFailure) {
this.cp = ImmutableList.copyOf(cp);
this.destDir = checkNotNull(destDir);
this.tolerateFailure = tolerateFailure;
}
public ImmutableList>
compileSrcs(Collection srcToCompile) {
final ImmutableList> output =
Compiler.compile(cp, destDir, srcToCompile);
if (!tolerateFailure) {
verify(output.isEmpty(), output.toString());
}
return output;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy