1 /*
2 * Copyright (c) 2014, 2024, 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 */
25 package jdk.tools.jlink.internal;
26
27 import static jdk.tools.jlink.internal.LinkableRuntimeImage.DIFF_PATTERN;
28 import static jdk.tools.jlink.internal.LinkableRuntimeImage.RESPATH_PATTERN;
29
30 import java.io.BufferedOutputStream;
31 import java.io.ByteArrayOutputStream;
32 import java.io.DataOutputStream;
33 import java.io.IOException;
34 import java.io.OutputStream;
35 import java.io.UncheckedIOException;
36 import java.nio.ByteOrder;
37 import java.nio.charset.StandardCharsets;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Objects;
47 import java.util.Optional;
48 import java.util.Set;
49 import java.util.stream.Collectors;
50 import java.util.stream.Stream;
51
52 import jdk.tools.jlink.internal.Archive.Entry;
53 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
54 import jdk.tools.jlink.internal.JRTArchive.ResourceFileEntry;
55 import jdk.tools.jlink.internal.ResourcePoolManager.CompressedModuleData;
56 import jdk.tools.jlink.internal.runtimelink.JimageDiffGenerator;
57 import jdk.tools.jlink.internal.runtimelink.JimageDiffGenerator.ImageResource;
58 import jdk.tools.jlink.internal.runtimelink.ResourceDiff;
59 import jdk.tools.jlink.internal.runtimelink.ResourcePoolReader;
60 import jdk.tools.jlink.plugin.PluginException;
61 import jdk.tools.jlink.plugin.ResourcePool;
62 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
63 import jdk.tools.jlink.plugin.ResourcePoolEntry;
64 import jdk.tools.jlink.plugin.ResourcePoolModule;
65
66 /**
67 * An image (native endian.)
68 * <pre>{@code
69 * {
70 * u4 magic;
71 * u2 major_version;
72 * u2 minor_version;
73 * u4 resource_count;
74 * u4 table_length;
75 * u4 location_attributes_size;
76 * u4 strings_size;
77 * u4 redirect[table_length];
78 * u4 offsets[table_length];
79 * u1 location_attributes[location_attributes_size];
80 * u1 strings[strings_size];
81 * u1 content[if !EOF];
82 * }
83 * }</pre>
84 */
85 public final class ImageFileCreator {
86 private static final byte[] EMPTY_RESOURCE_BYTES = new byte[] {};
87
88 private static final String JLINK_MOD_NAME = "jdk.jlink";
89 private static final String RESPATH = "/" + JLINK_MOD_NAME + "/" + RESPATH_PATTERN;
90 private static final String DIFF_PATH = "/" + JLINK_MOD_NAME + "/" + DIFF_PATTERN;
91 private final Map<String, List<Entry>> entriesForModule = new HashMap<>();
92 private final ImagePluginStack plugins;
93 private final boolean generateRuntimeImage;
94
95 private ImageFileCreator(ImagePluginStack plugins,
96 boolean generateRuntimeImage) {
97 this.plugins = Objects.requireNonNull(plugins);
98 this.generateRuntimeImage = generateRuntimeImage;
99 }
100
101 /**
102 * Create an executable image based on a set of input archives and a given
103 * plugin stack for a given byte order. It optionally generates a runtime
104 * that can be used for linking from the run-time image if
105 * {@code generateRuntimeImage} is set to {@code true}.
106 *
107 * @param archives The set of input archives
108 * @param byteOrder The desired byte order of the result
109 * @param plugins The plugin stack to apply to the input
110 * @param generateRuntimeImage if a runtime suitable for linking from the
111 * run-time image should get created.
112 * @return The executable image.
113 * @throws IOException
114 */
115 public static ExecutableImage create(Set<Archive> archives,
116 ByteOrder byteOrder,
117 ImagePluginStack plugins,
118 boolean generateRuntimeImage)
119 throws IOException
120 {
121 ImageFileCreator image = new ImageFileCreator(plugins,
122 generateRuntimeImage);
123 try {
124 image.readAllEntries(archives);
125 // write to modular image
126 image.writeImage(archives, byteOrder);
127 } catch (UncheckedIOException e) {
128 // When linking from the run-time image, readAllEntries() might
129 // throw this exception for a modified runtime. Unpack and
130 // re-throw as IOException.
131 throw e.getCause();
132 } finally {
133 // Close all archives
134 for (Archive a : archives) {
135 a.close();
136 }
137 }
138
139 return plugins.getExecutableImage();
140 }
141
142 private void readAllEntries(Set<Archive> archives) {
143 archives.forEach((archive) -> {
144 Map<Boolean, List<Entry>> es;
145 try (Stream<Entry> entries = archive.entries()) {
146 es = entries.collect(Collectors.partitioningBy(n -> n.type()
147 == EntryType.CLASS_OR_RESOURCE));
148 }
149 String mn = archive.moduleName();
150 List<Entry> all = new ArrayList<>();
151 all.addAll(es.get(false));
152 all.addAll(es.get(true));
153 entriesForModule.put(mn, all);
154 });
155 }
156
157 public static void recreateJimage(Path jimageFile,
158 Set<Archive> archives,
159 ImagePluginStack pluginSupport,
160 boolean generateRuntimeImage)
161 throws IOException {
162 try {
163 Map<String, List<Entry>> entriesForModule
164 = archives.stream().collect(Collectors.toMap(
165 Archive::moduleName,
166 a -> {
167 try (Stream<Entry> entries = a.entries()) {
168 return entries.toList();
169 }
170 }));
171 ByteOrder order = ByteOrder.nativeOrder();
172 BasicImageWriter writer = new BasicImageWriter(order);
173 ResourcePoolManager pool = createPoolManager(archives, entriesForModule, order, writer);
174 try (OutputStream fos = Files.newOutputStream(jimageFile);
175 BufferedOutputStream bos = new BufferedOutputStream(fos);
176 DataOutputStream out = new DataOutputStream(bos)) {
177 generateJImage(pool, writer, pluginSupport, out, generateRuntimeImage);
178 }
179 } finally {
180 //Close all archives
181 for (Archive a : archives) {
182 a.close();
183 }
184 }
185 }
186
187 private void writeImage(Set<Archive> archives,
188 ByteOrder byteOrder)
189 throws IOException {
190 BasicImageWriter writer = new BasicImageWriter(byteOrder);
191 ResourcePoolManager allContent = createPoolManager(archives,
192 entriesForModule, byteOrder, writer);
193 ResourcePool result = null;
194 try (DataOutputStream out = plugins.getJImageFileOutputStream()) {
195 result = generateJImage(allContent, writer, plugins, out, generateRuntimeImage);
196 }
197
198 //Handle files.
199 try {
200 plugins.storeFiles(allContent.resourcePool(), result, writer);
201 } catch (Exception ex) {
202 if (JlinkTask.DEBUG) {
203 ex.printStackTrace();
204 }
205 throw new IOException(ex);
206 }
207 }
208
209 /**
210 * Create a jimage based on content of the given ResourcePoolManager,
211 * optionally creating a runtime that can be used for linking from the
212 * run-time image
213 *
214 * @param allContent The content that needs to get added to the resulting
215 * lib/modules (jimage) file.
216 * @param writer The writer for the jimage file.
217 * @param pluginSupport The stack of all plugins to apply.
218 * @param out The output stream to write the jimage to.
219 * @param generateRuntimeImage if a runtime suitable for linking from the
220 * run-time image should get created.
221 * @return A pool of the actual result resources.
222 * @throws IOException
223 */
224 private static ResourcePool generateJImage(ResourcePoolManager allContent,
225 BasicImageWriter writer,
226 ImagePluginStack pluginSupport,
227 DataOutputStream out,
228 boolean generateRuntimeImage
229 ) throws IOException {
230 ResourcePool resultResources;
231 try {
232 resultResources = pluginSupport.visitResources(allContent);
233 if (generateRuntimeImage) {
234 // Keep track of non-modules resources for linking from a run-time image
235 resultResources = addNonClassResourcesTrackFiles(resultResources,
236 writer);
237 // Generate the diff between the input resources from packaged
238 // modules in 'allContent' to the plugin- or otherwise
239 // generated-content in 'resultResources'
240 resultResources = addResourceDiffFiles(allContent.resourcePool(),
241 resultResources,
242 writer);
243 }
244 } catch (PluginException pe) {
245 if (JlinkTask.DEBUG) {
246 pe.printStackTrace();
247 }
248 throw pe;
249 } catch (Exception ex) {
250 if (JlinkTask.DEBUG) {
251 ex.printStackTrace();
252 }
253 throw new IOException(ex);
254 }
255 Set<String> duplicates = new HashSet<>();
256 long[] offset = new long[1];
257
258 List<ResourcePoolEntry> content = new ArrayList<>();
259 List<String> paths = new ArrayList<>();
260 // the order of traversing the resources and the order of
261 // the module content being written must be the same
262 resultResources.entries().forEach(res -> {
263 if (res.type().equals(ResourcePoolEntry.Type.CLASS_OR_RESOURCE)) {
264 String path = res.path();
265 content.add(res);
266 long uncompressedSize = res.contentLength();
267 long compressedSize = 0;
268 if (res instanceof CompressedModuleData) {
269 CompressedModuleData comp
270 = (CompressedModuleData) res;
271 compressedSize = res.contentLength();
272 uncompressedSize = comp.getUncompressedSize();
273 }
274 long onFileSize = res.contentLength();
275
276 if (duplicates.contains(path)) {
277 System.err.format("duplicate resource \"%s\", skipping%n",
278 path);
279 // TODO Need to hang bytes on resource and write
280 // from resource not zip.
281 // Skipping resource throws off writing from zip.
282 offset[0] += onFileSize;
283 return;
284 }
285 duplicates.add(path);
286 writer.addLocation(path, offset[0], compressedSize, uncompressedSize);
287 paths.add(path);
288 offset[0] += onFileSize;
289 }
290 });
291
292 ImageResourcesTree tree = new ImageResourcesTree(offset[0], writer, paths);
293
294 // write header and indices
295 byte[] bytes = writer.getBytes();
296 out.write(bytes, 0, bytes.length);
297
298 // write module content
299 content.forEach((res) -> {
300 res.write(out);
301 });
302
303 tree.addContent(out);
304
305 out.close();
306
307 return resultResources;
308 }
309
310 /**
311 * Support for creating a runtime suitable for linking from the run-time
312 * image.
313 *
314 * Generates differences between the packaged modules "view" in
315 * {@code jmodContent} to the optimized image in {@code resultContent} and
316 * adds the result to the returned resource pool.
317 *
318 * @param jmodContent The resource pool view of packaged modules
319 * @param resultContent The optimized result generated from the jmodContent
320 * input by applying the plugin stack.
321 * @param writer The image writer.
322 * @return The resource pool with the difference file resources added to
323 * the {@code resultContent}
324 */
325 @SuppressWarnings("try")
326 private static ResourcePool addResourceDiffFiles(ResourcePool jmodContent,
327 ResourcePool resultContent,
328 BasicImageWriter writer) {
329 JimageDiffGenerator generator = new JimageDiffGenerator();
330 List<ResourceDiff> diff;
331 try (ImageResource jmods = new ResourcePoolReader(jmodContent);
332 ImageResource jimage = new ResourcePoolReader(resultContent)) {
333 diff = generator.generateDiff(jmods, jimage);
334 } catch (Exception e) {
335 throw new AssertionError("Failed to generate the runtime image diff", e);
336 }
337 Set<String> modules = resultContent.moduleView().modules()
338 .map(a -> a.name())
339 .collect(Collectors.toSet());
340 // Add resource diffs for the resource files we are about to add
341 modules.stream().forEach(m -> {
342 String resourceName = String.format(DIFF_PATH, m);
343 ResourceDiff.Builder builder = new ResourceDiff.Builder();
344 ResourceDiff d = builder.setKind(ResourceDiff.Kind.ADDED)
345 .setName(resourceName)
346 .build();
347 diff.add(d);
348 });
349 Map<String, List<ResourceDiff>> perModDiffs = preparePerModuleDiffs(diff,
350 modules);
351 return addDiffResourcesFiles(modules, perModDiffs, resultContent, writer);
352 }
353
354 private static Map<String, List<ResourceDiff>> preparePerModuleDiffs(List<ResourceDiff> resDiffs,
355 Set<String> modules) {
356 Map<String, List<ResourceDiff>> modToDiff = new HashMap<>();
357 resDiffs.forEach(d -> {
358 int secondSlash = d.getName().indexOf("/", 1);
359 if (secondSlash == -1) {
360 throw new AssertionError("Module name not present");
361 }
362 String module = d.getName().substring(1, secondSlash);
363 List<ResourceDiff> perModDiff = modToDiff.computeIfAbsent(module,
364 a -> new ArrayList<>());
365 perModDiff.add(d);
366 });
367 Map<String, List<ResourceDiff>> allModsToDiff = new HashMap<>();
368 modules.stream().forEach(m -> {
369 List<ResourceDiff> d = modToDiff.get(m);
370 if (d == null) {
371 // Not all modules will have a diff
372 allModsToDiff.put(m, Collections.emptyList());
373 } else {
374 allModsToDiff.put(m, d);
375 }
376 });
377 return allModsToDiff;
378 }
379
380 private static ResourcePool addDiffResourcesFiles(Set<String> modules,
381 Map<String, List<ResourceDiff>> perModDiffs,
382 ResourcePool resultResources,
383 BasicImageWriter writer) {
384 ResourcePoolManager mgr = createPoolManager(resultResources, writer);
385 ResourcePoolBuilder out = mgr.resourcePoolBuilder();
386 modules.stream().sorted().forEach(module -> {
387 String mResource = String.format(DIFF_PATH, module);
388 List<ResourceDiff> diff = perModDiffs.get(module);
389 // Note that for modules without diff to the packaged modules view
390 // we create resource diff files with just the header and no content.
391 ByteArrayOutputStream bout = new ByteArrayOutputStream();
392 try {
393 ResourceDiff.write(diff, bout);
394 } catch (IOException e) {
395 throw new AssertionError("Failed to write resource diff file" +
396 " for module " + module, e);
397 }
398 out.add(ResourcePoolEntry.create(mResource, bout.toByteArray()));
399 });
400 return out.build();
401 }
402
403 /**
404 * Support for creating runtimes that can be used for linking from the
405 * run-time image. Adds meta-data files for resources not in the lib/modules
406 * file of the JDK. That is, mapping files for which on-disk files belong to
407 * which module.
408 *
409 * @param resultResources
410 * The original resources which serve as the basis for generating
411 * the meta-data files.
412 * @param writer
413 * The image writer.
414 *
415 * @return An amended resource pool which includes meta-data files.
416 */
417 private static ResourcePool addNonClassResourcesTrackFiles(ResourcePool resultResources,
418 BasicImageWriter writer) {
419 // Only add resources if jdk.jlink module is present in the target image
420 Optional<ResourcePoolModule> jdkJlink = resultResources.moduleView()
421 .findModule(JLINK_MOD_NAME);
422 if (jdkJlink.isPresent()) {
423 Map<String, List<String>> nonClassResources = recordAndFilterEntries(resultResources);
424 return addModuleResourceEntries(resultResources, nonClassResources, writer);
425 } else {
426 return resultResources; // No-op
427 }
428 }
429
430 /**
431 * Support for creating runtimes that can be used for linking from the
432 * run-time image. Adds the given mapping of files as a meta-data file to
433 * the given resource pool.
434 *
435 * @param resultResources
436 * The resource pool to add files to.
437 * @param nonClassResEntries
438 * The per module mapping for which to create the meta-data files
439 * for.
440 * @param writer
441 * The image writer.
442 *
443 * @return A resource pool with meta-data files added.
444 */
445 private static ResourcePool addModuleResourceEntries(ResourcePool resultResources,
446 Map<String, List<String>> nonClassResEntries,
447 BasicImageWriter writer) {
448 Set<String> inputModules = resultResources.moduleView().modules()
449 .map(rm -> rm.name())
450 .collect(Collectors.toSet());
451 ResourcePoolManager mgr = createPoolManager(resultResources, writer);
452 ResourcePoolBuilder out = mgr.resourcePoolBuilder();
453 inputModules.stream().sorted().forEach(module -> {
454 String mResource = String.format(RESPATH, module);
455 List<String> mResources = nonClassResEntries.get(module);
456 if (mResources == null) {
457 // We create empty resource files for modules in the resource
458 // pool view that don't themselves contain native resources
459 // or config files.
460 out.add(ResourcePoolEntry.create(mResource, EMPTY_RESOURCE_BYTES));
461 } else {
462 String mResContent = mResources.stream().sorted()
463 .collect(Collectors.joining("\n"));
464 out.add(ResourcePoolEntry.create(mResource,
465 mResContent.getBytes(StandardCharsets.UTF_8)));
466 }
467 });
468 return out.build();
469 }
470
471 /**
472 * Support for creating runtimes that can be used for linking from the
473 * run-time image. Generates a per module mapping of files not part of the
474 * modules image (jimage). This mapping is needed so as to know which files
475 * of the installed JDK belong to which module.
476 *
477 * @param resultResources
478 * The resources from which the mapping gets generated
479 * @return A mapping with the module names as keys and the list of files not
480 * part of the modules image (jimage) as values.
481 */
482 private static Map<String, List<String>> recordAndFilterEntries(ResourcePool resultResources) {
483 Map<String, List<String>> nonClassResEntries = new HashMap<>();
484 Platform platform = getTargetPlatform(resultResources);
485 resultResources.entries().forEach(entry -> {
486 // Note that the fs_$module_files file is a resource file itself, so
487 // we cannot add fs_$module_files themselves due to the
488 // not(class_or_resources) condition. However, we also don't want
489 // to track 'release' file entries (not(top) condition) as those are
490 // handled by the release info plugin.
491 if (entry.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE &&
492 entry.type() != ResourcePoolEntry.Type.TOP) {
493 List<String> mRes = nonClassResEntries.computeIfAbsent(entry.moduleName(),
494 a -> new ArrayList<>());
495 ResourceFileEntry rfEntry = ResourceFileEntry.toResourceFileEntry(entry,
496 platform);
497 mRes.add(rfEntry.encodeToString());
498 }
499 });
500 return nonClassResEntries;
501 }
502
503 private static Platform getTargetPlatform(ResourcePool in) {
504 String platform = in.moduleView().findModule("java.base")
505 .map(ResourcePoolModule::targetPlatform)
506 .orElseThrow(() -> new AssertionError("java.base not found"));
507 return Platform.parsePlatform(platform);
508 }
509
510 private static ResourcePoolManager createPoolManager(Set<Archive> archives,
511 Map<String, List<Entry>> entriesForModule,
512 ByteOrder byteOrder,
513 BasicImageWriter writer) throws IOException {
514 ResourcePoolManager resources = createBasicResourcePoolManager(byteOrder, writer);
515 archives.stream()
516 .map(Archive::moduleName)
517 .sorted()
518 .flatMap(mn ->
519 entriesForModule.get(mn).stream()
520 .map(e -> new ArchiveEntryResourcePoolEntry(mn,
521 e.getResourcePoolEntryName(), e)))
522 .forEach(resources::add);
523 return resources;
524 }
525
526 private static ResourcePoolManager createBasicResourcePoolManager(ByteOrder byteOrder,
527 BasicImageWriter writer) {
528 return new ResourcePoolManager(byteOrder, new StringTable() {
529
530 @Override
531 public int addString(String str) {
532 return writer.addString(str);
533 }
534
535 @Override
536 public String getString(int id) {
537 return writer.getString(id);
538 }
539 });
540 }
541
542 /**
543 * Creates a ResourcePoolManager from existing resources so that more
544 * resources can be appended.
545 *
546 * @param resultResources The existing resources to initially add.
547 * @param writer The basic image writer.
548 * @return An appendable ResourcePoolManager.
549 */
550 private static ResourcePoolManager createPoolManager(ResourcePool resultResources,
551 BasicImageWriter writer) {
552 ResourcePoolManager resources = createBasicResourcePoolManager(resultResources.byteOrder(),
553 writer);
554 // Note that resources are already sorted in the correct order.
555 // The underlying ResourcePoolManager keeps track of entries via
556 // LinkedHashMap, which keeps values in insertion order. Therefore
557 // adding resources here, preserving that same order is OK.
558 resultResources.entries().forEach(resources::add);
559 return resources;
560 }
561
562 /**
563 * Helper method that splits a Resource path onto 3 items: module, parent
564 * and resource name.
565 *
566 * @param path
567 * @return An array containing module, parent and name.
568 */
569 public static String[] splitPath(String path) {
570 Objects.requireNonNull(path);
571 String noRoot = path.substring(1);
572 int pkgStart = noRoot.indexOf("/");
573 String module = noRoot.substring(0, pkgStart);
574 List<String> result = new ArrayList<>();
575 result.add(module);
576 String pkg = noRoot.substring(pkgStart + 1);
577 String resName;
578 int pkgEnd = pkg.lastIndexOf("/");
579 if (pkgEnd == -1) { // No package.
580 resName = pkg;
581 } else {
582 resName = pkg.substring(pkgEnd + 1);
583 }
584
585 pkg = toPackage(pkg, false);
586 result.add(pkg);
587 result.add(resName);
588
589 String[] array = new String[result.size()];
590 return result.toArray(array);
591 }
592
593 /**
594 * Returns the path of the resource.
595 */
596 public static String resourceName(String path) {
597 Objects.requireNonNull(path);
598 String s = path.substring(1);
599 int index = s.indexOf("/");
600 return s.substring(index + 1);
601 }
602
603 public static String toPackage(String name) {
604 return toPackage(name, false);
605 }
606
607 private static String toPackage(String name, boolean log) {
608 int index = name.lastIndexOf('/');
609 if (index > 0) {
610 return name.substring(0, index).replace('/', '.');
611 } else {
612 // ## unnamed package
613 if (log) {
614 System.err.format("Warning: %s in unnamed package%n", name);
615 }
616 return "";
617 }
618 }
619 }