JlinkTask.java revision 16909:085c764a3e5b
1/*
2 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25package jdk.tools.jlink.internal;
26
27import java.io.File;
28import java.io.IOException;
29import java.io.PrintWriter;
30import java.io.UncheckedIOException;
31import java.lang.module.Configuration;
32import java.lang.module.FindException;
33import java.lang.module.ModuleDescriptor;
34import java.lang.module.ModuleFinder;
35import java.lang.module.ModuleReference;
36import java.lang.module.ResolutionException;
37import java.lang.module.ResolvedModule;
38import java.net.URI;
39import java.nio.ByteOrder;
40import java.nio.file.Files;
41import java.nio.file.Path;
42import java.nio.file.Paths;
43import java.util.*;
44import java.util.stream.Collectors;
45import java.util.stream.Stream;
46
47import jdk.tools.jlink.internal.TaskHelper.BadArgs;
48import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE;
49import jdk.tools.jlink.internal.Jlink.JlinkConfiguration;
50import jdk.tools.jlink.internal.Jlink.PluginsConfiguration;
51import jdk.tools.jlink.internal.TaskHelper.Option;
52import jdk.tools.jlink.internal.TaskHelper.OptionsHelper;
53import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider;
54import jdk.tools.jlink.plugin.PluginException;
55import jdk.tools.jlink.builder.DefaultImageBuilder;
56import jdk.tools.jlink.plugin.Plugin;
57import jdk.internal.module.ModulePath;
58import jdk.internal.module.ModuleResolution;
59
60/**
61 * Implementation for the jlink tool.
62 *
63 * ## Should use jdk.joptsimple some day.
64 */
65public class JlinkTask {
66    static final boolean DEBUG = Boolean.getBoolean("jlink.debug");
67
68    // jlink API ignores by default. Remove when signing is implemented.
69    static final boolean IGNORE_SIGNING_DEFAULT = true;
70
71    private static final TaskHelper taskHelper
72            = new TaskHelper(JLINK_BUNDLE);
73
74    private static final Option<?>[] recognizedOptions = {
75        new Option<JlinkTask>(false, (task, opt, arg) -> {
76            task.options.help = true;
77        }, "--help", "-h"),
78        new Option<JlinkTask>(true, (task, opt, arg) -> {
79            // if used multiple times, the last one wins!
80            // So, clear previous values, if any.
81            task.options.modulePath.clear();
82            String[] dirs = arg.split(File.pathSeparator);
83            int i = 0;
84            Arrays.stream(dirs)
85                  .map(Paths::get)
86                  .forEach(task.options.modulePath::add);
87        }, "--module-path", "-p"),
88        new Option<JlinkTask>(true, (task, opt, arg) -> {
89            // if used multiple times, the last one wins!
90            // So, clear previous values, if any.
91            task.options.limitMods.clear();
92            for (String mn : arg.split(",")) {
93                if (mn.isEmpty()) {
94                    throw taskHelper.newBadArgs("err.mods.must.be.specified",
95                            "--limit-modules");
96                }
97                task.options.limitMods.add(mn);
98            }
99        }, "--limit-modules"),
100        new Option<JlinkTask>(true, (task, opt, arg) -> {
101            for (String mn : arg.split(",")) {
102                if (mn.isEmpty()) {
103                    throw taskHelper.newBadArgs("err.mods.must.be.specified",
104                            "--add-modules");
105                }
106                task.options.addMods.add(mn);
107            }
108        }, "--add-modules"),
109        new Option<JlinkTask>(true, (task, opt, arg) -> {
110            Path path = Paths.get(arg);
111            task.options.output = path;
112        }, "--output"),
113        new Option<JlinkTask>(true, (task, opt, arg) -> {
114            String[] values = arg.split("=");
115            // check values
116            if (values.length != 2 || values[0].isEmpty() || values[1].isEmpty()) {
117                throw taskHelper.newBadArgs("err.launcher.value.format", arg);
118            } else {
119                String commandName = values[0];
120                String moduleAndMain = values[1];
121                int idx = moduleAndMain.indexOf("/");
122                if (idx != -1) {
123                    if (moduleAndMain.substring(0, idx).isEmpty()) {
124                        throw taskHelper.newBadArgs("err.launcher.module.name.empty", arg);
125                    }
126
127                    if (moduleAndMain.substring(idx + 1).isEmpty()) {
128                        throw taskHelper.newBadArgs("err.launcher.main.class.empty", arg);
129                    }
130                }
131                task.options.launchers.put(commandName, moduleAndMain);
132            }
133        }, "--launcher"),
134        new Option<JlinkTask>(true, (task, opt, arg) -> {
135            if ("little".equals(arg)) {
136                task.options.endian = ByteOrder.LITTLE_ENDIAN;
137            } else if ("big".equals(arg)) {
138                task.options.endian = ByteOrder.BIG_ENDIAN;
139            } else {
140                throw taskHelper.newBadArgs("err.unknown.byte.order", arg);
141            }
142        }, "--endian"),
143        new Option<JlinkTask>(false, (task, opt, arg) -> {
144            task.options.version = true;
145        }, "--version"),
146        new Option<JlinkTask>(true, (task, opt, arg) -> {
147            Path path = Paths.get(arg);
148            if (Files.exists(path)) {
149                throw taskHelper.newBadArgs("err.dir.exists", path);
150            }
151            task.options.packagedModulesPath = path;
152        }, true, "--keep-packaged-modules"),
153        new Option<JlinkTask>(true, (task, opt, arg) -> {
154            task.options.saveoptsfile = arg;
155        }, "--save-opts"),
156        new Option<JlinkTask>(false, (task, opt, arg) -> {
157            task.options.fullVersion = true;
158        }, true, "--full-version"),
159        new Option<JlinkTask>(false, (task, opt, arg) -> {
160            task.options.ignoreSigning = true;
161        }, "--ignore-signing-information"),};
162
163    private static final String PROGNAME = "jlink";
164    private final OptionsValues options = new OptionsValues();
165
166    private static final OptionsHelper<JlinkTask> optionsHelper
167            = taskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions);
168    private PrintWriter log;
169
170    void setLog(PrintWriter out, PrintWriter err) {
171        log = out;
172        taskHelper.setLog(log);
173    }
174
175    /**
176     * Result codes.
177     */
178    static final int
179            EXIT_OK = 0, // Completed with no errors.
180            EXIT_ERROR = 1, // Completed but reported errors.
181            EXIT_CMDERR = 2, // Bad command-line arguments
182            EXIT_SYSERR = 3, // System error or resource exhaustion.
183            EXIT_ABNORMAL = 4;// terminated abnormally
184
185    static class OptionsValues {
186        boolean help;
187        String  saveoptsfile;
188        boolean version;
189        boolean fullVersion;
190        final List<Path> modulePath = new ArrayList<>();
191        final Set<String> limitMods = new HashSet<>();
192        final Set<String> addMods = new HashSet<>();
193        Path output;
194        final Map<String, String> launchers = new HashMap<>();
195        Path packagedModulesPath;
196        ByteOrder endian = ByteOrder.nativeOrder();
197        boolean ignoreSigning = false;
198    }
199
200    int run(String[] args) {
201        if (log == null) {
202            setLog(new PrintWriter(System.out, true),
203                   new PrintWriter(System.err, true));
204        }
205        try {
206            optionsHelper.handleOptionsNoUnhandled(this, args);
207            if (options.help) {
208                optionsHelper.showHelp(PROGNAME);
209                return EXIT_OK;
210            }
211            if (optionsHelper.shouldListPlugins()) {
212                optionsHelper.listPlugins();
213                return EXIT_OK;
214            }
215            if (options.version || options.fullVersion) {
216                taskHelper.showVersion(options.fullVersion);
217                return EXIT_OK;
218            }
219
220            if (taskHelper.getExistingImage() == null) {
221                if (options.modulePath.isEmpty()) {
222                    throw taskHelper.newBadArgs("err.modulepath.must.be.specified").showUsage(true);
223                }
224                createImage();
225            } else {
226                postProcessOnly(taskHelper.getExistingImage());
227            }
228
229            if (options.saveoptsfile != null) {
230                Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes());
231            }
232
233            return EXIT_OK;
234        } catch (PluginException | IllegalArgumentException |
235                 UncheckedIOException |IOException | FindException | ResolutionException e) {
236            log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage());
237            if (DEBUG) {
238                e.printStackTrace(log);
239            }
240            return EXIT_ERROR;
241        } catch (BadArgs e) {
242            taskHelper.reportError(e.key, e.args);
243            if (e.showUsage) {
244                log.println(taskHelper.getMessage("main.usage.summary", PROGNAME));
245            }
246            if (DEBUG) {
247                e.printStackTrace(log);
248            }
249            return EXIT_CMDERR;
250        } catch (Throwable x) {
251            log.println(taskHelper.getMessage("error.prefix") + " " + x.getMessage());
252            x.printStackTrace(log);
253            return EXIT_ABNORMAL;
254        } finally {
255            log.flush();
256        }
257    }
258
259    /*
260     * Jlink API entry point.
261     */
262    public static void createImage(JlinkConfiguration config,
263                                   PluginsConfiguration plugins)
264            throws Exception {
265        Objects.requireNonNull(config);
266        Objects.requireNonNull(config.getOutput());
267        plugins = plugins == null ? new PluginsConfiguration() : plugins;
268
269        if (config.getModulepaths().isEmpty()) {
270            throw new IllegalArgumentException("Empty module paths");
271        }
272
273        ModuleFinder finder = newModuleFinder(config.getModulepaths(),
274                                              config.getLimitmods(),
275                                              config.getModules());
276
277        if (config.getModules().isEmpty()) {
278            throw new IllegalArgumentException("No modules to add");
279        }
280
281        // First create the image provider
282        ImageProvider imageProvider =
283                createImageProvider(finder,
284                                    config.getModules(),
285                                    config.getByteOrder(),
286                                    null,
287                                    IGNORE_SIGNING_DEFAULT,
288                                    null);
289
290        // Then create the Plugin Stack
291        ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
292
293        //Ask the stack to proceed;
294        stack.operate(imageProvider);
295    }
296
297    /*
298     * Jlink API entry point.
299     */
300    public static void postProcessImage(ExecutableImage image, List<Plugin> postProcessorPlugins)
301            throws Exception {
302        Objects.requireNonNull(image);
303        Objects.requireNonNull(postProcessorPlugins);
304        PluginsConfiguration config = new PluginsConfiguration(postProcessorPlugins);
305        ImagePluginStack stack = ImagePluginConfiguration.
306                parseConfiguration(config);
307
308        stack.operate((ImagePluginStack stack1) -> image);
309    }
310
311    private void postProcessOnly(Path existingImage) throws Exception {
312        PluginsConfiguration config = taskHelper.getPluginsConfig(null, null);
313        ExecutableImage img = DefaultImageBuilder.getExecutableImage(existingImage);
314        if (img == null) {
315            throw taskHelper.newBadArgs("err.existing.image.invalid");
316        }
317        postProcessImage(img, config.getPlugins());
318    }
319
320    // the token for "all modules on the module path"
321    private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
322    private void createImage() throws Exception {
323        if (options.output == null) {
324            throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true);
325        }
326
327        if (options.addMods.isEmpty()) {
328            throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules")
329                    .showUsage(true);
330        }
331
332        Set<String> roots = new HashSet<>();
333        for (String mod : options.addMods) {
334            if (mod.equals(ALL_MODULE_PATH)) {
335                ModuleFinder finder = modulePathFinder();
336                finder.findAll()
337                      .stream()
338                      .map(ModuleReference::descriptor)
339                      .map(ModuleDescriptor::name)
340                      .forEach(mn -> roots.add(mn));
341            } else {
342                roots.add(mod);
343            }
344        }
345
346        ModuleFinder finder = newModuleFinder(options.modulePath,
347                                              options.limitMods,
348                                              roots);
349
350
351        // First create the image provider
352        ImageProvider imageProvider = createImageProvider(finder,
353                                                          roots,
354                                                          options.endian,
355                                                          options.packagedModulesPath,
356                                                          options.ignoreSigning,
357                                                          log);
358
359        // Then create the Plugin Stack
360        ImagePluginStack stack = ImagePluginConfiguration.
361                parseConfiguration(taskHelper.getPluginsConfig(options.output, options.launchers));
362
363        //Ask the stack to proceed
364        stack.operate(imageProvider);
365    }
366
367    /**
368     * Returns a module finder to find the observable modules specified in
369     * the --module-path and --limit-modules options
370     */
371    private ModuleFinder modulePathFinder() {
372        Path[] entries = options.modulePath.toArray(new Path[0]);
373        ModuleFinder finder = ModulePath.of(Runtime.version(), true, entries);
374        if (!options.limitMods.isEmpty()) {
375            finder = limitFinder(finder, options.limitMods, Collections.emptySet());
376        }
377        return finder;
378    }
379
380    /*
381     * Returns a module finder of the given module path that limits
382     * the observable modules to those in the transitive closure of
383     * the modules specified in {@code limitMods} plus other modules
384     * specified in the {@code roots} set.
385     */
386    public static ModuleFinder newModuleFinder(List<Path> paths,
387                                               Set<String> limitMods,
388                                               Set<String> roots)
389    {
390        Path[] entries = paths.toArray(new Path[0]);
391        ModuleFinder finder = ModulePath.of(Runtime.version(), true, entries);
392
393        // if limitmods is specified then limit the universe
394        if (!limitMods.isEmpty()) {
395            finder = limitFinder(finder, limitMods, roots);
396        }
397        return finder;
398    }
399
400    private static Path toPathLocation(ResolvedModule m) {
401        Optional<URI> ouri = m.reference().location();
402        if (!ouri.isPresent())
403            throw new InternalError(m + " does not have a location");
404        URI uri = ouri.get();
405        return Paths.get(uri);
406    }
407
408    private static ImageProvider createImageProvider(ModuleFinder finder,
409                                                     Set<String> roots,
410                                                     ByteOrder order,
411                                                     Path retainModulesPath,
412                                                     boolean ignoreSigning,
413                                                     PrintWriter log)
414            throws IOException
415    {
416        if (roots.isEmpty()) {
417            throw new IllegalArgumentException("empty modules and limitmods");
418        }
419
420        Configuration cf = Configuration.empty()
421                .resolve(finder,
422                         ModuleFinder.of(),
423                         roots);
424
425        // emit a warning for any incubating modules in the configuration
426        if (log != null) {
427            String im = cf.modules()
428                          .stream()
429                          .map(ResolvedModule::reference)
430                          .filter(ModuleResolution::hasIncubatingWarning)
431                          .map(ModuleReference::descriptor)
432                          .map(ModuleDescriptor::name)
433                          .collect(Collectors.joining(", "));
434
435            if (!"".equals(im))
436                log.println("WARNING: Using incubator modules: " + im);
437        }
438
439        Map<String, Path> mods = cf.modules().stream()
440            .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation));
441        return new ImageHelper(cf, mods, order, retainModulesPath, ignoreSigning);
442    }
443
444    /*
445     * Returns a ModuleFinder that limits observability to the given root
446     * modules, their transitive dependences, plus a set of other modules.
447     */
448    private static ModuleFinder limitFinder(ModuleFinder finder,
449                                            Set<String> roots,
450                                            Set<String> otherMods) {
451
452        // resolve all root modules
453        Configuration cf = Configuration.empty()
454                .resolve(finder,
455                         ModuleFinder.of(),
456                         roots);
457
458        // module name -> reference
459        Map<String, ModuleReference> map = new HashMap<>();
460        cf.modules().forEach(m -> {
461            ModuleReference mref = m.reference();
462            map.put(mref.descriptor().name(), mref);
463        });
464
465        // add the other modules
466        otherMods.stream()
467            .map(finder::find)
468            .flatMap(Optional::stream)
469            .forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref));
470
471        // set of modules that are observable
472        Set<ModuleReference> mrefs = new HashSet<>(map.values());
473
474        return new ModuleFinder() {
475            @Override
476            public Optional<ModuleReference> find(String name) {
477                return Optional.ofNullable(map.get(name));
478            }
479
480            @Override
481            public Set<ModuleReference> findAll() {
482                return mrefs;
483            }
484        };
485    }
486
487    private String getSaveOpts() {
488        StringBuilder sb = new StringBuilder();
489        sb.append('#').append(new Date()).append("\n");
490        for (String c : optionsHelper.getInputCommand()) {
491            sb.append(c).append(" ");
492        }
493
494        return sb.toString();
495    }
496
497    private static String getBomHeader() {
498        StringBuilder sb = new StringBuilder();
499        sb.append("#").append(new Date()).append("\n");
500        sb.append("#Please DO NOT Modify this file").append("\n");
501        return sb.toString();
502    }
503
504    private String genBOMContent() throws IOException {
505        StringBuilder sb = new StringBuilder();
506        sb.append(getBomHeader());
507        StringBuilder command = new StringBuilder();
508        for (String c : optionsHelper.getInputCommand()) {
509            command.append(c).append(" ");
510        }
511        sb.append("command").append(" = ").append(command);
512        sb.append("\n");
513
514        return sb.toString();
515    }
516
517    private static String genBOMContent(JlinkConfiguration config,
518            PluginsConfiguration plugins)
519            throws IOException {
520        StringBuilder sb = new StringBuilder();
521        sb.append(getBomHeader());
522        sb.append(config);
523        sb.append(plugins);
524        return sb.toString();
525    }
526
527    private static class ImageHelper implements ImageProvider {
528        final ByteOrder order;
529        final Path packagedModulesPath;
530        final boolean ignoreSigning;
531        final Set<Archive> archives;
532
533        ImageHelper(Configuration cf,
534                    Map<String, Path> modsPaths,
535                    ByteOrder order,
536                    Path packagedModulesPath,
537                    boolean ignoreSigning) throws IOException {
538            this.order = order;
539            this.packagedModulesPath = packagedModulesPath;
540            this.ignoreSigning = ignoreSigning;
541            this.archives = modsPaths.entrySet().stream()
542                                .map(e -> newArchive(e.getKey(), e.getValue()))
543                                .collect(Collectors.toSet());
544        }
545
546        private Archive newArchive(String module, Path path) {
547            if (path.toString().endsWith(".jmod")) {
548                return new JmodArchive(module, path);
549            } else if (path.toString().endsWith(".jar")) {
550                ModularJarArchive modularJarArchive = new ModularJarArchive(module, path);
551
552                Stream<Archive.Entry> signatures = modularJarArchive.entries().filter((entry) -> {
553                    String name = entry.name().toUpperCase(Locale.ENGLISH);
554
555                    return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && (
556                                name.endsWith(".SF") ||
557                                name.endsWith(".DSA") ||
558                                name.endsWith(".RSA") ||
559                                name.endsWith(".EC") ||
560                                name.startsWith("META-INF/SIG-")
561                            );
562                });
563
564                if (signatures.count() != 0) {
565                    if (ignoreSigning) {
566                        System.err.println(taskHelper.getMessage("warn.signing", path));
567                    } else {
568                        throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path));
569                    }
570                }
571
572                return modularJarArchive;
573            } else if (Files.isDirectory(path)) {
574                return new DirArchive(path);
575            } else {
576                throw new IllegalArgumentException(
577                    taskHelper.getMessage("err.not.modular.format", module, path));
578            }
579        }
580
581        @Override
582        public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
583            ExecutableImage image = ImageFileCreator.create(archives, order, stack);
584            if (packagedModulesPath != null) {
585                // copy the packaged modules to the given path
586                Files.createDirectories(packagedModulesPath);
587                for (Archive a : archives) {
588                    Path file = a.getPath();
589                    Path dest = packagedModulesPath.resolve(file.getFileName());
590                    Files.copy(file, dest);
591                }
592            }
593            return image;
594        }
595    }
596}
597