diff --git a/README.md b/README.md index 69093c6..daf7144 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,10 @@ Maven plugin to run JavaFX 11+ applications ## Install -Clone the project, set JDK 11 and run +The plugin is available via Maven Central. + +In case you want to build and install the latest snapshot, you can +clone the project, set JDK 11 and run ``` mvn install @@ -25,7 +28,7 @@ JavaFX dependencies are added as usual: org.openjfx javafx-controls - 11.0.2 + 12.0.1 ``` @@ -37,7 +40,7 @@ Add the plugin: javafx-maven-plugin 0.0.1 - org.openjfx.App + hellofx/org.openjfx.App ``` @@ -54,11 +57,50 @@ To run the project: mvn javafx:run ``` -### Optional arguments: +For modular projects, to create and run a custom image: + +``` +mvn javafx:jlink + +target/image/bin/java -m hellofx/org.openjfx.App +``` -The plugin includes by default: `--module-path`, `--add-modules` and `-classpath`. +### javafx:compile options -Optionally, other VM arguments and runtime arguments can be set: +Optionally, when compiling with ``javafx:compile``, the source level, +target level and/or the release level for the Java compiler can be set. +The default value is 11. + +This configuration changes these levels to 12, for instance: + +``` + + org.openjfx + javafx-maven-plugin + 0.0.1 + + 12 + 12 + 12 + org.openjfx.hellofx/org.openjfx.App + + +``` + +### javafx:run options + +The plugin includes by default: `--module-path`, `--add-modules` and `-classpath` options. + +Optionally, the configuration can be modified with: + +- `mainClass`: The main class, fully qualified name, with or without module name +- `workingDirectory`: The current working directory +- `skip`: Skip the execution. Values: false (default), true +- `outputFile` File to redirect the process output +- `options`: A list of VM options passed to the executable. +- `commandlineArgs`: Arguments separated by space for the executed program + +For instance, the following configuration adds some VM options and a command line argument: ``` @@ -76,8 +118,24 @@ Optionally, other VM arguments and runtime arguments can be set: ``` -Optionally, when compiling with ``javafx:compile``, the source level, -target level and/or the release level for the Java compiler can be set: +### javafx:jlink options + +The same command line options for `jlink` can be set: + +- `stripDebug`: Strips debug information out. Values: false (default) or true +- `compress`: Compression level of the resources being used. Values: 0 (default), 1, 2. +- `noHeaderFiles`: Removes the `includes` directory in the resulting runtime image. Values: false (default) or true +- `noManPages`: Removes the `man` directory in the resulting runtime image. Values: false (default) or true +- `bindServices`: Adds the option to bind services. Values: false (default) or true +- `ignoreSigningInformation`: Adds the option to ignore signing information. Values: false (default) or true +- `jlinkVerbose`: Adds the verbose option. Values: false (default) or true +- `launcher`: Adds a launcher script with the given name +- `jlinkImageName`: The name of the folder with the resulting runtime image +- `jlinkZipName`: When set, creates a zip of the resulting runtime image +- `jlinkExecutable`: The `jlink` executable. It can be a full path or the name of the executable, if it is in the PATH. + + +For instance, with the following configuration: ``` @@ -85,9 +143,22 @@ target level and/or the release level for the Java compiler can be set: javafx-maven-plugin 0.0.1 - 12 - 12 - 12 - org.openjfx.hellofx/org.openjfx.App + true + 2 + true + true + hellofx + hello + hellozip + hellofx/org.openjfx.MainApp +``` + +a custom image can be created and run as: + +``` +mvn clean javafx:jlink + +target/hello/bin/hellofx +``` diff --git a/pom.xml b/pom.xml index fcb72ed..aac1728 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,11 @@ plexus-java 0.9.11 + + org.codehaus.plexus + plexus-archiver + 3.6.0 + org.apache.commons commons-exec diff --git a/src/main/java/org/openjfx/JavaFXBaseMojo.java b/src/main/java/org/openjfx/JavaFXBaseMojo.java index a43d4fd..3a48012 100644 --- a/src/main/java/org/openjfx/JavaFXBaseMojo.java +++ b/src/main/java/org/openjfx/JavaFXBaseMojo.java @@ -89,7 +89,7 @@ abstract class JavaFXBaseMojo extends AbstractMojo { /** * The current working directory. Optional. If not specified, basedir will be used. */ - @Parameter(property = "javafx.workingdir") + @Parameter(property = "javafx.workingDirectory") File workingDirectory; @Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true) diff --git a/src/main/java/org/openjfx/JavaFXJLinkMojo.java b/src/main/java/org/openjfx/JavaFXJLinkMojo.java new file mode 100644 index 0000000..dd3e044 --- /dev/null +++ b/src/main/java/org/openjfx/JavaFXJLinkMojo.java @@ -0,0 +1,296 @@ +/* + * Copyright 2019 Gluon + * + * 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.openjfx; + +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.ExecuteException; +import org.apache.commons.exec.Executor; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.codehaus.plexus.archiver.Archiver; +import org.codehaus.plexus.archiver.ArchiverException; +import org.codehaus.plexus.archiver.zip.ZipArchiver; +import org.codehaus.plexus.util.IOUtil; +import org.codehaus.plexus.util.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Mojo(name = "jlink", requiresDependencyResolution = ResolutionScope.RUNTIME) +public class JavaFXJLinkMojo extends JavaFXBaseMojo { + + /** + * Strips debug information out, equivalent to -G, --strip-debug, + * default false + */ + @Parameter(property = "javafx.stripDebug", defaultValue = "false") + private boolean stripDebug; + + /** + * Compression level of the resources being used, equivalent to: + * -c, --compress=level. Valid values: 0, 1, 2, + * default 2 + */ + @Parameter(property = "javafx.compress", defaultValue = "2") + private Integer compress; + + /** + * Remove the includes directory in the resulting runtime image, + * equivalent to: --no-header-files, default false + */ + @Parameter(property = "javafx.noHeaderFiles", defaultValue = "false") + private boolean noHeaderFiles; + + /** + * Remove the man directory in the resulting Java runtime image, + * equivalent to: --no-man-pages, default false + */ + @Parameter(property = "javafx.noManPages", defaultValue = "false") + private boolean noManPages; + + /** + * Add the option --bind-services or not, default false. + */ + @Parameter(property = "javafx.bindServices", defaultValue = "false") + private boolean bindServices; + + /** + * --ignore-signing-information, default false + */ + @Parameter(property = "javafx.ignoreSigningInformation", defaultValue = "false") + private boolean ignoreSigningInformation; + + /** + * Turn on verbose mode, equivalent to: --verbose, default false + */ + @Parameter(property = "javafx.jlinkVerbose", defaultValue = "false") + private boolean jlinkVerbose; + + /** + * Add a launcher script, equivalent to: + * --launcher <name>=<module>[/<mainclass>]. + */ + @Parameter(property = "javafx.launcher") + private String launcher; + + /** + * The name of the folder with the resulting runtime image, + * equivalent to --output <path> + */ + @Parameter(property = "javafx.jlinkImageName", defaultValue = "image") + private String jlinkImageName; + + /** + * When set, creates a zip of the resulting runtime image. + */ + @Parameter(property = "javafx.jlinkZipName") + private String jlinkZipName; + + /** + *

