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