diff --git a/bom/pom.xml b/bom/pom.xml
index 8c17bb94f3c89..035bd00cefcb3 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -419,6 +419,18 @@
${project.version}
provided
+
+ io.quarkus
+ quarkus-command-line-runner
+ ${project.version}
+ provided
+
+
+ io.quarkus
+ quarkus-command-line-aesh
+ ${project.version}
+ provided
+
diff --git a/build-parent/pom.xml b/build-parent/pom.xml
index c592236aa0c64..09750aa4a0b2f 100644
--- a/build-parent/pom.xml
+++ b/build-parent/pom.xml
@@ -541,6 +541,26 @@
quarkus-legacy-launcher
${project.version}
+
+ io.quarkus
+ quarkus-command-line-runner
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-command-line-runner-runtime
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-command-line-aesh
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-command-line-aesh-runtime
+ ${project.version}
+
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainAfterStartupBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainAfterStartupBuildItem.java
new file mode 100644
index 0000000000000..712d4d0d37111
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainAfterStartupBuildItem.java
@@ -0,0 +1,39 @@
+package io.quarkus.deployment.builditem;
+
+import java.util.function.Consumer;
+
+import org.jboss.builder.item.SimpleBuildItem;
+
+import io.quarkus.gizmo.MethodCreator;
+import io.quarkus.gizmo.ResultHandle;
+
+public final class MainAfterStartupBuildItem extends SimpleBuildItem {
+
+ private final Consumer bytecodeCreator;
+
+ public MainAfterStartupBuildItem(Consumer bytecodeCreator) {
+ this.bytecodeCreator = bytecodeCreator;
+ }
+
+ public Consumer getBytecodeCreator() {
+ return bytecodeCreator;
+ }
+
+ public static class Input {
+ private final MethodCreator doStartMethod;
+ private final ResultHandle mainArgs;
+
+ public Input(MethodCreator doStartMethod, ResultHandle mainArgs) {
+ this.doStartMethod = doStartMethod;
+ this.mainArgs = mainArgs;
+ }
+
+ public MethodCreator getDoStartMethod() {
+ return doStartMethod;
+ }
+
+ public ResultHandle getMainArgs() {
+ return mainArgs;
+ }
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ShutdownBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ShutdownBuildItem.java
new file mode 100644
index 0000000000000..ba6b10bc1b84e
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ShutdownBuildItem.java
@@ -0,0 +1,6 @@
+package io.quarkus.deployment.builditem;
+
+import org.jboss.builder.item.SimpleBuildItem;
+
+public final class ShutdownBuildItem extends SimpleBuildItem {
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java
index 59be7ea0fccdc..1628d8fc375b2 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java
@@ -36,9 +36,11 @@
import io.quarkus.deployment.builditem.ClassOutputBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.JavaLibraryPathAdditionalPathBuildItem;
+import io.quarkus.deployment.builditem.MainAfterStartupBuildItem;
import io.quarkus.deployment.builditem.MainBytecodeRecorderBuildItem;
import io.quarkus.deployment.builditem.MainClassBuildItem;
import io.quarkus.deployment.builditem.ObjectSubstitutionBuildItem;
+import io.quarkus.deployment.builditem.ShutdownBuildItem;
import io.quarkus.deployment.builditem.SslTrustStoreSystemPropertyBuildItem;
import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
@@ -76,6 +78,8 @@ MainClassBuildItem build(List staticInitTasks,
List features,
BuildProducer appClassNameProducer,
List loaders,
+ Optional mainAfterStartupBuildItem,
+ Optional shutdownBuildItem,
ClassOutputBuildItem classOutput) {
String appClassName = APP_CLASS + COUNT.incrementAndGet();
@@ -131,6 +135,7 @@ MainClassBuildItem build(List staticInitTasks,
mv = file.getMethodCreator("doStart", void.class, String[].class);
mv.setModifiers(Modifier.PROTECTED | Modifier.FINAL);
+ ResultHandle mainMethodArgs = mv.getMethodParam(0);
// very first thing is to set system props (for run time, which use substitutions for a different
// storage from build-time)
@@ -214,8 +219,23 @@ MainClassBuildItem build(List staticInitTasks,
cb.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cb.getCaughtException());
cb.invokeVirtualMethod(ofMethod(StartupContext.class, "close", void.class), startupContext);
cb.throwException(RuntimeException.class, "Failed to start quarkus", cb.getCaughtException());
+
+ // Application.class: start method after startup
+ if (mainAfterStartupBuildItem.isPresent()) {
+ mainAfterStartupBuildItem.get().getBytecodeCreator().accept(
+ new MainAfterStartupBuildItem.Input(mv, mainMethodArgs));
+ }
+
mv.returnValue(null);
+ // Application class: doPostStart method
+ if (shutdownBuildItem.isPresent()) {
+ mv = file.getMethodCreator("doPostStart", void.class);
+ mv.setModifiers(Modifier.PROTECTED);
+ mv.invokeSpecialMethod(ofMethod(Application.class, "requestShutdown", void.class), mv.getThis());
+ mv.returnValue(null);
+ }
+
// Application class: stop method
mv = file.getMethodCreator("doStop", void.class);
diff --git a/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java b/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java
index 9dd58fed4b9f7..e4f14660198e2 100644
--- a/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java
+++ b/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java
@@ -35,6 +35,7 @@
import io.quarkus.deployment.QuarkusAugmentor;
import io.quarkus.deployment.builditem.ApplicationClassNameBuildItem;
import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem;
+import io.quarkus.deployment.builditem.ShutdownBuildItem;
import io.quarkus.runtime.Application;
import io.quarkus.runtime.LaunchMode;
@@ -108,6 +109,17 @@ public void run() {
transformerTarget.setTransformers(functions);
}
+ try {
+ // dev mode does not make sense when we a ShutdownBuildItem exists
+ // because such applications dont stay up
+ final ShutdownBuildItem shutdownBuildItem = result.consume(ShutdownBuildItem.class);
+ if ((shutdownBuildItem != null) && (launchMode == LaunchMode.DEVELOPMENT)) {
+ System.exit(99);
+ }
+ } catch (IllegalArgumentException e) {
+ // there were no ShutdownBuildItem so we can proceed
+ }
+
final Application application;
Class extends Application> appClass = loader
.loadClass(result.consume(ApplicationClassNameBuildItem.class).getClassName())
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Application.java b/core/runtime/src/main/java/io/quarkus/runtime/Application.java
index 877ccade2678f..f3444a1eacb32 100644
--- a/core/runtime/src/main/java/io/quarkus/runtime/Application.java
+++ b/core/runtime/src/main/java/io/quarkus/runtime/Application.java
@@ -108,10 +108,15 @@ public final void start(@SuppressWarnings("unused") String[] args) {
} finally {
stateLock.unlock();
}
+ doPostStart();
}
protected abstract void doStart(String[] args);
+ protected void doPostStart() {
+
+ }
+
/**
* Stop the application. If another thread is also trying to stop the application, this method waits for that
* thread to finish. Returns immediately if the application is already stopped. If an exception is thrown during
@@ -222,6 +227,10 @@ private void exit() {
}
}
+ protected void requestShutdown() {
+ shutdownRequested = true;
+ }
+
private static IllegalStateException interruptedOnAwaitStart() {
return new IllegalStateException("Interrupted while waiting for another thread to start the application");
}
diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java
index 8ace0592c9927..9bf62a33806de 100644
--- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java
+++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java
@@ -282,13 +282,22 @@ public void run() {
p.destroy();
}
}, "Development Mode Shutdown Hook"));
+
+ int exitCode;
try {
- p.waitFor();
+ exitCode = p.waitFor();
} catch (Exception e) {
p.destroy();
throw e;
}
+ if (exitCode != 0) {
+ String message = exitCode == 99 ? //TODO fix: this is pretty lame
+ "quarkus-dev cannot be used with command line applications"
+ : "The application did not terminate properly";
+ throw new RuntimeException(message);
+ }
+
} catch (Exception e) {
throw new GradleException("Failed to run", e);
}
diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
index 27578b8067df7..efc2a8b29a664 100644
--- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
+++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
@@ -288,13 +288,22 @@ public void run() {
p.destroy();
}
}, "Development Mode Shutdown Hook"));
+
+ int exitCode = 0;
try {
- p.waitFor();
+ exitCode = p.waitFor();
} catch (Exception e) {
p.destroy();
throw e;
}
+ if (exitCode != 0) {
+ String message = exitCode == 99 ? //TODO fix: this is pretty lame
+ "quarkus:dev cannot be used with command line applications"
+ : "The application did not terminate properly";
+ throw new RuntimeException(message);
+ }
+
} catch (Exception e) {
throw new MojoFailureException("Failed to run", e);
}
diff --git a/extensions/command-line-aesh/deployment/pom.xml b/extensions/command-line-aesh/deployment/pom.xml
new file mode 100644
index 0000000000000..b3f5db6d470c0
--- /dev/null
+++ b/extensions/command-line-aesh/deployment/pom.xml
@@ -0,0 +1,61 @@
+
+
+
+
+ quarkus-command-line-aesh-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+
+ 4.0.0
+
+ quarkus-command-line-aesh
+ Quarkus - Command Line Aesh - Deployment
+
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ io.quarkus
+ quarkus-command-line-aesh-runtime
+
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
+
diff --git a/extensions/command-line-aesh/deployment/src/main/java/io/quarkus/claesh/deployment/AeshCommandLineRunnerBuildItem.java b/extensions/command-line-aesh/deployment/src/main/java/io/quarkus/claesh/deployment/AeshCommandLineRunnerBuildItem.java
new file mode 100644
index 0000000000000..86448b27d7be1
--- /dev/null
+++ b/extensions/command-line-aesh/deployment/src/main/java/io/quarkus/claesh/deployment/AeshCommandLineRunnerBuildItem.java
@@ -0,0 +1,16 @@
+package io.quarkus.claesh.deployment;
+
+import org.jboss.builder.item.MultiBuildItem;
+
+public final class AeshCommandLineRunnerBuildItem extends MultiBuildItem {
+
+ private final String className;
+
+ public AeshCommandLineRunnerBuildItem(String className) {
+ this.className = className;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+}
diff --git a/extensions/command-line-aesh/deployment/src/main/java/io/quarkus/claesh/deployment/AeshCommandLineRunnerProcessor.java b/extensions/command-line-aesh/deployment/src/main/java/io/quarkus/claesh/deployment/AeshCommandLineRunnerProcessor.java
new file mode 100644
index 0000000000000..7ba6a228edaff
--- /dev/null
+++ b/extensions/command-line-aesh/deployment/src/main/java/io/quarkus/claesh/deployment/AeshCommandLineRunnerProcessor.java
@@ -0,0 +1,125 @@
+package io.quarkus.claesh.deployment;
+
+import static io.quarkus.gizmo.MethodDescriptor.ofMethod;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import javax.inject.Singleton;
+
+import org.aesh.command.CommandDefinition;
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.Type;
+
+import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
+import io.quarkus.arc.processor.BeanInfo;
+import io.quarkus.claesh.AeshCommandRunner;
+import io.quarkus.claesh.QuarkusCommand;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.ApplicationIndexBuildItem;
+import io.quarkus.deployment.builditem.MainAfterStartupBuildItem;
+import io.quarkus.deployment.builditem.ShutdownBuildItem;
+import io.quarkus.deployment.builditem.substrate.ReflectiveClassBuildItem;
+import io.quarkus.gizmo.MethodCreator;
+import io.quarkus.gizmo.MethodDescriptor;
+import io.quarkus.gizmo.ResultHandle;
+
+public class AeshCommandLineRunnerProcessor {
+
+ private static final DotName QUARKUS_COMMAND = DotName.createSimple(QuarkusCommand.class.getName());
+ private static final DotName SINGLETON = DotName.createSimple(Singleton.class.getName());
+ private static final DotName COMMAND_DEFINITION = DotName.createSimple(CommandDefinition.class.getName());
+
+ @BuildStep
+ void discover(ApplicationIndexBuildItem indexBuildItem,
+ BuildProducer commandsProducer,
+ BuildProducer unremovableProducer) {
+
+ for (ClassInfo info : indexBuildItem.getIndex().getAllKnownImplementors(QUARKUS_COMMAND)) {
+ if (info.classAnnotation(COMMAND_DEFINITION) == null) {
+ throw new RuntimeException("All implementations of " + QuarkusCommand.class.getName() +
+ " must be annotated with " + COMMAND_DEFINITION.toString());
+ }
+ commandsProducer.produce(new AeshCommandLineRunnerBuildItem(info.name().toString()));
+ }
+
+ // we need to make sure that all application Singleton beans are not removed
+ Set typesInjectedInCommands = new HashSet<>();
+ // TODO: figure out if @Singleton is the only thing that makes sense in a Command Line application?
+ for (AnnotationInstance annotationInstance : indexBuildItem.getIndex().getAnnotations(SINGLETON)) {
+ typesInjectedInCommands.add(annotationInstance.target().asClass().name());
+ }
+ if (!typesInjectedInCommands.isEmpty()) {
+ unremovableProducer.produce(new UnremovableBeanBuildItem(
+ new Predicate() {
+ @Override
+ public boolean test(BeanInfo beanInfo) {
+ Set types = beanInfo.getTypes();
+ for (Type t : types) {
+ if (typesInjectedInCommands.contains(t.name())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }));
+ }
+ }
+
+ @BuildStep
+ List reflection(List commands) {
+ final List result = new ArrayList<>();
+ for (AeshCommandLineRunnerBuildItem command : commands) {
+ result.add(new ReflectiveClassBuildItem(true, true, command.getClassName()));
+ }
+ return result;
+ }
+
+ @BuildStep
+ void shutdown(List commands, BuildProducer producer) {
+ // TODO: this has to be conditional only set when various things are not present
+ if (!commands.isEmpty()) {
+ producer.produce(new ShutdownBuildItem());
+ }
+ }
+
+ @BuildStep
+ void bytecodeCreation(List commands, BuildProducer producer) {
+ if (commands.isEmpty()) {
+ return;
+ }
+
+ // generate the bytecode that will call AeshCommandRunner with the proper parameters
+ final Consumer bytecodeCreator = new Consumer() {
+ @Override
+ public void accept(MainAfterStartupBuildItem.Input input) {
+ MethodCreator mv = input.getDoStartMethod();
+
+ ResultHandle commandsArray = mv.newArray(Class.class, mv.load(commands.size()));
+
+ for (int i = 0; i < commands.size(); i++) {
+ ResultHandle clazz = mv.invokeStaticMethod(
+ ofMethod(Class.class, "forName", Class.class, String.class),
+ mv.load(commands.get(i).getClassName()));
+ mv.writeArrayValue(commandsArray, i, clazz);
+ }
+
+ final ResultHandle runner = mv.newInstance(MethodDescriptor.ofConstructor(AeshCommandRunner.class));
+
+ mv.invokeVirtualMethod(
+ MethodDescriptor.ofMethod(AeshCommandRunner.class, "run", void.class, Class[].class, String[].class),
+ runner, commandsArray, input.getMainArgs());
+ }
+ };
+
+ producer.produce(new MainAfterStartupBuildItem(bytecodeCreator));
+ }
+}
diff --git a/extensions/command-line-aesh/pom.xml b/extensions/command-line-aesh/pom.xml
new file mode 100644
index 0000000000000..4a4050a297701
--- /dev/null
+++ b/extensions/command-line-aesh/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ quarkus-build-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../../build-parent/pom.xml
+
+ 4.0.0
+
+ quarkus-command-line-aesh-parent
+ Quarkus - Command Line Aesh
+ pom
+
+ deployment
+ runtime
+
+
+
diff --git a/extensions/command-line-aesh/runtime/pom.xml b/extensions/command-line-aesh/runtime/pom.xml
new file mode 100644
index 0000000000000..d284cdcf475b2
--- /dev/null
+++ b/extensions/command-line-aesh/runtime/pom.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ quarkus-command-line-aesh-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-command-line-aesh-runtime
+ Quarkus - Command Line Aesh - Runtime
+
+
+
+
+ io.quarkus
+ quarkus-arc-runtime
+
+
+
+ org.aesh
+ aesh
+ 2.0
+
+
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
diff --git a/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/AeshCommandRunner.java b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/AeshCommandRunner.java
new file mode 100644
index 0000000000000..1a9d4aa9d5668
--- /dev/null
+++ b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/AeshCommandRunner.java
@@ -0,0 +1,58 @@
+package io.quarkus.claesh;
+
+import java.io.IOException;
+
+import org.aesh.command.AeshCommandRuntimeBuilder;
+import org.aesh.command.CommandException;
+import org.aesh.command.CommandNotFoundException;
+import org.aesh.command.CommandRuntime;
+import org.aesh.command.impl.registry.AeshCommandRegistryBuilder;
+import org.aesh.command.parser.CommandLineParserException;
+import org.aesh.command.registry.CommandRegistry;
+import org.aesh.command.registry.CommandRegistryException;
+import org.aesh.command.validator.CommandValidatorException;
+import org.aesh.command.validator.OptionValidatorException;
+
+import io.quarkus.arc.Arc;
+
+public class AeshCommandRunner {
+
+ public void run(Class[] commands, String[] args) {
+ try {
+ final QuarkusContext quarkusContext = new QuarkusContext(Arc.container());
+
+ final CommandRegistry registry = AeshCommandRegistryBuilder
+ . builder()
+ .commands(commands)
+ .create();
+
+ final CommandRuntime runtime = AeshCommandRuntimeBuilder
+ . builder()
+ .commandRegistry(registry)
+ .commandInvocationProvider(new QuarkusCommandInvocationProvider(quarkusContext))
+ .build();
+
+ final StringBuilder sb = new StringBuilder();
+ if (args.length > 0) {
+ sb.append(" ");
+ if (args.length == 1) {
+ sb.append(args[0]);
+ } else {
+ for (String arg : args) {
+ if (arg.indexOf(' ') >= 0) {
+ sb.append('"').append(arg).append("\" ");
+ } else {
+ sb.append(arg).append(' ');
+ }
+ }
+ }
+ }
+ runtime.executeCommand(sb.toString());
+ }
+ //simplified exceptions for now
+ catch (CommandNotFoundException | CommandException | CommandLineParserException | CommandValidatorException
+ | OptionValidatorException | InterruptedException | IOException | CommandRegistryException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommand.java b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommand.java
new file mode 100644
index 0000000000000..f7942975f7e6d
--- /dev/null
+++ b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommand.java
@@ -0,0 +1,6 @@
+package io.quarkus.claesh;
+
+import org.aesh.command.Command;
+
+public interface QuarkusCommand extends Command {
+}
diff --git a/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommandInvocation.java b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommandInvocation.java
new file mode 100644
index 0000000000000..79f8c6a480b36
--- /dev/null
+++ b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommandInvocation.java
@@ -0,0 +1,113 @@
+package io.quarkus.claesh;
+
+import java.io.IOException;
+
+import org.aesh.command.CommandException;
+import org.aesh.command.CommandNotFoundException;
+import org.aesh.command.Executor;
+import org.aesh.command.invocation.CommandInvocation;
+import org.aesh.command.invocation.CommandInvocationConfiguration;
+import org.aesh.command.parser.CommandLineParserException;
+import org.aesh.command.shell.Shell;
+import org.aesh.command.validator.CommandValidatorException;
+import org.aesh.command.validator.OptionValidatorException;
+import org.aesh.readline.Prompt;
+import org.aesh.readline.action.KeyAction;
+
+public class QuarkusCommandInvocation implements CommandInvocation {
+
+ private final CommandInvocation delegate;
+ private final QuarkusContext context;
+
+ QuarkusCommandInvocation(CommandInvocation delegate, QuarkusContext context) {
+ this.delegate = delegate;
+ this.context = context;
+ }
+
+ @Override
+ public Shell getShell() {
+ return delegate.getShell();
+ }
+
+ @Override
+ public void setPrompt(Prompt prompt) {
+ delegate.setPrompt(prompt);
+ }
+
+ @Override
+ public Prompt getPrompt() {
+ return delegate.getPrompt();
+ }
+
+ @Override
+ public String getHelpInfo(String commandName) {
+ return delegate.getHelpInfo(commandName);
+ }
+
+ @Override
+ public String getHelpInfo() {
+ return delegate.getHelpInfo();
+ }
+
+ @Override
+ public void stop() {
+ delegate.stop();
+ }
+
+ @Override
+ public KeyAction input() throws InterruptedException {
+ return delegate.input();
+ }
+
+ @Override
+ public String inputLine() throws InterruptedException {
+ return delegate.inputLine();
+ }
+
+ @Override
+ public String inputLine(Prompt prompt) throws InterruptedException {
+ return delegate.inputLine(prompt);
+ }
+
+ @Override
+ public void executeCommand(String input) throws CommandNotFoundException,
+ CommandLineParserException, OptionValidatorException,
+ CommandValidatorException, CommandException, InterruptedException, IOException {
+ delegate.executeCommand(input);
+ }
+
+ @Override
+ public void print(String msg, boolean paging) {
+ delegate.print(msg, paging);
+ }
+
+ @Override
+ public void println(String msg, boolean paging) {
+ delegate.println(msg, paging);
+ }
+
+ @Override
+ public Executor buildExecutor(String line) throws CommandNotFoundException,
+ CommandLineParserException, OptionValidatorException, CommandValidatorException, IOException {
+ return delegate.buildExecutor(line);
+ }
+
+ @Override
+ public void print(String msg) {
+ delegate.print(msg);
+ }
+
+ @Override
+ public void println(String msg) {
+ delegate.println(msg);
+ }
+
+ @Override
+ public CommandInvocationConfiguration getConfiguration() {
+ return null;
+ }
+
+ public QuarkusContext quarkusContext() {
+ return context;
+ }
+}
diff --git a/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommandInvocationProvider.java b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommandInvocationProvider.java
new file mode 100644
index 0000000000000..8053e588ded9b
--- /dev/null
+++ b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusCommandInvocationProvider.java
@@ -0,0 +1,18 @@
+package io.quarkus.claesh;
+
+import org.aesh.command.invocation.CommandInvocation;
+import org.aesh.command.invocation.CommandInvocationProvider;
+
+public class QuarkusCommandInvocationProvider implements CommandInvocationProvider {
+
+ private final QuarkusContext context;
+
+ public QuarkusCommandInvocationProvider(QuarkusContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public QuarkusCommandInvocation enhanceCommandInvocation(CommandInvocation commandInvocation) {
+ return new QuarkusCommandInvocation(commandInvocation, context);
+ }
+}
diff --git a/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusContext.java b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusContext.java
new file mode 100644
index 0000000000000..47f0c35095a00
--- /dev/null
+++ b/extensions/command-line-aesh/runtime/src/main/java/io/quarkus/claesh/QuarkusContext.java
@@ -0,0 +1,20 @@
+package io.quarkus.claesh;
+
+import io.quarkus.arc.ArcContainer;
+
+public class QuarkusContext {
+
+ private final ArcContainer arcContainer;
+
+ public QuarkusContext(ArcContainer arcContainer) {
+ this.arcContainer = arcContainer;
+ }
+
+ public String name() {
+ return "Quarkus";
+ }
+
+ public ArcContainer getArcContainer() {
+ return arcContainer;
+ }
+}
diff --git a/extensions/command-line-runner/deployment/pom.xml b/extensions/command-line-runner/deployment/pom.xml
new file mode 100644
index 0000000000000..a6c8658849f12
--- /dev/null
+++ b/extensions/command-line-runner/deployment/pom.xml
@@ -0,0 +1,61 @@
+
+
+
+
+ quarkus-command-line-runner-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+
+ 4.0.0
+
+ quarkus-command-line-runner
+ Quarkus - Command Line Runner - Deployment
+
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ io.quarkus
+ quarkus-command-line-runner-runtime
+
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
+
diff --git a/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerBuildItem.java b/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerBuildItem.java
new file mode 100644
index 0000000000000..43879f0e10c6e
--- /dev/null
+++ b/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerBuildItem.java
@@ -0,0 +1,16 @@
+package io.quarkus.clrunner.deployment;
+
+import org.jboss.builder.item.SimpleBuildItem;
+
+public final class CommandLineRunnerBuildItem extends SimpleBuildItem {
+
+ private final String className;
+
+ public CommandLineRunnerBuildItem(String className) {
+ this.className = className;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+}
diff --git a/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerProcessor.java b/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerProcessor.java
new file mode 100644
index 0000000000000..a30a6d6ab9fc9
--- /dev/null
+++ b/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerProcessor.java
@@ -0,0 +1,106 @@
+package io.quarkus.clrunner.deployment;
+
+import static io.quarkus.gizmo.MethodDescriptor.ofMethod;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.arc.ArcContainer;
+import io.quarkus.arc.InstanceHandle;
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
+import io.quarkus.clrunner.CommandLineRunner;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.deployment.builditem.MainAfterStartupBuildItem;
+import io.quarkus.deployment.builditem.ShutdownBuildItem;
+import io.quarkus.gizmo.MethodCreator;
+import io.quarkus.gizmo.MethodDescriptor;
+import io.quarkus.gizmo.ResultHandle;
+
+public class CommandLineRunnerProcessor {
+
+ private static final DotName COMMAND_LINER_RUNNER = DotName.createSimple(CommandLineRunner.class.getName());
+
+ @BuildStep
+ void discover(CombinedIndexBuildItem combinedIndexBuildItem,
+ BuildProducer producer) {
+ final List names = new ArrayList<>();
+
+ for (ClassInfo info : combinedIndexBuildItem.getIndex().getAllKnownImplementors(COMMAND_LINER_RUNNER)) {
+ final DotName name = info.name();
+ names.add(name.toString());
+ }
+
+ if (names.size() == 0) {
+ return;
+ }
+ if (names.size() > 1) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (String name : names) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(",");
+ }
+ sb.append(name);
+ }
+ throw new RuntimeException("Multiple classes ( " + sb.toString()
+ + ") have been annotated with @ApplicationPath which is currently not supported");
+ }
+
+ producer.produce(new CommandLineRunnerBuildItem(names.get(0)));
+ }
+
+ @BuildStep
+ void beans(Optional runner, BuildProducer producer) {
+ if (runner.isPresent()) {
+ producer.produce(new AdditionalBeanBuildItem(false, runner.get().getClassName()));
+ }
+ }
+
+ @BuildStep
+ void shutdown(Optional runner, BuildProducer producer) {
+ // TODO: this has to be conditional only set when various things are not present
+ if (runner.isPresent()) {
+ producer.produce(new ShutdownBuildItem());
+ }
+ }
+
+ @BuildStep
+ void bytecodeCreation(Optional bi, BuildProducer producer) {
+ if (bi.isPresent()) {
+ final Consumer bytecodeCreator = new Consumer() {
+ @Override
+ public void accept(MainAfterStartupBuildItem.Input input) {
+ MethodCreator mv = input.getDoStartMethod();
+ ResultHandle clazz = mv.invokeStaticMethod(
+ ofMethod(Class.class, "forName", Class.class, String.class),
+ mv.load(bi.get().getClassName()));
+ ResultHandle arcContainer = mv.invokeStaticMethod(
+ MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class));
+ ResultHandle instanceHandle = mv.invokeInterfaceMethod(
+ MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class,
+ Annotation[].class),
+ arcContainer, clazz, mv.newArray(Annotation.class, mv.load(0)));
+ ResultHandle runner = mv.invokeInterfaceMethod(
+ ofMethod(InstanceHandle.class, "get", Object.class),
+ instanceHandle);
+ mv.invokeInterfaceMethod(
+ MethodDescriptor.ofMethod(CommandLineRunner.class, "run", void.class, String[].class),
+ runner, input.getMainArgs());
+ }
+ };
+
+ producer.produce(new MainAfterStartupBuildItem(bytecodeCreator));
+ }
+ }
+}
diff --git a/extensions/command-line-runner/pom.xml b/extensions/command-line-runner/pom.xml
new file mode 100644
index 0000000000000..ea88743a770c4
--- /dev/null
+++ b/extensions/command-line-runner/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ quarkus-build-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../../build-parent/pom.xml
+
+ 4.0.0
+
+ quarkus-command-line-runner-parent
+ Quarkus - Command Line Runner
+ pom
+
+ deployment
+ runtime
+
+
+
diff --git a/extensions/command-line-runner/runtime/pom.xml b/extensions/command-line-runner/runtime/pom.xml
new file mode 100644
index 0000000000000..1459f21ada6a0
--- /dev/null
+++ b/extensions/command-line-runner/runtime/pom.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+ quarkus-command-line-runner-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-command-line-runner-runtime
+ Quarkus - Command Line Runner - Runtime
+
+
+
+
+ io.quarkus
+ quarkus-arc-runtime
+
+
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
diff --git a/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/CommandLineRunner.java b/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/CommandLineRunner.java
new file mode 100644
index 0000000000000..88a0607c179d5
--- /dev/null
+++ b/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/CommandLineRunner.java
@@ -0,0 +1,6 @@
+package io.quarkus.clrunner;
+
+public interface CommandLineRunner {
+
+ void run(String[] args);
+}
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 14eacc9a26c88..954caa72e697d 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -87,6 +87,9 @@
camel
kotlin
+
+ command-line-runner
+ command-line-aesh
diff --git a/integration-tests/command-line-apps/pom.xml b/integration-tests/command-line-apps/pom.xml
new file mode 100644
index 0000000000000..716ef8bc8cbeb
--- /dev/null
+++ b/integration-tests/command-line-apps/pom.xml
@@ -0,0 +1,112 @@
+
+
+
+ 4.0.0
+
+
+ quarkus-integration-tests-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+
+ quarkus-integration-test-command-line-apps
+ Quarkus - Integration Tests - Command Line Apps
+
+
+
+
+ io.quarkus
+ quarkus-command-line-runner
+ provided
+
+
+ io.quarkus
+ quarkus-command-line-aesh
+ provided
+
+
+
+
+ io.quarkus
+ quarkus-devtools-common
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+
+
+ org.assertj
+ assertj-core
+
+
+ org.apache.maven.shared
+ maven-invoker
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ org.jprocesses
+ jProcesses
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+ ${maven.home}
+ ${settings.localRepository}
+ ${project.version}
+
+
+
+
+
+
+
diff --git a/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/CommandLineRunnerIT.java b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/CommandLineRunnerIT.java
new file mode 100644
index 0000000000000..26add967d2ae6
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/CommandLineRunnerIT.java
@@ -0,0 +1,125 @@
+package io.quarkus.clrunner.tests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.maven.shared.invoker.MavenInvocationException;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.clrunner.tests.support.MavenProcessInvocationResult;
+import io.quarkus.clrunner.tests.support.MojoTestBase;
+import io.quarkus.clrunner.tests.support.RunningInvoker;
+
+public class CommandLineRunnerIT extends MojoTestBase {
+
+ private RunningInvoker running;
+ private File testDir;
+
+ @Test
+ public void testCommandLineRunnerIsBuiltProperlyAndExecutesAsExpected()
+ throws MavenInvocationException, IOException, InterruptedException {
+ // copy the template
+ testDir = initProject("projects/classic", "projects/project-classic-clir");
+
+ // invoke the build
+ running = new RunningInvoker(testDir, false);
+ final MavenProcessInvocationResult result = running.execute(Collections.singletonList("package"),
+ Collections.emptyMap());
+
+ // ensure the build completed successfully
+ assertThat(result.getProcess().waitFor()).isEqualTo(0);
+
+ // ensure the runner jar was created
+ final File targetDir = getTargetDir();
+ final List runnerJarFiles = getFilesEndingWith(targetDir, "-runner.jar");
+ assertThat(runnerJarFiles).hasSize(1);
+
+ final File runnerJarFile = runnerJarFiles.get(0);
+ final String expectedOutputFile = runnerJarFile.getParentFile().getAbsolutePath() + "/dummy.txt";
+
+ // launch the jar we just created by passing the file we expect it write and the some input we expect it to capitalize and place in the output
+ final Process process = new ProcessBuilder()
+ .directory(runnerJarFile.getParentFile())
+ .command("java", "-jar", runnerJarFile.getAbsolutePath(), expectedOutputFile, "a", "b", "c")
+ .start();
+
+ // ensure the application completed successfully
+ assertThat(process.waitFor()).isEqualTo(0);
+
+ // ensure it created the expected output
+ final String lineSep = System.lineSeparator();
+ assertThat(new File(expectedOutputFile))
+ .exists()
+ .hasContent("A" + lineSep + "B" + lineSep + "C" + lineSep);
+ }
+
+ @Test
+ public void testAeshCommandIsBuiltProperlyAndExecutesAsExpected()
+ throws MavenInvocationException, IOException, InterruptedException {
+ // copy the template
+ testDir = initProject("projects/aesh", "projects/project-classic-aesh");
+
+ // invoke the build
+ running = new RunningInvoker(testDir, false);
+ final MavenProcessInvocationResult result = running.execute(Collections.singletonList("package"),
+ Collections.emptyMap());
+
+ // ensure the build completed successfully
+ assertThat(result.getProcess().waitFor()).isEqualTo(0);
+
+ // ensure the runner jar was created
+ final File targetDir = getTargetDir();
+ final List runnerJarFiles = getFilesEndingWith(targetDir, "-runner.jar");
+ assertThat(runnerJarFiles).hasSize(1);
+
+ final File runnerJarFile = runnerJarFiles.get(0);
+ final String expectedOutputFile = runnerJarFile.getParentFile().getAbsolutePath() + "/p1.txt";
+
+ // launch a second run to use the capitalize command
+ final Process process = new ProcessBuilder()
+ .directory(runnerJarFile.getParentFile())
+ .command("java", "-jar", runnerJarFile.getAbsolutePath(), "capitalize", "--file", expectedOutputFile, "a", "b",
+ "c")
+ .start();
+
+ // ensure the application completed successfully
+ assertThat(process.waitFor()).isEqualTo(0);
+
+ // ensure it created the expected output
+ final String lineSep = System.lineSeparator();
+ assertThat(new File(expectedOutputFile))
+ .exists()
+ .hasContent("A" + lineSep + "B" + lineSep + "C" + lineSep);
+
+ // launch a second run to use the lowecase command
+ final String secondRunOutputFile = runnerJarFile.getParentFile().getAbsolutePath() + "/p2.txt";
+ final Process secondProcess = new ProcessBuilder()
+ .directory(runnerJarFile.getParentFile())
+ .command("java", "-jar", runnerJarFile.getAbsolutePath(), "lowercase", "--file", secondRunOutputFile, "A", "B",
+ "C")
+ .start();
+
+ // ensure the application completed successfully
+ assertThat(secondProcess.waitFor()).isEqualTo(0);
+
+ // ensure it created the expected output
+ assertThat(new File(secondRunOutputFile))
+ .exists()
+ .hasContent("a" + lineSep + "b" + lineSep + "c" + lineSep);
+ }
+
+ private File getTargetDir() {
+ return new File(testDir.getAbsoluteFile() + "/target");
+ }
+
+ private List getFilesEndingWith(File dir, String suffix) {
+ final File[] files = dir.listFiles((d, name) -> name.endsWith(suffix));
+ return files != null ? Arrays.asList(files) : new ArrayList<>();
+ }
+}
diff --git a/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvocationResult.java b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvocationResult.java
new file mode 100644
index 0000000000000..2feb27e82dc58
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvocationResult.java
@@ -0,0 +1,45 @@
+package io.quarkus.clrunner.tests.support;
+
+import org.apache.maven.shared.invoker.InvocationResult;
+import org.apache.maven.shared.utils.cli.CommandLineException;
+
+/*
+ * Copied and adapted from devtools-maven
+ */
+public class MavenProcessInvocationResult implements InvocationResult {
+
+ private Process process;
+ private CommandLineException exception;
+
+ MavenProcessInvocationResult setProcess(Process process) {
+ this.process = process;
+ return this;
+ }
+
+ public MavenProcessInvocationResult setException(CommandLineException exception) {
+ // Print the stack trace immediately to give some feedback early
+ // In intellij, the used `mvn` executable is not "executable" by default on Mac and probably linux.
+ // You need to chmod +x the file.
+ exception.printStackTrace();
+ this.exception = exception;
+ return this;
+ }
+
+ @Override
+ public CommandLineException getExecutionException() {
+ return exception;
+ }
+
+ @Override
+ public int getExitCode() {
+ if (process == null) {
+ throw new IllegalStateException("No process");
+ } else {
+ return process.exitValue();
+ }
+ }
+
+ public Process getProcess() {
+ return process;
+ }
+}
diff --git a/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvoker.java b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvoker.java
new file mode 100644
index 0000000000000..172180c3ad46e
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvoker.java
@@ -0,0 +1,115 @@
+package io.quarkus.clrunner.tests.support;
+
+import java.io.File;
+
+import org.apache.maven.shared.invoker.CommandLineConfigurationException;
+import org.apache.maven.shared.invoker.DefaultInvoker;
+import org.apache.maven.shared.invoker.InvocationOutputHandler;
+import org.apache.maven.shared.invoker.InvocationRequest;
+import org.apache.maven.shared.invoker.InvocationResult;
+import org.apache.maven.shared.invoker.InvokerLogger;
+import org.apache.maven.shared.invoker.MavenCommandLineBuilder;
+import org.apache.maven.shared.invoker.MavenInvocationException;
+import org.apache.maven.shared.invoker.SystemOutHandler;
+import org.apache.maven.shared.utils.cli.CommandLineException;
+import org.apache.maven.shared.utils.cli.Commandline;
+import org.apache.maven.shared.utils.cli.StreamConsumer;
+import org.apache.maven.shared.utils.cli.StreamPumper;
+
+/*
+ * Copied from devtools-maven
+ */
+public class MavenProcessInvoker extends DefaultInvoker {
+
+ private static final InvocationOutputHandler DEFAULT_OUTPUT_HANDLER = new SystemOutHandler();
+
+ private InvocationOutputHandler outputHandler = DEFAULT_OUTPUT_HANDLER;
+
+ private InvocationOutputHandler errorHandler = DEFAULT_OUTPUT_HANDLER;
+
+ @Override
+ public InvocationResult execute(InvocationRequest request) throws MavenInvocationException {
+ MavenCommandLineBuilder cliBuilder = new MavenCommandLineBuilder();
+
+ InvokerLogger logger = getLogger();
+ if (logger != null) {
+ cliBuilder.setLogger(getLogger());
+ }
+
+ File localRepo = getLocalRepositoryDirectory();
+ if (localRepo != null) {
+ cliBuilder.setLocalRepositoryDirectory(getLocalRepositoryDirectory());
+ }
+
+ File mavenHome = getMavenHome();
+ if (mavenHome != null) {
+ cliBuilder.setMavenHome(getMavenHome());
+ }
+
+ File mavenExecutable = getMavenExecutable();
+ if (mavenExecutable != null) {
+ cliBuilder.setMavenExecutable(mavenExecutable);
+ }
+
+ File workingDirectory = getWorkingDirectory();
+ if (workingDirectory != null) {
+ cliBuilder.setWorkingDirectory(getWorkingDirectory());
+ }
+
+ request.setBatchMode(true);
+
+ Commandline cli;
+ try {
+ cli = cliBuilder.build(request);
+ } catch (CommandLineConfigurationException e) {
+ throw new MavenInvocationException("Error configuring command-line. Reason: " + e.getMessage(), e);
+ }
+
+ MavenProcessInvocationResult result = new MavenProcessInvocationResult();
+
+ try {
+ Process process = executeCommandLine(cli, request);
+ result.setProcess(process);
+ } catch (CommandLineException e) {
+ result.setException(e);
+ }
+
+ return result;
+ }
+
+ private Process executeCommandLine(Commandline cli, InvocationRequest request)
+ throws CommandLineException {
+ InvocationOutputHandler outputHandler = request.getOutputHandler(this.outputHandler);
+ InvocationOutputHandler errorHandler = request.getErrorHandler(this.errorHandler);
+ return executeCommandLine(cli, outputHandler, errorHandler);
+ }
+
+ private static Process executeCommandLine(Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr)
+ throws CommandLineException {
+ if (cl == null) {
+ throw new IllegalArgumentException("the command line cannot be null.");
+ } else {
+ final Process p = cl.execute();
+ final StreamPumper outputPumper = new StreamPumper(p.getInputStream(), systemOut);
+ final StreamPumper errorPumper = new StreamPumper(p.getErrorStream(), systemErr);
+
+ outputPumper.start();
+ errorPumper.start();
+
+ new Thread(() -> {
+ try {
+ // Wait for termination
+ p.waitFor();
+ outputPumper.waitUntilDone();
+ errorPumper.waitUntilDone();
+ } catch (Exception e) {
+ outputPumper.disable();
+ errorPumper.disable();
+ e.printStackTrace();
+ }
+ }).start();
+
+ return p;
+ }
+ }
+}
diff --git a/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MojoTestBase.java b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MojoTestBase.java
new file mode 100644
index 0000000000000..2a1d1383ac170
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/MojoTestBase.java
@@ -0,0 +1,86 @@
+package io.quarkus.clrunner.tests.support;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.maven.shared.utils.StringUtils;
+import org.junit.jupiter.api.BeforeAll;
+
+import com.google.common.collect.ImmutableMap;
+
+import io.quarkus.maven.utilities.MojoUtils;
+
+/*
+ * Copied and adapted from devtools-maven
+ */
+public class MojoTestBase {
+ private static ImmutableMap VARIABLES;
+
+ @BeforeAll
+ public static void init() {
+ VARIABLES = ImmutableMap.of(
+ "@project.groupId@", MojoUtils.getPluginGroupId(),
+ "@project.artifactId@", MojoUtils.getPluginArtifactId(),
+ "@project.version@", MojoUtils.getPluginVersion());
+ }
+
+ public static File initProject(String name, String output) {
+ File tc = new File("target/test-classes");
+ if (!tc.isDirectory()) {
+ boolean mkdirs = tc.mkdirs();
+ Logger.getLogger(MojoTestBase.class.getName())
+ .log(Level.FINE, "test-classes created? " + mkdirs);
+ }
+
+ File in = new File("src/test/resources", name);
+ if (!in.isDirectory()) {
+ throw new RuntimeException("Cannot find directory: " + in.getAbsolutePath());
+ }
+
+ File out = new File(tc, output);
+ if (out.isDirectory()) {
+ FileUtils.deleteQuietly(out);
+ }
+ boolean mkdirs = out.mkdirs();
+ Logger.getLogger(MojoTestBase.class.getName())
+ .log(Level.FINE, out.getAbsolutePath() + " created? " + mkdirs);
+ try {
+ org.codehaus.plexus.util.FileUtils.copyDirectoryStructure(in, out);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot copy project resources", e);
+ }
+
+ File pom = new File(out, "pom.xml");
+ try {
+ filter(pom, VARIABLES);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+
+ return out;
+ }
+
+ static void filter(File input, Map variables) throws IOException {
+ assertThat(input).isFile();
+ String data = FileUtils.readFileToString(input, "UTF-8");
+
+ for (Map.Entry token : variables.entrySet()) {
+ String value = String.valueOf(token.getValue());
+ data = StringUtils.replace(data, token.getKey(), value);
+ }
+ FileUtils.write(input, data, "UTF-8");
+ }
+
+}
diff --git a/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/RunningInvoker.java b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/RunningInvoker.java
new file mode 100644
index 0000000000000..e709bb111c1a2
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/java/io/quarkus/clrunner/tests/support/RunningInvoker.java
@@ -0,0 +1,74 @@
+package io.quarkus.clrunner.tests.support;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.shared.invoker.DefaultInvocationRequest;
+import org.apache.maven.shared.invoker.InvocationRequest;
+import org.apache.maven.shared.invoker.InvocationResult;
+import org.apache.maven.shared.invoker.InvokerLogger;
+import org.apache.maven.shared.invoker.MavenInvocationException;
+import org.apache.maven.shared.invoker.PrintStreamHandler;
+import org.apache.maven.shared.invoker.PrintStreamLogger;
+
+/*
+ * Copied and adapted from devtools-maven
+ */
+public class RunningInvoker extends MavenProcessInvoker {
+
+ private final boolean debug;
+ private MavenProcessInvocationResult result;
+ private final File log;
+ private final PrintStreamHandler logHandler;
+
+ public RunningInvoker(File basedir, boolean debug) throws FileNotFoundException {
+ this.debug = debug;
+ setWorkingDirectory(basedir);
+ String repo = System.getProperty("maven.repo");
+ if (repo == null) {
+ repo = new File(System.getProperty("user.home"), ".m2/repository").getAbsolutePath();
+ }
+ log = new File(basedir, "build-" + basedir.getName() + ".log");
+ PrintStream stream = null;
+ try {
+ stream = new PrintStream(log, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ stream = new PrintStream(log);
+ }
+ logHandler = new PrintStreamHandler(stream, true);
+ setErrorHandler(logHandler);
+ setOutputHandler(logHandler);
+ setLogger(new PrintStreamLogger(stream, InvokerLogger.DEBUG));
+ }
+
+ public MavenProcessInvocationResult execute(List goals, Map envVars)
+ throws MavenInvocationException {
+ DefaultInvocationRequest request = new DefaultInvocationRequest();
+ request.setGoals(goals);
+ request.setDebug(debug);
+ request.setLocalRepositoryDirectory(getLocalRepositoryDirectory());
+ request.setBaseDirectory(getWorkingDirectory());
+ request.setPomFile(new File(getWorkingDirectory(), "pom.xml"));
+
+ if (System.getProperty("mavenOpts") != null) {
+ request.setMavenOpts(System.getProperty("mavenOpts"));
+ }
+
+ request.setShellEnvironmentInherited(true);
+ envVars.forEach(request::addShellEnvironment);
+ request.setOutputHandler(logHandler);
+ request.setErrorHandler(logHandler);
+ this.result = (MavenProcessInvocationResult) execute(request);
+ return result;
+ }
+
+ @Override
+ public InvocationResult execute(InvocationRequest request) throws MavenInvocationException {
+ return super.execute(request);
+ }
+
+}
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/aesh/pom.xml b/integration-tests/command-line-apps/src/test/resources/projects/aesh/pom.xml
new file mode 100644
index 0000000000000..85fa434ab9a35
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/aesh/pom.xml
@@ -0,0 +1,62 @@
+
+
+ 4.0.0
+ org.acme
+ acme
+ 1.0-SNAPSHOT
+
+ @project.version@
+ 1.8
+ UTF-8
+ 1.8
+
+
+
+ @project.groupId@
+ quarkus-command-line-aesh
+ ${quarkus.version}
+ provided
+
+
+
+
+
+ @project.groupId@
+ @project.artifactId@
+ ${quarkus.version}
+
+
+
+ build
+
+
+
+
+
+
+
+
+ native
+
+
+
+ @project.groupId@
+ @project.artifactId@
+ ${quarkus.version}
+
+
+
+ native-image
+
+
+ true
+
+
+
+
+
+
+
+
+
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/CapitalizeCommand.java b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/CapitalizeCommand.java
new file mode 100644
index 0000000000000..990b35462c0ac
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/CapitalizeCommand.java
@@ -0,0 +1,41 @@
+package org.acme;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import io.quarkus.claesh.QuarkusCommand;
+import io.quarkus.claesh.QuarkusCommandInvocation;
+import org.aesh.command.CommandDefinition;
+import org.aesh.command.CommandException;
+import org.aesh.command.CommandResult;
+import org.aesh.command.option.Arguments;
+import org.aesh.command.option.Option;
+
+@CommandDefinition(name = "capitalize", description = "capitalizer")
+public class CapitalizeCommand implements QuarkusCommand {
+
+ @Option
+ private String file;
+
+ @Arguments
+ private List arguments;
+
+ @Override
+ public CommandResult execute(QuarkusCommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ final Path out = Paths.get(file);
+
+ try {
+ final Capitalizer capitalizer = commandInvocation.quarkusContext().getArcContainer().instance(Capitalizer.class).get();
+ Files.write(out, arguments.stream().map(capitalizer::perform).collect(Collectors.toList()), Charset.defaultCharset());
+ } catch (IOException e) {
+ throw new CommandException(e);
+ }
+
+ return CommandResult.SUCCESS;
+ }
+}
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/Capitalizer.java b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/Capitalizer.java
new file mode 100644
index 0000000000000..81da4a5537ee2
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/Capitalizer.java
@@ -0,0 +1,13 @@
+package org.acme;
+
+import javax.inject.Singleton;
+
+
+@Singleton
+public class Capitalizer {
+
+ public String perform(String input) {
+ return input.toUpperCase();
+ }
+
+}
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/LowercaseCommand.java b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/LowercaseCommand.java
new file mode 100644
index 0000000000000..28e8605a0a789
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/LowercaseCommand.java
@@ -0,0 +1,41 @@
+package org.acme;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import io.quarkus.claesh.QuarkusCommand;
+import io.quarkus.claesh.QuarkusCommandInvocation;
+import org.aesh.command.CommandDefinition;
+import org.aesh.command.CommandException;
+import org.aesh.command.CommandResult;
+import org.aesh.command.option.Arguments;
+import org.aesh.command.option.Option;
+
+@CommandDefinition(name = "lowercase", description = "lowercaser")
+public class LowercaseCommand implements QuarkusCommand {
+
+ @Option
+ private String file;
+
+ @Arguments
+ private List arguments;
+
+ @Override
+ public CommandResult execute(QuarkusCommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ final Path out = Paths.get(file);
+
+ try {
+ final Lowercaser capitalizer = commandInvocation.quarkusContext().getArcContainer().instance(Lowercaser.class).get();
+ Files.write(out, arguments.stream().map(capitalizer::perform).collect(Collectors.toList()), Charset.defaultCharset());
+ } catch (IOException e) {
+ throw new CommandException(e);
+ }
+
+ return CommandResult.SUCCESS;
+ }
+}
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/Lowercaser.java b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/Lowercaser.java
new file mode 100644
index 0000000000000..f60309c472e83
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/java/org/acme/Lowercaser.java
@@ -0,0 +1,13 @@
+package org.acme;
+
+import javax.inject.Singleton;
+
+
+@Singleton
+public class Lowercaser {
+
+ public String perform(String input) {
+ return input.toLowerCase();
+ }
+
+}
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/resources/META-INF/microprofile-config.properties b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/resources/META-INF/microprofile-config.properties
new file mode 100644
index 0000000000000..a4bf738bbafb3
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/aesh/src/main/resources/META-INF/microprofile-config.properties
@@ -0,0 +1,3 @@
+# Configuration file
+key = value
+greeting=bonjour
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/classic/pom.xml b/integration-tests/command-line-apps/src/test/resources/projects/classic/pom.xml
new file mode 100644
index 0000000000000..18a18a6da4184
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/classic/pom.xml
@@ -0,0 +1,62 @@
+
+
+ 4.0.0
+ org.acme
+ acme
+ 1.0-SNAPSHOT
+
+ @project.version@
+ 1.8
+ UTF-8
+ 1.8
+
+
+
+ @project.groupId@
+ quarkus-command-line-runner
+ ${quarkus.version}
+ provided
+
+
+
+
+
+ @project.groupId@
+ @project.artifactId@
+ ${quarkus.version}
+
+
+
+ build
+
+
+
+
+
+
+
+
+ native
+
+
+
+ @project.groupId@
+ @project.artifactId@
+ ${quarkus.version}
+
+
+
+ native-image
+
+
+ true
+
+
+
+
+
+
+
+
+
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/classic/src/main/java/org/acme/Capitalizer.java b/integration-tests/command-line-apps/src/test/resources/projects/classic/src/main/java/org/acme/Capitalizer.java
new file mode 100644
index 0000000000000..81da4a5537ee2
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/classic/src/main/java/org/acme/Capitalizer.java
@@ -0,0 +1,13 @@
+package org.acme;
+
+import javax.inject.Singleton;
+
+
+@Singleton
+public class Capitalizer {
+
+ public String perform(String input) {
+ return input.toUpperCase();
+ }
+
+}
diff --git a/integration-tests/command-line-apps/src/test/resources/projects/classic/src/main/java/org/acme/ExampleRunner.java b/integration-tests/command-line-apps/src/test/resources/projects/classic/src/main/java/org/acme/ExampleRunner.java
new file mode 100644
index 0000000000000..abe000aca08b7
--- /dev/null
+++ b/integration-tests/command-line-apps/src/test/resources/projects/classic/src/main/java/org/acme/ExampleRunner.java
@@ -0,0 +1,40 @@
+package org.acme;
+
+import io.quarkus.clrunner.CommandLineRunner;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.charset.Charset;
+
+import javax.inject.Inject;
+
+
+
+public class ExampleRunner implements CommandLineRunner {
+
+ @Inject
+ Capitalizer capitalizer;
+
+ @Override
+ public void run(String[] args) {
+ final List lines = new ArrayList<>();
+ for (int i=1; ielytron-security
camel-core
camel-salesforce
+ command-line-apps