+ * The executable. Can be a full path or the name of the executable. + * In the latter case, the executable must be in the PATH for the execution to work. + *

+ */ + @Parameter(property = "javafx.jlinkExecutable", defaultValue = "jlink") + private String jlinkExecutable; + + /** + * The JAR archiver needed for archiving the environments. + */ + @Component(role = Archiver.class, hint = "zip") + private ZipArchiver zipArchiver; + + public void execute() throws MojoExecutionException { + if (skip) { + getLog().info( "skipping execute as per configuration" ); + return; + } + + if (jlinkExecutable == null) { + throw new MojoExecutionException("The parameter 'jlinkExecutable' is missing or invalid"); + } + + if (basedir == null) { + throw new IllegalStateException( "basedir is null. Should not be possible." ); + } + + try { + handleWorkingDirectory(); + + List commandArguments = new ArrayList<>(); + handleArguments(commandArguments); + + Map enviro = handleSystemEnvVariables(); + CommandLine commandLine = getExecutablePath(jlinkExecutable, enviro, workingDirectory); + String[] args = commandArguments.toArray(new String[commandArguments.size()]); + commandLine.addArguments(args, false); + getLog().debug("Executing command line: " + commandLine); + + Executor exec = new DefaultExecutor(); + exec.setWorkingDirectory(workingDirectory); + + try { + int resultCode; + if (outputFile != null) { + if ( !outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) { + getLog().warn( "Could not create non existing parent directories for log file: " + outputFile ); + } + + FileOutputStream outputStream = null; + try { + outputStream = new FileOutputStream(outputFile); + resultCode = executeCommandLine(exec, commandLine, enviro, outputStream); + } finally { + IOUtil.close(outputStream); + } + } else { + resultCode = executeCommandLine(exec, commandLine, enviro, System.out, System.err); + } + + if (resultCode != 0) { + String message = "Result of " + commandLine.toString() + " execution is: '" + resultCode + "'."; + getLog().error(message); + throw new MojoExecutionException(message); + } + + if (jlinkZipName != null && ! jlinkZipName.isEmpty()) { + getLog().debug("Creating zip of runtime image"); + File createZipArchiveFromImage = createZipArchiveFromImage(); + project.getArtifact().setFile(createZipArchiveFromImage); + } + + } catch (ExecuteException e) { + getLog().error("Command execution failed.", e); + e.printStackTrace(); + throw new MojoExecutionException("Command execution failed.", e); + } catch (IOException e) { + getLog().error("Command execution failed.", e); + throw new MojoExecutionException("Command execution failed.", e); + } + } catch (Exception e) { + throw new MojoExecutionException("Error", e); + } + + } + + private void handleArguments(List commandArguments) throws MojoExecutionException, MojoFailureException { + preparePaths(); + + if (options != null) { + options.stream() + .filter(Objects::nonNull) + .filter(String.class::isInstance) + .map(String.class::cast) + .forEach(commandArguments::add); + } + + 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 { + throw new MojoExecutionException("jlink requires a module descriptor"); + } + } + + commandArguments.add(" --output"); + File image = new File(builddir, jlinkImageName); + getLog().debug("image output: " + image.getAbsolutePath()); + if (image.exists()) { + try { + Files.walk(image.toPath()) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (IOException e) { + throw new MojoExecutionException("Image can't be removed " + image.getAbsolutePath(), e); + } + } + commandArguments.add(" " + image.getAbsolutePath()); + + if (stripDebug) { + commandArguments.add(" --strip-debug"); + } + if (bindServices) { + commandArguments.add(" --bind-services"); + } + if (ignoreSigningInformation) { + commandArguments.add(" --ignore-signing-information"); + } + if (compress != null) { + commandArguments.add(" --compress"); + if (compress < 0 || compress > 2) { + throw new MojoFailureException("The given compress parameters " + compress + " is not in the valid value range from 0..2"); + } + commandArguments.add(" " + compress); + } + if (noHeaderFiles) { + commandArguments.add(" --no-header-files"); + } + if (noManPages) { + commandArguments.add(" --no-man-pages"); + } + if (jlinkVerbose) { + commandArguments.add(" --verbose"); + } + + if (launcher != null && ! launcher.isEmpty()) { + commandArguments.add(" --launcher"); + String moduleMainClass; + if (mainClass.contains("/")) { + moduleMainClass = mainClass; + } else { + moduleMainClass = moduleDescriptor.name() + "/" + mainClass; + } + commandArguments.add(" " + launcher + "=" + moduleMainClass); + } + } + + private File createZipArchiveFromImage() throws MojoExecutionException { + File imageArchive = new File(builddir, jlinkImageName); + zipArchiver.addDirectory(imageArchive); + + File resultArchive = new File(builddir, jlinkZipName + ".zip"); + zipArchiver.setDestFile(resultArchive); + try { + zipArchiver.createArchive(); + } catch (ArchiverException | IOException e) { + throw new MojoExecutionException(e.getMessage(), e); + } + return resultArchive; + } + + // for tests +}