1 /* 2 * Copyright (c) 2018, 2019, 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 24 import java.io.File; 25 import java.io.IOException; 26 import java.io.PrintWriter; 27 import java.io.StringWriter; 28 import java.io.BufferedWriter; 29 import java.nio.file.FileVisitResult; 30 31 import java.nio.file.Files; 32 import java.nio.file.Path; 33 import java.nio.file.SimpleFileVisitor; 34 import java.nio.file.attribute.BasicFileAttributes; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.stream.Collectors; 38 import java.util.stream.Stream; 39 40 import java.util.spi.ToolProvider; 41 import jdk.incubator.jpackage.ToolProviderFactory; 42 43 public class JPackageHelper { 44 45 private static final boolean VERBOSE = false; 46 private static final String OS = System.getProperty("os.name").toLowerCase(); 47 private static final String JAVA_HOME = System.getProperty("java.home"); 48 public static final String TEST_SRC_ROOT; 49 public static final String TEST_SRC; 50 private static final Path BIN_DIR = Path.of(JAVA_HOME, "bin"); 51 private static final Path JPACKAGE; 52 private static final Path JAVAC; 53 private static final Path JAR; 54 private static final Path JLINK; 55 56 public static class ModuleArgs { 57 private final String version; 58 private final String mainClass; 59 60 ModuleArgs(String version, String mainClass) { 61 this.version = version; 62 this.mainClass = mainClass; 63 } 64 65 public String getVersion() { 66 return version; 67 } 68 69 public String getMainClass() { 70 return mainClass; 71 } 72 } 73 74 static { 75 if (OS.startsWith("win")) { 76 JPACKAGE = BIN_DIR.resolve("jpackage.exe"); 77 JAVAC = BIN_DIR.resolve("javac.exe"); 78 JAR = BIN_DIR.resolve("jar.exe"); 79 JLINK = BIN_DIR.resolve("jlink.exe"); 80 } else { 81 JPACKAGE = BIN_DIR.resolve("jpackage"); 82 JAVAC = BIN_DIR.resolve("javac"); 83 JAR = BIN_DIR.resolve("jar"); 84 JLINK = BIN_DIR.resolve("jlink"); 85 } 86 87 // Figure out test src based on where we called 88 TEST_SRC = System.getProperty("test.src"); 89 Path root = Path.of(TEST_SRC); 90 Path apps = Path.of(TEST_SRC, "apps"); 91 if (apps.toFile().exists()) { 92 // fine - test is at root 93 } else { 94 apps = Path.of(TEST_SRC, "..", "apps"); 95 if (apps.toFile().exists()) { 96 root = apps.getParent().normalize(); // test is 1 level down 97 } else { 98 apps = Path.of(TEST_SRC, "..", "..", "apps"); 99 if (apps.toFile().exists()) { 100 root = apps.getParent().normalize(); // 2 levels down 101 } else { 102 apps = Path.of(TEST_SRC, "..", "..", "..", "apps"); 103 if (apps.toFile().exists()) { 104 root = apps.getParent().normalize(); // 3 levels down 105 } else { 106 // if we ever have tests more than three levels 107 // down we need to add code here 108 throw new RuntimeException("we should never get here"); 109 } 110 } 111 } 112 } 113 TEST_SRC_ROOT = root.toString(); 114 } 115 116 static final ToolProvider JPACKAGE_TOOL = 117 ToolProviderFactory.findFirst("jpackage").orElseThrow( 118 () -> new RuntimeException("jpackage tool not found")); 119 120 public static int execute(File out, String... command) throws Exception { 121 if (VERBOSE) { 122 System.out.print("Execute command: "); 123 for (String c : command) { 124 System.out.print(c); 125 System.out.print(" "); 126 } 127 System.out.println(); 128 } 129 130 ProcessBuilder builder = new ProcessBuilder(command); 131 if (out != null) { 132 builder.redirectErrorStream(true); 133 builder.redirectOutput(out); 134 } 135 136 Process process = builder.start(); 137 return process.waitFor(); 138 } 139 140 public static Process executeNoWait(File out, String... command) throws Exception { 141 if (VERBOSE) { 142 System.out.print("Execute command: "); 143 for (String c : command) { 144 System.out.print(c); 145 System.out.print(" "); 146 } 147 System.out.println(); 148 } 149 150 ProcessBuilder builder = new ProcessBuilder(command); 151 if (out != null) { 152 builder.redirectErrorStream(true); 153 builder.redirectOutput(out); 154 } 155 156 return builder.start(); 157 } 158 159 private static String[] getCommand(String... args) { 160 String[] command; 161 if (args == null) { 162 command = new String[1]; 163 } else { 164 command = new String[args.length + 1]; 165 } 166 167 int index = 0; 168 command[index] = JPACKAGE.toString(); 169 170 if (args != null) { 171 for (String arg : args) { 172 index++; 173 command[index] = arg; 174 } 175 } 176 177 return command; 178 } 179 180 public static void deleteRecursive(File path) throws IOException { 181 if (!path.exists()) { 182 return; 183 } 184 185 Path directory = path.toPath(); 186 Files.walkFileTree(directory, new SimpleFileVisitor<Path>() { 187 @Override 188 public FileVisitResult visitFile(Path file, 189 BasicFileAttributes attr) throws IOException { 190 file.toFile().setWritable(true); 191 if (OS.startsWith("win")) { 192 try { 193 Files.setAttribute(file, "dos:readonly", false); 194 } catch (Exception ioe) { 195 // just report and try to contune 196 System.err.println("IOException: " + ioe); 197 ioe.printStackTrace(System.err); 198 } 199 } 200 Files.delete(file); 201 return FileVisitResult.CONTINUE; 202 } 203 204 @Override 205 public FileVisitResult preVisitDirectory(Path dir, 206 BasicFileAttributes attr) throws IOException { 207 if (OS.startsWith("win")) { 208 Files.setAttribute(dir, "dos:readonly", false); 209 } 210 return FileVisitResult.CONTINUE; 211 } 212 213 @Override 214 public FileVisitResult postVisitDirectory(Path dir, IOException e) 215 throws IOException { 216 Files.delete(dir); 217 return FileVisitResult.CONTINUE; 218 } 219 }); 220 } 221 222 public static void deleteOutputFolder(String output) throws IOException { 223 File outputFolder = new File(output); 224 System.out.println("deleteOutputFolder: " + outputFolder.getAbsolutePath()); 225 try { 226 deleteRecursive(outputFolder); 227 } catch (IOException ioe) { 228 System.err.println("IOException: " + ioe); 229 ioe.printStackTrace(System.err); 230 deleteRecursive(outputFolder); 231 } 232 } 233 234 public static String executeCLI(boolean retValZero, String... args) throws Exception { 235 int retVal; 236 File outfile = new File("output.log"); 237 String[] command = getCommand(args); 238 try { 239 retVal = execute(outfile, command); 240 } catch (Exception ex) { 241 if (outfile.exists()) { 242 System.err.println(Files.readString(outfile.toPath())); 243 } 244 throw ex; 245 } 246 247 String output = Files.readString(outfile.toPath()); 248 if (retValZero) { 249 if (retVal != 0) { 250 System.err.println("command run:"); 251 for (String s : command) { System.err.println(s); } 252 System.err.println("command output:"); 253 System.err.println(output); 254 throw new AssertionError("jpackage exited with error: " + retVal); 255 } 256 } else { 257 if (retVal == 0) { 258 System.err.println(output); 259 throw new AssertionError("jpackage exited without error: " + retVal); 260 } 261 } 262 263 if (VERBOSE) { 264 System.out.println("output ="); 265 System.out.println(output); 266 } 267 268 return output; 269 } 270 271 public static String executeToolProvider(boolean retValZero, String... args) throws Exception { 272 StringWriter writer = new StringWriter(); 273 PrintWriter pw = new PrintWriter(writer); 274 int retVal = JPACKAGE_TOOL.run(pw, pw, args); 275 String output = writer.toString(); 276 277 if (retValZero) { 278 if (retVal != 0) { 279 System.err.println(output); 280 throw new AssertionError("jpackage exited with error: " + retVal); 281 } 282 } else { 283 if (retVal == 0) { 284 System.err.println(output); 285 throw new AssertionError("jpackage exited without error"); 286 } 287 } 288 289 if (VERBOSE) { 290 System.out.println("output ="); 291 System.out.println(output); 292 } 293 294 return output; 295 } 296 297 public static boolean isWindows() { 298 return (OS.contains("win")); 299 } 300 301 public static boolean isOSX() { 302 return (OS.contains("mac")); 303 } 304 305 public static boolean isLinux() { 306 return ((OS.contains("nix") || OS.contains("nux"))); 307 } 308 309 public static void createHelloImageJar(String inputDir) throws Exception { 310 createJar(false, "Hello", "image", inputDir); 311 } 312 313 public static void createHelloImageJar() throws Exception { 314 createJar(false, "Hello", "image", "input"); 315 } 316 317 public static void createHelloImageJarWithMainClass() throws Exception { 318 createJar(true, "Hello", "image", "input"); 319 } 320 321 public static void createHelloInstallerJar() throws Exception { 322 createJar(false, "Hello", "installer", "input"); 323 } 324 325 public static void createHelloInstallerJarWithMainClass() throws Exception { 326 createJar(true, "Hello", "installer", "input"); 327 } 328 329 private static void createJar(boolean mainClassAttribute, String name, 330 String testType, String inputDir) throws Exception { 331 int retVal; 332 333 File input = new File(inputDir); 334 if (!input.exists()) { 335 input.mkdirs(); 336 } 337 338 Path src = Path.of(TEST_SRC_ROOT + File.separator + "apps" 339 + File.separator + testType + File.separator + name + ".java"); 340 Path dst = Path.of(name + ".java"); 341 342 if (dst.toFile().exists()) { 343 Files.delete(dst); 344 } 345 Files.copy(src, dst); 346 347 348 File javacLog = new File("javac.log"); 349 try { 350 retVal = execute(javacLog, JAVAC.toString(), name + ".java"); 351 } catch (Exception ex) { 352 if (javacLog.exists()) { 353 System.err.println(Files.readString(javacLog.toPath())); 354 } 355 throw ex; 356 } 357 358 if (retVal != 0) { 359 if (javacLog.exists()) { 360 System.err.println(Files.readString(javacLog.toPath())); 361 } 362 throw new AssertionError("javac exited with error: " + retVal); 363 } 364 365 File jarLog = new File("jar.log"); 366 try { 367 List<String> args = new ArrayList<>(); 368 args.add(JAR.toString()); 369 args.add("-c"); 370 args.add("-v"); 371 args.add("-f"); 372 args.add(inputDir + File.separator + name.toLowerCase() + ".jar"); 373 if (mainClassAttribute) { 374 args.add("-e"); 375 args.add(name); 376 } 377 args.add(name + ".class"); 378 retVal = execute(jarLog, args.stream().toArray(String[]::new)); 379 } catch (Exception ex) { 380 if (jarLog.exists()) { 381 System.err.println(Files.readString(jarLog.toPath())); 382 } 383 throw ex; 384 } 385 386 if (retVal != 0) { 387 if (jarLog.exists()) { 388 System.err.println(Files.readString(jarLog.toPath())); 389 } 390 throw new AssertionError("jar exited with error: " + retVal); 391 } 392 } 393 394 public static void createHelloModule() throws Exception { 395 createModule("Hello.java", "input", "hello", null, true); 396 } 397 398 public static void createHelloModule(ModuleArgs moduleArgs) throws Exception { 399 createModule("Hello.java", "input", "hello", moduleArgs, true); 400 } 401 402 public static void createOtherModule() throws Exception { 403 createModule("Other.java", "input-other", "other", null, false); 404 } 405 406 private static void createModule(String javaFile, String inputDir, String aName, 407 ModuleArgs moduleArgs, boolean createModularJar) throws Exception { 408 int retVal; 409 410 File input = new File(inputDir); 411 if (!input.exists()) { 412 input.mkdir(); 413 } 414 415 File module = new File("module" + File.separator + "com." + aName); 416 if (!module.exists()) { 417 module.mkdirs(); 418 } 419 420 File javacLog = new File("javac.log"); 421 try { 422 List<String> args = new ArrayList<>(); 423 args.add(JAVAC.toString()); 424 args.add("-d"); 425 args.add("module" + File.separator + "com." + aName); 426 args.add(TEST_SRC_ROOT + File.separator + "apps" + File.separator 427 + "com." + aName + File.separator + "module-info.java"); 428 args.add(TEST_SRC_ROOT + File.separator + "apps" 429 + File.separator + "com." + aName + File.separator + "com" 430 + File.separator + aName + File.separator + javaFile); 431 retVal = execute(javacLog, args.stream().toArray(String[]::new)); 432 } catch (Exception ex) { 433 if (javacLog.exists()) { 434 System.err.println(Files.readString(javacLog.toPath())); 435 } 436 throw ex; 437 } 438 439 if (retVal != 0) { 440 if (javacLog.exists()) { 441 System.err.println(Files.readString(javacLog.toPath())); 442 } 443 throw new AssertionError("javac exited with error: " + retVal); 444 } 445 446 if (createModularJar) { 447 File jarLog = new File("jar.log"); 448 try { 449 List<String> args = new ArrayList<>(); 450 args.add(JAR.toString()); 451 args.add("--create"); 452 args.add("--file"); 453 args.add(inputDir + File.separator + "com." + aName + ".jar"); 454 if (moduleArgs != null) { 455 if (moduleArgs.getVersion() != null) { 456 args.add("--module-version"); 457 args.add(moduleArgs.getVersion()); 458 } 459 460 if (moduleArgs.getMainClass()!= null) { 461 args.add("--main-class"); 462 args.add(moduleArgs.getMainClass()); 463 } 464 } 465 args.add("-C"); 466 args.add("module" + File.separator + "com." + aName); 467 args.add("."); 468 469 retVal = execute(jarLog, args.stream().toArray(String[]::new)); 470 } catch (Exception ex) { 471 if (jarLog.exists()) { 472 System.err.println(Files.readString(jarLog.toPath())); 473 } 474 throw ex; 475 } 476 477 if (retVal != 0) { 478 if (jarLog.exists()) { 479 System.err.println(Files.readString(jarLog.toPath())); 480 } 481 throw new AssertionError("jar exited with error: " + retVal); 482 } 483 } 484 } 485 486 public static void createRuntime() throws Exception { 487 List<String> moreArgs = new ArrayList<>(); 488 createRuntime(moreArgs); 489 } 490 491 public static void createRuntime(List<String> moreArgs) throws Exception { 492 int retVal; 493 494 File jlinkLog = new File("jlink.log"); 495 try { 496 List<String> args = new ArrayList<>(); 497 args.add(JLINK.toString()); 498 args.add("--output"); 499 args.add("runtime"); 500 args.add("--add-modules"); 501 args.add("java.base"); 502 args.addAll(moreArgs); 503 504 retVal = execute(jlinkLog, args.stream().toArray(String[]::new)); 505 } catch (Exception ex) { 506 if (jlinkLog.exists()) { 507 System.err.println(Files.readString(jlinkLog.toPath())); 508 } 509 throw ex; 510 } 511 512 if (retVal != 0) { 513 if (jlinkLog.exists()) { 514 System.err.println(Files.readString(jlinkLog.toPath())); 515 } 516 throw new AssertionError("jlink exited with error: " + retVal); 517 } 518 } 519 520 public static String listToArgumentsMap(List<String> arguments, boolean toolProvider) { 521 if (arguments.isEmpty()) { 522 return ""; 523 } 524 525 String argsStr = ""; 526 for (int i = 0; i < arguments.size(); i++) { 527 String arg = arguments.get(i); 528 argsStr += quote(arg, toolProvider); 529 if ((i + 1) != arguments.size()) { 530 argsStr += " "; 531 } 532 } 533 534 if (!toolProvider && isWindows()) { 535 if (argsStr.contains(" ")) { 536 if (argsStr.contains("\"")) { 537 argsStr = escapeQuote(argsStr, toolProvider); 538 } 539 argsStr = "\"" + argsStr + "\""; 540 } 541 } 542 return argsStr; 543 } 544 545 public static String[] cmdWithAtFilename(String [] cmd, int ndx, int len) 546 throws IOException { 547 ArrayList<String> newAList = new ArrayList<>(); 548 String fileString = null; 549 for (int i=0; i<cmd.length; i++) { 550 if (i == ndx) { 551 newAList.add("@argfile.cmds"); 552 fileString = cmd[i]; 553 } else if (i > ndx && i < ndx + len) { 554 fileString += " " + cmd[i]; 555 } else { 556 newAList.add(cmd[i]); 557 } 558 } 559 if (fileString != null) { 560 Path path = new File("argfile.cmds").toPath(); 561 try (BufferedWriter bw = Files.newBufferedWriter(path); 562 PrintWriter out = new PrintWriter(bw)) { 563 out.println(fileString); 564 } 565 } 566 return newAList.toArray(new String[0]); 567 } 568 569 public static String [] splitAndFilter(String output) { 570 if (output == null) { 571 return null; 572 } 573 574 return Stream.of(output.split("\\R")) 575 .filter(str -> !str.startsWith("Picked up")) 576 .filter(str -> !str.startsWith("WARNING: Using incubator")) 577 .filter(str -> !str.startsWith("hello: ")) 578 .collect(Collectors.toList()).toArray(String[]::new); 579 } 580 581 private static String quote(String in, boolean toolProvider) { 582 if (in == null) { 583 return null; 584 } 585 586 if (in.isEmpty()) { 587 return ""; 588 } 589 590 if (!in.contains("=")) { 591 // Not a property 592 if (in.contains(" ")) { 593 in = escapeQuote(in, toolProvider); 594 return "\"" + in + "\""; 595 } 596 return in; 597 } 598 599 if (!in.contains(" ")) { 600 return in; // No need to quote 601 } 602 603 int paramIndex = in.indexOf("="); 604 if (paramIndex <= 0) { 605 return in; // Something wrong, just skip quoting 606 } 607 608 String param = in.substring(0, paramIndex); 609 String value = in.substring(paramIndex + 1); 610 611 if (value.length() == 0) { 612 return in; // No need to quote 613 } 614 615 value = escapeQuote(value, toolProvider); 616 617 return param + "=" + "\"" + value + "\""; 618 } 619 620 private static String escapeQuote(String in, boolean toolProvider) { 621 if (in == null) { 622 return null; 623 } 624 625 if (in.isEmpty()) { 626 return ""; 627 } 628 629 if (in.contains("\"")) { 630 // Use code points to preserve non-ASCII chars 631 StringBuilder sb = new StringBuilder(); 632 int codeLen = in.codePointCount(0, in.length()); 633 for (int i = 0; i < codeLen; i++) { 634 int code = in.codePointAt(i); 635 // Note: No need to escape '\' on Linux or OS X 636 // jpackage expects us to pass arguments and properties with 637 // quotes and spaces as a map 638 // with quotes being escaped with additional \ for 639 // internal quotes. 640 // So if we want two properties below: 641 // -Djnlp.Prop1=Some "Value" 1 642 // -Djnlp.Prop2=Some Value 2 643 // jpackage will need: 644 // "-Djnlp.Prop1=\"Some \\"Value\\" 1\" -Djnlp.Prop2=\"Some Value 2\"" 645 // but since we using ProcessBuilder to run jpackage we will need to escape 646 // our escape symbols as well, so we will need to pass string below to ProcessBuilder: 647 // "-Djnlp.Prop1=\\\"Some \\\\\\\"Value\\\\\\\" 1\\\" -Djnlp.Prop2=\\\"Some Value 2\\\"" 648 switch (code) { 649 case '"': 650 // " -> \" -> \\\" 651 if (i == 0 || in.codePointAt(i - 1) != '\\') { 652 sb.appendCodePoint('\\'); 653 sb.appendCodePoint(code); 654 } 655 break; 656 case '\\': 657 // We need to escape already escaped symbols as well 658 if ((i + 1) < codeLen) { 659 int nextCode = in.codePointAt(i + 1); 660 if (nextCode == '"') { 661 // \" -> \\\" 662 sb.appendCodePoint('\\'); 663 sb.appendCodePoint('\\'); 664 sb.appendCodePoint('\\'); 665 sb.appendCodePoint(nextCode); 666 } else { 667 sb.appendCodePoint('\\'); 668 sb.appendCodePoint(code); 669 } 670 } else { 671 sb.appendCodePoint(code); 672 } 673 break; 674 default: 675 sb.appendCodePoint(code); 676 break; 677 } 678 } 679 return sb.toString(); 680 } 681 682 return in; 683 } 684 }