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 }