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