30
30
import java .io .InputStream ;
31
31
import java .io .InputStreamReader ;
32
32
import java .lang .reflect .Method ;
33
+ import java .net .URI ;
33
34
import java .nio .charset .StandardCharsets ;
34
35
import java .nio .file .Files ;
35
36
import java .nio .file .InvalidPathException ;
44
45
import java .util .HashMap ;
45
46
import java .util .HashSet ;
46
47
import java .util .Iterator ;
48
+ import java .util .LinkedHashMap ;
47
49
import java .util .LinkedHashSet ;
48
50
import java .util .List ;
49
51
import java .util .ListIterator ;
50
52
import java .util .Map ;
53
+ import java .util .Objects ;
51
54
import java .util .Optional ;
52
55
import java .util .Properties ;
53
56
import java .util .Set ;
56
59
import java .util .function .BiFunction ;
57
60
import java .util .function .Consumer ;
58
61
import java .util .function .Function ;
62
+ import java .util .function .Predicate ;
59
63
import java .util .jar .Attributes ;
60
64
import java .util .jar .JarFile ;
61
65
import java .util .jar .Manifest ;
@@ -1178,9 +1182,6 @@ private int completeImageBuild() {
1178
1182
return ExitStatus .FALLBACK_IMAGE .getValue ();
1179
1183
}
1180
1184
1181
- if (!addModules .isEmpty ()) {
1182
- imageBuilderJavaArgs .add ("-D" + ModuleSupport .PROPERTY_IMAGE_EXPLICITLY_ADDED_MODULES + "=" + String .join ("," , addModules ));
1183
- }
1184
1185
if (!limitModules .isEmpty ()) {
1185
1186
imageBuilderJavaArgs .add ("-D" + ModuleSupport .PROPERTY_IMAGE_EXPLICITLY_LIMITED_MODULES + "=" + String .join ("," , limitModules ));
1186
1187
}
@@ -1425,20 +1426,13 @@ protected int buildImage(List<String> javaArgs, LinkedHashSet<Path> cp, LinkedHa
1425
1426
if (!cp .isEmpty ()) {
1426
1427
arguments .addAll (Arrays .asList ("-cp" , cp .stream ().map (Path ::toString ).collect (Collectors .joining (File .pathSeparator ))));
1427
1428
}
1429
+
1428
1430
if (!mp .isEmpty ()) {
1429
1431
List <String > strings = Arrays .asList ("--module-path" , mp .stream ().map (Path ::toString ).collect (Collectors .joining (File .pathSeparator )));
1430
1432
arguments .addAll (strings );
1431
1433
}
1432
1434
1433
- arguments .addAll (config .getGeneratorMainClass ());
1434
-
1435
- if (IS_AOT && OS .getCurrent ().hasProcFS ) {
1436
- /*
1437
- * GR-8254: Ensure image-building VM shuts down even if native-image dies unexpected
1438
- * (e.g. using CTRL-C in Gradle daemon mode)
1439
- */
1440
- arguments .addAll (Arrays .asList (SubstrateOptions .WATCHPID_PREFIX , "" + ProcessProperties .getProcessID ()));
1441
- }
1435
+ String javaExecutable = canonicalize (config .getJavaExecutable ()).toString ();
1442
1436
1443
1437
if (useBundle ()) {
1444
1438
LogUtils .warning ("Native Image Bundles are an experimental feature." );
@@ -1450,12 +1444,68 @@ protected int buildImage(List<String> javaArgs, LinkedHashSet<Path> cp, LinkedHa
1450
1444
Function <Path , Path > substituteClassPath = useBundle () ? bundleSupport ::substituteClassPath : Function .identity ();
1451
1445
List <Path > finalImageClassPath = imagecp .stream ().map (substituteClassPath ).collect (Collectors .toList ());
1452
1446
Function <Path , Path > substituteModulePath = useBundle () ? bundleSupport ::substituteModulePath : Function .identity ();
1453
- List <Path > finalImageModulePath = imagemp .stream ().map (substituteModulePath ).collect (Collectors .toList ());
1447
+ List <Path > substitutedImageModulePath = imagemp .stream ().map (substituteModulePath ).toList ();
1448
+
1449
+ Map <String , Path > modules = listModulesFromPath (javaExecutable , Stream .concat (mp .stream (), imagemp .stream ()).distinct ().toList ());
1450
+ if (!addModules .isEmpty ()) {
1451
+
1452
+ arguments .add ("-D" + ModuleSupport .PROPERTY_IMAGE_EXPLICITLY_ADDED_MODULES + "=" +
1453
+ String .join ("," , addModules ));
1454
+
1455
+ List <String > addModulesForBuilderVM = new ArrayList <>();
1456
+ for (String module : addModules ) {
1457
+ Path jarPath = modules .get (module );
1458
+ if (jarPath == null ) {
1459
+ // boot module
1460
+ addModulesForBuilderVM .add (module );
1461
+ }
1462
+ }
1463
+
1464
+ if (!addModulesForBuilderVM .isEmpty ()) {
1465
+ arguments .add (DefaultOptionHandler .addModulesOption + "=" + String .join ("," , addModulesForBuilderVM ));
1466
+ }
1467
+ }
1468
+
1469
+ arguments .addAll (config .getGeneratorMainClass ());
1470
+
1471
+ if (IS_AOT && OS .getCurrent ().hasProcFS ) {
1472
+ /*
1473
+ * GR-8254: Ensure image-building VM shuts down even if native-image dies unexpected
1474
+ * (e.g. using CTRL-C in Gradle daemon mode)
1475
+ */
1476
+ arguments .addAll (Arrays .asList (SubstrateOptions .WATCHPID_PREFIX , "" + ProcessProperties .getProcessID ()));
1477
+ }
1478
+
1479
+ /*
1480
+ * Workaround for GR-47186: Native image cannot handle modules on the image module path,
1481
+ * that are also already installed in the JDK as boot module. As a workaround we filter all
1482
+ * modules from the module-path that are either already installed in the JDK as boot module,
1483
+ * or were explicitly added to the builder module-path.
1484
+ *
1485
+ * First compute all module-jar paths that are not on the builder module-path.
1486
+ */
1487
+ Set <Path > nonBuilderModulePaths = modules .values ().stream ()
1488
+ .filter (Objects ::nonNull )
1489
+ .filter (Predicate .not (mp ::contains ))
1490
+ .collect (Collectors .toSet ());
1491
+
1492
+ /*
1493
+ * Now we need to filter the substituted module path list for all the modules that may
1494
+ * remain on the module-path.
1495
+ *
1496
+ * This should normally not be necessary, as the nonBuilderModulePaths should already be the
1497
+ * set of jar files for the image module path. Nevertheless, we use the original definition
1498
+ * of the module path to preserve the order of the original module path and as a precaution
1499
+ * to protect against --list-modules returning too many modules.
1500
+ */
1501
+ List <Path > finalImageModulePath = substitutedImageModulePath .stream ()
1502
+ .filter (nonBuilderModulePaths ::contains )
1503
+ .toList ();
1504
+
1454
1505
List <String > finalImageBuilderArgs = createImageBuilderArgs (finalImageArgs , finalImageClassPath , finalImageModulePath );
1455
1506
1456
1507
/* Construct ProcessBuilder command from final arguments */
1457
1508
List <String > command = new ArrayList <>();
1458
- String javaExecutable = canonicalize (config .getJavaExecutable ()).toString ();
1459
1509
command .add (javaExecutable );
1460
1510
command .add (createVMInvocationArgumentFile (arguments ));
1461
1511
command .add (createImageBuilderArgumentFile (finalImageBuilderArgs ));
@@ -1520,6 +1570,68 @@ protected int buildImage(List<String> javaArgs, LinkedHashSet<Path> cp, LinkedHa
1520
1570
}
1521
1571
}
1522
1572
1573
+ /**
1574
+ * Resolves and lists all modules given a module path.
1575
+ *
1576
+ * @see #callListModules(String, List)
1577
+ */
1578
+ private static Map <String , Path > listModulesFromPath (String javaExecutable , Collection <Path > modulePath ) {
1579
+ if (modulePath .isEmpty ()) {
1580
+ return Map .of ();
1581
+ }
1582
+ String modulePathEntries = modulePath .stream ()
1583
+ .map (Path ::toString )
1584
+ .collect (Collectors .joining (File .pathSeparator ));
1585
+ return callListModules (javaExecutable , List .of ("-p" , modulePathEntries ));
1586
+ }
1587
+
1588
+ /**
1589
+ * Calls <code>java $arguments --list-modules</code> to list all modules and parse the output.
1590
+ * The output consists of a map with module name as key and {@link Path} to jar file if the
1591
+ * module is not installed as part of the JDK. If the module is installed as part of the
1592
+ * jdk/boot-layer then a <code>null</code> path will be returned.
1593
+ * <p>
1594
+ * This is a much more robust solution then trying to parse the JDK file structure manually.
1595
+ */
1596
+ private static Map <String , Path > callListModules (String javaExecutable , List <String > arguments ) {
1597
+ Process listModulesProcess = null ;
1598
+ Map <String , Path > result = new LinkedHashMap <>();
1599
+ try {
1600
+ var pb = new ProcessBuilder (javaExecutable );
1601
+ pb .command ().addAll (arguments );
1602
+ pb .command ().add ("--list-modules" );
1603
+ pb .environment ().clear ();
1604
+ listModulesProcess = pb .start ();
1605
+ try (var br = new BufferedReader (new InputStreamReader (listModulesProcess .getInputStream ()))) {
1606
+ while (true ) {
1607
+ var line = br .readLine ();
1608
+ if (line == null ) {
1609
+ break ;
1610
+ }
1611
+ String [] splitString = StringUtil .split (line , " " , 3 );
1612
+ String [] splitModuleNameAndVersion = StringUtil .split (splitString [0 ], "@" , 2 );
1613
+ Path externalPath = null ;
1614
+ if (splitString .length > 1 ) {
1615
+ String pathURI = splitString [1 ]; // url: file://path/to/file
1616
+ externalPath = Path .of (URI .create (pathURI )).toAbsolutePath ();
1617
+ }
1618
+ result .put (splitModuleNameAndVersion [0 ], externalPath );
1619
+ }
1620
+ }
1621
+ int exitStatus = listModulesProcess .waitFor ();
1622
+ if (exitStatus != 0 ) {
1623
+ throw showError ("Determining image-builder observable modules failed (Exit status %d)." .formatted (exitStatus ));
1624
+ }
1625
+ } catch (IOException | InterruptedException e ) {
1626
+ throw showError (e .getMessage ());
1627
+ } finally {
1628
+ if (listModulesProcess != null ) {
1629
+ listModulesProcess .destroy ();
1630
+ }
1631
+ }
1632
+ return result ;
1633
+ }
1634
+
1523
1635
/**
1524
1636
* Adds a shutdown hook to kill the image builder process if it's still alive.
1525
1637
*
0 commit comments