Skip to content

Commit

Permalink
Adding support for jbang integration
Browse files Browse the repository at this point in the history
Done by implementing jbang experimental
SPI to let Quarkus participate in the build of a jar
+ passing //Q:CONFIG in as system properties.

Initial version supports building jar as well as native
image via docker.

 - various fixes for handling default packages in arc and metrics.
 - fixes a few places of class.forname not honoring TCCL
 - slightly open up appmodel to enable this but still reliant
   on pom.xml being bundled (future versions shouldn't)

Co-Authored: Alexey Loubyansky <olubyans@redhat.com>
Co-Authored: Stuart Douglas <stuart.w.douglas@gmail.com>
  • Loading branch information
maxandersen committed Aug 29, 2020
1 parent f585d9d commit 545eff5
Show file tree
Hide file tree
Showing 20 changed files with 488 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ void runtime(BuildProducer<RuntimeReinitializedClassBuildItem> reinitialized) {

private void registerIfExists(BuildProducer<RuntimeReinitializedClassBuildItem> reinitialized, String className) {
try {
Class.forName(className);
Class.forName(className, false, Thread.currentThread().getContextClassLoader());
reinitialized.produce(new RuntimeReinitializedClassBuildItem(className));
} catch (ClassNotFoundException ignored) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package io.quarkus.deployment.jbang;

import java.io.File;
import java.nio.file.Path;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import org.jboss.logging.Logger;

import io.quarkus.bootstrap.BootstrapGradleException;
import io.quarkus.bootstrap.app.AdditionalDependency;
import io.quarkus.bootstrap.app.CuratedApplication;
import io.quarkus.bootstrap.app.QuarkusBootstrap;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.bootstrap.model.AppArtifactKey;
import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject;
import io.quarkus.bootstrap.resolver.model.WorkspaceModule;
import io.quarkus.bootstrap.util.QuarkusModelHelper;
import io.quarkus.builder.BuildChainBuilder;
import io.quarkus.builder.BuildResult;
import io.quarkus.builder.BuildStepBuilder;
import io.quarkus.deployment.QuarkusAugmentor;
import io.quarkus.deployment.builditem.ApplicationClassNameBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.LiveReloadBuildItem;
import io.quarkus.deployment.builditem.TransformedClassesBuildItem;
import io.quarkus.deployment.dev.DevModeContext;
import io.quarkus.deployment.dev.IDEDevModeMain;
import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem;
import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem;
import io.quarkus.deployment.pkg.builditem.ProcessInheritIODisabled;
import io.quarkus.runtime.LaunchMode;

public class JBangAugmentorImpl implements BiConsumer<CuratedApplication, Map<String, Object>> {

private static final Logger log = Logger.getLogger(IDEDevModeMain.class.getName());

@Override
public void accept(CuratedApplication curatedApplication, Map<String, Object> resultMap) {

QuarkusClassLoader classLoader = curatedApplication.getAugmentClassLoader();

QuarkusBootstrap quarkusBootstrap = curatedApplication.getQuarkusBootstrap();
QuarkusAugmentor.Builder builder = QuarkusAugmentor.builder()
.setRoot(quarkusBootstrap.getApplicationRoot())
.setClassLoader(classLoader)
.addFinal(ApplicationClassNameBuildItem.class)
.setTargetDir(quarkusBootstrap.getTargetDirectory())
.setDeploymentClassLoader(curatedApplication.createDeploymentClassLoader())
.setBuildSystemProperties(quarkusBootstrap.getBuildSystemProperties())
.setEffectiveModel(curatedApplication.getAppModel());
if (quarkusBootstrap.getBaseName() != null) {
builder.setBaseName(quarkusBootstrap.getBaseName());
}

builder.setLaunchMode(LaunchMode.NORMAL);
builder.setRebuild(quarkusBootstrap.isRebuild());
builder.setLiveReloadState(new LiveReloadBuildItem(false, Collections.emptySet(), new HashMap<>()));
for (AdditionalDependency i : quarkusBootstrap.getAdditionalApplicationArchives()) {
//this gets added to the class path either way
//but we only need to add it to the additional app archives
//if it is forced as an app archive
if (i.isForceApplicationArchive()) {
builder.addAdditionalApplicationArchive(i.getArchivePath());
}
}
builder.addBuildChainCustomizer(new Consumer<BuildChainBuilder>() {
@Override
public void accept(BuildChainBuilder builder) {
final BuildStepBuilder stepBuilder = builder.addBuildStep((ctx) -> {
ctx.produce(new ProcessInheritIODisabled());
});
stepBuilder.produces(ProcessInheritIODisabled.class).build();
}
});
builder.excludeFromIndexing(quarkusBootstrap.getExcludeFromClassPath());
builder.addFinal(GeneratedClassBuildItem.class);
builder.addFinal(GeneratedResourceBuildItem.class);
builder.addFinal(TransformedClassesBuildItem.class);
boolean nativeRequested = "native".equals(System.getProperty("quarkus.package.type"));
boolean containerBuildRequested = Boolean.getBoolean("quarkus.container-image.build");
if (nativeRequested) {
builder.addFinal(NativeImageBuildItem.class);
}
if (containerBuildRequested) {
//TODO: this is a bit ugly
//we don't nessesarily need these artifacts
//but if we include them it does mean that you can auto create docker images
//and deploy to kube etc
//for an ordinary build with no native and no docker this is a waste
builder.addFinal(ArtifactResultBuildItem.class);
}

try {
BuildResult buildResult = builder.build().run();
Map<String, byte[]> result = new HashMap<>();
for (GeneratedClassBuildItem i : buildResult.consumeMulti(GeneratedClassBuildItem.class)) {
result.put(i.getName().replace(".", "/") + ".class", i.getClassData());
}
for (GeneratedResourceBuildItem i : buildResult.consumeMulti(GeneratedResourceBuildItem.class)) {
result.put(i.getName(), i.getClassData());
}
for (Map.Entry<Path, Set<TransformedClassesBuildItem.TransformedClass>> entry : buildResult
.consume(TransformedClassesBuildItem.class).getTransformedClassesByJar().entrySet()) {
for (TransformedClassesBuildItem.TransformedClass transformed : entry.getValue()) {
result.put(transformed.getFileName(), transformed.getData());
}
}
resultMap.put("files", result);
List javaargs = new ArrayList<String>();
javaargs.add("-Djava.util.logging.manager=org.jboss.logmanager.LogManager");
resultMap.put("java-args", javaargs);
if (nativeRequested) {
resultMap.put("native-image", buildResult.consume(NativeImageBuildItem.class).getPath());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private DevModeContext.ModuleInfo toModule(WorkspaceModule module) throws BootstrapGradleException {
AppArtifactKey key = new AppArtifactKey(module.getArtifactCoords().getGroupId(),
module.getArtifactCoords().getArtifactId(), module.getArtifactCoords().getClassifier());

Set<String> sourceDirectories = new HashSet<>();
Set<String> sourceParents = new HashSet<>();
for (File srcDir : module.getSourceSourceSet().getSourceDirectories()) {
sourceDirectories.add(srcDir.getPath());
sourceParents.add(srcDir.getParent());
}

return new DevModeContext.ModuleInfo(key,
module.getArtifactCoords().getArtifactId(),
module.getProjectRoot().getPath(),
sourceDirectories,
QuarkusModelHelper.getClassPath(module).toAbsolutePath().toString(),
module.getSourceSourceSet().getResourceDirectory().toString(),
module.getSourceSet().getResourceDirectory().getPath(),
sourceParents,
module.getBuildDir().toPath().resolve("generated-sources").toAbsolutePath().toString(),
module.getBuildDir().toString());
}

private DevModeContext.ModuleInfo toModule(LocalProject project) {
return new DevModeContext.ModuleInfo(project.getKey(), project.getArtifactId(),
project.getDir().toAbsolutePath().toString(),
Collections.singleton(project.getSourcesSourcesDir().toAbsolutePath().toString()),
project.getClassesDir().toAbsolutePath().toString(),
project.getResourcesSourcesDir().toAbsolutePath().toString(),
project.getSourcesDir().toString(),
project.getCodeGenOutputDir().toString(),
project.getOutputDir().toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.quarkus.launcher;

import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

import io.quarkus.bootstrap.BootstrapConstants;

public class JBangIntegration {

public static final String CONFIG = "//Q:CONFIG";

public static Map<String, Object> postBuild(Path appClasses, Path pomFile, List<Map.Entry<String, String>> repositories,
List<Map.Entry<String, Path>> dependencies,
List<String> comments, boolean nativeImage) {
for (String comment : comments) {
//we allow config to be provided via //Q:CONFIG name=value
if (comment.startsWith(CONFIG)) {
String conf = comment.substring(CONFIG.length()).trim();
int equals = conf.indexOf("=");
if (equals == -1) {
throw new RuntimeException("invalid config " + comment);
}
System.setProperty(conf.substring(0, equals), conf.substring(equals + 1));
}
}

ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
RuntimeLaunchClassLoader loader = new RuntimeLaunchClassLoader(
new ClassLoader(JBangIntegration.class.getClassLoader()) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("org.")) {
//jbang has some but not all of the maven resolver classes we need on its
//class path. These all start with org. so we filter them out to make sure
//we get a complete class path
throw new ClassNotFoundException();
}
return super.loadClass(name, resolve);
}

@Override
public URL getResource(String name) {
if (name.startsWith("org/")) {
//jbang has some but not all of the maven resolver classes we need on its
//class path. These all start with org. so we filter them out to make sure
//we get a complete class path
return null;
}
return super.getResource(name);
}

@Override
public Enumeration<URL> getResources(String name) throws IOException {
if (name.startsWith("org/")) {
//jbang has some but not all of the maven resolver classes we need on its
//class path. These all start with org. so we filter them out to make sure
//we get a complete class path
return Collections.emptyEnumeration();
}
return super.getResources(name);
}
});
Thread.currentThread().setContextClassLoader(loader);
Class<?> launcher = loader.loadClass("io.quarkus.bootstrap.JBangBuilderImpl");
return (Map<String, Object>) launcher
.getDeclaredMethod("postBuild", Path.class, Path.class, List.class, List.class, boolean.class).invoke(
null,
appClasses,
pomFile,
repositories,
dependencies,
nativeImage);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
System.clearProperty(BootstrapConstants.SERIALIZED_APP_MODEL);
Thread.currentThread().setContextClassLoader(old);
}
}

}
104 changes: 12 additions & 92 deletions core/launcher/src/main/java/io/quarkus/launcher/QuarkusLauncher.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package io.quarkus.launcher;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
Expand All @@ -33,7 +31,15 @@ public static void launch(String callingClass, String quarkusApplication, Consum
path = path.substring(0, path.length() - classResource.length());
URL newResource = new URL(resource.getProtocol(), resource.getHost(), resource.getPort(), path);

Path appClasses = Paths.get(newResource.toURI());
URI uri = newResource.toURI();
Path appClasses;
if ("jar".equals(uri.getScheme())) {
JarURLConnection connection = (JarURLConnection) uri.toURL().openConnection();
connection.setDefaultUseCaches(false);
appClasses = Paths.get(connection.getJarFileURL().toURI());
} else {
appClasses = Paths.get(uri);
}
if (quarkusApplication != null) {
System.setProperty("quarkus.package.main-class", quarkusApplication);
}
Expand All @@ -42,7 +48,7 @@ public static void launch(String callingClass, String quarkusApplication, Consum
context.put("app-classes", appClasses);
context.put("args", args);

IDEClassLoader loader = new IDEClassLoader(QuarkusLauncher.class.getClassLoader());
RuntimeLaunchClassLoader loader = new RuntimeLaunchClassLoader(QuarkusLauncher.class.getClassLoader());
Thread.currentThread().setContextClassLoader(loader);

Class<?> launcher = loader.loadClass("io.quarkus.bootstrap.IDELauncherImpl");
Expand All @@ -55,90 +61,4 @@ public static void launch(String callingClass, String quarkusApplication, Consum
}
}

public static class IDEClassLoader extends ClassLoader {

static {
registerAsParallelCapable();
}

public IDEClassLoader(ClassLoader parent) {
super(parent);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String resourceName = name.replace(".", "/") + ".class";
try {
try (InputStream is = getResourceAsStream(resourceName)) {
if (is == null) {
throw new ClassNotFoundException(name);
}
definePackage(name);
byte[] buf = new byte[1024];
int r;
ByteArrayOutputStream out = new ByteArrayOutputStream();
while ((r = is.read(buf)) > 0) {
out.write(buf, 0, r);
}
byte[] bytes = out.toByteArray();

return defineClass(name, bytes, 0, bytes.length);
}
} catch (IOException e) {
throw new RuntimeException(e);
}

}

private void definePackage(String name) {
final String pkgName = getPackageNameFromClassName(name);
if ((pkgName != null) && getPackage(pkgName) == null) {
synchronized (getClassLoadingLock(pkgName)) {
if (getPackage(pkgName) == null) {
// this could certainly be improved to use the actual manifest
definePackage(pkgName, null, null, null, null, null, null, null);
}
}
}
}

private String getPackageNameFromClassName(String className) {
final int index = className.lastIndexOf('.');
if (index == -1) {
// we return null here since in this case no package is defined
// this is same behavior as Package.getPackage(clazz) exhibits
// when the class is in the default package
return null;
}
return className.substring(0, index);
}

protected Class<?> findClass(String moduleName, String name) {
try {
return findClass(name);
} catch (ClassNotFoundException e) {
return null;
}
}

protected URL findResource(String moduleName, String name) throws IOException {
return findResource(name);
}

@Override
protected URL findResource(String name) {
if (!name.startsWith("/")) {
name = "/" + name;
}
return getParent().getResource("META-INF/ide-deps" + name + ".ide-launcher-res");
}

@Override
protected Enumeration<URL> findResources(String name) throws IOException {
if (!name.startsWith("/")) {
name = "/" + name;
}
return getParent().getResources("META-INF/ide-deps" + name + ".ide-launcher-res");
}
}
}
Loading

0 comments on commit 545eff5

Please sign in to comment.