dev.jbang.spi.IntegrationManager Maven / Gradle / Ivy
Show all versions of jbang-cli Show documentation
package dev.jbang.spi;
import static dev.jbang.cli.BaseCommand.EXIT_GENERIC_ERROR;
import static dev.jbang.cli.BaseCommand.EXIT_INVALID_INPUT;
import static dev.jbang.util.JavaUtil.resolveInJavaHome;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dev.jbang.cli.ExitException;
import dev.jbang.dependencies.ArtifactInfo;
import dev.jbang.dependencies.MavenRepo;
import dev.jbang.source.BuildContext;
import dev.jbang.source.Project;
import dev.jbang.source.Source;
import dev.jbang.source.buildsteps.CompileBuildStep;
import dev.jbang.util.JavaUtil;
import dev.jbang.util.PathTypeAdapter;
import dev.jbang.util.Util;
/**
* JBang uses a 'convention based interface' for build time integration.
*/
public class IntegrationManager {
public static final String FILES = "files";
public static final String NATIVE_IMAGE = "native-image";
public static final String MAIN_CLASS = "main-class";
public static final String JAVA_ARGS = "java-args";
private static final GsonBuilder gsonb = new GsonBuilder()
.registerTypeHierarchyAdapter(Path.class,
new PathTypeAdapter());
/**
* Discovers all integration points and runs them.
*
* If an integration point created a native image it returns the resulting
* image.
*/
public static IntegrationResult runIntegrations(BuildContext ctx) {
IntegrationResult result = new IntegrationResult(null, null, null);
Project prj = ctx.getProject();
Path compileDir = ctx.getCompileDir();
Path pomPath = CompileBuildStep.getPomPath(ctx);
Source source = prj.getMainSource();
LinkedHashMap repos = new LinkedHashMap<>();
LinkedHashMap deps = new LinkedHashMap<>();
for (MavenRepo repo : prj.getRepositories()) {
repos.put(repo.getId(), repo.getUrl());
}
for (ArtifactInfo art : ctx.resolveClassPath().getArtifacts()) {
if (art.getCoordinate() != null) { // skipping dependencies that does not have a GAV
deps.put(art.getCoordinate().toCanonicalForm(), art.getFile());
}
}
List comments = source.getTags().map(s -> "//" + s).collect(Collectors.toList());
ClassLoader old = Thread.currentThread().getContextClassLoader();
PrintStream oldout = System.out;
try {
URLClassLoader integrationCl = getClassLoader(deps.values());
Thread.currentThread().setContextClassLoader(integrationCl);
String requestedJavaVersion = prj.getJavaVersion();
Set classNames = loadIntegrationClassNames(integrationCl);
for (String className : classNames) {
Path srcPath = (source.getResourceRef().getFile() != null)
? source.getResourceRef().getFile().toAbsolutePath()
: null;
IntegrationInput input = new IntegrationInput(className, srcPath, compileDir, pomPath, repos, deps,
comments, prj.isNativeImage(), Util.isVerbose());
IntegrationResult ir = requestedJavaVersion == null || JavaUtil.satisfiesRequestedVersion(
requestedJavaVersion, JavaUtil.getCurrentMajorJavaVersion())
? runIntegrationEmbedded(input, integrationCl)
: runIntegrationExternal(input, prj.getProperties(), requestedJavaVersion);
result = result.merged(ir);
}
} catch (ClassNotFoundException e) {
throw new ExitException(EXIT_INVALID_INPUT, "Unable to load integration class", e);
} catch (NoSuchMethodException e) {
throw new ExitException(
EXIT_INVALID_INPUT,
"Integration class missing method with signature public static Map postBuild(Path classesDir, Path pomFile, List> dependencies)",
e);
} catch (Exception e) {
throw new ExitException(EXIT_GENERIC_ERROR, "Issue running postBuild()", e);
} finally {
Thread.currentThread().setContextClassLoader(old);
System.setOut(oldout);
}
return result;
}
@Nonnull
private static URLClassLoader getClassLoader(Collection deps) {
URL[] urls = deps.stream().map(path -> {
try {
return path.toUri().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}).toArray(URL[]::new);
return new URLClassLoader(urls);
}
private static Set loadIntegrationClassNames(URLClassLoader integrationCl) throws IOException {
Set classNames = new HashSet<>();
Enumeration files = integrationCl.getResources("META-INF/jbang-integration.list");
while (files.hasMoreElements()) {
URL res = files.nextElement();
try (InputStream in = res.openStream()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.startsWith("#")) {
if (line.contains("#")) {
line = line.substring(0, line.indexOf("#"));
line = line.trim();
}
if (!line.isEmpty()) {
classNames.add(line);
}
}
}
}
}
return classNames;
}
private static IntegrationResult runIntegrationEmbedded(IntegrationInput input, URLClassLoader integrationCl)
throws Exception {
Util.infoMsg("Post build with " + input.integrationClassName);
if (input.source != null) {
// TODO: should we add new properties to the integration method?
System.setProperty("jbang.source", input.source.toString());
}
Class> clazz = Class.forName(input.integrationClassName, true, integrationCl);
Method method = clazz.getDeclaredMethod("postBuild", Path.class, Path.class, List.class, List.class,
List.class, boolean.class);
if (Util.isVerbose()) {
System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.err)));
} else {
System.setOut(new PrintStream(new OutputStream() {
public void write(int b) {
// DO NOTHING
// TODO: capture it for later print if error
}
}));
}
@SuppressWarnings("unchecked")
Map result = (Map) method.invoke(null, input.classes, input.pom,
mapToList(input.repositories), mapToList(input.dependencies), input.comments, input.nativeRequested);
@SuppressWarnings("unchecked")
Map files = (Map) result.get(FILES);
if (files != null) {
for (Map.Entry entry : files.entrySet()) {
Path target = input.classes.resolve(entry.getKey());
Files.createDirectories(target.getParent());
try (OutputStream out = Files.newOutputStream(target)) {
out.write(entry.getValue());
}
}
}
Path nativeImage = null;
String mainClass = null;
List javaArgs = null;
Path image = (Path) result.get(NATIVE_IMAGE);
if (image != null) {
nativeImage = image;
}
String mc = (String) result.get(MAIN_CLASS);
if (mc != null) {
mainClass = mc;
}
@SuppressWarnings("unchecked")
List ja = (List) result.get(JAVA_ARGS);
if (ja != null) {
javaArgs = ja;
}
return new IntegrationResult(nativeImage, mainClass, javaArgs);
}
private static IntegrationResult runIntegrationExternal(IntegrationInput input,
Map properties,
String requestedJavaVersion)
throws Exception {
Gson parser = gsonb.create();
Util.infoMsg("Running external post build for " + input.integrationClassName);
List args = new ArrayList<>();
args.add(resolveInJavaHome("java", requestedJavaVersion)); // TODO
for (Map.Entry entry : properties.entrySet()) {
args.add("-D" + entry.getKey() + "=" + entry.getValue());
}
Path jbangJar = Util.getJarLocation();
args.add("-cp");
if (jbangJar.toString().endsWith(".jar")) {
args.add(jbangJar.toString());
} else {
// We will assume that we're running inside an IDE or
// some kind of test environment and need to manually
// add the Gson dependency
Path gsonJar = Util.getJarLocation(Gson.class);
args.add(jbangJar + File.pathSeparator + gsonJar);
}
args.add("dev.jbang.spi.IntegrationManager");
if (Util.isVerbose()) {
Util.verboseMsg("Running: " + String.join(" ", args));
Util.verboseMsg("Input: " + parser.toJson(input));
}
Process process = new ProcessBuilder(args)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.start();
try (Writer w = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()))) {
parser.toJson(input, w);
}
String cmdOutput;
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
cmdOutput = br.lines().collect(Collectors.joining());
}
try {
process.waitFor();
} catch (InterruptedException e) {
throw new ExitException(EXIT_GENERIC_ERROR, "External post build was interrupted", e);
}
Util.verboseMsg("Output: #" + process.exitValue() + " - " + cmdOutput);
if (process.exitValue() == 0) {
return parser.fromJson(new StringReader(cmdOutput), IntegrationResult.class);
} else {
throw new ExitException(EXIT_GENERIC_ERROR, "External post exited with error: " + cmdOutput);
}
}
private static List> mapToList(Map map) {
return new ArrayList<>(map.entrySet());
}
public static void main(String... args) {
Gson parser = gsonb.create();
IntegrationInput input = parser.fromJson(new InputStreamReader(System.in), IntegrationInput.class);
ClassLoader old = Thread.currentThread().getContextClassLoader();
PrintStream oldout = System.out;
Util.setVerbose(input.verbose);
String output = "";
boolean ok = false;
try {
URLClassLoader integrationCl = getClassLoader(input.dependencies.values());
Thread.currentThread().setContextClassLoader(integrationCl);
IntegrationResult result = runIntegrationEmbedded(input, integrationCl);
output = parser.toJson(result);
ok = true;
} catch (ClassNotFoundException e) {
output = "Unable to load integration class";
} catch (NoSuchMethodException e) {
output = "Integration class missing method with signature public static Map postBuild(Path classesDir, Path pomFile, List> dependencies)";
} catch (Exception e) {
output = "Issue running postBuild()";
if (input.verbose) {
e.printStackTrace(System.err);
}
} finally {
Thread.currentThread().setContextClassLoader(old);
System.setOut(oldout);
}
System.out.println(output);
System.exit(ok ? 0 : 1);
}
}