com.zenjava.javafx.maven.plugin.NativeMojo Maven / Gradle / Ivy
/*
* Copyright 2012 Daniel Zwolenski.
*
* 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 com.zenjava.javafx.maven.plugin;
import com.oracle.tools.packager.AbstractBundler;
import com.oracle.tools.packager.Bundler;
import com.oracle.tools.packager.Bundlers;
import com.oracle.tools.packager.ConfigException;
import com.oracle.tools.packager.RelativeFileSet;
import com.oracle.tools.packager.StandardBundlerParam;
import com.oracle.tools.packager.UnsupportedPlatformException;
import com.oracle.tools.packager.linux.LinuxDebBundler;
import com.oracle.tools.packager.linux.LinuxRpmBundler;
import com.oracle.tools.packager.windows.WinExeBundler;
import com.oracle.tools.packager.windows.WinMsiBundler;
import com.sun.javafx.tools.packager.PackagerException;
import com.sun.javafx.tools.packager.SignJarParams;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
/**
* @goal build-native
*/
public class NativeMojo extends AbstractJfxToolsMojo {
/**
* Used as the 'id' of the application, and is used as the CFBundleDisplayName on Mac. See the official JavaFX
* Packaging tools documentation for other information on this. Will be used as GUID on some installers too.
*
* @parameter
*/
protected String identifier;
/**
* The vendor of the application (i.e. you). This is required for some of the installation bundles and it's
* recommended just to set it from the get-go to avoid problems. This will default to the project.organization.name
* element in you POM if you have one.
*
* @parameter property="project.organization.name"
* @required
*/
protected String vendor;
/**
*
* The output directory that the native bundles are to be built into. This will be the base directory only as the
* JavaFX packaging tools use sub-directories that can't be customised. Generally just have a rummage through the
* sub-directories until you find what you are looking for.
*
* This defaults to 'target/jfx/native' and the interesting files are usually under 'bundles'.
*
* @parameter property="jfx.nativeOutputDir" default-value="${project.build.directory}/jfx/native"
*/
protected File nativeOutputDir;
/**
* Specify the used bundler found by selected bundleType. May not be installed your OS and will fail in that case.
*
*
* By default this will be set to 'ALL', depending on your installed OS following values are possible for installers:
*
*
* - windows.app (Creates only Windows Executable, does not bundle into Installer)
* - linux.app (Creates only Linux Executable, does not bundle into Installer)
* - mac.app (Creates only Mac Executable, does not bundle into Installer)
* - mac.appStore (Creates a binary bundle ready for deployment into the Mac App Store)
* - exe (Microsoft Windows EXE Installer, via InnoIDE)
* - msi (Microsoft Windows MSI Installer, via WiX)
* - deb (Linux Debian Bundle)
* - rpm (Redhat Package Manager (RPM) bundler)
* - dmg (Mac DMG Installer Bundle)
* - pkg (Mac PKG Installer Bundle)
*
*
*
* For a full list of available bundlers on your system, call 'mvn jfx:list-bundlers' inside your project.
*
* @parameter property="jfx.bundler" default-value="ALL"
*/
private String bundler;
/**
* Properties passed to the Java Virtual Machine when the application is started (i.e. these properties are system
* properties of the JVM bundled in the native distribution and used to run the application once installed).
*
* @parameter property="jfx.jvmProperties"
*/
private Map jvmProperties;
/**
* JVM Flags to be passed into the JVM at invocation time. These are the arguments to the left of the main class
* name when launching Java on the command line. For example:
*
* <jvmArgs>
* <jvmArg>-Xmx8G</jvmArg>
* </jvmArgs>
*
*
* @parameter property="jfx.jvmArgs"
*/
private List jvmArgs;
/**
* Optional command line arguments passed to the application when it is started. These will be included in the
* native bundle that is generated and will be accessible via the main(String[] args) method on the main class that
* is launched at runtime.
*
* These options are user overridable for the value part of the entry via user preferences. The key and the value
* are concated without a joining character when invoking the JVM.
*
* @parameter property="jfx.userJvmArgs"
*/
private Map userJvmArgs;
/**
* You can specify arguments that gonna be passed when calling your application.
*
* @parameter property="jfx.launcherArguments"
*/
private List launcherArguments;
/**
* The release version as passed to the native installer. It would be nice to just use the project's version number
* but this must be a fairly traditional version string (like '1.34.5') with only numeric characters and dot
* separators, otherwise the JFX packaging tools bomb out. We default to 1.0 in case you can't be bothered to set
* a version and don't really care.
* Normally all non-number signs and dots are removed from the value, which can be disabled
* by setting 'skipNativeVersionNumberSanitizing' to true.
*
* @parameter property="jfx.nativeReleaseVersion" default-value="1.0"
*/
private String nativeReleaseVersion;
/**
* Set this to true if you would like your application to have a shortcut on the users desktop (or platform
* equivalent) when it is installed.
*
* @parameter property="jfx.needShortcut" default-value=false
*/
protected boolean needShortcut;
/**
* Set this to true if you would like your application to have a link in the main system menu (or platform
* equivalent) when it is installed.
*
* @parameter property="jfx.needMenu" default-value=false
*/
protected boolean needMenu;
/**
* A list of bundler arguments. The particular keys and the meaning of their values are dependent on the bundler
* that is reading the arguments. Any argument not recognized by a bundler is silently ignored, so that arguments
* that are specific to a specific bundler (for example, a Mac OS X Code signing key name) can be configured and
* ignored by bundlers that don't use the particular argument.
*
* To disable creating native bundles with JRE in it, just add "<runtime />" to bundleArguments.
*
* If there are bundle arguments that override other fields in the configuration, then it is an execution error.
*
* @parameter property="jfx.bundleArguments"
*/
protected Map bundleArguments;
/**
* The name of the JavaFX packaged executable to be built into the 'native/bundles' directory. By default this will
* be the finalName as set in your project. Change this if you want something nicer. This also has effect on the
* filename of icon-files, e.g. having 'NiceApp' as appName means you have to place that icon
* at 'src/main/deploy/package/[os]/NiceApp.[icon-extension]' for having it picked up by the bundler.
*
* @parameter property="jfx.appName" default-value="${project.build.finalName}"
*/
protected String appName;
/**
* Will be set when having goal "build-native" within package-phase and calling "jfx:native" from CLI. Internal usage only.
*
* @parameter default-value=false
*/
protected boolean jfxCallFromCLI;
/**
* When you need to add additional files to generated app-folder (e.g. README, license, third-party-tools, ...),
* you can specify the source-folder here. All files will be copied recursively.
*
* @parameter property="jfx.additionalAppResources"
*/
protected File additionalAppResources;
/**
* When you need to add additional files to the base-folder of all bundlers (additional non-overriding files like
* images, licenses or separated modules for encryption etc.) you can specify the source-folder here. All files
* will be copied recursively. Please make sure to inform yourself about the details of the used bundler.
*
* @parameter property="jfx.additionalBundlerResources"
*/
protected File additionalBundlerResources;
/**
* Since Java version 1.8.0 Update 40 the native launcher for linux was changed and includes a bug
* while searching for the generated configfile. This results in wrong ouput like this:
*
* client-1.1 No main class specified
* client-1.1 Failed to launch JVM
*
*
* Scenario (which would work on windows):
*
*
* - generated launcher: i-am.working.1.2.0-SNAPSHOT
* - launcher-algorithm extracts the "extension" (a concept not known in linux-space for executables) and now searches for i-am.working.1.2.cfg
*
*
* Change this to "true" when you don't want this workaround.
*
* @see https://github.com/javafx-maven-plugin/javafx-maven-plugin/issues/124
*
* @parameter property="jfx.skipNativeLauncherWorkaround124" default-value=false
*/
protected boolean skipNativeLauncherWorkaround124;
/**
* @parameter property="jfx.secondaryLaunchers"
*/
protected List secondaryLaunchers;
/**
* Since Java version 1.8.0 Update 60 the native launcher configuration for windows was changed
* and includes a bug: the file-format before was "property-file", now it's "INI-file" per default,
* but the runtime-configuration isn't honored like in property-files.
* This workaround enforces the property-file-format.
*
* Change this to "true" when you don't want this workaround.
*
* @see https://github.com/javafx-maven-plugin/javafx-maven-plugin/issues/167
* @parameter property="jfx.skipNativeLauncherWorkaround167" default-value=false
*/
protected boolean skipNativeLauncherWorkaround167;
/**
* It is possible to create file associations when using native installers. When specified,
* all file associations are bound to the main native launcher. There is no support for bunding
* them to second launchers.
*
* For more informatione, please see official information source: https://docs.oracle.com/javase/8/docs/technotes/guides/deploy/javafx_ant_task_reference.html#CIAIDHBJ
*
* @parameter property="jfx.fileAssociations"
*/
private List fileAssociations;
/**
* Since Java version 1.8.0 Update 60 a new bundler for generating JNLP-files was presented and includes
* a bug while generating relative file-references when building on windows.
*
* Change this to "true" when you don't want this workaround.
*
* @parameter property="jfx.skipJNLPRessourcePathWorkaround182"
*/
protected boolean skipJNLPRessourcePathWorkaround182;
/**
* The location of the keystore. If not set, this will default to src/main/deploy/kesytore.jks which is usually fine
* to use for most cases.
*
* @parameter property="jfx.keyStore" default-value="src/main/deploy/keystore.jks"
*/
protected File keyStore;
/**
* The alias to use when accessing the keystore. This will default to "myalias".
*
* @parameter property="jfx.keyStoreAlias" default-value="myalias"
*/
protected String keyStoreAlias;
/**
* The password to use when accessing the keystore. This will default to "password".
*
* @parameter property="jfx.keyStorePassword" default-value="password"
*/
protected String keyStorePassword;
/**
* The password to use when accessing the key within the keystore. If not set, this will default to
* keyStorePassword.
*
* @parameter property="jfx.keyPassword"
*/
protected String keyPassword;
/**
* The type of KeyStore being used. This defaults to "jks", which is the normal one.
*
* @parameter property="jfx.keyStoreType" default-value="jks"
*/
protected String keyStoreType;
/**
* Since Java version 1.8.0 Update 60 a new bundler for generating JNLP-files was introduced,
* but lacks the ability to sign jar-files by passing some flag. We are signing the files in the
* case of having "jnlp" as bundler. The MOJO with the goal "build-web" was deprecated in favor
* of that new bundler (mostly because the old one does not fit the bundler-list strategy).
*
* Change this to "true" when you don't want signing jar-files.
*
* @parameter property="jfx.skipSigningJarFilesJNLP185" default-value=false
*/
protected boolean skipSigningJarFilesJNLP185;
/**
* After signing is done, the sizes inside generated JNLP-files still point to unsigned jar-file sizes,
* so we have to fix these sizes to be correct. This sizes-fix even lacks in the old web-MOJO.
*
* Change this to "true" when you don't want to recalculate sizes of jar-files.
*
* @parameter property="jfx.skipSizeRecalculationForJNLP185" default-value=false
*/
protected boolean skipSizeRecalculationForJNLP185;
/**
* JavaFX introduced a new way for signing jar-files, which was called "BLOB signing".
*
* The tool "jarsigner" is not able to verify that signature and webstart doesn't
* accept that format either. For not having to call jarsigner yourself, set this to "true"
* for having your jar-files getting signed when generating JNLP-files.
*
* @see https://github.com/javafx-maven-plugin/javafx-maven-plugin/issues/190
*
* @parameter property="jfx.noBlobSigning" default-value=false
*/
protected boolean noBlobSigning;
/**
* As it is possible to extend existing bundlers, you don't have to use your private
* version of the javafx-maven-plugin. Just provide a list with the java-classes you
* want to use, declare them as compile-depencendies and run `mvn jfx:native`
* or by using maven lifecycle.
* You have to implement the Bundler-interface (@see com.oracle.tools.packager.Bundler).
*
* @parameter property="jfx.customBundlers"
*/
protected List customBundlers;
/**
* Same problem as workaround for bug 124 for native launchers, but this time regarding
* created native installers, where the workaround didn't apply.
*
* Change this to "true" when you don't want this workaround.
*
* Requires skipNativeLauncherWorkaround124 to be false.
*
* @see https://github.com/javafx-maven-plugin/javafx-maven-plugin/issues/205
*
* @parameter property="jfx.skipNativeLauncherWorkaround205" default-value=false
*/
protected boolean skipNativeLauncherWorkaround205;
/**
* @parameter property="jfx.skipMacBundlerWorkaround" default-value=false
*/
protected boolean skipMacBundlerWorkaround = false;
/**
* Per default his plugin does not break the build if any bundler is failing. If you want
* to fail the build and not just print a warning, please set this to true.
*
* @parameter property="jfx.failOnError" default-value=false
*/
protected boolean failOnError = false;
/**
* @parameter property="jfx.onlyCustomBundlers" default-value=false
*/
protected boolean onlyCustomBundlers = false;
/**
* @parameter property="jfx.skipJNLP" default-value=false
*/
protected boolean skipJNLP = false;
/**
* @parameter property="jfx.skipNativeVersionNumberSanitizing" default-value=false
*/
protected boolean skipNativeVersionNumberSanitizing = false;
/**
* Since it it possible to sign created jar-files using jarsigner, it might be required to
* add some special parameters for calling it (like -tsa and -tsacert). Just add them to this
* list to have them being applied.
*
* @parameter property="jfx.additionalJarsignerParameters"
*/
protected List additionalJarsignerParameters = new ArrayList<>();
/**
* Set this to true, to not scan for the specified main class inside the generated/copied jar-files.
*
* Check only works for the main launcher, any secondary launchers are not checked.
*
* @parameter property="jfx.skipMainClassScanning"
*/
protected boolean skipMainClassScanning = false;
protected Workarounds workarounds = null;
private static final String CFG_WORKAROUND_MARKER = "cfgWorkaroundMarker";
private static final String CFG_WORKAROUND_DONE_MARKER = CFG_WORKAROUND_MARKER + ".done";
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if( jfxCallFromCLI ){
getLog().info("call from CLI - skipping creation of Native Installers");
return;
}
if( skip ){
getLog().info("Skipping execution of NativeMojo MOJO.");
return;
}
getLog().info("Building Native Installers");
workarounds = new Workarounds(nativeOutputDir, getLog());
try{
Map params = new HashMap<>();
// make bundlers doing verbose output (might not always be as verbose as expected)
params.put(StandardBundlerParam.VERBOSE.getID(), verbose);
Optional.ofNullable(identifier).ifPresent(id -> {
params.put(StandardBundlerParam.IDENTIFIER.getID(), id);
});
params.put(StandardBundlerParam.APP_NAME.getID(), appName);
params.put(StandardBundlerParam.VERSION.getID(), nativeReleaseVersion);
// replace that value
if( !skipNativeVersionNumberSanitizing && nativeReleaseVersion != null ){
params.put(StandardBundlerParam.VERSION.getID(), nativeReleaseVersion.replaceAll("[^\\d.]", ""));
}
params.put(StandardBundlerParam.VENDOR.getID(), vendor);
params.put(StandardBundlerParam.SHORTCUT_HINT.getID(), needShortcut);
params.put(StandardBundlerParam.MENU_HINT.getID(), needMenu);
params.put(StandardBundlerParam.MAIN_CLASS.getID(), mainClass);
Optional.ofNullable(jvmProperties).ifPresent(jvmProps -> {
params.put(StandardBundlerParam.JVM_PROPERTIES.getID(), new HashMap<>(jvmProps));
});
Optional.ofNullable(jvmArgs).ifPresent(jvmOptions -> {
params.put(StandardBundlerParam.JVM_OPTIONS.getID(), new ArrayList<>(jvmOptions));
});
Optional.ofNullable(userJvmArgs).ifPresent(userJvmOptions -> {
params.put(StandardBundlerParam.USER_JVM_OPTIONS.getID(), new HashMap<>(userJvmOptions));
});
Optional.ofNullable(launcherArguments).ifPresent(arguments -> {
params.put(StandardBundlerParam.ARGUMENTS.getID(), new ArrayList<>(arguments));
});
// bugfix for #83 (by copying additional resources to /target/jfx/app folder)
// https://github.com/javafx-maven-plugin/javafx-maven-plugin/issues/83
Optional.ofNullable(additionalAppResources).filter(File::exists).ifPresent(appResources -> {
try{
Path targetFolder = jfxAppOutputDir.toPath();
Path sourceFolder = appResources.toPath();
copyRecursive(sourceFolder, targetFolder);
} catch(IOException e){
getLog().warn(e);
}
});
// gather all files for our application bundle
Set resourceFiles = new HashSet<>();
try{
Files.walk(jfxAppOutputDir.toPath())
.map(p -> p.toFile())
.filter(File::isFile)
.filter(File::canRead)
.forEach(f -> {
getLog().info(String.format("Add %s file to application resources.", f));
resourceFiles.add(f);
});
} catch(IOException e){
getLog().warn(e);
}
params.put(StandardBundlerParam.APP_RESOURCES.getID(), new RelativeFileSet(jfxAppOutputDir, resourceFiles));
// check for misconfiguration
Collection duplicateKeys = new HashSet<>();
Optional.ofNullable(bundleArguments).ifPresent(bArguments -> {
duplicateKeys.addAll(params.keySet());
duplicateKeys.retainAll(bArguments.keySet());
params.putAll(bArguments);
});
if( !duplicateKeys.isEmpty() ){
throw new MojoExecutionException("The following keys in duplicate other settings, please remove one or the other: " + duplicateKeys.toString());
}
if( !skipMainClassScanning ){
boolean mainClassInsideResourceJarFile = resourceFiles.stream().filter(resourceFile -> resourceFile.toString().endsWith(".jar")).filter(resourceJarFile -> isClassInsideJarFile(mainClass, resourceJarFile)).findFirst().isPresent();
if( !mainClassInsideResourceJarFile ){
// warn user about missing class-file
getLog().warn(String.format("Class with name %s was not found inside provided jar files!! JavaFX-application might not be working !!", mainClass));
}
}
// check for secondary launcher misconfiguration (their appName requires to be different as this would overwrite primary launcher)
Collection launcherNames = new ArrayList<>();
launcherNames.add(appName);
final AtomicBoolean nullLauncherNameFound = new AtomicBoolean(false);
// check "no launcher names" and gather all names
Optional.ofNullable(secondaryLaunchers).filter(list -> !list.isEmpty()).ifPresent(launchers -> {
getLog().info("Adding configuration for secondary native launcher");
nullLauncherNameFound.set(launchers.stream().anyMatch(launcher -> launcher.getAppName() == null));
if( !nullLauncherNameFound.get() ){
launcherNames.addAll(launchers.stream().map(launcher -> launcher.getAppName()).collect(Collectors.toList()));
// assume we have valid entry here
params.put(StandardBundlerParam.SECONDARY_LAUNCHERS.getID(), launchers.stream().map(launcher -> {
getLog().info("Adding secondary launcher: " + launcher.getAppName());
Map secondaryLauncher = new HashMap<>();
addToMapWhenNotNull(launcher.getAppName(), StandardBundlerParam.APP_NAME.getID(), secondaryLauncher);
addToMapWhenNotNull(launcher.getMainClass(), StandardBundlerParam.MAIN_CLASS.getID(), secondaryLauncher);
addToMapWhenNotNull(launcher.getJfxMainAppJarName(), StandardBundlerParam.MAIN_JAR.getID(), secondaryLauncher);
addToMapWhenNotNull(launcher.getNativeReleaseVersion(), StandardBundlerParam.VERSION.getID(), secondaryLauncher);
addToMapWhenNotNull(launcher.getVendor(), StandardBundlerParam.VENDOR.getID(), secondaryLauncher);
addToMapWhenNotNull(launcher.getIdentifier(), StandardBundlerParam.IDENTIFIER.getID(), secondaryLauncher);
addToMapWhenNotNull(launcher.isNeedMenu(), StandardBundlerParam.MENU_HINT.getID(), secondaryLauncher);
addToMapWhenNotNull(launcher.isNeedShortcut(), StandardBundlerParam.SHORTCUT_HINT.getID(), secondaryLauncher);
// as we can set another JAR-file, this might be completly different
addToMapWhenNotNull(launcher.getClasspath(), StandardBundlerParam.CLASSPATH.getID(), secondaryLauncher);
Optional.ofNullable(launcher.getJvmArgs()).ifPresent(jvmOptions -> {
secondaryLauncher.put(StandardBundlerParam.JVM_OPTIONS.getID(), new ArrayList<>(jvmOptions));
});
Optional.ofNullable(launcher.getJvmProperties()).ifPresent(jvmProps -> {
secondaryLauncher.put(StandardBundlerParam.JVM_PROPERTIES.getID(), new HashMap<>(jvmProps));
});
Optional.ofNullable(launcher.getUserJvmArgs()).ifPresent(userJvmOptions -> {
secondaryLauncher.put(StandardBundlerParam.USER_JVM_OPTIONS.getID(), new HashMap<>(userJvmOptions));
});
Optional.ofNullable(launcher.getLauncherArguments()).ifPresent(arguments -> {
params.put(StandardBundlerParam.ARGUMENTS.getID(), new ArrayList<>(arguments));
});
return secondaryLauncher;
}).collect(Collectors.toList()));
}
});
// check "no launcher names"
if( nullLauncherNameFound.get() ){
throw new MojoExecutionException("Not all secondary launchers have been configured properly.");
}
// check "duplicate launcher names"
Set duplicateLauncherNamesCheckSet = new HashSet<>();
launcherNames.stream().forEach(launcherName -> duplicateLauncherNamesCheckSet.add(launcherName));
if( duplicateLauncherNamesCheckSet.size() != launcherNames.size() ){
throw new MojoExecutionException("Secondary launcher needs to have different name, please adjust appName inside your configuration.");
}
// check and prepare for file-associations (might not be present on all bundlers)
Optional.ofNullable(fileAssociations).ifPresent(associations -> {
final List