All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.tools.ant.taskdefs.modules.Link Maven / Gradle / Ivy

There is a newer version: 2.0.27
Show newest version
/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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
 *
 *      https://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.apache.tools.ant.taskdefs.modules;

import java.io.File;
import java.io.PrintStream;
import java.io.ByteArrayOutputStream;
import java.io.Reader;
import java.io.IOException;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import java.nio.file.Files;
import java.nio.file.FileVisitResult;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;

import java.util.Collection;
import java.util.List;
import java.util.ArrayList;

import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Properties;

import java.util.Collections;
import java.util.Objects;

import java.util.spi.ToolProvider;

import java.util.stream.Stream;
import java.util.stream.Collectors;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;

import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.LogLevel;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.ResourceCollection;

import org.apache.tools.ant.util.CompositeMapper;
import org.apache.tools.ant.util.MergingMapper;

import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.ResourceUtils;

/**
 * Assembles jmod files into an executable image.  Equivalent to the
 * JDK {@code jlink} command.
 * 

* Supported attributes: *

*
{@code destDir} *
Root directory of created image. (required) *
{@code modulePath} *
Path of modules. Should be a list of .jmod files. Required, unless * nested module path or modulepathref is present. *
{@code modulePathRef} *
Reference to path of modules. Referenced path should be * a list of .jmod files. *
{@code modules} *
Comma-separated list of modules to assemble. Required, unless * one or more nested {@code } elements are present. *
{@code observableModules} *
Comma-separated list of explicit modules that comprise * "universe" visible to tool while linking. *
{@code launchers} *
Comma-separated list of commands, each of the form * name{@code =}module or * name{@code =}module{@code /}mainclass *
{@code excludeFiles} *
Comma-separated list of patterns specifying files to exclude from * linked image. * Each is either a standard PathMatcher pattern * or {@code @}filename. *
{@code excludeResources} *
Comma-separated list of patterns specifying resources to exclude from jmods. * Each is either a standard PathMatcher pattern * or {@code @}filename. *
{@code locales} *
Comma-separated list of extra locales to include, * requires {@code jdk.localedata} module *
{@code resourceOrder} *
Comma-separated list of patterns specifying resource search order. * Each is either a standard PathMatcher pattern * or {@code @}filename. *
{@code bindServices} *
boolean, whether to link service providers; default is false *
{@code ignoreSigning} *
boolean, whether to allow signed jar files; default is false *
{@code includeHeaders} *
boolean, whether to include header files; default is true *
{@code includeManPages} *
boolean, whether to include man pages; default is true *
{@code includeNativeCommands} *
boolean, whether to include native executables normally generated * for image; default is true *
{@code debug} *
boolean, whether to include debug information; default is true *
{@code verboseLevel} *
If set, jlink will produce verbose output, which will be logged at * the specified Ant log level ({@code DEBUG}, {@code VERBOSE}, * {@code INFO}}, {@code WARN}, or {@code ERR}). *
{@code compress} *
compression level, one of: *
*
{@code 0} *
{@code none} *
no compression (default) *
{@code 1} *
{@code strings} *
constant string sharing *
{@code 2} *
{@code zip} *
zip compression *
*
{@code endianness} *
Must be {@code little} or {@code big}, default is native endianness *
{@code checkDuplicateLegal} *
Boolean. When merging legal notices from different modules * because they have the same name, verify that their contents * are identical. Default is false, which means any license files * with the same name are assumed to have the same content, and no * checking is done. *
{@code vmType} *
Hotspot VM in image, one of: *
    *
  • {@code client} *
  • {@code server} *
  • {@code minimal} *
  • {@code all} (default) *
*
* *

* Supported nested elements *

*
{@code } *
path element *
{@code } *
May be specified multiple times. * Only attribute is required {@code name} attribute. *
{@code } *
May be specified multiple times. * Only attribute is required {@code name} attribute. *
{@code } *
May be specified multiple times. Attributes: *
    *
  • {@code name} (required) *
  • {@code module} (required) *
  • {@code mainClass} (optional) *
*
{@code } *
May be specified multiple times. * Only attribute is required {@code name} attribute. *
{@code } *
Explicit resource search order in image. May be specified multiple * times. Exactly one of these attributes must be specified: *
*
{@code pattern} *
A standard PathMatcher pattern *
{@code listFile} *
Text file containing list of resource names (not patterns), * one per line *
* If the {@code resourceOrder} attribute is also present on the task, its * patterns are treated as if they occur before patterns in nested * {@code } elements. *
{@code } *
Excludes files from linked image tree. May be specified multiple times. * Exactly one of these attributes is required: *
*
{@code pattern} *
A standard PathMatcher pattern *
{@code listFile} *
Text file containing list of file names (not patterns), * one per line *
*
{@code } *
Excludes resources from jmods. May be specified multiple times. * Exactly one of these attributes is required: *
*
{@code pattern} *
A standard PathMatcher pattern *
{@code listFile} *
Text file containing list of resource names (not patterns), * one per line *
*
{@code } *
Must have {@code level} attribute, whose permitted values are the same * as the {@code compress} task attribute described above. * May also have a {@code files} attribute, which is a comma-separated * list of patterns, and/or nested {@code } elements, each with * either a {@code pattern} attribute or {@code listFile} attribute. *
{@code } *
Replaces, augments, or trims the image's release info properties. * This may specify any of the following: *
    *
  • A {@code file} attribute, pointing to a Java properties file * containing new release info properties that will entirely replace * the current ones. *
  • A {@code delete} attribute, containing comma-separated property keys * to remove from application's release info, and/or any number of * nested {@code } elements, each with a required {@code key} * attribute. *
  • One or more nested {@code } elements, containing either * {@code key} and {@code value} attributes, or a {@code file} * attribute and an optional {@code charset} attribute. *
*
* * @see jlink tool reference * * @since 1.10.6 */ public class Link extends Task { /** * Error message for improperly formatted launcher attribute. */ private static final String INVALID_LAUNCHER_STRING = "Launcher command must take the form name=module " + "or name=module/mainclass"; /** Path of directories containing linkable modules. */ private Path modulePath; /** Modules to include in linked image. */ private final List modules = new ArrayList<>(); /** If non-empty, list of all modules linker is permitted to know about. */ private final List observableModules = new ArrayList<>(); /** * Additional runnable programs which linker will place in image's * bin directory. */ private final List launchers = new ArrayList<>(); /** * Locales to explicitly include from {@code jdk.localdata} module. * If empty, all locales are included. */ private final List locales = new ArrayList<>(); /** Resource ordering. */ private final List ordering = new ArrayList<>(); /** Files to exclude from linked image. */ private final List excludedFiles = new ArrayList<>(); /** * Resources in linked modules which should be excluded from linked image. */ private final List excludedResources = new ArrayList<>(); /** * Whether to include all service provides in linked image which are * present in the module path and which are needed by modules explicitly * linked. */ private boolean bindServices; /** * Whether to ignore signed jars (and jmods based on signed jars) when * linking, instead of emitting an error. */ private boolean ignoreSigning; /** Whether to include header files from linked modules in image. */ private boolean includeHeaders = true; /** Whether to include man pages from linked modules in image. */ private boolean includeManPages = true; /** Whether to include native commands from linked modules in image. */ private boolean includeNativeCommands = true; /** Whether to include classes' debug information or strip it. */ private boolean debug = true; /** * The Ant logging level at which verbose output of linked should be * emitted. If null, verbose output is disabled. */ private LogLevel verboseLevel; /** Directory into which linked image will be placed. */ private File outputDir; /** Endianness of some files (?) in linked image. */ private Endianness endianness; /** * Simple compression level applied to linked image. * This or {@link #compression} may be set, but not both. */ private CompressionLevel compressionLevel; /** * Describes which files in image to compress, and how to compress them. * This or {@link #compressionLevel} may be set, but not both. */ private Compression compression; /** * Whether to check duplicate legal notices from different modules * actually have identical content, not just indentical names, * before merging them. * Forced to true as of Java 11. */ private boolean checkDuplicateLegal; /** Type of VM in linked image. */ private VMType vmType; /** Changes to linked image's default release info. */ private final List releaseInfo = new ArrayList<>(); /** * Adds child {@code } element. * * @return new, empty child element * * @see #setModulePath(Path) */ public Path createModulePath() { if (modulePath == null) { modulePath = new Path(getProject()); } return modulePath.createPath(); } /** * Attribute containing path of directories containing linkable modules. * * @return current module path, possibly {@code null} * * @see #setModulePath(Path) * @see #createModulePath() */ public Path getModulePath() { return modulePath; } /** * Sets attribute containing path of directories containing * linkable modules. * * @param path new module path * * @see #getModulePath() * @see #setModulePathRef(Reference) * @see #createModulePath() */ public void setModulePath(final Path path) { if (modulePath == null) { this.modulePath = path; } else { modulePath.append(path); } } /** * Sets module path as a reference. * * @param ref path reference * * @see #setModulePath(Path) * @see #createModulePath() */ public void setModulePathRef(final Reference ref) { createModulePath().setRefid(ref); } /** * Adds child {@code } element, specifying a module to link. * * @return new, unconfigured child element * * @see #setModules(String) */ public ModuleSpec createModule() { ModuleSpec module = new ModuleSpec(); modules.add(module); return module; } /** * Sets attribute containing list of modules to link. * * @param moduleList comma-separated list of module names */ public void setModules(final String moduleList) { for (String moduleName : moduleList.split(",")) { modules.add(new ModuleSpec(moduleName)); } } /** * Creates child {@code } element that represents * one of the modules the linker is permitted to know about. * * @return new, unconfigured child element */ public ModuleSpec createObservableModule() { ModuleSpec module = new ModuleSpec(); observableModules.add(module); return module; } /** * Sets attribute containing modules linker is permitted to know about. * * @param moduleList comma-separated list of module names */ public void setObservableModules(final String moduleList) { for (String moduleName : moduleList.split(",")) { observableModules.add(new ModuleSpec(moduleName)); } } /** * Creates child {@code } element that can contain information * on additional executable in the linked image. * * @return new, unconfigured child element * * @see #setLaunchers(String) */ public Launcher createLauncher() { Launcher command = new Launcher(); launchers.add(command); return command; } /** * Sets attribute containing comma-separated list of information needed for * additional executables in the linked image. Each item must be of the * form * name{@code =}module or * name{@code =}module{@code /}mainclass. * * @param launcherList comma-separated list of launcher data */ public void setLaunchers(final String launcherList) { for (String launcherSpec : launcherList.split(",")) { launchers.add(new Launcher(launcherSpec)); } } /** * Creates child {@code } element that specifies a Java locale, * or set of locales, to include from the {@code jdk.localedata} module * in the linked image. * * @return new, unconfigured child element */ public LocaleSpec createLocale() { LocaleSpec locale = new LocaleSpec(); locales.add(locale); return locale; } /** * Sets attribute containing a list of locale patterns, to specify * Java locales to include from {@code jdk.localedata} module in * linked image. Asterisks ({@code *}) are permitted for wildcard * matches. * * @param localeList comma-separated list of locale patterns */ public void setLocales(final String localeList) { for (String localeName : localeList.split(",")) { locales.add(new LocaleSpec(localeName)); } } /** * Creates child {@code } element that specifies * files to exclude from linked modules when assembling linked image. * * @return new, unconfigured child element * * @see #setExcludeFiles(String) */ public PatternListEntry createExcludeFiles() { PatternListEntry entry = new PatternListEntry(); excludedFiles.add(entry); return entry; } /** * Sets attribute containing a list of patterns denoting files * to exclude from linked modules when assembling linked image. * * @param patternList comman-separated list of patterns * * @see Link.PatternListEntry */ public void setExcludeFiles(String patternList) { for (String pattern : patternList.split(",")) { excludedFiles.add(new PatternListEntry(pattern)); } } /** * Creates child {@code } element that specifies * resources in linked modules that will be excluded from linked image. * * @return new, unconfigured child element * * @see #setExcludeResources(String) */ public PatternListEntry createExcludeResources() { PatternListEntry entry = new PatternListEntry(); excludedResources.add(entry); return entry; } /** * Sets attribute containing a list of patterns denoting resources * to exclude from linked modules in linked image. * * @param patternList comma-separated list of patterns * * @see #createExcludeResources() * @see Link.PatternListEntry */ public void setExcludeResources(String patternList) { for (String pattern : patternList.split(",")) { excludedResources.add(new PatternListEntry(pattern)); } } /** * Creates child {@code orderList = new ArrayList<>(); for (String pattern : patternList.split(",")) { orderList.add(new PatternListEntry(pattern)); } // Attribute value comes before nested elements. ordering.addAll(0, orderList); } /** * Attribute indicating whether linked image should pull in providers * in the module path of services used by explicitly linked modules. * * @return true if linked will pull in service provides, false if not * * @see #setBindServices(boolean) */ public boolean getBindServices() { return bindServices; } /** * Sets attribute indicating whether linked image should pull in providers * in the module path of services used by explicitly linked modules. * * @param bind whether to include service providers * * @see #getBindServices() */ public void setBindServices(final boolean bind) { this.bindServices = bind; } /** * Attribute indicating whether linker should allow modules made from * signed jars. * * @return true if signed jars are allowed, false if modules based on * signed jars cause an error * * @see #setIgnoreSigning(boolean) */ public boolean getIgnoreSigning() { return ignoreSigning; } /** * Sets attribute indicating whether linker should allow modules made from * signed jars. *

* Note: As of Java 11, this attribute is internally forced to true. See * the source. * * @param ignore true to have linker allow signed jars, * false to have linker emit an error for signed jars * * * @see #getIgnoreSigning() */ public void setIgnoreSigning(final boolean ignore) { this.ignoreSigning = ignore; } /** * Attribute indicating whether to include header files from linked modules * in image. * * @return true if header files should be included, false to exclude them * * @see #setIncludeHeaders(boolean) */ public boolean getIncludeHeaders() { return includeHeaders; } /** * Sets attribute indicating whether to include header files from * linked modules in image. * * @param include true if header files should be included, * false to exclude them * * @see #getIncludeHeaders() */ public void setIncludeHeaders(final boolean include) { this.includeHeaders = include; } /** * Attribute indicating whether to include man pages from linked modules * in image. * * @return true if man pages should be included, false to exclude them * * @see #setIncludeManPages(boolean) */ public boolean getIncludeManPages() { return includeManPages; } /** * Sets attribute indicating whether to include man pages from * linked modules in image. * * @param include true if man pages should be included, * false to exclude them * * @see #getIncludeManPages() */ public void setIncludeManPages(final boolean include) { this.includeManPages = include; } /** * Attribute indicating whether to include generated native commands, * and native commands from linked modules, in image. * * @return true if native commands should be included, false to exclude them * * @see #setIncludeNativeCommands(boolean) */ public boolean getIncludeNativeCommands() { return includeNativeCommands; } /** * Sets attribute indicating whether to include generated native commands, * and native commands from linked modules, in image. * * @param include true if native commands should be included, * false to exclude them * * @see #getIncludeNativeCommands() */ public void setIncludeNativeCommands(final boolean include) { this.includeNativeCommands = include; } /** * Attribute indicating whether linker should keep or strip * debug information in classes. * * @return true if debug information will be retained, * false if it will be stripped * * @see #setDebug(boolean) */ public boolean getDebug() { return debug; } /** * Sets attribute indicating whether linker should keep or strip * debug information in classes. * * @param debug true if debug information should be retained, * false if it should be stripped * * @see #getDebug() */ public void setDebug(final boolean debug) { this.debug = debug; } /** * Attribute indicating whether linker should produce verbose output, * and at what logging level that output should be shown. * * @return logging level at which to show linker's verbose output, * or {@code null} to disable verbose output * * @see #setVerboseLevel(LogLevel) */ public LogLevel getVerboseLevel() { return verboseLevel; } /** * Sets attribute indicating whether linker should produce verbose output, * and at what logging level that output should be shown. * * @param level level logging level at which to show linker's * verbose output, or {@code null} to disable verbose output * * @see #getVerboseLevel() */ public void setVerboseLevel(final LogLevel level) { this.verboseLevel = level; } /** * Required attribute containing directory where linked image will be * created. * * @return directory where linked image will reside * * @see #setDestDir(File) */ public File getDestDir() { return outputDir; } /** * Sets attribute indicating directory where linked image will be created. * * @param dir directory in which image will be created by linker * * @see #getDestDir() */ public void setDestDir(final File dir) { this.outputDir = dir; } /** * Attribute indicating level of compression linker will apply to image. * This is exclusive with regard to {@link #createCompress()}: only one * of the two may be specified. * * @return compression level to apply, or {@code null} for none * * @see #setCompress(Link.CompressionLevel) * @see #createCompress() */ public CompressionLevel getCompress() { return compressionLevel; } /** * Sets attribute indicating level of compression linker will apply * to image. This is exclusive with regard to {@link #createCompress()}: * only one of the two may be specified. * * @param level compression level to apply, or {@code null} for none * * @see #getCompress() * @see #createCompress() */ public void setCompress(final CompressionLevel level) { this.compressionLevel = level; } /** * Creates child {@code } element that specifies the level of * compression the linker will apply, and optionally, which files in the * image will be compressed. This is exclusive with regard to the * {@link #setCompress compress} attribute: only one of the two may be * specified. * * @return new, unconfigured child element * * @see #setCompress(Link.CompressionLevel) */ public Compression createCompress() { if (compression != null) { throw new BuildException( "Only one nested compression element is permitted.", getLocation()); } compression = new Compression(); return compression; } /** * Attribute which indicates whether certain files in the linked image * will be big-endian or little-endian. If {@code null}, the underlying * platform's endianness is used. * * @return endianness to apply, or {@code null} to platform default * * @see #setEndianness(Link.Endianness) */ public Endianness getEndianness() { return endianness; } /** * Sets attribute which indicates whether certain files in the linked image * will be big-endian or little-endian. If {@code null}, the underlying * platform's endianness is used. * * @param endianness endianness to apply, or {@code null} to use * platform default * * @see #getEndianness() */ public void setEndianness(final Endianness endianness) { this.endianness = endianness; } /** * Attribute indicating whether linker should check legal notices with * duplicate names, and refuse to merge them (usually using symbolic links) * if their respective content is not identical. * * @return true if legal notice files with same name should be checked * for identical content, false to suppress check * * @see #setCheckDuplicateLegal(boolean) */ public boolean getCheckDuplicateLegal() { return checkDuplicateLegal; } /** * Sets attribute indicating whether linker should check legal notices with * duplicate names, and refuse to merge them (usually using symbolic links) * if their respective content is not identical. * * @param check true if legal notice files with same name should be checked * for identical content, false to suppress check * * @see #getCheckDuplicateLegal() */ public void setCheckDuplicateLegal(final boolean check) { this.checkDuplicateLegal = check; } /** * Attribute indicating what type of JVM the linked image should have. * If {@code null}, all JVM types are included. * * @return type of JVM linked image will have * * @see #setVmType(Link.VMType) */ public VMType getVmType() { return vmType; } /** * Set attribute indicating what type of JVM the linked image should have. * If {@code null}, all JVM types are included. * * @param type type of JVM linked image will have * * @see #getVmType() */ public void setVmType(final VMType type) { this.vmType = type; } /** * Creates child {@code } element that modifies the default * release properties of the linked image. * * @return new, unconfigured child element */ public ReleaseInfo createReleaseInfo() { ReleaseInfo info = new ReleaseInfo(); releaseInfo.add(info); return info; } /** * Child element that explicitly names a Java module. */ public class ModuleSpec { /** Module's name. Required. */ private String name; /** Creates an unconfigured element. */ public ModuleSpec() { // Deliberately empty. } /** * Creates an element with the given module name. * * @param name module's name */ public ModuleSpec(final String name) { setName(name); } /** * Attribute containing name of module this element represents. * * @return name of module */ public String getName() { return name; } /** * Sets attribute representing the name of this module this element * represents. * * @param name module's name */ public void setName(final String name) { this.name = name; } /** * Verifies this element's state. * * @throws BuildException if name is not set */ public void validate() { if (name == null) { throw new BuildException("name is required for module.", getLocation()); } } } /** * Child element that contains a pattern matching Java locales. */ public class LocaleSpec { /** Pattern of locale names to match. */ private String name; /** Creates an unconfigured element. */ public LocaleSpec() { // Deliberately empty. } /** * Creates an element with the given name pattern. * * @param name pattern of locale names to match */ public LocaleSpec(final String name) { setName(name); } /** * Attribute containing a pattern which matches Java locale names. * May be an explicit Java locale, or may contain an asterisk * ({@code *)} for wildcard matching. * * @return this element's locale name pattern */ public String getName() { return name; } /** * Sets attribute containing a pattern which matches Java locale names. * May be an explicit Java locale, or may contain an asterisk * ({@code *)} for wildcard matching. * * @param name new locale name or pattern matching locale names */ public void setName(final String name) { this.name = name; } /** * Verifies this element's state. * * @throws BuildException if name is not set */ public void validate() { if (name == null) { throw new BuildException("name is required for locale.", getLocation()); } } } /** * Child element type which specifies a jlink files pattern. Each * instance may specify a string * PathMatcher pattern * or a text file containing a list of such patterns, one per line. */ public class PatternListEntry { /** PathMatcher pattern of files to match. */ private String pattern; /** Plain text list file with one PathMatcher pattern per line. */ private File file; /** Creates an unconfigured element. */ public PatternListEntry() { // Deliberately empty. } /** * Creates a new element from either a pattern or listing file. * If the argument starts with "{@code @}", the remainder of it * is assumed to be a listing file; otherwise, it is treated as * a PathMatcher pattern. * * @param pattern a PathMatcher pattern or {@code @}-filename */ public PatternListEntry(final String pattern) { if (pattern.startsWith("@")) { setListFile(new File(pattern.substring(1))); } else { setPattern(pattern); } } /** * Returns this element's PathMatcher pattern attribute, if set. * * @return this element's files pattern */ public String getPattern() { return pattern; } /** * Sets this element's * PathMatcher pattern * attribute for matching files. * * @param pattern new files pattern */ public void setPattern(final String pattern) { this.pattern = pattern; } /** * Returns this element's list file attribute, if set. * * @return this element's list file * * @see #setListFile(File) */ public File getListFile() { return file; } /** * Sets this element's list file attribute. The file must be a * plain text file with one PathMatcher pattern per line. * * @param file list file containing patterns * * @see #getListFile() */ public void setListFile(final File file) { this.file = file; } /** * Verifies this element's state. * * @throws BuildException if both pattern and file are set * @throws BuildException if neither pattern nor file is set */ public void validate() { if ((pattern == null && file == null) || (pattern != null && file != null)) { throw new BuildException( "Each entry in a pattern list must specify " + "exactly one of pattern or file.", getLocation()); } } /** * Converts this element to a jlink command line attribute, * either this element's bare pattern, or its list file * preceded by "{@code @}". * * @return this element's information converted to a command line value */ public String toOptionValue() { return pattern != null ? pattern : ("@" + file); } } /** * Child element representing a custom launcher command in a linked image. * A launcher has a name, which is typically used as a file name for an * executable file, a Java module name, and optionally a class within * that module which can act as a standard Java main class. */ public class Launcher { /** This launcher's name, usually used to create an executable file. */ private String name; /** The name of the Java module this launcher launches. */ private String module; /** * The class within this element's {@link #module} to run. * Optional if the Java module specifies its own main class. */ private String mainClass; /** Creates a new, unconfigured element. */ public Launcher() { // Deliberately empty. } /** * Creates a new element from a {@code jlink}-compatible string * specifier, which must take the form * name{@code =}module or * name{@code =}module{@code /}mainclass. * * @param textSpec name, module, and optional main class, as described * above * * @throws NullPointerException if argument is {@code null} * @throws BuildException if argument does not conform to above * requirements */ public Launcher(final String textSpec) { Objects.requireNonNull(textSpec, "Text cannot be null"); int equals = textSpec.lastIndexOf('='); if (equals < 1) { throw new BuildException(INVALID_LAUNCHER_STRING); } setName(textSpec.substring(0, equals)); int slash = textSpec.indexOf('/', equals); if (slash < 0) { setModule(textSpec.substring(equals + 1)); } else if (slash > equals + 1 && slash < textSpec.length() - 1) { setModule(textSpec.substring(equals + 1, slash)); setMainClass(textSpec.substring(slash + 1)); } else { throw new BuildException(INVALID_LAUNCHER_STRING); } } /** * Returns this element's name attribute, typically used as the basis * of an executable file name. * * @return this element's name * * @see #setName(String) */ public String getName() { return name; } /** * Sets this element's name attribute, which is typically used by the * linker to create an executable file with a similar name. Thus, * the name should contain only characters safe for file names. * * @param name name of launcher */ public void setName(final String name) { this.name = name; } /** * Returns the attribute of this element which contains the * name of the Java module to execute. * * @return this element's module name */ public String getModule() { return module; } /** * Sets the attribute of this element which contains the name of * a Java module to execute. * * @param module name of module to execute */ public void setModule(final String module) { this.module = module; } /** * Returns the attribute of this element which contains the main class * to execute in this element's {@linkplain #getModule() module}, if * that module doesn't define its main class. * * @return name of main class to execute */ public String getMainClass() { return mainClass; } /** * Sets the attribute which contains the main class to execute in * this element's {@linkplain #getModule() module}, if that module * doesn't define its main class. * * @param className name of class to execute */ public void setMainClass(final String className) { this.mainClass = className; } /** * Verifies this element's state. * * @throws BuildException if name or module is not set */ public void validate() { if (name == null || name.isEmpty()) { throw new BuildException("Launcher must have a name", getLocation()); } if (module == null || module.isEmpty()) { throw new BuildException("Launcher must have specify a module", getLocation()); } } /** * Returns this element's information in jlink launcher format: * name{@code =}module or * name{@code =}module{@code /}mainclass. * * @return name, module and optional main class in jlink format */ @Override public String toString() { if (mainClass != null) { return name + "=" + module + "/" + mainClass; } else { return name + "=" + module; } } } /** * Possible values for linked image endianness: * {@code little} and {@code big}. */ public static class Endianness extends EnumeratedAttribute { @Override public String[] getValues() { return new String[] { "little", "big" }; } } /** * Possible values for JVM type in linked image: * {@code client}, {@code server}, {@code minimal}, or {@code all}. */ public static class VMType extends EnumeratedAttribute { @Override public String[] getValues() { return new String[] { "client", "server", "minimal", "all" }; } } /** * Possible attribute values for compression level of a linked image: *

*
{@code 0} *
{@code none} *
no compression (default) *
{@code 1} *
{@code strings} *
constant string sharing *
{@code 2} *
{@code zip} *
zip compression *
*/ public static class CompressionLevel extends EnumeratedAttribute { private static final Map KEYWORDS; static { Map map = new LinkedHashMap<>(); map.put("0", "0"); map.put("1", "1"); map.put("2", "2"); map.put("none", "0"); map.put("strings", "1"); map.put("zip", "2"); KEYWORDS = Collections.unmodifiableMap(map); } @Override public String[] getValues() { return KEYWORDS.keySet().toArray(new String[0]); } /** * Converts this value to a string suitable for use in a * jlink command. * * @return jlink keyword corresponding to this value */ String toCommandLineOption() { return KEYWORDS.get(getValue()); } } /** * Child element fully describing compression of a linked image. * This includes the level, and optionally, the names of files to compress. */ public class Compression { /** Compression level. Required attribute. */ private CompressionLevel level; /** * Patterns specifying files to compress. If empty, all files are * compressed. */ private final List patterns = new ArrayList<>(); /** * Required attribute containing level of compression. * * @return compression level */ public CompressionLevel getLevel() { return level; } /** * Sets attribute indicating level of compression. * * @param level type of compression to apply to linked image */ public void setLevel(final CompressionLevel level) { this.level = level; } /** * Creates a nested element which can specify a pattern of files * to compress. * * @return new, unconfigured child element */ public PatternListEntry createFiles() { PatternListEntry pattern = new PatternListEntry(); patterns.add(pattern); return pattern; } /** * Sets an attribute that represents a list of file patterns to * compress in the linked image, as a comma-separated list of * PathMatcher patterns or pattern list files. * * @param patternList comma-separated list of patterns and/or file names * * @see Link.PatternListEntry */ public void setFiles(final String patternList) { patterns.clear(); for (String pattern : patternList.split(",")) { patterns.add(new PatternListEntry(pattern)); } } /** * Verifies this element's state. * * @throws BuildException if compression level is not set * @throws BuildException if any nested patterns are invalid */ public void validate() { if (level == null) { throw new BuildException("Compression level must be specified.", getLocation()); } patterns.forEach(PatternListEntry::validate); } /** * Converts this element to a single jlink option value. * * @return command line option representing this element's state */ public String toCommandLineOption() { StringBuilder option = new StringBuilder(level.toCommandLineOption()); if (!patterns.isEmpty()) { String separator = ":filter="; for (PatternListEntry entry : patterns) { option.append(separator).append(entry.toOptionValue()); separator = ","; } } return option.toString(); } } /** * Grandchild element representing deletable key in a linked image's * release properties. */ public class ReleaseInfoKey { /** Required attribute holding property key to delete. */ private String key; /** Creates a new, unconfigured element. */ public ReleaseInfoKey() { // Deliberately empty. } /** * Creates a new element with the specified key. * * @param key property key to delete from release info */ public ReleaseInfoKey(final String key) { setKey(key); } /** * Attribute holding the release info property key to delete. * * @return property key to be deleted */ public String getKey() { return key; } /** * Sets attribute containing property key to delete from * linked image's release info. * * @param key propert key to be deleted */ public void setKey(final String key) { this.key = key; } /** * Verifies this element's state is valid. * * @throws BuildException if key is not set */ public void validate() { if (key == null) { throw new BuildException( "Release info key must define a 'key' attribute.", getLocation()); } } } /** * Grandchild element describing additional release info properties for a * linked image. To be valid, an instance must have either a non-null * key and value, or a non-null file. */ public class ReleaseInfoEntry { /** New release property's key. */ private String key; /** New release property's value. */ private String value; /** File containing additional release properties. */ private File file; /** Charset of {@link #file}. */ private String charset = StandardCharsets.ISO_8859_1.name(); /** Creates a new, unconfigured element. */ public ReleaseInfoEntry() { // Deliberately empty. } /** * Creates a new element which specifies a single additional property. * * @param key new property's key * @param value new property's value */ public ReleaseInfoEntry(final String key, final String value) { setKey(key); setValue(value); } /** * Attribute containing the key of this element's additional property. * * @return additional property's key * * @see #getValue() */ public String getKey() { return key; } /** * Sets attribute containing the key of this element's * additional property. * * @param key additional property's key * * @see #setValue(String) */ public void setKey(final String key) { this.key = key; } /** * Attribute containing the value of this element's additional property. * * @return additional property's value * * @see #getKey() */ public String getValue() { return value; } /** * Sets attributes containing the value of this element's * additional property. * * @param value additional property's value * * @see #setKey(String) */ public void setValue(final String value) { this.value = value; } /** * Attribute containing a Java properties file which contains * additional release info properties. This is exclusive with * respect to the {@linkplain #getKey() key} and * {@linkplain #getValue() value} of this instance: either the * file must be set, or the key and value must be set. * * @return this element's properties file */ public File getFile() { return file; } /** * Sets attribute containing a Java properties file which contains * additional release info properties. This is exclusive with * respect to the {@linkplain #setKey(String) key} and * {@linkplain #setValue(String) value} of this instance: either the * file must be set, or the key and value must be set. * * @param file this element's properties file */ public void setFile(final File file) { this.file = file; } /** * Attribute containing the character set of this object's * {@linkplain #getFile() file}. This is {@code ISO_8859_1} * by default, in accordance with the java.util.Properties default. * * @return character set of this element's file */ public String getCharset() { return charset; } /** * Sets attribute containing the character set of this object's * {@linkplain #setFile(File) file}. If not set, this is * {@code ISO_8859_1} by default, in accordance with the * java.util.Properties default. * * @param charset character set of this element's file */ public void setCharset(final String charset) { this.charset = charset; } /** * Verifies the state of this element. * * @throws BuildException if file is set, and key and/or value are set * @throws BuildException if file is not set, and key and value are not both set * @throws BuildException if charset is not a valid Java Charset name */ public void validate() { if (file == null && (key == null || value == null)) { throw new BuildException( "Release info must define 'key' and 'value' attributes, " + "or a 'file' attribute.", getLocation()); } if (file != null && (key != null || value != null)) { throw new BuildException( "Release info cannot define both a file attribute and " + "key/value attributes.", getLocation()); } // This can't happen from a build file, but can theoretically // happen if called from Java code. if (charset == null) { throw new BuildException("Charset cannot be null.", getLocation()); } try { Charset.forName(charset); } catch (IllegalArgumentException e) { throw new BuildException(e, getLocation()); } } /** * Converts this element to a Java properties object containing * the additional properties this element represents. If this * element's file is set, it is read; otherwise, a Properties * object containing just one property, consisting of this element's * key and value, is returned. * * @return new Properties object obtained from this element's file or * its key and value * * @throws BuildException if file is set, but cannot be read */ public Properties toProperties() { Properties props = new Properties(); if (file != null) { try (Reader reader = Files.newBufferedReader( file.toPath(), Charset.forName(charset))) { props.load(reader); } catch (IOException e) { throw new BuildException( "Cannot read release info file \"" + file + "\": " + e, e, getLocation()); } } else { props.setProperty(key, value); } return props; } } /** * Child element describing changes to the default release properties * of a linked image. */ public class ReleaseInfo { /** * File that contains replacement release properties for linked image. */ private File file; /** * Properties to add to default release properties of linked image. */ private final List propertiesToAdd = new ArrayList<>(); /** * Property keys to remove from release properties of linked image. */ private final List propertiesToDelete = new ArrayList<>(); /** * Attribute specifying Java properties file which will replace the * default release info properties for the linked image. * * @return release properties file */ public File getFile() { return file; } /** * Sets attribute specifying Java properties file which will replace * the default release info properties for the linked image. * * @param file replacement release properties file */ public void setFile(final File file) { this.file = file; } /** * Creates an uninitialized child element which can represent properties * to add to the default release properties of a linked image. * * @return new, unconfigured child element */ public ReleaseInfoEntry createAdd() { ReleaseInfoEntry property = new ReleaseInfoEntry(); propertiesToAdd.add(property); return property; } /** * Creates an uninitialized child element which can represent * a property key to delete from the release properties of * a linked image. * * @return new, unconfigured child element */ public ReleaseInfoKey createDelete() { ReleaseInfoKey key = new ReleaseInfoKey(); propertiesToDelete.add(key); return key; } /** * Sets attribute which contains a comma-separated list of * property keys to delete from the release properties of * a linked image. * * @param keyList comma-separated list of property keys * * @see #createDelete() */ public void setDelete(final String keyList) { for (String key : keyList.split(",")) { propertiesToDelete.add(new ReleaseInfoKey(key)); } } /** * Verifies the state of this element. * * @throws BuildException if any child element is invalid * * @see Link.ReleaseInfoEntry#validate() * @see Link.ReleaseInfoKey#validate() */ public void validate() { propertiesToAdd.forEach(ReleaseInfoEntry::validate); propertiesToDelete.forEach(ReleaseInfoKey::validate); } /** * Converts all of this element's state to a series of * jlink options. * * @return new collection of jlink options based on this element's * attributes and child elements */ public Collection toCommandLineOptions() { Collection options = new ArrayList<>(); if (file != null) { options.add("--release-info=" + file); } if (!propertiesToAdd.isEmpty()) { StringBuilder option = new StringBuilder("--release-info=add"); for (ReleaseInfoEntry entry : propertiesToAdd) { Properties props = entry.toProperties(); for (String key : props.stringPropertyNames()) { option.append(":").append(key).append("="); option.append(props.getProperty(key)); } } options.add(option.toString()); } if (!propertiesToDelete.isEmpty()) { StringBuilder option = new StringBuilder("--release-info=del:keys="); String separator = ""; for (ReleaseInfoKey key : propertiesToDelete) { option.append(separator).append(key.getKey()); // jlink docs aren't clear on whether property keys // to delete should be separated by commas or colons. separator = ","; } options.add(option.toString()); } return options; } } /** * Invokes the jlink tool to create a new linked image, unless the * output directory exists and all of its files are files are newer * than all files in the module path. * * @throws BuildException if destDir is not set * @throws BuildException if module path is unset or empty * @throws BuildException if module list is empty * @throws BuildException if compressionLevel attribute and compression * child element are both specified */ @Override public void execute() throws BuildException { if (outputDir == null) { throw new BuildException("Destination directory is required.", getLocation()); } if (modulePath == null || modulePath.isEmpty()) { throw new BuildException("Module path is required.", getLocation()); } if (modules.isEmpty()) { throw new BuildException("At least one module must be specified.", getLocation()); } if (outputDir.exists()) { CompositeMapper imageMapper = new CompositeMapper(); try (Stream imageTree = Files.walk(outputDir.toPath())) { /* * Is this sufficient? What if part of the image tree was * deleted or altered? Should we check for standard * files and directories, like 'bin', 'lib', 'conf', 'legal', * and 'release'? (Some, like 'include', may not be present, * if the image was previously built with options that * omitted them.) */ imageTree.forEach( p -> imageMapper.add(new MergingMapper(p.toString()))); ResourceCollection outOfDate = ResourceUtils.selectOutOfDateSources(this, modulePath, imageMapper, getProject(), FileUtils.getFileUtils().getFileTimestampGranularity()); if (outOfDate.isEmpty()) { log("Skipping image creation, since " + "\"" + outputDir + "\" is already newer than " + "all constituent modules.", Project.MSG_VERBOSE); return; } } catch (IOException e) { throw new BuildException( "Could not scan \"" + outputDir + "\" " + "for being up-to-date: " + e, e, getLocation()); } } modules.forEach(ModuleSpec::validate); observableModules.forEach(ModuleSpec::validate); launchers.forEach(Launcher::validate); locales.forEach(LocaleSpec::validate); ordering.forEach(PatternListEntry::validate); excludedFiles.forEach(PatternListEntry::validate); excludedResources.forEach(PatternListEntry::validate); Collection args = buildJlinkArgs(); ToolProvider jlink = ToolProvider.findFirst("jlink").orElseThrow( () -> new BuildException("jlink tool not found in JDK.", getLocation())); if (outputDir.exists()) { log("Deleting existing " + outputDir, Project.MSG_VERBOSE); deleteTree(outputDir.toPath()); } log("Executing: jlink " + String.join(" ", args), Project.MSG_VERBOSE); ByteArrayOutputStream stdout = new ByteArrayOutputStream(); ByteArrayOutputStream stderr = new ByteArrayOutputStream(); int exitCode; try (PrintStream out = new PrintStream(stdout); PrintStream err = new PrintStream(stderr)) { exitCode = jlink.run(out, err, args.toArray(new String[0])); } if (exitCode != 0) { StringBuilder message = new StringBuilder(); message.append("jlink failed (exit code ").append(exitCode).append(")"); if (stdout.size() > 0) { message.append(", output is: ").append(stdout); } if (stderr.size() > 0) { message.append(", error output is: ").append(stderr); } throw new BuildException(message.toString(), getLocation()); } if (verboseLevel != null) { int level = verboseLevel.getLevel(); if (stdout.size() > 0) { log(stdout.toString(), level); } if (stderr.size() > 0) { log(stderr.toString(), level); } } log("Created " + outputDir.getAbsolutePath(), Project.MSG_INFO); } /** * Recursively deletes a file tree. * * @param dir root of tree to delete * * @throws BuildException if deletion fails */ private void deleteTree(java.nio.file.Path dir) { try { Files.walkFileTree(dir, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(final java.nio.file.Path file, final BasicFileAttributes attr) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(final java.nio.file.Path dir, IOException e) throws IOException { if (e == null) { Files.delete(dir); } return super.postVisitDirectory(dir, e); } }); } catch (IOException e) { throw new BuildException( "Could not delete \"" + dir + "\": " + e, e, getLocation()); } } /** * Creates list of arguments to jlink tool, based on this * instance's current state. * * @return new list of jlink arguments * * @throws BuildException if any inconsistencies attributes/elements * is found */ private Collection buildJlinkArgs() { Collection args = new ArrayList<>(); args.add("--output"); args.add(outputDir.toString()); args.add("--module-path"); args.add(modulePath.toString()); args.add("--add-modules"); args.add(modules.stream().map(ModuleSpec::getName).collect( Collectors.joining(","))); if (!observableModules.isEmpty()) { args.add("--limit-modules"); args.add(observableModules.stream().map(ModuleSpec::getName).collect( Collectors.joining(","))); } if (!locales.isEmpty()) { args.add("--include-locales=" + locales.stream().map(LocaleSpec::getName).collect( Collectors.joining(","))); } for (Launcher launcher : launchers) { args.add("--launcher"); args.add(launcher.toString()); } if (!ordering.isEmpty()) { args.add("--order-resources=" + ordering.stream().map(PatternListEntry::toOptionValue).collect( Collectors.joining(","))); } if (!excludedFiles.isEmpty()) { args.add("--exclude-files=" + excludedFiles.stream().map(PatternListEntry::toOptionValue).collect( Collectors.joining(","))); } if (!excludedResources.isEmpty()) { args.add("--exclude-resources=" + excludedResources.stream().map(PatternListEntry::toOptionValue).collect( Collectors.joining(","))); } if (bindServices) { args.add("--bind-services"); } if (ignoreSigning) { args.add("--ignore-signing-information"); } if (!includeHeaders) { args.add("--no-header-files"); } if (!includeManPages) { args.add("--no-man-pages"); } if (!includeNativeCommands) { args.add("--strip-native-commands"); } if (!debug) { args.add("--strip-debug"); } if (verboseLevel != null) { args.add("--verbose"); } if (endianness != null) { args.add("--endian"); args.add(endianness.getValue()); } if (compressionLevel != null) { if (compression != null) { throw new BuildException("compressionLevel attribute " + "and child element cannot both be present.", getLocation()); } args.add("--compress=" + compressionLevel.toCommandLineOption()); } if (compression != null) { compression.validate(); args.add("--compress=" + compression.toCommandLineOption()); } if (vmType != null) { args.add("--vm=" + vmType.getValue()); } if (checkDuplicateLegal) { args.add("--dedup-legal-notices=error-if-not-same-content"); } for (ReleaseInfo info : releaseInfo) { info.validate(); args.addAll(info.toCommandLineOptions()); } return args; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy