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