Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to run an application on either CLASSPATH or MODULEPATH #92

Merged
merged 26 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e3e9a2c
Working directory should be resolved before querying for executable path
abhinayagarwal Jun 24, 2020
6b352e8
Add dependencies to module-path only when the project is modular
abhinayagarwal Jun 24, 2020
9652045
Add runtimePath parameter
abhinayagarwal Jun 24, 2020
71c0eb2
Merge branch 'master' of https://github.com/openjfx/javafx-maven-plug…
abhinayagarwal Jun 24, 2020
594cc51
Add `jlinkOptions` parameter to pass options to jlink executable
abhinayagarwal Jun 26, 2020
0bbf8fb
Remove DEFAULT from RuntimePath
abhinayagarwal Aug 21, 2020
d4f2fb0
Remove defaultValue for runtimePath
abhinayagarwal Sep 4, 2020
af073ed
Changes as per review
abhinayagarwal Sep 21, 2020
54e5682
Refactor JavaFXRunMojo
abhinayagarwal Sep 21, 2020
22b7c01
Remove unnecessary leading space in entries
abhinayagarwal Sep 21, 2020
b3c891f
Ignore module-info.java file if runtimePathOption is set as CLASSPATH
abhinayagarwal Sep 21, 2020
fb34a43
Merge branch 'master' of https://github.com/openjfx/javafx-maven-plug…
abhinayagarwal Sep 21, 2020
87970cc
Update README.md
abhinayagarwal Oct 5, 2020
217c0c0
Add check for Application class
abhinayagarwal Oct 7, 2020
9c67ae0
Add tests for CLASSPATH runtimePathOption
abhinayagarwal Oct 7, 2020
8f915b1
Update JavaFX version in test pom
abhinayagarwal Oct 7, 2020
351a7c6
Revert changes made for jlinkOptions
abhinayagarwal Oct 7, 2020
aafdb53
Remove unused import
abhinayagarwal Oct 7, 2020
9db5fa9
Revert test changes made for jlinkOptions
abhinayagarwal Oct 7, 2020
ff163ca
More reverts
abhinayagarwal Oct 7, 2020
319d878
Ignore module-name from mainClass if runtimePathOption is CLASSPATH
abhinayagarwal Oct 7, 2020
ebcdee1
Changes as per review
abhinayagarwal Oct 8, 2020
e9835f8
Update README.md
abhinayagarwal Oct 9, 2020
8ba1c15
Update README.md
abhinayagarwal Oct 10, 2020
130a2bd
Remove unnecessary checks while moving dependencies when runtimePathO…
abhinayagarwal Oct 13, 2020
182586b
Changes as per feedback
abhinayagarwal Oct 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,21 @@ Optionally, the configuration can be modified with:
- `options`: A list of VM options passed to the executable.
- `commandlineArgs`: Arguments separated by space for the executed program
- `includePathExceptionsInClasspath`: When resolving the module-path, setting this value to true will include the
dependencies that generate path exceptions in the classpath. By default the value is false, and these dependencies
dependencies that generate path exceptions in the classpath. By default, the value is false, and these dependencies
won't be included.
- `runtimePathOption`: By default, the plugin will place *each* dependency either on modulepath or on classpath (based on certain factors).
When `runtimePathOption` configuration is set, the plugin will place *all* the dependencies on either modulepath or classpath.

For instance, the following configuration adds some VM options and a command line argument:
If set as `MODULEPATH`, a module descriptor is required. All dependencies need to be either modularized or contain an Automatic-Module-Name.

