org.tentackle.maven.plugin.jlink.AbstractJLinkMojo Maven / Gradle / Ivy
Show all versions of tentackle-jlink-maven-plugin Show documentation
/*
* Tentackle - https://tentackle.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.tentackle.maven.plugin.jlink;
import org.apache.maven.artifact.Artifact;
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.Parameter;
import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.zip.ZipArchiver;
import org.codehaus.plexus.languages.java.jpms.LocationManager;
import org.tentackle.common.FileHelper;
import org.tentackle.common.StringHelper;
import org.tentackle.common.ToolRunner;
import org.tentackle.maven.AbstractTentackleMojo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Base class for all mojos of the tentackle-jlink-maven-plugin
.
*
* @author harald
*/
public abstract class AbstractJLinkMojo extends AbstractTentackleMojo {
/**
* Name of the destination subfolder containing the classpath artifacts.
*/
public static final String DEST_CLASSPATH = "cp";
/**
* Name of the destination subfolder containing the modulepath artifacts.
*/
public static final String DEST_MODULEPATH = "mp";
/**
* Name of the destination subfolder containing the resources.
*/
public static final String DEST_RESOURCES = "conf";
/**
* File extension for ZIP files.
*/
public static final String ZIP_EXTENSION = "zip";
/**
* The name of the module holding the main class.
* If missing, the plugin will build an application running on the classpath.
*/
@Parameter
private String mainModule;
/**
* The name of the main class.
*/
@Parameter(required = true)
private String mainClass;
/**
* Dependencies that should go to the classpath instead of modulepath.
*/
@Parameter
private List classpathDependencies;
/**
* Extra directories to copy to the image.
*/
@Parameter
private List extraDirectories;
/**
* Extra classpath elements.
* Pathnames are relative to the image root.
*/
@Parameter
private List extraClasspathElements;
/**
* The directory holding the script templates.
*/
@Parameter(defaultValue = "${project.basedir}/templates")
private File templateDir;
/**
* Explicit path to the jdeps
tool.
* Only if toolchains cannot be used.
* @see #jdkToolchain
*/
@Parameter(property = "jdepsTool")
private File jdepsTool;
/**
* Explicit path to the jlink
tool.
* Only if toolchains cannot be used.
* @see #jdkToolchain
*/
@Parameter(property = "jlinkTool")
private File jlinkTool;
/**
* The optional file holding the jlink options.
* Corresponds to the jlink
option --save-opts
.
*/
@Parameter
private File saveOpts;
/**
* Enables the auto update feature.
*
* For the jlink
gaol an extra update script will be generated
* via the {@link JLinkMojo#updateTemplate}.
* The update script will be named bin/update.<extension>
where
* the extension is derived from the platform (cmd
on Windows,
* otherwise sh
for unixes).
*
* For the jpackage
goal an extra ZIP-file is created holding
* the jpackage image's files and an update script generated via
* {@link JPackageMojo#packageUpdateTemplate}.
*
* For an implementation of the auto-update feature see the Tentackle modules
* tentackle-update
and tentackle-fx-rdc-update
.
* Whereas tentackle-update
can be used by any kind of application,
* the frontend part tentackle-fx-rdc-update
requires a Tentackle FX
* application.
*/
@Parameter
private boolean withUpdater;
/**
* List of full-blown modules that should be moved to the module path explicitly.
* By default, all full-blown modules are passed to the jlink tool. If a module is listed
* for the module path only, the module it will be moved to the mp
folder instead.
* This may become necessary, if a 3rd-party full-blown module (illegally!) requires automatic modules, which
* would cause all artifacts to be moved to the module path by this plugin automatically, since the
* jlink tool can only handle full-blown modules.
*
* If all modules must be moved to the mp
folder, the single values true
,
* all
or *
can be used as a shortcut.
*
* Examples:
*
* <modulePathOnly>
* <module>tentackle.log.log4j2v</module>
* <module>org.apache.logging.log4j</module>
* </modulePathOnly>
*
* or
*
* <modulePathOnly>all</modulePathOnly>
*
*/
@Parameter
private List modulePathOnly;
/**
* The directory where to store the ZIP files.
*/
@Parameter(defaultValue = "${project.build.directory}")
private File zipDirectory;
/**
* Strip debug information.
* Corresponds to the jlink
option --strip-debug
.
*/
@Parameter(defaultValue = "false")
private boolean stripDebug;
/**
* Compression of resources.
* Corresponds to the jlink
option --compress
.
*/
@Parameter
private Integer compress;
/**
* Suppresses a fatal error when signed modular JARs are linked in the runtime image.
* Corresponds to the jlink
option --ignore-signing-information
*/
@Parameter(defaultValue = "false")
private boolean ignoreSigningInformation;
/**
* Suppress the includes
directory.
* Corresponds to the jlink
option --no-header-files
.
*/
@Parameter(defaultValue = "false")
private boolean noHeaderFiles;
/**
* Suppress the man
directory.
* Corresponds to the jlink
option --no-man-pages
*/
@Parameter(defaultValue = "false")
private boolean noManPages;
/**
* The directory holding extra resources, such as logger configurations or database
* credentials, that will be copied to the conf
directory.
* Defaults to the maven-resources-plugin's destination directory, usually target/classes
.
* If no resources must be copied, for example in server-images for production, set this to any
* non-existent directory, for example "none"
.
*/
@Parameter(defaultValue = "${project.build.directory}/classes")
private File resourcesDirectory;
/**
* Extra jre or jdk modules to add to the image.
* Useful to add the runtime localization module or extra tools in bin.
*
* Example:
*
*
* <addModules>
* <addModule>jdk.localedata</addModule>
* <addModule>jdk.jcmd</addModule>
* </addModules>
*
*/
@Parameter
private List addModules;
/**
* Modules to exclude.
* Necessary only if a dependency refers to removed api even if not used at all at runtime.
*
*
* <excludeModules>
* <excludeModule>jdk8internals</excludeModule>
* </excludeModules>
*
*/
@Parameter
private List excludeModules;
/**
* Extra variables for template or artifact generation.
*
* For example the subproject's basedir:
*
* <variables>
* <project>${project.basedir}</project>
* </variables>
*
* And then use it in package-image.ftl
for file paths, such as:
*
* --icon ${project}/src/pkg/myapp.ico
*
*/
@Parameter
private Map variables;
/**
* Defines the precedence of properties and variables.
* By default, system properties are overridden by maven properties which are overridden by the extra template variables.
*
* The case-insensitive keywords are:
*
* - {@code system, sys}: for system properties
* - {@code maven, mvn, properties, props, pom}: maven properties
* - {@code variables, vars, var, extras, extra, template}: extra template variables
*
*/
@Parameter(defaultValue = "system, maven, variables", required = true)
private String variablesPrecedence;
/**
* The directory created by jlink holding the image.
*/
@Parameter(defaultValue = "${project.build.directory}/jlink", required = true)
private File imageDirectory;
/**
* Additional classifier to add to deployed artifacts.
*/
@Parameter
private String extraClassifier;
/**
* The generated ZIP file name.
*/
@Parameter(defaultValue = "${project.build.finalName}", required = true, readonly = true)
private String finalName;
/**
* JPMS support.
*/
@Component
private LocationManager locationManager;
/**
* To attach artifacts with classifier.
*/
@Component
private MavenProjectHelper projectHelper;
/**
* The archiver to zip the image.
*/
@Component(role = Archiver.class, hint = ZIP_EXTENSION)
private ZipArchiver zipArchiver;
/**
* The java runtime version used by the application.
*/
private String javaRuntimeVersion;
/**
* The java major version as a comparable integer.
*/
private int javaMajorRuntimeVersion;
/**
* Gets the ZIP file's name without extension.
* Invoked by the {@link ArtifactCreator}.
*
* @return the final name
*/
public String getFinalName() {
return finalName;
}
/**
* Returns whether the update feature is enabled.
*
* @return true if generate artifacts with auto-update feature
*/
public boolean isWithUpdater() {
return withUpdater;
}
/**
* Returns whether all application modules should go to the module path and
* not the jimage file.
*
* @return true if forced to mp folder
*/
public boolean isModulePathOnly() {
return modulePathOnly != null && modulePathOnly.size() == 1 &&
("true".equals(modulePathOnly.get(0)) ||
"all".equals(modulePathOnly.get(0)) ||
"*".equals(modulePathOnly.get(0)));
}
/**
* Returns whether given module name should go to the module path and
* not the jimage file.
*
* @param moduleName the name of the full-blown module
* @return true if forced to mp folder
*/
public boolean isModulePathOnly(String moduleName) {
return modulePathOnly != null && modulePathOnly.contains(moduleName);
}
/**
* Gets the directory where to store the ZIP-file.
*
* @return the ZIP target folder
*/
public File getZipDirectory() {
return zipDirectory;
}
/**
* Gets the template directory.
*
* @return the directory holding the wizard templates
*/
public File getTemplateDir() {
return templateDir;
}
/**
* Gets the directory created by jlink holding the image.
*
* @return the image dir, never null
*/
public File getImageDirectory() {
return imageDirectory;
}
/**
* Gets the name of the module holding the main class.
*
* @return the main module, null or empty string to build an application running on the classpath
*/
public String getMainModule() {
return mainModule;
}
/**
* Gets the name of the main class.
*
* @return the main class, never null
*/
public String getMainClass() {
return mainClass;
}
/**
* Gets the location manager to extract module information.
*
* @return the JPMS manager
*/
public LocationManager getLocationManager() {
return locationManager;
}
/**
* Gets the project helper to attach artifacts.
*
* @return the project helper
*/
public MavenProjectHelper getProjectHelper() {
return projectHelper;
}
/**
* Gets the jdeps tool.
*
* @return the file, never null
*/
public File getJdepsTool() {
return jdepsTool;
}
/**
* Gets extra jre or jdk modules to add to the image.
*
* @return extra module names, never null
*/
public List getAddModules() {
return addModules == null ? Collections.emptyList() : addModules;
}
/**
* Gets the modules to exclude.
*
* @return excluded module names, never null
*/
public List getExcludeModules() {
return excludeModules == null ? Collections.emptyList() : excludeModules;
}
/**
* Gets the extra variables for templates or artifact generation.
*
* @return the variables, never null
*/
public Map getVariables() {
return variables == null ? Collections.emptyMap() : variables;
}
/**
* Gets the order how properties and variables override each other.
*
* @return default is system, maven, variables
*/
public String getVariablesPrecedence() {
return variablesPrecedence;
}
/**
* Gets the optional extra deployment classifier.
*
* @return the extra classifier
*/
public String getExtraClassifier() {
return extraClassifier;
}
/**
* Gets optional extra classpath elements.
*
* @return pathnames relative to the image root, null if none
*/
public List getExtraClasspathElements() {
return extraClasspathElements;
}
/**
* Extra directories to copy to the image.
*
* @return the directories, null if none
*/
public List getExtraDirectories() {
return extraDirectories;
}
/**
* Gets the java runtime version used by the application.
*
* @return the java version
*/
public String getJavaRuntimeVersion() {
return javaRuntimeVersion;
}
/**
* Gets the java major runtime version as a comparable integer.
*
* @return the major version
*/
public int getJavaMajorRuntimeVersion() {
return javaMajorRuntimeVersion;
}
@Override
public void prepareExecute() throws MojoExecutionException, MojoFailureException {
if (jdepsTool == null) {
jdepsTool = getToolFinder().find("jdeps");
}
if (jlinkTool == null) {
jlinkTool = getToolFinder().find("jlink");
}
}
@Override
public void executeImpl() throws MojoExecutionException, MojoFailureException {
// analyze project, determine jlink strategy
JLinkResolver.Result result = new JLinkResolver(this, getMavenProject().getArtifacts()).resolve();
// generate image
createImage(result);
// and the shell scripts
generateFiles(result);
}
/**
* The zip file will be installed and deployed with an additional classifier
* showing the operating system and the platform.
*
* @return the classifier
* @throws MojoExecutionException if no os.name
*/
public String getClassifier() throws MojoExecutionException {
try {
String classifier = StringHelper.getPlatform() + "-" + StringHelper.getArchitecture();
if (!StringHelper.isAllWhitespace(getExtraClassifier())) {
classifier += "-" + getExtraClassifier();
}
return classifier;
}
catch (RuntimeException ex) {
throw new MojoExecutionException(ex.getMessage());
}
}
/**
* Gets the prefix to the jars and dirs in the runtime image.
*
* @return the prefix, empty string if none (never null)
*/
public String getImagePathPrefix() {
return "";
}
/**
* Creates the zip file.
* Invoked by the {@link ArtifactCreator}.
*
* @param dir the directory to create a zip file from
* @param name the zip filename without extension
* @return the created zip file
* @throws MojoExecutionException if some archiver or IO errors occurred
*/
public File createZipFile(File dir, String name) throws MojoExecutionException {
File zipFile = new File(zipDirectory, name + "." + ZIP_EXTENSION);
zipArchiver.addDirectory(dir);
zipArchiver.setDestFile(zipFile);
try {
zipArchiver.createArchive();
}
catch (ArchiverException | IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
return zipFile;
}
/**
* Returns whether an artifact should be moved to the classpath explicitly.
*
* @param artifact the artifact
* @return true if move to classpath, else modulepath
*/
public boolean isClasspathDependency(Artifact artifact) {
if (classpathDependencies != null) {
for (ClasspathDependency classpathDependency: classpathDependencies) {
if (Objects.equals(classpathDependency.getGroupId(), artifact.getGroupId()) &&
Objects.equals(classpathDependency.getArtifactId(), artifact.getArtifactId()) &&
Objects.equals(classpathDependency.getClassifier(), artifact.getClassifier())) {
return true;
}
}
}
return false;
}
/**
* Generates additional files such as shell scripts or command files.
*
* @param result the resolver result
* @throws MojoExecutionException if generation failed
*/
abstract protected void generateFiles(JLinkResolver.Result result) throws MojoExecutionException;
/**
* Copies the templates to the template directory.
*
* @param overwrite true if overwrite existing templates, false if install only missing
* @throws MojoExecutionException if installation failed
*/
abstract protected void installTemplates(boolean overwrite) throws MojoExecutionException;
/**
* Invokes the jlink tool and copies other artifacts and resources.
*
* @param result the resolver result
*/
protected void createImage(JLinkResolver.Result result) throws MojoFailureException, MojoExecutionException {
// run jlink
createJLinkImage(result);
// copy artifacts not covered by jlink to modulepath and classpath subdirectories
copyArtifacts(result);
// copy resources such as backend.properties or logging config to the conf subdirectory
copyResources(result);
// extra directories, if any
copyExtraDirectories();
}
/**
* Creates the jlink image.
*
* @param result the resolver info
* @throws MojoExecutionException if building the JPMS info failed
* @throws MojoFailureException if jlink returned an error code
*/
protected void createJLinkImage(JLinkResolver.Result result) throws MojoExecutionException, MojoFailureException {
getLog().info("creating jlink image for a " +
(result.isPlainModular() ? "plain " : "") +
(result.isModular() ? "modular" : "classpath") + " application with Java " + javaRuntimeVersion);
getLog().debug(result.toString());
ToolRunner jlinkRunner = new ToolRunner(jlinkTool);
if (saveOpts != null) {
jlinkRunner.arg("--save-opts").arg(saveOpts);
}
jlinkRunner.arg("--output").arg(getImageDirectory());
if (stripDebug) {
jlinkRunner.arg("--strip-debug");
}
if (compress != null) {
jlinkRunner.arg("--compress=" + compress);
}
if (ignoreSigningInformation) {
jlinkRunner.arg("--ignore-signing-information");
}
if (noHeaderFiles) {
jlinkRunner.arg("--no-header-files");
}
if (noManPages) {
jlinkRunner.arg("--no-man-pages"); // is this implemented in jlink at all???
}
result.generateJlinkModulePath(jlinkRunner);
result.generateJlinkModules(jlinkRunner);
getLog().debug(jlinkRunner.toString());
int errCode = jlinkRunner.run().getErrCode();
if (errCode != 0) {
throw new MojoFailureException("jlink failed: " + errCode + "\n" + jlinkRunner.getErrorsAsString() +
"\n" + jlinkRunner.getOutputAsString());
}
}
/**
* Copies artifacts not already in the created jlink image to module- or classpath.
*
* @param result the resolver info
* @throws MojoExecutionException if copy failed
*/
protected void copyArtifacts(JLinkResolver.Result result) throws MojoExecutionException {
try {
// module path, if any
if (result.isModular()) {
List modulePath = result.getModulePath();
if (!modulePath.isEmpty()) {
File mpDir = new File(getImageDirectory(), DEST_MODULEPATH);
mpDir.mkdir();
for (ModularArtifact artifact : modulePath) {
File dest = new File(mpDir, artifact.getFileName());
getLog().debug("copying " + artifact.getPath() + " to " + mpDir);
FileHelper.copy(artifact.getFile(), dest);
}
}
}
// classpath, if any
List classPath = result.getClassPath();
if (!classPath.isEmpty()) {
File cpDir = new File(getImageDirectory(), DEST_CLASSPATH);
cpDir.mkdir();
for (Artifact artifact : classPath) {
File dest = new File(cpDir, artifact.getFile().getName());
getLog().debug("copying " + artifact.getFile() + " to " + cpDir);
FileHelper.copy(artifact.getFile(), dest);
}
}
}
catch (IOException ex) {
throw new MojoExecutionException("copying artifacts failed", ex);
}
}
/**
* Copies the resources to the conf
directory.
*
* @param result the resolver info
* @throws MojoExecutionException if copy failed
*/
protected void copyResources(JLinkResolver.Result result) throws MojoExecutionException {
// copy resources such as backend.properties or logging config to the conf subdirectory
if (resourcesDirectory != null && resourcesDirectory.isDirectory()) {
int resCount = copyResources(resourcesDirectory, new File(getImageDirectory(), DEST_RESOURCES));
if (resCount > 0) {
result.addToClasspath(DEST_RESOURCES);
}
}
}
/**
* Copies optional extra directories to the image directory.
*
* @throws MojoExecutionException if failed
*/
protected void copyExtraDirectories() throws MojoExecutionException {
// extra directories, if any
try {
if (getExtraDirectories() != null) {
for (File fromDir : getExtraDirectories()) {
File toDir = new File(getImageDirectory(), fromDir.getName());
FileHelper.copyDirectory(fromDir, toDir, true, null);
}
}
}
catch (IOException ex) {
throw new MojoExecutionException("copying extra directories failed", ex);
}
}
@Override
protected boolean validate() throws MojoExecutionException {
if (super.validate()) {
if (jdepsTool == null) {
throw new MojoExecutionException("jdeps tool not found");
}
getLog().debug("using " + jdepsTool);
javaRuntimeVersion = determineJavaToolVersion(jdepsTool);
javaMajorRuntimeVersion = getMajorVersion(javaRuntimeVersion);
if (jlinkTool == null) {
throw new MojoExecutionException("jlink tool not found");
}
getLog().debug("using " + jlinkTool);
if (templateDir.exists()) {
if (!templateDir.isDirectory()) {
throw new MojoExecutionException(templateDir.getPath() + " is not a directory");
}
}
else {
getLog().info("template directory created: " + templateDir.getPath());
}
installTemplates(false); // install any missing template
if (mainClass == null) {
throw new MojoExecutionException("mainClass missing");
}
return true;
}
return false;
}
/**
* Installs the template with the given name.
*
* @param pluginTemplate the plugin's name of the template
* @param projectTemplate the project's name of the template
* @param overwrite true if overwrite existing template, false if leave unchanged
* @throws MojoExecutionException if installation failed
*/
protected void installTemplate(String pluginTemplate, String projectTemplate, boolean overwrite) throws MojoExecutionException {
templateDir.mkdirs();
File file = new File(templateDir, projectTemplate);
if (overwrite || !file.exists()) {
String path = "/templates/" + pluginTemplate;
String text = loadResourceFileIntoString(path);
try (PrintStream ps = new PrintStream(new FileOutputStream(file))) {
ps.print(text);
getLog().info("installed template " + projectTemplate);
}
catch (IOException e) {
throw new MojoExecutionException("cannot install template " + path, e);
}
}
}
/**
* Logs the toolrunner's output at info level.
*
* @param runner the toolrunner
* @throws MojoExecutionException if runner not invoked so far
*/
protected void logToolOutput(ToolRunner runner) throws MojoExecutionException {
String out = runner.getOutputAsString();
if (!out.isEmpty()) {
getLog().info(out);
}
}
/**
* Copies the resources.
*
* @param srcDir the resource directory
* @param dstDir the destination directory
* @return number of resource files copied
* @throws MojoExecutionException if copy failed
*/
private int copyResources(File srcDir, File dstDir) throws MojoExecutionException {
try {
return FileHelper.copyDirectory(srcDir, dstDir,true, null);
}
catch (IOException ex) {
throw new MojoExecutionException("copying resources failed", ex);
}
}
}