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