/* * 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.commons.exec.OS; 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.Execute; import org.apache.maven.plugins.annotations.LifecyclePhase; 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.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import java.util.stream.Collectors; @Mojo(name = "jlink", requiresDependencyResolution = ResolutionScope.RUNTIME) @Execute(phase = LifecyclePhase.PROCESS_CLASSES) public class JavaFXJLinkMojo extends JavaFXBaseMojo { private static final Pattern JLINK_VERSION_PATTERN = Pattern.compile("(1[3-9]|[2-9][0-9]|\\d{3,})"); /** * Strips debug information out, equivalent to <code>-G, --strip-debug</code>, * default false */ @Parameter(property = "javafx.stripDebug", defaultValue = "false") private boolean stripDebug; /** * Strip Java debug attributes out, equivalent to <code>--strip-java-debug-attributes</code>, * default false */ @Parameter(property = "javafx.stripJavaDebugAttributes", defaultValue = "false") private boolean stripJavaDebugAttributes; /** * Compression level of the resources being used, equivalent to: * <code>-c, --compress=level</code>. Valid values: <code>0, 1, 2</code>, * default 0 */ @Parameter(property = "javafx.compress", defaultValue = "0") private Integer compress; /** * Remove the <code>includes</code> directory in the resulting runtime image, * equivalent to: <code>--no-header-files</code>, default false */ @Parameter(property = "javafx.noHeaderFiles", defaultValue = "false") private boolean noHeaderFiles; /** * Remove the <code>man</code> directory in the resulting Java runtime image, * equivalent to: <code>--no-man-pages</code>, default false */ @Parameter(property = "javafx.noManPages", defaultValue = "false") private boolean noManPages; /** * Add the option <code>--bind-services</code> or not, default false. */ @Parameter(property = "javafx.bindServices", defaultValue = "false") private boolean bindServices; /** * <code>--ignore-signing-information</code>, default false */ @Parameter(property = "javafx.ignoreSigningInformation", defaultValue = "false") private boolean ignoreSigningInformation; /** * Turn on verbose mode, equivalent to: <code>--verbose</code>, default false */ @Parameter(property = "javafx.jlinkVerbose", defaultValue = "false") private boolean jlinkVerbose; /** * Add a launcher script, equivalent to: * <code>--launcher <name>=<module>[/<mainclass>]</code>. */ @Parameter(property = "javafx.launcher") private String launcher; /** * The name of the folder with the resulting runtime image, * equivalent to <code>--output <path></code> */ @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; /** * <p> * 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. * </p> */ @Parameter(property = "javafx.jlinkExecutable", defaultValue = "jlink") private String jlinkExecutable; /** * Optional jmodsPath path for local builds. */ @Parameter(property = "javafx.jmodsPath") private String jmodsPath; /** * 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." ); } handleWorkingDirectory(); Map<String, String> enviro = handleSystemEnvVariables(); CommandLine commandLine = getExecutablePath(jlinkExecutable, enviro, workingDirectory); if (isTargetUsingJava8(commandLine)) { getLog().info("Jlink not supported with Java 1.8"); return; } if (stripJavaDebugAttributes && !isJLinkVersion13orHigher(commandLine.getExecutable())) { stripJavaDebugAttributes = false; getLog().warn("JLink parameter --strip-java-debug-attributes only supported for version 13 and higher"); getLog().warn("The option 'stripJavaDebugAttributes' was skipped"); } try { List<String> commandArguments = createCommandArguments(); 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 (launcher != null && ! launcher.isEmpty()) { patchLauncherScript(launcher); if (OS.isFamilyWindows()) { patchLauncherScript(launcher + ".bat"); } } 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 patchLauncherScript(String launcherFilename) throws IOException { Path launcherPath = Paths.get(builddir.getAbsolutePath(), jlinkImageName, "bin", launcherFilename); if (!Files.exists(launcherPath)) { getLog().debug("Launcher file not exist: " + launcherPath); return; } if (options != null) { String optionsString = options.stream() .filter(Objects::nonNull) .filter(String.class::isInstance) .map(String.class::cast) .collect(Collectors.joining(" ")); // Add vm options to launcher script List<String> lines = Files.lines(launcherPath) .map(line -> { boolean unixOptionsLine = "JLINK_VM_OPTIONS=".equals(line); boolean winOptionsLine = "set JLINK_VM_OPTIONS=".equals(line); if (unixOptionsLine || winOptionsLine) { String lineWrapper = unixOptionsLine ? "\"" : ""; return line + lineWrapper + optionsString + lineWrapper; } return line; }) .collect(Collectors.toList()); Files.write(launcherPath, lines); } if (commandlineArgs != null) { // Add options to launcher script List<String> lines = Files.lines(launcherPath) .map(line -> { if (line.endsWith("$@")) { return line.replace("$@", commandlineArgs + " $@"); } return line; }) .collect(Collectors.toList()); Files.write(launcherPath, lines); } } private List<String> createCommandArguments() throws MojoExecutionException, MojoFailureException { List<String> commandArguments = new ArrayList<>(); preparePaths(getParent(Paths.get(jlinkExecutable), 2)); if (modulepathElements != null && !modulepathElements.isEmpty()) { commandArguments.add(" --module-path"); String modulePath = StringUtils.join(modulepathElements.iterator(), File.pathSeparator); if (jmodsPath != null && ! jmodsPath.isEmpty()) { getLog().debug("Including jmods from local path: " + jmodsPath); modulePath = jmodsPath + File.pathSeparator + modulePath; } 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 (stripJavaDebugAttributes) { commandArguments.add(" --strip-java-debug-attributes"); } 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); } return commandArguments; } 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; } private boolean isJLinkVersion13orHigher(String jlinkExePath) { CommandLine versionCommandLine = new CommandLine(jlinkExePath) .addArgument("--version"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int resultCode = -1; try { resultCode = executeCommandLine(new DefaultExecutor(), versionCommandLine, null, baos, System.err); } catch (IOException e) { if (getLog().isDebugEnabled()) { getLog().error("Error getting JLink version", e); } } if (resultCode != 0) { getLog().error("Unable to get JLink version"); getLog().error("Result of " + versionCommandLine + " execution is: '" + resultCode + "'"); return false; } String versionStr = new String(baos.toByteArray()); return JLINK_VERSION_PATTERN.matcher(versionStr).lookingAt(); } // for tests }