diff --git a/core/creator/src/main/java/org/jboss/shamrock/creator/demo/AppCreatorDemo.java b/core/creator/src/main/java/org/jboss/shamrock/creator/demo/AppCreatorDemo.java index eb94d65994df5..66b636979b4d9 100644 --- a/core/creator/src/main/java/org/jboss/shamrock/creator/demo/AppCreatorDemo.java +++ b/core/creator/src/main/java/org/jboss/shamrock/creator/demo/AppCreatorDemo.java @@ -30,11 +30,11 @@ import org.jboss.shamrock.creator.AppCreator; import org.jboss.shamrock.creator.phase.augment.AugmentPhase; -import org.jboss.shamrock.creator.phase.curate.PomStateCreationPhase; -import org.jboss.shamrock.creator.phase.curate.PomStateReadingPhase; +import org.jboss.shamrock.creator.phase.curate.CuratePhase; import org.jboss.shamrock.creator.phase.nativeimage.NativeImageOutcome; import org.jboss.shamrock.creator.phase.nativeimage.NativeImagePhase; import org.jboss.shamrock.creator.phase.runnerjar.RunnerJarOutcome; +import org.jboss.shamrock.creator.phase.runnerjar.RunnerJarPhase; import org.jboss.shamrock.creator.util.IoUtils; import org.jboss.shamrock.creator.util.PropertyUtils; @@ -68,42 +68,16 @@ public static void main(String[] args) throws Exception { //buildNativeImage(appJar, demoDir); //curateRunnableJar(appJar, demoDir); - logLibDiff(exampleTarget, demoDir); - } - - private static void curateRunnableJar(Path userApp, Path outputDir) throws Exception { - - try (AppCreator appCreator = AppCreator.builder() - .addPhase(new PomStateCreationPhase()) - .addPhase(new PomStateReadingPhase()) - .setAppJar(userApp) - .setWorkDir(outputDir) - .build()) { - appCreator.resolveOutcome(PomStateReadingPhase.Outcome.class); - } -/* - new AppCreator() - - // enabling debug allows to see resolved application dependencies in the terminal - .setDebug(true) - - // setting a work dir can be useful if you want to see the temporary content used - // by various phases during app building - .setWorkDir(outputDir) - - .addPhase(new PomStateCreationPhase()) - .addPhase(new PomStateReadingPhase()) - //.addPhase(new CuratePhase()) - //.addPhase(new AugmentPhase().setOutputDir(outputDir)) - .create(userApp); - */ + //logLibDiff(exampleTarget, demoDir); } private static void buildRunnableJar(Path userApp, Path outputDir) throws Exception { final RunnerJarOutcome runnerJar; try (AppCreator appCreator = AppCreator.builder() + .addPhase(new CuratePhase()) .addPhase(new AugmentPhase()/*.setOutputDir(outputDir)*/) + .addPhase(new RunnerJarPhase()/*.setOutputDir(outputDir)*/) .addPhase(new NativeImagePhase()) .setAppJar(userApp) .build()) { @@ -117,7 +91,9 @@ private static void buildNativeImage(Path userApp, Path outputDir) throws Except try (AppCreator appCreator = AppCreator.builder() //.setOutput(outputDir) + .addPhase(new CuratePhase()) .addPhase(new AugmentPhase()) + .addPhase(new RunnerJarPhase()) .addPhase(new NativeImagePhase().setOutputDir(outputDir)) .setAppJar(userApp) .build()) { diff --git a/core/creator/src/main/java/org/jboss/shamrock/creator/phase/augment/AugmentOutcome.java b/core/creator/src/main/java/org/jboss/shamrock/creator/phase/augment/AugmentOutcome.java index 6f75ae6fc0d52..a256cb3dd99ee 100644 --- a/core/creator/src/main/java/org/jboss/shamrock/creator/phase/augment/AugmentOutcome.java +++ b/core/creator/src/main/java/org/jboss/shamrock/creator/phase/augment/AugmentOutcome.java @@ -18,6 +18,7 @@ package org.jboss.shamrock.creator.phase.augment; import java.nio.file.Path; +import org.jboss.shamrock.creator.AppDependency; /** * @@ -27,5 +28,9 @@ public interface AugmentOutcome { Path getAppClassesDir(); + Path getTransformedClassesDir(); + Path getWiringClassesDir(); + + boolean isWhitelisted(AppDependency dep); } diff --git a/core/creator/src/main/java/org/jboss/shamrock/creator/phase/augment/AugmentPhase.java b/core/creator/src/main/java/org/jboss/shamrock/creator/phase/augment/AugmentPhase.java index 09ac4879022bf..c528e7d5c0dd8 100644 --- a/core/creator/src/main/java/org/jboss/shamrock/creator/phase/augment/AugmentPhase.java +++ b/core/creator/src/main/java/org/jboss/shamrock/creator/phase/augment/AugmentPhase.java @@ -17,11 +17,8 @@ package org.jboss.shamrock.creator.phase.augment; -import java.io.BufferedOutputStream; import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -31,7 +28,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -45,13 +41,8 @@ import java.util.concurrent.Future; import java.util.function.BiFunction; import java.util.function.Consumer; -import java.util.jar.Attributes; -import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; - import org.eclipse.microprofile.config.Config; import org.jboss.builder.BuildResult; import org.jboss.logging.Logger; @@ -65,7 +56,6 @@ import org.jboss.shamrock.creator.config.reader.PropertiesHandler; import org.jboss.shamrock.creator.outcome.OutcomeProviderRegistration; import org.jboss.shamrock.creator.phase.curate.CurateOutcome; -import org.jboss.shamrock.creator.phase.runnerjar.RunnerJarOutcome; import org.jboss.shamrock.creator.util.IoUtils; import org.jboss.shamrock.creator.util.ZipUtils; import org.jboss.shamrock.deployment.ClassOutput; @@ -86,9 +76,8 @@ * * @author Alexey Loubyansky */ -public class AugmentPhase implements AppCreationPhase, AugmentOutcome, RunnerJarOutcome { +public class AugmentPhase implements AppCreationPhase, AugmentOutcome { - private static final String DEFAULT_MAIN_CLASS = "org.jboss.shamrock.runner.GeneratedMain"; private static final String DEPENDENCIES_RUNTIME = "dependencies.runtime"; private static final String PROVIDED = "provided"; @@ -96,15 +85,9 @@ public class AugmentPhase implements AppCreationPhase, AugmentOutc private Path outputDir; private Path appClassesDir; + private Path transformedClassesDir; private Path wiringClassesDir; - private Path libDir; - private Path runnerJar; - - private String finalName; - - private String mainClass = DEFAULT_MAIN_CLASS; - - private boolean uberJar; + private Set whitelist = new HashSet<>(); /** * Output directory for the outcome of this phase. @@ -133,61 +116,27 @@ public AugmentPhase setAppClassesDir(Path appClassesDir) { } /** - * The directory for generated classes. If none is set by the user, - * wiring-classes directory will be created in the work directory of the creator. - * - * @param wiringClassesDir directory for generated classes - * @return this phase instance - */ - public AugmentPhase setWiringClassesDir(Path wiringClassesDir) { - this.wiringClassesDir = wiringClassesDir; - return this; - } - - /** - * Directory for application dependencies. If none set by the user - * lib directory will be created in the output directory of the phase. - * - * @param libDir directory for project dependencies - * @return this phase instance - */ - public AugmentPhase setLibDir(Path libDir) { - this.libDir = libDir; - return this; - } - - /** - * Name for the runnable JAR. If none is provided by the user - * the name will derived from the user application JAR filename. + * Directory containing transformed application classes. If none is set by + * the user, transformed-classes directory will be created in the work + * directory of the creator. * - * @param finalName runnable JAR name + * @param transformedClassesDir directory for transformed application classes * @return this phase instance */ - public AugmentPhase setFinalName(String finalName) { - this.finalName = finalName; + public AugmentPhase setTransformedClassesDir(Path transformedClassesDir) { + this.transformedClassesDir = transformedClassesDir; return this; } /** - * Main class name fir the runnable JAR. If none is set by the user - * org.jboss.shamrock.runner.GeneratedMain will be use by default. - * - * @param mainClass main class name for the runnable JAR - * @return - */ - public AugmentPhase setMainClass(String mainClass) { - this.mainClass = mainClass; - return this; - } - - /** - * Whether to build an uber JAR. The default is false. + * The directory for generated classes. If none is set by the user, + * wiring-classes directory will be created in the work directory of the creator. * - * @param uberJar whether to build an uber JAR + * @param wiringClassesDir directory for generated classes * @return this phase instance */ - public AugmentPhase setUberJar(boolean uberJar) { - this.uberJar = uberJar; + public AugmentPhase setWiringClassesDir(Path wiringClassesDir) { + this.wiringClassesDir = wiringClassesDir; return this; } @@ -197,24 +146,23 @@ public Path getAppClassesDir() { } @Override - public Path getWiringClassesDir() { - return wiringClassesDir; + public Path getTransformedClassesDir() { + return transformedClassesDir; } @Override - public Path getRunnerJar() { - return runnerJar; + public Path getWiringClassesDir() { + return wiringClassesDir; } @Override - public Path getLibDir() { - return libDir; + public boolean isWhitelisted(AppDependency dep) { + return whitelist.contains(getDependencyConflictId(dep.getArtifact())); } @Override public void register(OutcomeProviderRegistration registration) throws AppCreatorException { registration.provides(AugmentOutcome.class); - registration.provides(RunnerJarOutcome.class); } @Override @@ -224,7 +172,7 @@ public void provideOutcome(AppCreator ctx) throws AppCreatorException { outputDir = outputDir == null ? ctx.getWorkPath() : IoUtils.mkdirs(outputDir); if (appClassesDir == null) { - appClassesDir = ctx.createWorkDir("classes"); + appClassesDir = outputDir.resolve("classes"); final Path appJar = appState.getArtifactResolver().resolve(appState.getAppArtifact()); try { ZipUtils.unzip(appJar, appClassesDir); @@ -237,22 +185,12 @@ public void provideOutcome(AppCreator ctx) throws AppCreatorException { IoUtils.recursiveDelete(metaInf.resolve("MANIFEST.MF")); } - wiringClassesDir = IoUtils.mkdirs(wiringClassesDir == null ? ctx.getWorkPath("wiring-classes") : wiringClassesDir); - - libDir = IoUtils.mkdirs(libDir == null ? outputDir.resolve("lib") : libDir); - - if (finalName == null) { - final String name = appState.getArtifactResolver().resolve(appState.getAppArtifact()).getFileName().toString(); - int i = name.lastIndexOf('.'); - if (i > 0) { - finalName = name.substring(0, i); - } - } + transformedClassesDir = IoUtils.mkdirs(transformedClassesDir == null ? outputDir.resolve("transformed-classes") : transformedClassesDir); + wiringClassesDir = IoUtils.mkdirs(wiringClassesDir == null ? outputDir.resolve("wiring-classes") : wiringClassesDir); doProcess(appState); ctx.pushOutcome(AugmentOutcome.class, this); - ctx.pushOutcome(RunnerJarOutcome.class, this); } private void doProcess(CurateOutcome appState) throws AppCreatorException { @@ -276,16 +214,24 @@ private void doProcess(CurateOutcome appState) throws AppCreatorException { final List appDeps = appState.getEffectiveDeps(); try { - StringBuilder classPath = new StringBuilder(); - List problems = new ArrayList<>(); - Set whitelist = new HashSet<>(); + // we need to make sure all the deployment artifacts are on the class path + final List cpUrls = new ArrayList<>(); + cpUrls.add(appClassesDir.toUri().toURL()); + + List problems = null; for (AppDependency appDep : appDeps) { - final AppArtifact depCoords = appDep.getArtifact(); - if (!"jar".equals(depCoords.getType())) { + final AppArtifact depArtifact = appDep.getArtifact(); + final Path resolvedDep = depResolver.resolve(depArtifact); + cpUrls.add(resolvedDep.toUri().toURL()); + + if (!"jar".equals(depArtifact.getType())) { continue; } - try (ZipFile zip = openZipFile(depResolver.resolve(depCoords))) { + try (ZipFile zip = openZipFile(resolvedDep)) { if (!appDep.getScope().equals(PROVIDED) && zip.getEntry("META-INF/services/org.jboss.shamrock.deployment.ShamrockSetup") != null) { + if(problems == null) { + problems = new ArrayList<>(); + } problems.add("Artifact " + appDep + " is a deployment artifact, however it does not have scope required. This will result in unnecessary jars being included in the final image"); } ZipEntry deps = zip.getEntry(DEPENDENCIES_RUNTIME); @@ -312,226 +258,115 @@ private void doProcess(CurateOutcome appState) throws AppCreatorException { } } } - } } - if (!problems.isEmpty()) { + if (problems != null) { //TODO: add a config option to just log an error instead throw new AppCreatorException(problems.toString()); } - Set seen = new HashSet<>(); - runnerJar = outputDir.resolve(finalName + "-runner.jar"); - log.info("Building jar: " + runnerJar); - try (ZipOutputStream runner = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(runnerJar)))) { - Map> services = new HashMap<>(); - - for (AppDependency appDep : appDeps) { - final AppArtifact depCoords = appDep.getArtifact(); - if (appDep.getScope().equals(PROVIDED) && !whitelist.contains(getDependencyConflictId(depCoords))) { - continue; - } - if (depCoords.getArtifactId().equals("svm") && depCoords.getGroupId().equals("com.oracle.substratevm")) { - continue; - } - final File artifactFile = depResolver.resolve(depCoords).toFile(); - if (uberJar) { - try (ZipInputStream in = new ZipInputStream(new FileInputStream(artifactFile))) { - for (ZipEntry e = in.getNextEntry(); e != null; e = in.getNextEntry()) { - if (e.getName().startsWith("META-INF/services/") && e.getName().length() > 18) { - services.computeIfAbsent(e.getName(), (u) -> new ArrayList<>()).add(read(in)); - continue; - } else if (e.getName().equals("META-INF/MANIFEST.MF")) { - continue; - } - if (!seen.add(e.getName())) { - if (!e.getName().endsWith("/")) { - log.warn("Duplicate entry " + e.getName() + " entry from " + appDep + " will be ignored"); - } - continue; - } - runner.putNextEntry(new ZipEntry(e.getName())); - doCopy(runner, in); - } - } - } else { - final String fileName = depCoords.getGroupId() + "." + artifactFile.getName(); - final Path targetPath = libDir.resolve(fileName); - Files.copy(artifactFile.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING); - classPath.append(" lib/" + fileName); + final URLClassLoader runnerClassLoader = new URLClassLoader(cpUrls.toArray(new URL[cpUrls.size()]), getClass().getClassLoader()); + final Path wiringClassesDirectory = wiringClassesDir; + ClassOutput classOutput = new ClassOutput() { + @Override + public void writeClass(boolean applicationClass, String className, byte[] data) throws IOException { + String location = className.replace('.', '/'); + final Path p = wiringClassesDirectory.resolve(location + ".class"); + Files.createDirectories(p.getParent()); + try (OutputStream out = Files.newOutputStream(p)) { + out.write(data); } } - List classPathUrls = new ArrayList<>(); - for (AppDependency appDep : appDeps) { - final AppArtifact depCoords = appDep.getArtifact(); - final Path p = depResolver.resolve(depCoords); - classPathUrls.add(p.toUri().toURL()); - } - - //we need to make sure all the deployment artifacts are on the class path - //to do this we need to create a new class loader to actually use for the runner - List cpCopy = new ArrayList<>(); - - cpCopy.add(appClassesDir.toUri().toURL()); - cpCopy.addAll(classPathUrls); - - URLClassLoader runnerClassLoader = new URLClassLoader(cpCopy.toArray(new URL[0]), getClass().getClassLoader()); - final Path wiringClassesDirectory = wiringClassesDir; - ClassOutput classOutput = new ClassOutput() { - @Override - public void writeClass(boolean applicationClass, String className, byte[] data) throws IOException { - String location = className.replace('.', '/'); - final Path p = wiringClassesDirectory.resolve(location + ".class"); - Files.createDirectories(p.getParent()); - try (OutputStream out = Files.newOutputStream(p)) { - out.write(data); - } + @Override + public void writeResource(String name, byte[] data) throws IOException { + final Path p = wiringClassesDirectory.resolve(name); + Files.createDirectories(p.getParent()); + try (OutputStream out = Files.newOutputStream(p)) { + out.write(data); } - - @Override - public void writeResource(String name, byte[] data) throws IOException { - final Path p = wiringClassesDirectory.resolve(name); - Files.createDirectories(p.getParent()); - try (OutputStream out = Files.newOutputStream(p)) { - out.write(data); - } - } - }; - - - ClassLoader old = Thread.currentThread().getContextClassLoader(); - BuildResult result; - try { - Thread.currentThread().setContextClassLoader(runnerClassLoader); - - ShamrockAugmentor.Builder builder = ShamrockAugmentor.builder(); - builder.setRoot(appClassesDir); - builder.setClassLoader(runnerClassLoader); - builder.setOutput(classOutput); - builder.addFinal(BytecodeTransformerBuildItem.class) - .addFinal(MainClassBuildItem.class) - .addFinal(SubstrateOutputBuildItem.class); - result = builder.build().run(); - } finally { - Thread.currentThread().setContextClassLoader(old); } + }; - Map>> bytecodeTransformers = new HashMap<>(); - List bytecodeTransformerBuildItems = result.consumeMulti(BytecodeTransformerBuildItem.class); + ClassLoader old = Thread.currentThread().getContextClassLoader(); + BuildResult result; + try { + Thread.currentThread().setContextClassLoader(runnerClassLoader); + + ShamrockAugmentor.Builder builder = ShamrockAugmentor.builder(); + builder.setRoot(appClassesDir); + builder.setClassLoader(runnerClassLoader); + builder.setOutput(classOutput); + builder.addFinal(BytecodeTransformerBuildItem.class).addFinal(MainClassBuildItem.class) + .addFinal(SubstrateOutputBuildItem.class); + result = builder.build().run(); + } finally { + Thread.currentThread().setContextClassLoader(old); + } + + final List bytecodeTransformerBuildItems = result.consumeMulti(BytecodeTransformerBuildItem.class); + if (!bytecodeTransformerBuildItems.isEmpty()) { + final Map>> bytecodeTransformers = new HashMap<>(bytecodeTransformerBuildItems.size()); if (!bytecodeTransformerBuildItems.isEmpty()) { for (BytecodeTransformerBuildItem i : bytecodeTransformerBuildItems) { bytecodeTransformers.computeIfAbsent(i.getClassToTransform(), (h) -> new ArrayList<>()).add(i.getVisitorFunction()); } } - Files.walk(wiringClassesDirectory).forEach(new Consumer() { - @Override - public void accept(Path path) { - try { - String pathName = wiringClassesDirectory.relativize(path).toString(); - if (Files.isDirectory(path)) { - String p = pathName + "/"; - if (seen.contains(p)) { - return; - } - seen.add(p); - if (!pathName.isEmpty()) { - runner.putNextEntry(new ZipEntry(p)); - } - } else if (pathName.startsWith("META-INF/services/") && pathName.length() > 18) { - services.computeIfAbsent(pathName, (u) -> new ArrayList<>()).add(CopyUtils.readFileContent(path)); - } else { - seen.add(pathName); - runner.putNextEntry(new ZipEntry(pathName)); - try (FileInputStream in = new FileInputStream(path.toFile())) { - doCopy(runner, in); - } - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }); - - Manifest manifest = new Manifest(); - manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); - manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, classPath.toString()); - manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, mainClass); - runner.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); - manifest.write(runner); - //now copy all the contents to the runner jar - //I am not 100% sure about this idea, but if we are going to support bytecode transforms it seems - //like the cleanest way to do it - //at the end of the PoC phase all this needs review - Path appJar = appClassesDir; - ExecutorService executorPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - ConcurrentLinkedDeque> transformed = new ConcurrentLinkedDeque<>(); + // now copy all the contents to the runner jar + // I am not 100% sure about this idea, but if we are going to support bytecode transforms it seems + // like the cleanest way to do it + // at the end of the PoC phase all this needs review + final ExecutorService executorPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + final ConcurrentLinkedDeque> transformed = new ConcurrentLinkedDeque<>(); try { - Files.walk(appJar).forEach(new Consumer() { + Files.walk(appClassesDir).forEach(new Consumer() { @Override public void accept(Path path) { - try { - final String pathName = appJar.relativize(path).toString(); - if (Files.isDirectory(path)) { -// if (!pathName.isEmpty()) { -// out.putNextEntry(new ZipEntry(pathName + "/")); -// } - } else if (pathName.endsWith(".class") && !bytecodeTransformers.isEmpty()) { - String className = pathName.substring(0, pathName.length() - 6).replace('/', '.'); - List> visitors = bytecodeTransformers.get(className); - - if (visitors == null || visitors.isEmpty()) { - runner.putNextEntry(new ZipEntry(pathName)); - try (FileInputStream in = new FileInputStream(path.toFile())) { - doCopy(runner, in); - } - } else { - transformed.add(executorPool.submit(new Callable() { - @Override - public FutureEntry call() throws Exception { - final byte[] fileContent = CopyUtils.readFileContent(path); - ClassReader cr = new ClassReader(fileContent); - ClassWriter writer = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); - ClassVisitor visitor = writer; - for (BiFunction i : visitors) { - visitor = i.apply(className, visitor); - } - cr.accept(visitor, 0); - return new FutureEntry(writer.toByteArray(), pathName); - } - })); - } - } else { - runner.putNextEntry(new ZipEntry(pathName)); - try (FileInputStream in = new FileInputStream(path.toFile())) { - doCopy(runner, in); + if (Files.isDirectory(path)) { + return; + } + final String pathName = appClassesDir.relativize(path).toString(); + if (!pathName.endsWith(".class") || bytecodeTransformers.isEmpty()) { + return; + } + final String className = pathName.substring(0, pathName.length() - 6).replace('/', '.'); + final List> visitors = bytecodeTransformers.get(className); + if (visitors == null || visitors.isEmpty()) { + return; + } + transformed.add(executorPool.submit(new Callable() { + @Override + public FutureEntry call() throws Exception { + final byte[] fileContent = CopyUtils.readFileContent(path); + ClassReader cr = new ClassReader(fileContent); + ClassWriter writer = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + ClassVisitor visitor = writer; + for (BiFunction i : visitors) { + visitor = i.apply(className, visitor); } + cr.accept(visitor, 0); + return new FutureEntry(writer.toByteArray(), pathName); } - } catch (Exception e) { - throw new RuntimeException(e); - } + })); } }); - for (Future i : transformed) { - - FutureEntry res = i.get(); - runner.putNextEntry(new ZipEntry(res.location)); - runner.write(res.data); - } } finally { executorPool.shutdown(); } - for (Map.Entry> entry : services.entrySet()) { - runner.putNextEntry(new ZipEntry(entry.getKey())); - for (byte[] i : entry.getValue()) { - runner.write(i); - runner.write('\n'); + if (!transformed.isEmpty()) { + for (Future i : transformed) { + final FutureEntry res = i.get(); + final Path classFile = transformedClassesDir.resolve(res.location); + Files.createDirectories(classFile.getParent()); + try(OutputStream out = Files.newOutputStream(classFile)) { + IoUtils.copy(out, new ByteArrayInputStream(res.data)); + } } } } } catch (Exception e) { - throw new AppCreatorException("Failed to run", e); + throw new AppCreatorException("Failed to augment application classes", e); } } @@ -560,24 +395,6 @@ private ZipFile openZipFile(Path p) { } } - private static void doCopy(OutputStream out, InputStream in) throws IOException { - byte[] buffer = new byte[1024]; - int r; - while ((r = in.read(buffer)) > 0) { - out.write(buffer, 0, r); - } - } - - private static byte[] read(InputStream in) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int r; - while ((r = in.read(buffer)) > 0) { - out.write(buffer, 0, r); - } - return out.toByteArray(); - } - private static final class FutureEntry { final byte[] data; final String location; @@ -590,7 +407,7 @@ private FutureEntry(byte[] data, String location) { @Override public String getConfigPropertyName() { - return "augment"; + return "augment-only"; } @Override @@ -603,10 +420,6 @@ public AugmentPhase getTarget() { } .map("output", (AugmentPhase t, String value) -> t.setOutputDir(Paths.get(value))) .map("classes", (AugmentPhase t, String value) -> t.setAppClassesDir(Paths.get(value))) - .map("wiring-classes", (AugmentPhase t, String value) -> t.setWiringClassesDir(Paths.get(value))) - .map("lib", (AugmentPhase t, String value) -> t.setLibDir(Paths.get(value))) - .map("final-name", AugmentPhase::setFinalName) - .map("main-class", AugmentPhase::setMainClass) - .map("uber-jar", (AugmentPhase t, String value) -> t.setUberJar(Boolean.parseBoolean(value))); + .map("wiring-classes", (AugmentPhase t, String value) -> t.setWiringClassesDir(Paths.get(value))); } } diff --git a/core/creator/src/main/java/org/jboss/shamrock/creator/phase/runnerjar/RunnerJarPhase.java b/core/creator/src/main/java/org/jboss/shamrock/creator/phase/runnerjar/RunnerJarPhase.java new file mode 100644 index 0000000000000..0e0c2e90e562f --- /dev/null +++ b/core/creator/src/main/java/org/jboss/shamrock/creator/phase/runnerjar/RunnerJarPhase.java @@ -0,0 +1,350 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jboss.shamrock.creator.phase.runnerjar; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystem; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import org.jboss.logging.Logger; +import org.jboss.shamrock.creator.AppArtifact; +import org.jboss.shamrock.creator.AppArtifactResolver; +import org.jboss.shamrock.creator.AppCreationPhase; +import org.jboss.shamrock.creator.AppCreator; +import org.jboss.shamrock.creator.AppCreatorException; +import org.jboss.shamrock.creator.AppDependency; +import org.jboss.shamrock.creator.config.reader.MappedPropertiesHandler; +import org.jboss.shamrock.creator.config.reader.PropertiesHandler; +import org.jboss.shamrock.creator.outcome.OutcomeProviderRegistration; +import org.jboss.shamrock.creator.phase.augment.AugmentOutcome; +import org.jboss.shamrock.creator.phase.curate.CurateOutcome; +import org.jboss.shamrock.creator.util.IoUtils; +import org.jboss.shamrock.creator.util.ZipUtils; +import org.jboss.shamrock.dev.CopyUtils; + +/** + * This phase at the moment actually combines augmentation and runnable JAR building. + * + * @author Alexey Loubyansky + */ +public class RunnerJarPhase implements AppCreationPhase, RunnerJarOutcome { + + private static final String DEFAULT_MAIN_CLASS = "org.jboss.shamrock.runner.GeneratedMain"; + private static final String PROVIDED = "provided"; + + private static final Logger log = Logger.getLogger(RunnerJarPhase.class); + + private Path outputDir; + private Path libDir; + private Path runnerJar; + + private String finalName; + + private String mainClass = DEFAULT_MAIN_CLASS; + + private boolean uberJar; + + /** + * Output directory for the outcome of this phase. + * If not set by the user the work directory of the creator + * will be used instead. + * + * @param outputDir output directory for this phase + * @return this phase instance + */ + public RunnerJarPhase setOutputDir(Path outputDir) { + this.outputDir = outputDir; + return this; + } + + /** + * Directory for application dependencies. If none set by the user + * lib directory will be created in the output directory of the phase. + * + * @param libDir directory for project dependencies + * @return this phase instance + */ + public RunnerJarPhase setLibDir(Path libDir) { + this.libDir = libDir; + return this; + } + + /** + * Name for the runnable JAR. If none is provided by the user + * the name will derived from the user application JAR filename. + * + * @param finalName runnable JAR name + * @return this phase instance + */ + public RunnerJarPhase setFinalName(String finalName) { + this.finalName = finalName; + return this; + } + + /** + * Main class name fir the runnable JAR. If none is set by the user + * org.jboss.shamrock.runner.GeneratedMain will be use by default. + * + * @param mainClass main class name for the runnable JAR + * @return + */ + public RunnerJarPhase setMainClass(String mainClass) { + this.mainClass = mainClass; + return this; + } + + /** + * Whether to build an uber JAR. The default is false. + * + * @param uberJar whether to build an uber JAR + * @return this phase instance + */ + public RunnerJarPhase setUberJar(boolean uberJar) { + this.uberJar = uberJar; + return this; + } + + @Override + public Path getRunnerJar() { + return runnerJar; + } + + @Override + public Path getLibDir() { + return libDir; + } + + @Override + public void register(OutcomeProviderRegistration registration) throws AppCreatorException { + registration.provides(RunnerJarOutcome.class); + } + + @Override + public void provideOutcome(AppCreator ctx) throws AppCreatorException { + final CurateOutcome appState = ctx.resolveOutcome(CurateOutcome.class); + + outputDir = outputDir == null ? ctx.getWorkPath() : IoUtils.mkdirs(outputDir); + + libDir = IoUtils.mkdirs(libDir == null ? outputDir.resolve("lib") : libDir); + + if (finalName == null) { + final String name = appState.getArtifactResolver().resolve(appState.getAppArtifact()).getFileName().toString(); + int i = name.lastIndexOf('.'); + if (i > 0) { + finalName = name.substring(0, i); + } + } + + runnerJar = outputDir.resolve(finalName + "-runner.jar"); + + try (FileSystem zipFs = ZipUtils.newZip(runnerJar)) { + buildRunner(zipFs, appState, ctx.resolveOutcome(AugmentOutcome.class)); + } catch (Exception e) { + throw new AppCreatorException("Failed to build a runner jar", e); + } + + ctx.pushOutcome(RunnerJarOutcome.class, this); + } + + private void buildRunner(FileSystem runnerZipFs, CurateOutcome appState, AugmentOutcome augmentOutcome) throws Exception { + + log.info("Building jar: " + runnerJar); + + final AppArtifactResolver depResolver = appState.getArtifactResolver(); + final List appDeps = appState.getEffectiveDeps(); + final Set seen = new HashSet<>(); + final StringBuilder classPath = new StringBuilder(); + final Map> services = new HashMap<>(); + + for (AppDependency appDep : appDeps) { + if (appDep.getScope().equals(PROVIDED) && !augmentOutcome.isWhitelisted(appDep)) { + continue; + } + final AppArtifact depArtifact = appDep.getArtifact(); + if (depArtifact.getArtifactId().equals("svm") && depArtifact.getGroupId().equals("com.oracle.substratevm")) { + continue; + } + final Path resolvedDep = depResolver.resolve(depArtifact); + if (uberJar) { + try(FileSystem artifactFs = ZipUtils.newFileSystem(resolvedDep)) { + for(final Path root : artifactFs.getRootDirectories()) { + Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, + new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + addDir(runnerZipFs, dir, root.relativize(dir).toString()); + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + final String relativePath = root.relativize(file).toString(); + if (relativePath.startsWith("META-INF/services/") && relativePath.length() > 18) { + services.computeIfAbsent(relativePath, (u) -> new ArrayList<>()).add(read(file)); + } else if (!relativePath.equals("META-INF/MANIFEST.MF")) { + if (seen.add(relativePath)) { + Files.copy(file, runnerZipFs.getPath(relativePath), StandardCopyOption.REPLACE_EXISTING); + } else { + log.warn("Duplicate entry " + relativePath + " entry from " + appDep + " will be ignored"); + } + } + return FileVisitResult.CONTINUE; + } + }); + } + + } + } else { + final String fileName = depArtifact.getGroupId() + "." + resolvedDep.getFileName(); + final Path targetPath = libDir.resolve(fileName); + Files.copy(resolvedDep, targetPath, StandardCopyOption.REPLACE_EXISTING); + classPath.append(" lib/" + fileName); + } + } + + final Path wiringClassesDir = augmentOutcome.getWiringClassesDir(); + Files.walk(wiringClassesDir).forEach(new Consumer() { + @Override + public void accept(Path path) { + try { + final String relativePath = wiringClassesDir.relativize(path).toString(); + if (Files.isDirectory(path)) { + if (seen.add(relativePath + "/") && !relativePath.isEmpty()) { + addDir(runnerZipFs, path, relativePath); + } + return; + } + if (relativePath.startsWith("META-INF/services/") && relativePath.length() > 18) { + services.computeIfAbsent(relativePath, (u) -> new ArrayList<>()).add(CopyUtils.readFileContent(path)); + return; + } + seen.add(relativePath); + Files.copy(path, runnerZipFs.getPath(relativePath), StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + + final Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, classPath.toString()); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, mainClass); + try(OutputStream os = Files.newOutputStream(runnerZipFs.getPath("META-INF", "MANIFEST.MF"))) { + manifest.write(os); + } + + copyFiles(augmentOutcome.getAppClassesDir(), runnerZipFs); + copyFiles(augmentOutcome.getTransformedClassesDir(), runnerZipFs); + + for (Map.Entry> entry : services.entrySet()) { + try(OutputStream os = Files.newOutputStream(runnerZipFs.getPath(entry.getKey()))) { + for (byte[] i : entry.getValue()) { + os.write(i); + os.write('\n'); + } + } + } + } + + private void copyFiles(Path dir, FileSystem fs) throws IOException { + Files.walk(dir).forEach(new Consumer() { + @Override + public void accept(Path path) { + final String relativePath = dir.relativize(path).toString(); + if(relativePath.isEmpty()) { + return; + } + try { + if (Files.isDirectory(path)) { + addDir(fs, path, relativePath); + } else { + Files.copy(path, fs.getPath(relativePath), StandardCopyOption.REPLACE_EXISTING); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + + private void addDir(FileSystem fs, Path dir, final String relativePath) + throws IOException, FileAlreadyExistsException { + final Path targetDir = fs.getPath(relativePath); + try { + Files.copy(dir, targetDir); + } catch (FileAlreadyExistsException e) { + if (!Files.isDirectory(targetDir)) { + throw e; + } + } + } + + private static byte[] read(Path p) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int r; + try (InputStream in = Files.newInputStream(p)) { + while ((r = in.read(buffer)) > 0) { + out.write(buffer, 0, r); + } + } + return out.toByteArray(); + } + + @Override + public String getConfigPropertyName() { + return "runner-jar"; + } + + @Override + public PropertiesHandler getPropertiesHandler() { + return new MappedPropertiesHandler() { + @Override + public RunnerJarPhase getTarget() { + return RunnerJarPhase.this; + } + } + .map("output", (RunnerJarPhase t, String value) -> t.setOutputDir(Paths.get(value))) + .map("lib", (RunnerJarPhase t, String value) -> t.setLibDir(Paths.get(value))) + .map("final-name", RunnerJarPhase::setFinalName) + .map("main-class", RunnerJarPhase::setMainClass) + .map("uber-jar", (RunnerJarPhase t, String value) -> t.setUberJar(Boolean.parseBoolean(value))); + } +} diff --git a/core/creator/src/main/java/org/jboss/shamrock/creator/util/IoUtils.java b/core/creator/src/main/java/org/jboss/shamrock/creator/util/IoUtils.java index 77a8ffa0aaa13..bc7a1f11c319e 100644 --- a/core/creator/src/main/java/org/jboss/shamrock/creator/util/IoUtils.java +++ b/core/creator/src/main/java/org/jboss/shamrock/creator/util/IoUtils.java @@ -19,6 +19,8 @@ import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; @@ -42,7 +44,6 @@ public class IoUtils { private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; - private static char[] charBuffer; private static final Path TMP_DIR = Paths.get(PropertyUtils.getProperty("java.io.tmpdir")); @@ -137,9 +138,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) } public static String readFile(Path file) throws IOException { - if(charBuffer == null) { - charBuffer = new char[DEFAULT_BUFFER_SIZE]; - } + final char[] charBuffer = new char[DEFAULT_BUFFER_SIZE]; int n = 0; final StringWriter output = new StringWriter(); try (BufferedReader input = Files.newBufferedReader(file)) { @@ -150,6 +149,14 @@ public static String readFile(Path file) throws IOException { return output.getBuffer().toString(); } + public static void copy(OutputStream out, InputStream in) throws IOException { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int r; + while ((r = in.read(buffer)) > 0) { + out.write(buffer, 0, r); + } + } + public static void writeFile(Path file, String content) throws IOException { Files.write(file, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); } diff --git a/core/creator/src/main/java/org/jboss/shamrock/creator/util/ZipUtils.java b/core/creator/src/main/java/org/jboss/shamrock/creator/util/ZipUtils.java index cbaf4b7042502..5bd0de592454d 100644 --- a/core/creator/src/main/java/org/jboss/shamrock/creator/util/ZipUtils.java +++ b/core/creator/src/main/java/org/jboss/shamrock/creator/util/ZipUtils.java @@ -89,7 +89,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) } public static void zip(Path src, Path zipFile) throws IOException { - try (FileSystem zipfs = newFileSystem(toZipUri(zipFile), Files.exists(zipFile) ? Collections.emptyMap() : CREATE_ENV)) { + try (FileSystem zipfs = newZip(zipFile)) { if(Files.isDirectory(src)) { try (DirectoryStream stream = Files.newDirectoryStream(src)) { for(Path srcPath : stream) { @@ -102,6 +102,10 @@ public static void zip(Path src, Path zipFile) throws IOException { } } + public static FileSystem newZip(Path zipFile) throws IOException { + return newFileSystem(toZipUri(zipFile), Files.exists(zipFile) ? Collections.emptyMap() : CREATE_ENV); + } + private static void copyToZip(Path srcRoot, Path srcPath, FileSystem zipfs) throws IOException { Files.walkFileTree(srcPath, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor() { diff --git a/core/creator/src/main/resources/META-INF/services/org.jboss.shamrock.creator.AppCreationPhase b/core/creator/src/main/resources/META-INF/services/org.jboss.shamrock.creator.AppCreationPhase index c9cbf96873a32..0b7763e4367ba 100644 --- a/core/creator/src/main/resources/META-INF/services/org.jboss.shamrock.creator.AppCreationPhase +++ b/core/creator/src/main/resources/META-INF/services/org.jboss.shamrock.creator.AppCreationPhase @@ -1,3 +1,4 @@ -org.jboss.shamrock.creator.phase.curate.CuratePhase org.jboss.shamrock.creator.phase.augment.AugmentPhase +org.jboss.shamrock.creator.phase.curate.CuratePhase org.jboss.shamrock.creator.phase.nativeimage.NativeImagePhase +org.jboss.shamrock.creator.phase.runnerjar.RunnerJarPhase diff --git a/maven/src/main/java/org/jboss/shamrock/maven/BuildMojo.java b/maven/src/main/java/org/jboss/shamrock/maven/BuildMojo.java index 3c1d72996121b..ed3fffb35535a 100644 --- a/maven/src/main/java/org/jboss/shamrock/maven/BuildMojo.java +++ b/maven/src/main/java/org/jboss/shamrock/maven/BuildMojo.java @@ -38,6 +38,7 @@ import org.jboss.shamrock.creator.phase.augment.AugmentPhase; import org.jboss.shamrock.creator.phase.curate.CurateOutcome; import org.jboss.shamrock.creator.phase.runnerjar.RunnerJarOutcome; +import org.jboss.shamrock.creator.phase.runnerjar.RunnerJarPhase; import org.jboss.shamrock.creator.resolver.maven.ResolvedMavenArtifactDeps; /** @@ -91,6 +92,12 @@ public class BuildMojo extends AbstractMojo { @Parameter(defaultValue = "${project}", readonly = true, required = true) protected MavenProject project; + /** + * The directory for application classes transformed by processing. + */ + @Parameter(defaultValue = "${project.build.directory}/transformed-classes") + private File transformedClassesDirectory; + /** * The directory for classes generated by processing. */ @@ -125,15 +132,17 @@ public BuildMojo() { public void execute() throws MojoExecutionException, MojoFailureException { try(AppCreator appCreator = AppCreator.builder() - // configure the build phase we want the app to go through + // configure the build phases we want the app to go through .addPhase(new AugmentPhase() - .setOutputDir(buildDir.toPath()) .setAppClassesDir(outputDirectory.toPath()) - .setWiringClassesDir(wiringClassesDirectory.toPath()) + .setTransformedClassesDir(transformedClassesDirectory.toPath()) + .setWiringClassesDir(wiringClassesDirectory.toPath())) + .addPhase(new RunnerJarPhase() .setLibDir(libDir.toPath()) .setFinalName(finalName) .setMainClass(mainClass) .setUberJar(uberJar)) + .setWorkDir(buildDir.toPath()) .build()) { final AppArtifact appArtifact = new AppArtifact(project.getGroupId(), project.getArtifactId(), project.getVersion()); diff --git a/maven/src/main/java/org/jboss/shamrock/maven/NativeImageMojo.java b/maven/src/main/java/org/jboss/shamrock/maven/NativeImageMojo.java index 28c8883a062c2..47dbd727f94a8 100644 --- a/maven/src/main/java/org/jboss/shamrock/maven/NativeImageMojo.java +++ b/maven/src/main/java/org/jboss/shamrock/maven/NativeImageMojo.java @@ -29,6 +29,7 @@ import org.apache.maven.project.MavenProject; import org.jboss.shamrock.creator.AppCreator; import org.jboss.shamrock.creator.AppCreatorException; +import org.jboss.shamrock.creator.AppDependency; import org.jboss.shamrock.creator.phase.augment.AugmentOutcome; import org.jboss.shamrock.creator.phase.nativeimage.NativeImageOutcome; import org.jboss.shamrock.creator.phase.nativeimage.NativeImagePhase; @@ -49,9 +50,6 @@ public class NativeImageMojo extends AbstractMojo { @Parameter(readonly = true, required = true, defaultValue = "${project.build.directory}") private File outputDirectory; - @Parameter(defaultValue = "${project.build.directory}/wiring-classes") - private File wiringClassesDirectory; - @Parameter(defaultValue = "false") private boolean reportErrorsAtRuntime; @@ -164,8 +162,19 @@ public Path getAppClassesDir() { return classesDir; } @Override + public Path getTransformedClassesDir() { + // not relevant for this mojo + throw new UnsupportedOperationException(); + } + @Override public Path getWiringClassesDir() { - return wiringClassesDirectory.toPath(); + // not relevant for this mojo + throw new UnsupportedOperationException(); + } + @Override + public boolean isWhitelisted(AppDependency dep) { + // not relevant for this mojo + throw new UnsupportedOperationException(); } }) .pushOutcome(RunnerJarOutcome.class, new RunnerJarOutcome() {