blob: f854f8584456fd932458e8f2baf895ab1a5cf405 [file] [log] [blame]
/*
* Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.tools.jlink.internal;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.lang.module.Configuration;
import java.lang.module.FindException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.module.ResolutionException;
import java.lang.module.ResolvedModule;
import java.net.URI;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.tools.jlink.internal.TaskHelper.BadArgs;
import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE;
import jdk.tools.jlink.internal.Jlink.JlinkConfiguration;
import jdk.tools.jlink.internal.Jlink.PluginsConfiguration;
import jdk.tools.jlink.internal.TaskHelper.Option;
import jdk.tools.jlink.internal.TaskHelper.OptionsHelper;
import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider;
import jdk.tools.jlink.plugin.PluginException;
import jdk.tools.jlink.builder.DefaultImageBuilder;
import jdk.tools.jlink.plugin.Plugin;
import jdk.internal.module.ModulePath;
import jdk.internal.module.ModuleResolution;
/**
* Implementation for the jlink tool.
*
* ## Should use jdk.joptsimple some day.
*/
public class JlinkTask {
static final boolean DEBUG = Boolean.getBoolean("jlink.debug");
// jlink API ignores by default. Remove when signing is implemented.
static final boolean IGNORE_SIGNING_DEFAULT = true;
private static final TaskHelper taskHelper
= new TaskHelper(JLINK_BUNDLE);
private static final Option<?>[] recognizedOptions = {
new Option<JlinkTask>(false, (task, opt, arg) -> {
task.options.help = true;
}, "--help", "-h"),
new Option<JlinkTask>(true, (task, opt, arg) -> {
// if used multiple times, the last one wins!
// So, clear previous values, if any.
task.options.modulePath.clear();
String[] dirs = arg.split(File.pathSeparator);
int i = 0;
Arrays.stream(dirs)
.map(Paths::get)
.forEach(task.options.modulePath::add);
}, "--module-path", "-p"),
new Option<JlinkTask>(true, (task, opt, arg) -> {
// if used multiple times, the last one wins!
// So, clear previous values, if any.
task.options.limitMods.clear();
for (String mn : arg.split(",")) {
if (mn.isEmpty()) {
throw taskHelper.newBadArgs("err.mods.must.be.specified",
"--limit-modules");
}
task.options.limitMods.add(mn);
}
}, "--limit-modules"),
new Option<JlinkTask>(true, (task, opt, arg) -> {
for (String mn : arg.split(",")) {
if (mn.isEmpty()) {
throw taskHelper.newBadArgs("err.mods.must.be.specified",
"--add-modules");
}
task.options.addMods.add(mn);
}
}, "--add-modules"),
new Option<JlinkTask>(true, (task, opt, arg) -> {
Path path = Paths.get(arg);
task.options.output = path;
}, "--output"),
new Option<JlinkTask>(false, (task, opt, arg) -> {
task.options.bindServices = true;
}, "--bind-services"),
new Option<JlinkTask>(false, (task, opt, arg) -> {
task.options.suggestProviders = true;
}, "--suggest-providers", "", true),
new Option<JlinkTask>(true, (task, opt, arg) -> {
String[] values = arg.split("=");
// check values
if (values.length != 2 || values[0].isEmpty() || values[1].isEmpty()) {
throw taskHelper.newBadArgs("err.launcher.value.format", arg);
} else {
String commandName = values[0];
String moduleAndMain = values[1];
int idx = moduleAndMain.indexOf("/");
if (idx != -1) {
if (moduleAndMain.substring(0, idx).isEmpty()) {
throw taskHelper.newBadArgs("err.launcher.module.name.empty", arg);
}
if (moduleAndMain.substring(idx + 1).isEmpty()) {
throw taskHelper.newBadArgs("err.launcher.main.class.empty", arg);
}
}
task.options.launchers.put(commandName, moduleAndMain);
}
}, "--launcher"),
new Option<JlinkTask>(true, (task, opt, arg) -> {
if ("little".equals(arg)) {
task.options.endian = ByteOrder.LITTLE_ENDIAN;
} else if ("big".equals(arg)) {
task.options.endian = ByteOrder.BIG_ENDIAN;
} else {
throw taskHelper.newBadArgs("err.unknown.byte.order", arg);
}
}, "--endian"),
new Option<JlinkTask>(false, (task, opt, arg) -> {
task.options.verbose = true;
}, "--verbose", "-v"),
new Option<JlinkTask>(false, (task, opt, arg) -> {
task.options.version = true;
}, "--version"),
new Option<JlinkTask>(true, (task, opt, arg) -> {
Path path = Paths.get(arg);
if (Files.exists(path)) {
throw taskHelper.newBadArgs("err.dir.exists", path);
}
task.options.packagedModulesPath = path;
}, true, "--keep-packaged-modules"),
new Option<JlinkTask>(true, (task, opt, arg) -> {
task.options.saveoptsfile = arg;
}, "--save-opts"),
new Option<JlinkTask>(false, (task, opt, arg) -> {
task.options.fullVersion = true;
}, true, "--full-version"),
new Option<JlinkTask>(false, (task, opt, arg) -> {
task.options.ignoreSigning = true;
}, "--ignore-signing-information"),};
private static final String PROGNAME = "jlink";
private final OptionsValues options = new OptionsValues();
private static final OptionsHelper<JlinkTask> optionsHelper
= taskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions);
private PrintWriter log;
void setLog(PrintWriter out, PrintWriter err) {
log = out;
taskHelper.setLog(log);
}
/**
* Result codes.
*/
static final int
EXIT_OK = 0, // Completed with no errors.
EXIT_ERROR = 1, // Completed but reported errors.
EXIT_CMDERR = 2, // Bad command-line arguments
EXIT_SYSERR = 3, // System error or resource exhaustion.
EXIT_ABNORMAL = 4;// terminated abnormally
static class OptionsValues {
boolean help;
String saveoptsfile;
boolean verbose;
boolean version;
boolean fullVersion;
final List<Path> modulePath = new ArrayList<>();
final Set<String> limitMods = new HashSet<>();
final Set<String> addMods = new HashSet<>();
Path output;
final Map<String, String> launchers = new HashMap<>();
Path packagedModulesPath;
ByteOrder endian = ByteOrder.nativeOrder();
boolean ignoreSigning = false;
boolean bindServices = false;
boolean suggestProviders = false;
}
int run(String[] args) {
if (log == null) {
setLog(new PrintWriter(System.out, true),
new PrintWriter(System.err, true));
}
try {
List<String> remaining = optionsHelper.handleOptions(this, args);
if (remaining.size() > 0 && !options.suggestProviders) {
throw taskHelper.newBadArgs("err.orphan.arguments", toString(remaining))
.showUsage(true);
}
if (options.help) {
optionsHelper.showHelp(PROGNAME);
return EXIT_OK;
}
if (optionsHelper.shouldListPlugins()) {
optionsHelper.listPlugins();
return EXIT_OK;
}
if (options.version || options.fullVersion) {
taskHelper.showVersion(options.fullVersion);
return EXIT_OK;
}
if (taskHelper.getExistingImage() != null) {
postProcessOnly(taskHelper.getExistingImage());
return EXIT_OK;
}
if (options.modulePath.isEmpty()) {
throw taskHelper.newBadArgs("err.modulepath.must.be.specified")
.showUsage(true);
}
JlinkConfiguration config = initJlinkConfig();
if (options.suggestProviders) {
suggestProviders(config, remaining);
} else {
createImage(config);
if (options.saveoptsfile != null) {
Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes());
}
}
return EXIT_OK;
} catch (PluginException | IllegalArgumentException |
UncheckedIOException |IOException | FindException | ResolutionException e) {
log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage());
if (DEBUG) {
e.printStackTrace(log);
}
return EXIT_ERROR;
} catch (BadArgs e) {
taskHelper.reportError(e.key, e.args);
if (e.showUsage) {
log.println(taskHelper.getMessage("main.usage.summary", PROGNAME));
}
if (DEBUG) {
e.printStackTrace(log);
}
return EXIT_CMDERR;
} catch (Throwable x) {
log.println(taskHelper.getMessage("error.prefix") + " " + x.getMessage());
x.printStackTrace(log);
return EXIT_ABNORMAL;
} finally {
log.flush();
}
}
/*
* Jlink API entry point.
*/
public static void createImage(JlinkConfiguration config,
PluginsConfiguration plugins)
throws Exception {
Objects.requireNonNull(config);
Objects.requireNonNull(config.getOutput());
plugins = plugins == null ? new PluginsConfiguration() : plugins;
// First create the image provider
ImageProvider imageProvider =
createImageProvider(config,
null,
IGNORE_SIGNING_DEFAULT,
false,
false,
null);
// Then create the Plugin Stack
ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
//Ask the stack to proceed;
stack.operate(imageProvider);
}
/*
* Jlink API entry point.
*/
public static void postProcessImage(ExecutableImage image, List<Plugin> postProcessorPlugins)
throws Exception {
Objects.requireNonNull(image);
Objects.requireNonNull(postProcessorPlugins);
PluginsConfiguration config = new PluginsConfiguration(postProcessorPlugins);
ImagePluginStack stack = ImagePluginConfiguration.
parseConfiguration(config);
stack.operate((ImagePluginStack stack1) -> image);
}
private void postProcessOnly(Path existingImage) throws Exception {
PluginsConfiguration config = taskHelper.getPluginsConfig(null, null);
ExecutableImage img = DefaultImageBuilder.getExecutableImage(existingImage);
if (img == null) {
throw taskHelper.newBadArgs("err.existing.image.invalid");
}
postProcessImage(img, config.getPlugins());
}
// the token for "all modules on the module path"
private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
private JlinkConfiguration initJlinkConfig() throws BadArgs {
Set<String> roots = new HashSet<>();
for (String mod : options.addMods) {
if (mod.equals(ALL_MODULE_PATH)) {
Path[] entries = options.modulePath.toArray(new Path[0]);
ModuleFinder finder = ModulePath.of(Runtime.version(), true, entries);
if (!options.limitMods.isEmpty()) {
// finder for the observable modules specified in
// the --module-path and --limit-modules options
finder = limitFinder(finder, options.limitMods, Collections.emptySet());
}
// all observable modules are roots
finder.findAll()
.stream()
.map(ModuleReference::descriptor)
.map(ModuleDescriptor::name)
.forEach(mn -> roots.add(mn));
} else {
roots.add(mod);
}
}
return new JlinkConfiguration(options.output,
options.modulePath,
roots,
options.limitMods,
options.endian);
}
private void createImage(JlinkConfiguration config) throws Exception {
if (options.output == null) {
throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true);
}
if (options.addMods.isEmpty()) {
throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules")
.showUsage(true);
}
// First create the image provider
ImageProvider imageProvider = createImageProvider(config,
options.packagedModulesPath,
options.ignoreSigning,
options.bindServices,
options.verbose,
log);
// Then create the Plugin Stack
ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
taskHelper.getPluginsConfig(options.output, options.launchers));
//Ask the stack to proceed
stack.operate(imageProvider);
}
/*
* Returns a module finder of the given module path that limits
* the observable modules to those in the transitive closure of
* the modules specified in {@code limitMods} plus other modules
* specified in the {@code roots} set.
*/
public static ModuleFinder newModuleFinder(List<Path> paths,
Set<String> limitMods,
Set<String> roots)
{
Path[] entries = paths.toArray(new Path[0]);
ModuleFinder finder = ModulePath.of(Runtime.version(), true, entries);
// if limitmods is specified then limit the universe
if (!limitMods.isEmpty()) {
finder = limitFinder(finder, limitMods, roots);
}
return finder;
}
private static Path toPathLocation(ResolvedModule m) {
Optional<URI> ouri = m.reference().location();
if (!ouri.isPresent())
throw new InternalError(m + " does not have a location");
URI uri = ouri.get();
return Paths.get(uri);
}
private static ImageProvider createImageProvider(JlinkConfiguration config,
Path retainModulesPath,
boolean ignoreSigning,
boolean bindService,
boolean verbose,
PrintWriter log)
throws IOException
{
Configuration cf = bindService ? config.resolveAndBind()
: config.resolve();
if (verbose && log != null) {
// print modules to be linked in
cf.modules().stream()
.sorted(Comparator.comparing(ResolvedModule::name))
.forEach(rm -> log.format("%s %s%n",
rm.name(), rm.reference().location().get()));
// print provider info
Set<ModuleReference> references = cf.modules().stream()
.map(ResolvedModule::reference).collect(Collectors.toSet());
String msg = String.format("%n%s:", taskHelper.getMessage("providers.header"));
printProviders(log, msg, references);
}
// emit a warning for any incubating modules in the configuration
if (log != null) {
String im = cf.modules()
.stream()
.map(ResolvedModule::reference)
.filter(ModuleResolution::hasIncubatingWarning)
.map(ModuleReference::descriptor)
.map(ModuleDescriptor::name)
.collect(Collectors.joining(", "));
if (!"".equals(im))
log.println("WARNING: Using incubator modules: " + im);
}
Map<String, Path> mods = cf.modules().stream()
.collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation));
return new ImageHelper(cf, mods, config.getByteOrder(), retainModulesPath, ignoreSigning);
}
/*
* Returns a ModuleFinder that limits observability to the given root
* modules, their transitive dependences, plus a set of other modules.
*/
public static ModuleFinder limitFinder(ModuleFinder finder,
Set<String> roots,
Set<String> otherMods) {
// resolve all root modules
Configuration cf = Configuration.empty()
.resolve(finder,
ModuleFinder.of(),
roots);
// module name -> reference
Map<String, ModuleReference> map = new HashMap<>();
cf.modules().forEach(m -> {
ModuleReference mref = m.reference();
map.put(mref.descriptor().name(), mref);
});
// add the other modules
otherMods.stream()
.map(finder::find)
.flatMap(Optional::stream)
.forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref));
// set of modules that are observable
Set<ModuleReference> mrefs = new HashSet<>(map.values());
return new ModuleFinder() {
@Override
public Optional<ModuleReference> find(String name) {
return Optional.ofNullable(map.get(name));
}
@Override
public Set<ModuleReference> findAll() {
return mrefs;
}
};
}
/*
* Returns a map of each service type to the modules that use it
* It will include services that are provided by a module but may not used
* by any of the observable modules.
*/
private static Map<String, Set<String>> uses(Set<ModuleReference> modules) {
// collects the services used by the modules and print uses
Map<String, Set<String>> services = new HashMap<>();
modules.stream()
.map(ModuleReference::descriptor)
.forEach(md -> {
// include services that may not be used by any observable modules
md.provides().forEach(p ->
services.computeIfAbsent(p.service(), _k -> new HashSet<>()));
md.uses().forEach(s -> services.computeIfAbsent(s, _k -> new HashSet<>())
.add(md.name()));
});
return services;
}
private static void printProviders(PrintWriter log,
String header,
Set<ModuleReference> modules) {
printProviders(log, header, modules, uses(modules));
}
/*
* Prints the providers that are used by the specified services.
*
* The specified services maps a service type name to the modules
* using the service type which may be empty if no observable module uses
* that service.
*/
private static void printProviders(PrintWriter log,
String header,
Set<ModuleReference> modules,
Map<String, Set<String>> serviceToUses) {
if (modules.isEmpty())
return;
// Build a map of a service type to the provider modules
Map<String, Set<ModuleDescriptor>> providers = new HashMap<>();
modules.stream()
.map(ModuleReference::descriptor)
.forEach(md -> {
md.provides().stream()
.filter(p -> serviceToUses.containsKey(p.service()))
.forEach(p -> providers.computeIfAbsent(p.service(), _k -> new HashSet<>())
.add(md));
});
if (!providers.isEmpty()) {
log.println(header);
}
// print the providers of the service types used by the specified modules
// sorted by the service type name and then provider's module name
providers.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> {
String service = e.getKey();
e.getValue().stream()
.sorted(Comparator.comparing(ModuleDescriptor::name))
.forEach(md ->
md.provides().stream()
.filter(p -> p.service().equals(service))
.forEach(p -> {
String usedBy;
if (serviceToUses.get(p.service()).isEmpty()) {
usedBy = "not used by any observable module";
} else {
usedBy = serviceToUses.get(p.service()).stream()
.sorted()
.collect(Collectors.joining(",", "used by ", ""));
}
log.format(" %s provides %s %s%n",
md.name(), p.service(), usedBy);
})
);
});
}
private void suggestProviders(JlinkConfiguration config, List<String> args)
throws BadArgs
{
if (args.size() > 1) {
throw taskHelper.newBadArgs("err.orphan.argument",
toString(args.subList(1, args.size())))
.showUsage(true);
}
if (options.bindServices) {
log.println(taskHelper.getMessage("no.suggested.providers"));
return;
}
ModuleFinder finder = config.finder();
if (args.isEmpty()) {
// print providers used by the observable modules without service binding
Set<ModuleReference> mrefs = finder.findAll();
// print uses of the modules that would be linked into the image
mrefs.stream()
.sorted(Comparator.comparing(mref -> mref.descriptor().name()))
.forEach(mref -> {
ModuleDescriptor md = mref.descriptor();
log.format("%s %s%n", md.name(),
mref.location().get());
md.uses().stream().sorted()
.forEach(s -> log.format(" uses %s%n", s));
});
String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
printProviders(log, msg, mrefs, uses(mrefs));
} else {
// comma-separated service types, if specified
Set<String> names = Stream.of(args.get(0).split(","))
.collect(Collectors.toSet());
// find the modules that provide the specified service
Set<ModuleReference> mrefs = finder.findAll().stream()
.filter(mref -> mref.descriptor().provides().stream()
.map(ModuleDescriptor.Provides::service)
.anyMatch(names::contains))
.collect(Collectors.toSet());
// find the modules that uses the specified services
Map<String, Set<String>> uses = new HashMap<>();
names.forEach(s -> uses.computeIfAbsent(s, _k -> new HashSet<>()));
finder.findAll().stream()
.map(ModuleReference::descriptor)
.forEach(md -> md.uses().stream()
.filter(names::contains)
.forEach(s -> uses.get(s).add(md.name())));
// check if any name given on the command line are not provided by any module
mrefs.stream()
.flatMap(mref -> mref.descriptor().provides().stream()
.map(ModuleDescriptor.Provides::service))
.forEach(names::remove);
if (!names.isEmpty()) {
log.println(taskHelper.getMessage("warn.provider.notfound",
toString(names)));
}
String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
printProviders(log, msg, mrefs, uses);
}
}
private static String toString(Collection<String> collection) {
return collection.stream().sorted()
.collect(Collectors.joining(","));
}
private String getSaveOpts() {
StringBuilder sb = new StringBuilder();
sb.append('#').append(new Date()).append("\n");
for (String c : optionsHelper.getInputCommand()) {
sb.append(c).append(" ");
}
return sb.toString();
}
private static String getBomHeader() {
StringBuilder sb = new StringBuilder();
sb.append("#").append(new Date()).append("\n");
sb.append("#Please DO NOT Modify this file").append("\n");
return sb.toString();
}
private String genBOMContent() throws IOException {
StringBuilder sb = new StringBuilder();
sb.append(getBomHeader());
StringBuilder command = new StringBuilder();
for (String c : optionsHelper.getInputCommand()) {
command.append(c).append(" ");
}
sb.append("command").append(" = ").append(command);
sb.append("\n");
return sb.toString();
}
private static String genBOMContent(JlinkConfiguration config,
PluginsConfiguration plugins)
throws IOException {
StringBuilder sb = new StringBuilder();
sb.append(getBomHeader());
sb.append(config);
sb.append(plugins);
return sb.toString();
}
private static class ImageHelper implements ImageProvider {
final ByteOrder order;
final Path packagedModulesPath;
final boolean ignoreSigning;
final Set<Archive> archives;
ImageHelper(Configuration cf,
Map<String, Path> modsPaths,
ByteOrder order,
Path packagedModulesPath,
boolean ignoreSigning) throws IOException {
this.order = order;
this.packagedModulesPath = packagedModulesPath;
this.ignoreSigning = ignoreSigning;
this.archives = modsPaths.entrySet().stream()
.map(e -> newArchive(e.getKey(), e.getValue()))
.collect(Collectors.toSet());
}
private Archive newArchive(String module, Path path) {
if (path.toString().endsWith(".jmod")) {
return new JmodArchive(module, path);
} else if (path.toString().endsWith(".jar")) {
ModularJarArchive modularJarArchive = new ModularJarArchive(module, path);
Stream<Archive.Entry> signatures = modularJarArchive.entries().filter((entry) -> {
String name = entry.name().toUpperCase(Locale.ENGLISH);
return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && (
name.endsWith(".SF") ||
name.endsWith(".DSA") ||
name.endsWith(".RSA") ||
name.endsWith(".EC") ||
name.startsWith("META-INF/SIG-")
);
});
if (signatures.count() != 0) {
if (ignoreSigning) {
System.err.println(taskHelper.getMessage("warn.signing", path));
} else {
throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path));
}
}
return modularJarArchive;
} else if (Files.isDirectory(path)) {
return new DirArchive(path);
} else {
throw new IllegalArgumentException(
taskHelper.getMessage("err.not.modular.format", module, path));
}
}
@Override
public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
ExecutableImage image = ImageFileCreator.create(archives, order, stack);
if (packagedModulesPath != null) {
// copy the packaged modules to the given path
Files.createDirectories(packagedModulesPath);
for (Archive a : archives) {
Path file = a.getPath();
Path dest = packagedModulesPath.resolve(file.getFileName());
Files.copy(file, dest);
}
}
return image;
}
}
}