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 private static void createModule(String javaFile, String inputDir, String aName, 402 ModuleArgs moduleArgs, boolean createModularJar) throws Exception { 403 int retVal; 404 405 File input = new File(inputDir); 406 if (!input.exists()) { 407 input.mkdir(); 408 } 409 410 File module = new File("module" + File.separator + "com." + aName); 411 if (!module.exists()) { 412 module.mkdirs(); 413 } 414 415 File javacLog = new File("javac.log"); 416 try { 417 List<String> args = new ArrayList<>(); 418 args.add(JAVAC.toString()); 419 args.add("-d"); 420 args.add("module" + File.separator + "com." + aName); 421 args.add(TEST_SRC_ROOT + File.separator + "apps" + File.separator 422 + "com." + aName + File.separator + "module-info.java"); 423 args.add(TEST_SRC_ROOT + File.separator + "apps" 424 + File.separator + "com." + aName + File.separator + "com" 425 + File.separator + aName + File.separator + javaFile); 426 retVal = execute(javacLog, args.stream().toArray(String[]::new)); 427 } catch (Exception ex) { 428 if (javacLog.exists()) { 429 System.err.println(Files.readString(javacLog.toPath())); 430 } 431 throw ex; 432 } 433 434 if (retVal != 0) { 435 if (javacLog.exists()) { 436 System.err.println(Files.readString(javacLog.toPath())); 437 } 438 throw new AssertionError("javac exited with error: " + retVal); 439 } 440 441 if (createModularJar) { 442 File jarLog = new File("jar.log"); 443 try { 444 List<String> args = new ArrayList<>(); 445 args.add(JAR.toString()); 446 args.add("--create"); 447 args.add("--file"); 448 args.add(inputDir + File.separator + "com." + aName + ".jar"); 449 if (moduleArgs != null) { 450 if (moduleArgs.getVersion() != null) { 451 args.add("--module-version"); 452 args.add(moduleArgs.getVersion()); 453 } 454 455 if (moduleArgs.getMainClass()!= null) { 456 args.add("--main-class"); 457 args.add(moduleArgs.getMainClass()); 458 } 459 } 460 args.add("-C"); 461 args.add("module" + File.separator + "com." + aName); 462 args.add("."); 463 464 retVal = execute(jarLog, args.stream().toArray(String[]::new)); 465 } catch (Exception ex) { 466 if (jarLog.exists()) { 467 System.err.println(Files.readString(jarLog.toPath())); 468 } 469 throw ex; 470 } 471 472 if (retVal != 0) { 473 if (jarLog.exists()) { 474 System.err.println(Files.readString(jarLog.toPath())); 475 } 476 throw new AssertionError("jar exited with error: " + retVal); 477 } 478 } 479 } 480 481 public static void createRuntime() throws Exception { 482 List<String> moreArgs = new ArrayList<>(); 483 createRuntime(moreArgs); 484 } 485 486 public static void createRuntime(List<String> moreArgs) throws Exception { 487 int retVal; 488 489 File jlinkLog = new File("jlink.log"); 490 try { 491 List<String> args = new ArrayList<>(); 492 args.add(JLINK.toString()); 493 args.add("--output"); 494 args.add("runtime"); 495 args.add("--add-modules"); 496 args.add("java.base"); 497 args.addAll(moreArgs); 498 499 retVal = execute(jlinkLog, args.stream().toArray(String[]::new)); 500 } catch (Exception ex) { 501 if (jlinkLog.exists()) { 502 System.err.println(Files.readString(jlinkLog.toPath())); 503 } 504 throw ex; 505 } 506 507 if (retVal != 0) { 508 if (jlinkLog.exists()) { 509 System.err.println(Files.readString(jlinkLog.toPath())); 510 } 511 throw new AssertionError("jlink exited with error: " + retVal); 512 } 513 } 514 515 public static String listToArgumentsMap(List<String> arguments, boolean toolProvider) { 516 if (arguments.isEmpty()) { 517 return ""; 518 } 519 520 String argsStr = ""; 521 for (int i = 0; i < arguments.size(); i++) { 522 String arg = arguments.get(i); 523 argsStr += quote(arg, toolProvider); 524 if ((i + 1) != arguments.size()) { 525 argsStr += " "; 526 } 527 } 528 529 if (!toolProvider && isWindows()) { 530 if (argsStr.contains(" ")) { 531 if (argsStr.contains("\"")) { 532 argsStr = escapeQuote(argsStr, toolProvider); 533 } 534 argsStr = "\"" + argsStr + "\""; 535 } 536 } 537 return argsStr; 538 } 539 540 public static String[] cmdWithAtFilename(String [] cmd, int ndx, int len) 541 throws IOException { 542 ArrayList<String> newAList = new ArrayList<>(); 543 String fileString = null; 544 for (int i=0; i<cmd.length; i++) { 545 if (i == ndx) { 546 newAList.add("@argfile.cmds"); 547 fileString = cmd[i]; 548 } else if (i > ndx && i < ndx + len) { 549 fileString += " " + cmd[i]; 550 } else { 551 newAList.add(cmd[i]); 552 } 553 } 554 if (fileString != null) { 555 Path path = new File("argfile.cmds").toPath(); 556 try (BufferedWriter bw = Files.newBufferedWriter(path); 557 PrintWriter out = new PrintWriter(bw)) { 558 out.println(fileString); 559 } 560 } 561 return newAList.toArray(new String[0]); 562 } 563 564 public static String [] splitAndFilter(String output) { 565 if (output == null) { 566 return null; 567 } 568 569 return Stream.of(output.split("\\R")) 570 .filter(str -> !str.startsWith("Picked up")) 571 .filter(str -> !str.startsWith("WARNING: Using incubator")) 572 .filter(str -> !str.startsWith("hello: ")) 573 .collect(Collectors.toList()).toArray(String[]::new); 574 } 575 576 private static String quote(String in, boolean toolProvider) { 577 if (in == null) { 578 return null; 579 } 580 581 if (in.isEmpty()) { 582 return ""; 583 } 584 585 if (!in.contains("=")) { 586 // Not a property 587 if (in.contains(" ")) { 588 in = escapeQuote(in, toolProvider); 589 return "\"" + in + "\""; 590 } 591 return in; 592 } 593 594 if (!in.contains(" ")) { 595 return in; // No need to quote 596 } 597 598 int paramIndex = in.indexOf("="); 599 if (paramIndex <= 0) { 600 return in; // Something wrong, just skip quoting 601 } 602 603 String param = in.substring(0, paramIndex); 604 String value = in.substring(paramIndex + 1); 605 606 if (value.length() == 0) { 607 return in; // No need to quote 608 } 609 610 value = escapeQuote(value, toolProvider); 611 612 return param + "=" + "\"" + value + "\""; 613 } 614 615 private static String escapeQuote(String in, boolean toolProvider) { 616 if (in == null) { 617 return null; 618 } 619 620 if (in.isEmpty()) { 621 return ""; 622 } 623 624 if (in.contains("\"")) { 625 // Use code points to preserve non-ASCII chars 626 StringBuilder sb = new StringBuilder(); 627 int codeLen = in.codePointCount(0, in.length()); 628 for (int i = 0; i < codeLen; i++) { 629 int code = in.codePointAt(i); 630 // Note: No need to escape '\' on Linux or OS X 631 // jpackage expects us to pass arguments and properties with 632 // quotes and spaces as a map 633 // with quotes being escaped with additional \ for 634 // internal quotes. 635 // So if we want two properties below: 636 // -Djnlp.Prop1=Some "Value" 1 637 // -Djnlp.Prop2=Some Value 2 638 // jpackage will need: 639 // "-Djnlp.Prop1=\"Some \\"Value\\" 1\" -Djnlp.Prop2=\"Some Value 2\"" 640 // but since we using ProcessBuilder to run jpackage we will need to escape 641 // our escape symbols as well, so we will need to pass string below to ProcessBuilder: 642 // "-Djnlp.Prop1=\\\"Some \\\\\\\"Value\\\\\\\" 1\\\" -Djnlp.Prop2=\\\"Some Value 2\\\"" 643 switch (code) { 644 case '"': 645 // " -> \" -> \\\" 646 if (i == 0 || in.codePointAt(i - 1) != '\\') { 647 sb.appendCodePoint('\\'); 648 sb.appendCodePoint(code); 649 } 650 break; 651 case '\\': 652 // We need to escape already escaped symbols as well 653 if ((i + 1) < codeLen) { 654 int nextCode = in.codePointAt(i + 1); 655 if (nextCode == '"') { 656 // \" -> \\\" 657 sb.appendCodePoint('\\'); 658 sb.appendCodePoint('\\'); 659 sb.appendCodePoint('\\'); 660 sb.appendCodePoint(nextCode); 661 } else { 662 sb.appendCodePoint('\\'); 663 sb.appendCodePoint(code); 664 } 665 } else { 666 sb.appendCodePoint(code); 667 } 668 break; 669 default: 670 sb.appendCodePoint(code); 671 break; 672 } 673 } 674 return sb.toString(); 675 } 676 677 return in; 678 } 679 }