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