If set as `CLASSPATH`, a Launcher class ([like this one](https://github.com/openjfx/samples/blob/master/CommandLine/Non-modular/CLI/hellofx/src/hellofx/Launcher.java))
jperedadnr marked this conversation as resolved.
Show resolved Hide resolved
is required to run a JavaFX application. Also, if a module-info descriptor is present, it will be ignored.

Values: MODULEPATH or CLASSPATH.

### Example

The following configuration adds some VM options, and a command line argument:

```
<plugin>
Expand Down
181 changes: 128 additions & 53 deletions src/main/java/org/openjfx/JavaFXBaseMojo.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 Gluon
* Copyright 2019, 2020, Gluon
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,7 @@
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.exec.ShutdownHookProcessDestroyer;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.BuildPluginManager;
Expand All @@ -39,12 +40,16 @@
import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.openjfx.model.RuntimePathOption;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -55,12 +60,17 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.openjfx.model.RuntimePathOption.CLASSPATH;
import static org.openjfx.model.RuntimePathOption.MODULEPATH;

abstract class JavaFXBaseMojo extends AbstractMojo {

private static final String JAVAFX_APPLICATION_CLASS_NAME = "javafx.application.Application";
static final String JAVAFX_PREFIX = "javafx";

@Parameter(defaultValue = "${project}", readonly = true)
Expand Down Expand Up @@ -90,6 +100,12 @@ abstract class JavaFXBaseMojo extends AbstractMojo {
@Parameter(readonly = true, required = true, defaultValue = "${project.build.directory}")
File builddir;

/**
* Type of {@link RuntimePathOption} to run the application.
*/
@Parameter(property = "javafx.runtimePathOption")
RuntimePathOption runtimePathOption;

/**
* The current working directory. Optional. If not specified, basedir will be used.
*/
Expand Down Expand Up @@ -201,69 +217,62 @@ void preparePaths(Path jdkHome) throws MojoExecutionException {
getLog().debug("Total dependencyArtifacts: " + dependencyArtifacts.size());
ResolvePathsRequest<File> fileResolvePathsRequest = ResolvePathsRequest.ofFiles(dependencyArtifacts);

ResolvePathsResult<File> resolvePathsResult;
getLog().debug("module descriptor path: " + moduleDescriptorPath);
if (moduleDescriptorPath != null) {
getLog().debug("module descriptor: " + moduleDescriptorPath);
fileResolvePathsRequest.setMainModuleDescriptor(moduleDescriptorPath);
}
if (jdkHome != null) {
fileResolvePathsRequest.setJdkHome(jdkHome.toFile());
}
resolvePathsResult = locationManager.resolvePaths(fileResolvePathsRequest);

if (!resolvePathsResult.getPathExceptions().isEmpty() && !isMavenUsingJava8()) {
// for each path exception, show a warning to plugin user...
for (Map.Entry<File, Exception> pathException : resolvePathsResult.getPathExceptions().entrySet()) {
Throwable cause = pathException.getValue();
while (cause.getCause() != null) {
cause = cause.getCause();
}
String fileName = pathException.getKey().getName();
getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage());
}
// ...if includePathExceptionsInClasspath is NOT enabled; provide configuration hint to plugin user
if (!includePathExceptionsInClasspath) {
getLog().warn("Some dependencies encountered issues while attempting to be resolved as modules" +
" and will not be included in the classpath; you can change this behavior via the " +
" 'includePathExceptionsInClasspath' configuration parameter.");
}
}
ResolvePathsResult<File> resolvePathsResult = locationManager.resolvePaths(fileResolvePathsRequest);
resolvePathsResult.getPathElements().forEach((key, value) -> pathElements.put(key.getPath(), value));

if (moduleDescriptorPath != null) {
moduleDescriptor = resolvePathsResult.getMainModuleDescriptor();
if (runtimePathOption == MODULEPATH && moduleDescriptorPath == null) {
throw new MojoExecutionException("module-info.java file is required for MODULEPATH runtimePathOption");
}

for (Map.Entry<File, ModuleNameSource> entry : resolvePathsResult.getModulepathElements().entrySet()) {
if (ModuleNameSource.FILENAME.equals(entry.getValue())) {
final String message = "Required filename-based automodules detected. "
+ "Please don't publish this project to a public artifact repository!";

if (moduleDescriptor != null && moduleDescriptor.exports().isEmpty()) {
// application
getLog().info(message);
} else {
// library
getLog().warn(message);
if (moduleDescriptorPath != null) {
if (!resolvePathsResult.getPathExceptions().isEmpty() && !isMavenUsingJava8()) {
// for each path exception, show a warning to plugin user...
for (Map.Entry<File, Exception> pathException : resolvePathsResult.getPathExceptions().entrySet()) {
Throwable cause = pathException.getValue();
while (cause.getCause() != null) {
cause = cause.getCause();
}
String fileName = pathException.getKey().getName();
getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage());
}
// ...if includePathExceptionsInClasspath is NOT enabled; provide configuration hint to plugin user
if (!includePathExceptionsInClasspath) {
getLog().warn("Some dependencies encountered issues while attempting to be resolved as modules" +
" and will not be included in the classpath; you can change this behavior via the " +
" 'includePathExceptionsInClasspath' configuration parameter.");
}
break;
}
}

getLog().debug("pathElements: " + resolvePathsResult.getPathElements().size());
resolvePathsResult.getPathElements().forEach((key, value) -> pathElements.put(key.getPath(), value));
getLog().debug("classpathElements: " + resolvePathsResult.getClasspathElements().size());
resolvePathsResult.getClasspathElements()
.forEach(file -> classpathElements.add(file.getPath()));
getLog().debug("modulepathElements: " + resolvePathsResult.getModulepathElements().size());
resolvePathsResult.getModulepathElements().keySet()
.forEach(file -> modulepathElements.add(file.getPath()));

if (includePathExceptionsInClasspath) {
resolvePathsResult.getPathExceptions().keySet()
.forEach(file -> classpathElements.add(file.getPath()));
}
moduleDescriptor = createModuleDescriptor(resolvePathsResult);
for (Map.Entry<File, ModuleNameSource> entry : resolvePathsResult.getModulepathElements().entrySet()) {
if (ModuleNameSource.FILENAME.equals(entry.getValue())) {
final String message = "Required filename-based automodules detected. "
+ "Please don't publish this project to a public artifact repository!";

if (moduleDescriptor != null && moduleDescriptor.exports().isEmpty()) {
// application
getLog().info(message);
} else {
// library
getLog().warn(message);
}
break;
}
}
resolvePathsResult.getClasspathElements().forEach(file -> classpathElements.add(file.getPath()));
resolvePathsResult.getModulepathElements().keySet().forEach(file -> modulepathElements.add(file.getPath()));

if (moduleDescriptorPath == null) {
if (includePathExceptionsInClasspath) {
resolvePathsResult.getPathExceptions().keySet()
.forEach(file -> classpathElements.add(file.getPath()));
}
} else {
// non-modular projects
pathElements.forEach((k, v) -> {
if (v != null && v.name() != null && v.name().startsWith(JAVAFX_PREFIX)) {
Expand All @@ -274,11 +283,26 @@ void preparePaths(Path jdkHome) throws MojoExecutionException {
}
});
}

} catch (Exception e) {
getLog().warn(e.getMessage());
}

if (runtimePathOption == MODULEPATH) {
getLog().debug(runtimePathOption + " runtimePathOption set by user. Moving all jars to modulepath.");
modulepathElements.addAll(classpathElements);
classpathElements.clear();
} else if (runtimePathOption == CLASSPATH) {
getLog().debug(runtimePathOption + " runtimePathOption set by user. Moving all jars to classpath.");
classpathElements.addAll(modulepathElements);
modulepathElements.clear();
abhinayagarwal marked this conversation as resolved.
Show resolved Hide resolved
if (mainClass.contains("/")) {
getLog().warn("Module name found in <mainClass> with runtimePathOption set as CLASSPATH. Module name will be ignored.");
}
if (doesExtendFXApplication(createMainClassString(mainClass, moduleDescriptor, runtimePathOption))) {
throw new MojoExecutionException("Launcher class is required. Main-class cannot extend Application when running JavaFX application on CLASSPATH");
}
}

getLog().debug("Classpath:" + classpathElements.size());
classpathElements.forEach(s -> getLog().debug(" " + s));

Expand All @@ -289,6 +313,14 @@ void preparePaths(Path jdkHome) throws MojoExecutionException {
pathElements.forEach((k, v) -> getLog().debug(" " + k + " :: " + (v != null && v.name() != null ? v.name() : v)));
}

private JavaModuleDescriptor createModuleDescriptor(ResolvePathsResult<File> resolvePathsResult) throws MojoExecutionException {
if (runtimePathOption == CLASSPATH) {
getLog().info(CLASSPATH + " runtimePathOption set by user. module-info.java will be ignored.");
return null;
}
return resolvePathsResult.getMainModuleDescriptor();
}

private List<File> getCompileClasspathElements(MavenProject project) {
List<File> list = new ArrayList<>();
list.add(new File(project.getBuild().getOutputDirectory()));
Expand Down Expand Up @@ -409,6 +441,21 @@ static Path getParent(Path path, int depth) {
return path.getRoot().resolve(path.subpath(0, path.getNameCount() - depth));
}

String createMainClassString(String mainClass, JavaModuleDescriptor moduleDescriptor, RuntimePathOption runtimePathOption) {
Objects.requireNonNull(mainClass, "Main class cannot be null");
if (runtimePathOption == CLASSPATH) {
jperedadnr marked this conversation as resolved.
Show resolved Hide resolved
if (mainClass.contains("/")) {
return mainClass.substring(mainClass.indexOf("/") + 1);
}
return mainClass;
}
if (moduleDescriptor != null && !mainClass.contains("/")) {
getLog().warn("Module name not found in <mainClass>. Module name will be assumed from module-info.java");
return moduleDescriptor.name() + "/" + mainClass;
}
return mainClass;
}

private static String findExecutable(final String executable, final List<String> paths) {
File f = null;
search: for (final String path : paths) {
Expand Down Expand Up @@ -519,4 +566,32 @@ private ProcessDestroyer getProcessDestroyer() {
}
return processDestroyer;
}

private boolean doesExtendFXApplication(String mainClass) {
boolean fxApplication = false;
try {
List<URL> pathUrls = new ArrayList<>();
for (String path : project.getCompileClasspathElements()) {
pathUrls.add(new File(path).toURI().toURL());
}
URL[] urls = pathUrls.toArray(new URL[0]);
URLClassLoader classLoader = new URLClassLoader(urls, JavaFXBaseMojo.class.getClassLoader());
Class<?> clazz = Class.forName(mainClass, false, classLoader);
fxApplication = doesExtendFXApplication(clazz);
getLog().debug("Main Class " + clazz.toString() + " extends Application: " + fxApplication);
} catch (NoClassDefFoundError | ClassNotFoundException | DependencyResolutionRequiredException | MalformedURLException e) {
getLog().debug(e);
}
return fxApplication;
}

private static boolean doesExtendFXApplication(Class<?> mainClass) {
for (Class<?> sc = mainClass.getSuperclass(); sc != null;
sc = sc.getSuperclass()) {
if (sc.getName().equals(JAVAFX_APPLICATION_CLASS_NAME)) {
return true;
}
}
return false;
}
}
56 changes: 26 additions & 30 deletions src/main/java/org/openjfx/JavaFXRunMojo.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 Gluon
* Copyright 2019, 2020, Gluon
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -41,6 +41,9 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.openjfx.model.RuntimePathOption.CLASSPATH;
import static org.openjfx.model.RuntimePathOption.MODULEPATH;

@Mojo(name = "run", requiresDependencyResolution = ResolutionScope.RUNTIME)
@Execute(phase = LifecyclePhase.PROCESS_CLASSES)
public class JavaFXRunMojo extends JavaFXBaseMojo {
Expand Down Expand Up @@ -134,30 +137,18 @@ private List<String> createCommandArguments(boolean oldJDK) throws MojoExecution
.forEach(commandArguments::add);
}
if (!oldJDK) {
if (modulepathElements != null && !modulepathElements.isEmpty()) {
commandArguments.add(" --module-path");
String modulePath = StringUtils.join(modulepathElements.iterator(), File.pathSeparator);
commandArguments.add(modulePath);

commandArguments.add(" --add-modules");
if (moduleDescriptor != null) {
commandArguments.add(" " + moduleDescriptor.name());
} else {
String modules = pathElements.values().stream()
.filter(Objects::nonNull)
.map(JavaModuleDescriptor::name)
.filter(Objects::nonNull)
.filter(module -> module.startsWith(JAVAFX_PREFIX) && !module.endsWith("Empty"))
.collect(Collectors.joining(","));
commandArguments.add(" " + modules);
}
if (runtimePathOption == MODULEPATH || modulepathElements != null && !modulepathElements.isEmpty()) {
commandArguments.add("--module-path");
commandArguments.add(StringUtils.join(modulepathElements.iterator(), File.pathSeparator));
commandArguments.add("--add-modules");
commandArguments.add(createAddModulesString(moduleDescriptor, pathElements));
}
}

if (classpathElements != null && (oldJDK || !classpathElements.isEmpty())) {
commandArguments.add(" -classpath");
if (classpathElements != null && !classpathElements.isEmpty()) {
commandArguments.add("-classpath");
String classpath = "";
if (oldJDK || moduleDescriptor != null) {
if (oldJDK || runtimePathOption == CLASSPATH) {
classpath = project.getBuild().getOutputDirectory() + File.pathSeparator;
}
classpath += StringUtils.join(classpathElements.iterator(), File.pathSeparator);
Expand All @@ -166,16 +157,9 @@ private List<String> createCommandArguments(boolean oldJDK) throws MojoExecution

if (mainClass != null) {
if (moduleDescriptor != null) {
commandArguments.add(" --module");
if (mainClass.contains("/")) {
commandArguments.add(" " + mainClass);
} else {
getLog().warn("Main module name not found in <mainClass>. Module name will be assumed from module-info.java");
commandArguments.add(" " + moduleDescriptor.name() + "/" + mainClass);
}
} else {
commandArguments.add(" " + mainClass);
commandArguments.add("--module");
}
commandArguments.add(createMainClassString(mainClass, moduleDescriptor, runtimePathOption));
}

if (commandlineArgs != null) {
Expand All @@ -184,6 +168,18 @@ private List<String> createCommandArguments(boolean oldJDK) throws MojoExecution
return commandArguments;
}

private String createAddModulesString(JavaModuleDescriptor moduleDescriptor, Map<String, JavaModuleDescriptor> pathElements) {
if (moduleDescriptor == null) {
return pathElements.values().stream()
.filter(Objects::nonNull)
.map(JavaModuleDescriptor::name)
.filter(Objects::nonNull)
.filter(module -> module.startsWith(JAVAFX_PREFIX) && !module.endsWith("Empty"))
.collect(Collectors.joining(","));
}
return moduleDescriptor.name();
}

private List<String> splitComplexArgumentString(String argumentString) {
char[] strArr = argumentString.trim().toCharArray();

Expand Down
Loading