1 /*
   2  * Copyright (c) 2015, 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.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package tests;
  24 
  25 import java.io.ByteArrayInputStream;
  26 import java.io.ByteArrayOutputStream;
  27 import java.io.File;
  28 import java.io.FileOutputStream;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.io.OutputStream;
  32 import java.io.PrintStream;
  33 import java.io.PrintWriter;
  34 import java.io.StringWriter;
  35 import java.nio.file.Files;
  36 import java.nio.file.Path;
  37 import java.nio.file.Paths;
  38 import java.nio.file.StandardCopyOption;
  39 import java.util.ArrayList;
  40 import java.util.Arrays;
  41 import java.util.Collections;
  42 import java.util.HashSet;
  43 import java.util.List;
  44 import java.util.Objects;
  45 import java.util.Set;
  46 import java.util.jar.JarEntry;
  47 import java.util.jar.JarInputStream;
  48 import java.util.jar.JarOutputStream;
  49 import java.util.stream.Collectors;
  50 import java.util.stream.Stream;
  51 import java.util.zip.ZipEntry;
  52 
  53 import javax.tools.JavaCompiler;
  54 import javax.tools.StandardJavaFileManager;
  55 import javax.tools.StandardLocation;
  56 import javax.tools.ToolProvider;
  57 
  58 /**
  59  *
  60  * A generator for jmods, jars and images.
  61  */
  62 public class JImageGenerator {
  63 
  64     public static final String LOAD_ALL_CLASSES_TEMPLATE = "package PACKAGE;\n"
  65             + "\n"
  66             + "import java.net.URI;\n"
  67             + "import java.nio.file.FileSystems;\n"
  68             + "import java.nio.file.Files;\n"
  69             + "import java.nio.file.Path;\n"
  70             + "import java.util.function.Function;\n"
  71             + "\n"
  72             + "public class CLASS {\n"
  73             + "    private static long total_time;\n"
  74             + "    private static long num_classes;\n"
  75             + "    public static void main(String[] args) throws Exception {\n"
  76             + "        Function<Path, String> formatter = (path) -> {\n"
  77             + "            String clazz = path.toString().substring(\"modules/\".length()+1, path.toString().lastIndexOf(\".\"));\n"
  78             + "            clazz = clazz.substring(clazz.indexOf(\"/\") + 1);\n"
  79             + "            return clazz.replaceAll(\"/\", \"\\\\.\");\n"
  80             + "        };\n"
  81             + "        Files.walk(FileSystems.getFileSystem(URI.create(\"jrt:/\")).getPath(\"/modules/\")).\n"
  82             + "                filter((p) -> {\n"
  83             + "                    return Files.isRegularFile(p) && p.toString().endsWith(\".class\")\n"
  84             + "                    && !p.toString().endsWith(\"module-info.class\");\n"
  85             + "                }).\n"
  86             + "                map(formatter).forEach((clazz) -> {\n"
  87             + "                    try {\n"
  88             + "                        long t = System.currentTimeMillis();\n"
  89             + "                        Class.forName(clazz, false, Thread.currentThread().getContextClassLoader());\n"
  90             + "                        total_time+= System.currentTimeMillis()-t;\n"
  91             + "                        num_classes+=1;\n"
  92             + "                    } catch (IllegalAccessError ex) {\n"
  93             + "                        // Security exceptions can occur, this is not what we are testing\n"
  94             + "                        System.err.println(\"Access error, OK \" + clazz);\n"
  95             + "                    } catch (Exception ex) {\n"
  96             + "                        System.err.println(\"ERROR \" + clazz);\n"
  97             + "                        throw new RuntimeException(ex);\n"
  98             + "                    }\n"
  99             + "                });\n"
 100             + "    double res = (double) total_time / num_classes;\n"
 101             + "    // System.out.println(\"Total time \" + total_time + \" num classes \" + num_classes + \" average \" + res);\n"
 102             + "    }\n"
 103             + "}\n";
 104 
 105     private static final String OUTPUT_OPTION = "--output";
 106     private static final String POST_PROCESS_OPTION = "--post-process-path";
 107     private static final String MAIN_CLASS_OPTION = "--main-class";
 108     private static final String CLASS_PATH_OPTION = "--class-path";
 109     private static final String MODULE_PATH_OPTION = "--module-path";
 110     private static final String ADD_MODULES_OPTION = "--add-modules";
 111     private static final String LIMIT_MODULES_OPTION = "--limit-modules";
 112     private static final String PLUGIN_MODULE_PATH = "--plugin-module-path";
 113 
 114     private static final String CMDS_OPTION = "--cmds";
 115     private static final String CONFIG_OPTION = "--config";
 116     private static final String HASH_MODULES_OPTION = "--hash-modules";
 117     private static final String LIBS_OPTION = "--libs";
 118     private static final String MODULE_VERSION_OPTION = "--module-version";
 119 
 120     private JImageGenerator() {}
 121 
 122     private static String optionsPrettyPrint(String... args) {
 123         return Stream.of(args).collect(Collectors.joining(" "));
 124     }
 125 
 126     public static File getJModsDir(File jdkHome) {
 127         File jdkjmods = new File(jdkHome, "jmods");
 128         if (!jdkjmods.exists()) {
 129             return null;
 130         }
 131         return jdkjmods;
 132     }
 133 
 134     public static Path addFiles(Path module, InMemoryFile... resources) throws IOException {
 135         Path tempFile = Files.createTempFile("jlink-test", "");
 136         try (JarInputStream in = new JarInputStream(Files.newInputStream(module));
 137              JarOutputStream out = new JarOutputStream(new FileOutputStream(tempFile.toFile()))) {
 138             ZipEntry entry;
 139             while ((entry = in.getNextEntry()) != null) {
 140                 String name = entry.getName();
 141                 out.putNextEntry(new ZipEntry(name));
 142                 copy(in, out);
 143                 out.closeEntry();
 144             }
 145             for (InMemoryFile r : resources) {
 146                 addFile(r, out);
 147             }
 148         }
 149         Files.move(tempFile, module, StandardCopyOption.REPLACE_EXISTING);
 150         return module;
 151     }
 152 
 153     private static void copy(InputStream in, OutputStream out) throws IOException {
 154         int len;
 155         byte[] buf = new byte[4096];
 156         while ((len = in.read(buf)) > 0) {
 157             out.write(buf, 0, len);
 158         }
 159     }
 160 
 161     public static JModTask getJModTask() {
 162         return new JModTask();
 163     }
 164 
 165     public static JLinkTask getJLinkTask() {
 166         return new JLinkTask();
 167     }
 168 
 169     public static JImageTask getJImageTask() {
 170         return new JImageTask();
 171     }
 172 
 173     private static void addFile(InMemoryFile resource, JarOutputStream target) throws IOException {
 174         String fileName = resource.getPath();
 175         fileName = fileName.replace("\\", "/");
 176         String[] ss = fileName.split("/");
 177         Path p = Paths.get("");
 178         for (int i = 0; i < ss.length; ++i) {
 179             if (i < ss.length - 1) {
 180                 if (!ss[i].isEmpty()) {
 181                     p = p.resolve(ss[i]);
 182                     JarEntry entry = new JarEntry(p.toString() + "/");
 183                     target.putNextEntry(entry);
 184                     target.closeEntry();
 185                 }
 186             } else {
 187                 p = p.resolve(ss[i]);
 188                 JarEntry entry = new JarEntry(p.toString());
 189                 target.putNextEntry(entry);
 190                 copy(resource.getBytes(), target);
 191                 target.closeEntry();
 192             }
 193         }
 194     }
 195 
 196     public static Path createNewFile(Path root, String pathName, String extension) {
 197         Path out = root.resolve(pathName + extension);
 198         int i = 1;
 199         while (Files.exists(out)) {
 200             out = root.resolve(pathName + "-" + (++i) + extension);
 201         }
 202         return out;
 203     }
 204 
 205     public static Path generateSources(Path output, String moduleName, List<InMemorySourceFile> sources) throws IOException {
 206         Path moduleDir = output.resolve(moduleName);
 207         Files.createDirectory(moduleDir);
 208         for (InMemorySourceFile source : sources) {
 209             Path fileDir = moduleDir;
 210             if (!source.packageName.isEmpty()) {
 211                 String dir = source.packageName.replace('.', File.separatorChar);
 212                 fileDir = moduleDir.resolve(dir);
 213                 Files.createDirectories(fileDir);
 214             }
 215             Files.write(fileDir.resolve(source.className + ".java"), source.source.getBytes());
 216         }
 217         return moduleDir;
 218     }
 219 
 220     public static Path generateSourcesFromTemplate(Path output, String moduleName, String... classNames) throws IOException {
 221         List<InMemorySourceFile> sources = new ArrayList<>();
 222         for (String className : classNames) {
 223             String packageName = getPackageName(className);
 224             String simpleName = getSimpleName(className);
 225             String content = LOAD_ALL_CLASSES_TEMPLATE
 226                     .replace("CLASS", simpleName);
 227             if (packageName.isEmpty()) {
 228                 content = content.replace("package PACKAGE;", packageName);
 229             } else {
 230                 content = content.replace("PACKAGE", packageName);
 231             }
 232             sources.add(new InMemorySourceFile(packageName, simpleName, content));
 233         }
 234         return generateSources(output, moduleName, sources);
 235     }
 236 
 237     public static void generateModuleInfo(Path moduleDir, List<String> packages, String... dependencies) throws IOException {
 238         StringBuilder moduleInfoBuilder = new StringBuilder();
 239         Path file = moduleDir.resolve("module-info.java");
 240         String moduleName = moduleDir.getFileName().toString();
 241         moduleInfoBuilder.append("module ").append(moduleName).append("{\n");
 242         for (String dep : dependencies) {
 243             moduleInfoBuilder.append("requires ").append(dep).append(";\n");
 244         }
 245         for (String pkg : packages) {
 246             if (!pkg.trim().isEmpty()) {
 247                 moduleInfoBuilder.append("exports ").append(pkg).append(";\n");
 248             }
 249         }
 250         moduleInfoBuilder.append("}");
 251         Files.write(file, moduleInfoBuilder.toString().getBytes());
 252     }
 253 
 254     public static void compileSuccess(Path source, Path destination, String... options) throws IOException {
 255         if (!compile(source, destination, options)) {
 256             throw new AssertionError("Compilation failed.");
 257         }
 258     }
 259 
 260     public static boolean compile(Path source, Path destination, String... options) throws IOException {
 261         JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
 262         try (StandardJavaFileManager jfm = compiler.getStandardFileManager(null, null, null)) {
 263             List<Path> sources
 264                     = Files.find(source, Integer.MAX_VALUE,
 265                     (file, attrs) -> file.toString().endsWith(".java"))
 266                     .collect(Collectors.toList());
 267 
 268             Files.createDirectories(destination);
 269             jfm.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Collections.singleton(destination));
 270 
 271             List<String> opts = Arrays.asList(options);
 272             JavaCompiler.CompilationTask task
 273                     = compiler.getTask(null, jfm, null, opts, null,
 274                     jfm.getJavaFileObjectsFromPaths(sources));
 275             List<String> list = new ArrayList<>(opts);
 276             list.addAll(sources.stream()
 277                     .map(Path::toString)
 278                     .collect(Collectors.toList()));
 279             System.err.println("javac options: " + optionsPrettyPrint(list.toArray(new String[list.size()])));
 280             return task.call();
 281         }
 282     }
 283 
 284     public static Path createJarFile(Path jarfile, Path dir) throws IOException {
 285         return createJarFile(jarfile, dir, Paths.get("."));
 286     }
 287 
 288     public static Path createJarFile(Path jarfile, Path dir, Path file) throws IOException {
 289         // create the target directory
 290         Path parent = jarfile.getParent();
 291         if (parent != null)
 292             Files.createDirectories(parent);
 293 
 294         List<Path> entries = Files.find(dir.resolve(file), Integer.MAX_VALUE,
 295                 (p, attrs) -> attrs.isRegularFile())
 296                 .map(dir::relativize)
 297                 .collect(Collectors.toList());
 298 
 299         try (OutputStream out = Files.newOutputStream(jarfile);
 300              JarOutputStream jos = new JarOutputStream(out)) {
 301             for (Path entry : entries) {
 302                 // map the file path to a name in the JAR file
 303                 Path normalized = entry.normalize();
 304                 String name = normalized
 305                         .subpath(0, normalized.getNameCount())  // drop root
 306                         .toString()
 307                         .replace(File.separatorChar, '/');
 308 
 309                 jos.putNextEntry(new JarEntry(name));
 310                 Files.copy(dir.resolve(entry), jos);
 311             }
 312         }
 313         return jarfile;
 314     }
 315 
 316     public static Set<String> getModuleContent(Path module) {
 317         Result result = JImageGenerator.getJModTask()
 318                 .jmod(module)
 319                 .list();
 320         result.assertSuccess();
 321         return Stream.of(result.getMessage().split("\r?\n"))
 322                 .collect(Collectors.toSet());
 323     }
 324 
 325     public static void checkModule(Path module, Set<String> expected) throws IOException {
 326         Set<String> actual = getModuleContent(module);
 327         if (!Objects.equals(actual, expected)) {
 328             Set<String> unexpected = new HashSet<>(actual);
 329             unexpected.removeAll(expected);
 330             Set<String> notFound = new HashSet<>(expected);
 331             notFound.removeAll(actual);
 332             System.err.println("Unexpected files:");
 333             unexpected.forEach(s -> System.err.println("\t" + s));
 334             System.err.println("Not found files:");
 335             notFound.forEach(s -> System.err.println("\t" + s));
 336             throw new AssertionError("Module check failed.");
 337         }
 338     }
 339 
 340     public static class JModTask {
 341         static final java.util.spi.ToolProvider JMOD_TOOL =
 342             java.util.spi.ToolProvider.findFirst("jmod").orElseThrow(() ->
 343                 new RuntimeException("jmod tool not found")
 344             );
 345 
 346         private final List<Path> classpath = new ArrayList<>();
 347         private final List<Path> libs = new ArrayList<>();
 348         private final List<Path> cmds = new ArrayList<>();
 349         private final List<Path> config = new ArrayList<>();
 350         private final List<Path> jars = new ArrayList<>();
 351         private final List<Path> jmods = new ArrayList<>();
 352         private final List<String> options = new ArrayList<>();
 353         private Path output;
 354         private String hashModules;
 355         private String mainClass;
 356         private String moduleVersion;
 357 
 358         public JModTask addNativeLibraries(Path cp) {
 359             this.libs.add(cp);
 360             return this;
 361         }
 362 
 363         public JModTask hashModules(String hash) {
 364             this.hashModules = hash;
 365             return this;
 366         }
 367 
 368         public JModTask addCmds(Path cp) {
 369             this.cmds.add(cp);
 370             return this;
 371         }
 372 
 373         public JModTask addClassPath(Path cp) {
 374             this.classpath.add(cp);
 375             return this;
 376         }
 377 
 378         public JModTask addConfig(Path cp) {
 379             this.config.add(cp);
 380             return this;
 381         }
 382 
 383         public JModTask addJars(Path jars) {
 384             this.jars.add(jars);
 385             return this;
 386         }
 387 
 388         public JModTask addJmods(Path jmods) {
 389             this.jmods.add(jmods);
 390             return this;
 391         }
 392 
 393         public JModTask jmod(Path output) {
 394             this.output = output;
 395             return this;
 396         }
 397 
 398         public JModTask moduleVersion(String moduleVersion) {
 399             this.moduleVersion = moduleVersion;
 400             return this;
 401         }
 402 
 403         public JModTask mainClass(String mainClass) {
 404             this.mainClass = mainClass;
 405             return this;
 406         }
 407 
 408         public JModTask option(String o) {
 409             this.options.add(o);
 410             return this;
 411         }
 412 
 413         private String modulePath() {
 414             // This is expect FIRST jmods THEN jars, if you change this, some tests could fail
 415             String jmods = toPath(this.jmods);
 416             String jars = toPath(this.jars);
 417             return jmods + File.pathSeparator + jars;
 418         }
 419 
 420         private String toPath(List<Path> paths) {
 421             return paths.stream()
 422                     .map(Path::toString)
 423                     .collect(Collectors.joining(File.pathSeparator));
 424         }
 425 
 426         private String[] optionsJMod(String cmd) {
 427             List<String> options = new ArrayList<>();
 428             options.add(cmd);
 429             if (!cmds.isEmpty()) {
 430                 options.add(CMDS_OPTION);
 431                 options.add(toPath(cmds));
 432             }
 433             if (!config.isEmpty()) {
 434                 options.add(CONFIG_OPTION);
 435                 options.add(toPath(config));
 436             }
 437             if (hashModules != null) {
 438                 options.add(HASH_MODULES_OPTION);
 439                 options.add(hashModules);
 440             }
 441             if (mainClass != null) {
 442                 options.add(MAIN_CLASS_OPTION);
 443                 options.add(mainClass);
 444             }
 445             if (!libs.isEmpty()) {
 446                 options.add(LIBS_OPTION);
 447                 options.add(toPath(libs));
 448             }
 449             if (!classpath.isEmpty()) {
 450                 options.add(CLASS_PATH_OPTION);
 451                 options.add(toPath(classpath));
 452             }
 453             if (!jars.isEmpty() || !jmods.isEmpty()) {
 454                 options.add(MODULE_PATH_OPTION);
 455                 options.add(modulePath());
 456             }
 457             if (moduleVersion != null) {
 458                 options.add(MODULE_VERSION_OPTION);
 459                 options.add(moduleVersion);
 460             }
 461             options.addAll(this.options);
 462             if (output != null) {
 463                 options.add(output.toString());
 464             }
 465             return options.toArray(new String[options.size()]);
 466         }
 467 
 468         public Result create() {
 469             return cmd("create");
 470         }
 471 
 472         public Result list() {
 473             return cmd("list");
 474         }
 475 
 476         public Result call() {
 477             return cmd("");
 478         }
 479 
 480         private Result cmd(String cmd) {
 481             String[] args = optionsJMod(cmd);
 482             System.err.println("jmod options: " + optionsPrettyPrint(args));
 483             ByteArrayOutputStream baos = new ByteArrayOutputStream();
 484             PrintStream ps = new PrintStream(baos);
 485             int exitCode = JMOD_TOOL.run(ps, ps, args);
 486             String msg = new String(baos.toByteArray());
 487             return new Result(exitCode, msg, output);
 488         }
 489     }
 490 
 491     public static String getPackageName(String canonicalName) {
 492         int index = canonicalName.lastIndexOf('.');
 493         return index > 0 ? canonicalName.substring(0, index) : "";
 494     }
 495 
 496     public static String getSimpleName(String canonicalName) {
 497         int index = canonicalName.lastIndexOf('.');
 498         return canonicalName.substring(index + 1);
 499     }
 500 
 501     public static class JImageTask {
 502 
 503         private final List<Path> pluginModulePath = new ArrayList<>();
 504         private final List<String> options = new ArrayList<>();
 505         private Path dir;
 506         private Path image;
 507 
 508         public JImageTask pluginModulePath(Path p) {
 509             this.pluginModulePath.add(p);
 510             return this;
 511         }
 512 
 513         public JImageTask image(Path image) {
 514             this.image = image;
 515             return this;
 516         }
 517 
 518         public JImageTask dir(Path dir) {
 519             this.dir = dir;
 520             return this;
 521         }
 522 
 523         public JImageTask option(String o) {
 524             this.options.add(o);
 525             return this;
 526         }
 527 
 528         private String toPath(List<Path> paths) {
 529             return paths.stream()
 530                     .map(Path::toString)
 531                     .collect(Collectors.joining(File.pathSeparator));
 532         }
 533 
 534         private String[] optionsJImage(String cmd) {
 535             List<String> options = new ArrayList<>();
 536             options.add(cmd);
 537             if (dir != null) {
 538                 options.add("--dir");
 539                 options.add(dir.toString());
 540             }
 541             if (!pluginModulePath.isEmpty()) {
 542                 options.add(PLUGIN_MODULE_PATH);
 543                 options.add(toPath(pluginModulePath));
 544             }
 545             options.addAll(this.options);
 546             options.add(image.toString());
 547             return options.toArray(new String[options.size()]);
 548         }
 549 
 550         private Result cmd(String cmd, Path returnPath) {
 551             String[] args = optionsJImage(cmd);
 552             System.err.println("jimage options: " + optionsPrettyPrint(args));
 553             StringWriter writer = new StringWriter();
 554             int exitCode = jdk.tools.jimage.Main.run(args, new PrintWriter(writer));
 555             return new Result(exitCode, writer.toString(), returnPath);
 556         }
 557 
 558         public Result extract() {
 559             return cmd("extract", dir);
 560         }
 561     }
 562 
 563     public static class JLinkTask {
 564         static final java.util.spi.ToolProvider JLINK_TOOL =
 565             java.util.spi.ToolProvider.findFirst("jlink").orElseThrow(() ->
 566                 new RuntimeException("jlink tool not found")
 567             );
 568 
 569         private final List<Path> jars = new ArrayList<>();
 570         private final List<Path> jmods = new ArrayList<>();
 571         private final List<Path> pluginModulePath = new ArrayList<>();
 572         private final List<String> addMods = new ArrayList<>();
 573         private final List<String> limitMods = new ArrayList<>();
 574         private final List<String> options = new ArrayList<>();
 575         private String modulePath;
 576         // if you want to specifiy repeated --module-path option
 577         private String repeatedModulePath;
 578         // if you want to specifiy repeated --limit-modules option
 579         private String repeatedLimitMods;
 580         private Path output;
 581         private Path existing;
 582 
 583         public JLinkTask modulePath(String modulePath) {
 584             this.modulePath = modulePath;
 585             return this;
 586         }
 587 
 588         public JLinkTask repeatedModulePath(String modulePath) {
 589             this.repeatedModulePath = modulePath;
 590             return this;
 591         }
 592 
 593         public JLinkTask addJars(Path jars) {
 594             this.jars.add(jars);
 595             return this;
 596         }
 597 
 598         public JLinkTask addJmods(Path jmods) {
 599             this.jmods.add(jmods);
 600             return this;
 601         }
 602 
 603         public JLinkTask pluginModulePath(Path p) {
 604             this.pluginModulePath.add(p);
 605             return this;
 606         }
 607 
 608         public JLinkTask addMods(String moduleName) {
 609             this.addMods.add(moduleName);
 610             return this;
 611         }
 612 
 613         public JLinkTask limitMods(String moduleName) {
 614             this.limitMods.add(moduleName);
 615             return this;
 616         }
 617 
 618         public JLinkTask repeatedLimitMods(String modules) {
 619             this.repeatedLimitMods = modules;
 620             return this;
 621         }
 622 
 623         public JLinkTask output(Path output) {
 624             this.output = output;
 625             return this;
 626         }
 627 
 628         public JLinkTask existing(Path existing) {
 629             this.existing = existing;
 630             return this;
 631         }
 632 
 633         public JLinkTask option(String o) {
 634             this.options.add(o);
 635             return this;
 636         }
 637 
 638         private String modulePath() {
 639             // This is expect FIRST jmods THEN jars, if you change this, some tests could fail
 640             String jmods = toPath(this.jmods);
 641             String jars = toPath(this.jars);
 642             return jmods + File.pathSeparator + jars;
 643         }
 644 
 645         private String toPath(List<Path> paths) {
 646             return paths.stream()
 647                     .map(Path::toString)
 648                     .collect(Collectors.joining(File.pathSeparator));
 649         }
 650 
 651         private String[] optionsJLink() {
 652             List<String> options = new ArrayList<>();
 653             if (output != null) {
 654                 options.add(OUTPUT_OPTION);
 655                 options.add(output.toString());
 656             }
 657             if (!addMods.isEmpty()) {
 658                 options.add(ADD_MODULES_OPTION);
 659                 options.add(addMods.stream().collect(Collectors.joining(",")));
 660             }
 661             if (!limitMods.isEmpty()) {
 662                 options.add(LIMIT_MODULES_OPTION);
 663                 options.add(limitMods.stream().collect(Collectors.joining(",")));
 664             }
 665             if (repeatedLimitMods != null) {
 666                 options.add(LIMIT_MODULES_OPTION);
 667                 options.add(repeatedLimitMods);
 668             }
 669             if (!jars.isEmpty() || !jmods.isEmpty()) {
 670                 options.add(MODULE_PATH_OPTION);
 671                 options.add(modulePath());
 672             }
 673             if (modulePath != null) {
 674                 options.add(MODULE_PATH_OPTION);
 675                 options.add(modulePath);
 676             }
 677             if (repeatedModulePath != null) {
 678                 options.add(MODULE_PATH_OPTION);
 679                 options.add(repeatedModulePath);
 680             }
 681             if (!pluginModulePath.isEmpty()) {
 682                 options.add(PLUGIN_MODULE_PATH);
 683                 options.add(toPath(pluginModulePath));
 684             }
 685             options.addAll(this.options);
 686             return options.toArray(new String[options.size()]);
 687         }
 688 
 689         private String[] optionsPostProcessJLink() {
 690             List<String> options = new ArrayList<>();
 691             if (existing != null) {
 692                 options.add(POST_PROCESS_OPTION);
 693                 options.add(existing.toString());
 694             }
 695             options.addAll(this.options);
 696             return options.toArray(new String[options.size()]);
 697         }
 698 
 699         public Result call() {
 700             String[] args = optionsJLink();
 701             System.err.println("jlink options: " + optionsPrettyPrint(args));
 702             StringWriter writer = new StringWriter();
 703             PrintWriter pw = new PrintWriter(writer);
 704             int exitCode = JLINK_TOOL.run(pw, pw, args);
 705             return new Result(exitCode, writer.toString(), output);
 706         }
 707 
 708         public Result callPostProcess() {
 709             String[] args = optionsPostProcessJLink();
 710             System.err.println("jlink options: " + optionsPrettyPrint(args));
 711             StringWriter writer = new StringWriter();
 712             PrintWriter pw = new PrintWriter(writer);
 713             int exitCode = JLINK_TOOL.run(pw, pw, args);
 714             return new Result(exitCode, writer.toString(), output);
 715         }
 716     }
 717 
 718     public static class InMemorySourceFile {
 719         public final String packageName;
 720         public final String className;
 721         public final String source;
 722 
 723         public InMemorySourceFile(String packageName, String simpleName, String source) {
 724             this.packageName = packageName;
 725             this.className = simpleName;
 726             this.source = source;
 727         }
 728     }
 729 
 730     public static class InMemoryFile {
 731         private final String path;
 732         private final byte[] bytes;
 733 
 734         public String getPath() {
 735             return path;
 736         }
 737 
 738         public InputStream getBytes() {
 739             return new ByteArrayInputStream(bytes);
 740         }
 741 
 742         public InMemoryFile(String path, byte[] bytes) {
 743             this.path = path;
 744             this.bytes = bytes;
 745         }
 746 
 747         public InMemoryFile(String path, InputStream is) throws IOException {
 748             this(path, readAllBytes(is));
 749         }
 750     }
 751 
 752     public static byte[] readAllBytes(InputStream is) throws IOException {
 753         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 754         byte[] buf = new byte[1024];
 755         while (true) {
 756             int n = is.read(buf);
 757             if (n < 0) {
 758                 break;
 759             }
 760             baos.write(buf, 0, n);
 761         }
 762         return baos.toByteArray();
 763     }
 764 }