1 /* 2 * Copyright (c) 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 package jdk.jpackage.test; 24 25 import java.io.FileOutputStream; 26 import java.io.IOException; 27 import java.nio.file.Files; 28 import java.nio.file.Path; 29 import java.security.SecureRandom; 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 import java.util.Collection; 33 import java.util.HashMap; 34 import java.util.List; 35 import java.util.ListIterator; 36 import java.util.Map; 37 import java.util.function.Consumer; 38 import java.util.function.Function; 39 import java.util.function.Supplier; 40 41 /** 42 * jpackage command line with prerequisite actions. Prerequisite actions can be 43 * anything. The simplest is to compile test application and pack in a jar for 44 * use on jpackage command line. 45 */ 46 public final class JPackageCommand extends CommandArguments<JPackageCommand> { 47 48 public JPackageCommand() { 49 actions = new ArrayList<>(); 50 } 51 52 JPackageCommand createImmutableCopy() { 53 JPackageCommand reply = new JPackageCommand(); 54 reply.immutable = true; 55 reply.args.addAll(args); 56 return reply; 57 } 58 59 public JPackageCommand setArgumentValue(String argName, String newValue) { 60 verifyMutable(); 61 62 String prevArg = null; 63 ListIterator<String> it = args.listIterator(); 64 while (it.hasNext()) { 65 String value = it.next(); 66 if (prevArg != null && prevArg.equals(argName)) { 67 if (newValue != null) { 68 it.set(newValue); 69 } else { 70 it.remove(); 71 it.previous(); 72 it.remove(); 73 } 74 return this; 75 } 76 prevArg = value; 77 } 78 79 if (newValue != null) { 80 addArguments(argName, newValue); 81 } 82 83 return this; 84 } 85 86 public JPackageCommand setArgumentValue(String argName, Path newValue) { 87 return setArgumentValue(argName, newValue.toString()); 88 } 89 90 public JPackageCommand removeArgument(String argName) { 91 return setArgumentValue(argName, (String)null); 92 } 93 94 public boolean hasArgument(String argName) { 95 return args.contains(argName); 96 } 97 98 public <T> T getArgumentValue(String argName, 99 Function<JPackageCommand, T> defaultValueSupplier, 100 Function<String, T> stringConverter) { 101 String prevArg = null; 102 for (String arg : args) { 103 if (prevArg != null && prevArg.equals(argName)) { 104 return stringConverter.apply(arg); 105 } 106 prevArg = arg; 107 } 108 if (defaultValueSupplier != null) { 109 return defaultValueSupplier.apply(this); 110 } 111 return null; 112 } 113 114 public String getArgumentValue(String argName, 115 Function<JPackageCommand, String> defaultValueSupplier) { 116 return getArgumentValue(argName, defaultValueSupplier, v -> v); 117 } 118 119 public <T> T getArgumentValue(String argName, 120 Supplier<T> defaultValueSupplier, 121 Function<String, T> stringConverter) { 122 return getArgumentValue(argName, (unused) -> defaultValueSupplier.get(), 123 stringConverter); 124 } 125 126 public String getArgumentValue(String argName, 127 Supplier<String> defaultValueSupplier) { 128 return getArgumentValue(argName, defaultValueSupplier, v -> v); 129 } 130 131 public String getArgumentValue(String argName) { 132 return getArgumentValue(argName, (Supplier<String>)null); 133 } 134 135 public String[] getAllArgumentValues(String argName) { 136 List<String> values = new ArrayList<>(); 137 String prevArg = null; 138 for (String arg : args) { 139 if (prevArg != null && prevArg.equals(argName)) { 140 values.add(arg); 141 } 142 prevArg = arg; 143 } 144 return values.toArray(String[]::new); 145 } 146 147 public JPackageCommand addArguments(String name, Path value) { 148 return addArguments(name, value.toString()); 149 } 150 151 public PackageType packageType() { 152 return getArgumentValue("--package-type", 153 () -> PackageType.DEFAULT, 154 (v) -> PACKAGE_TYPES.get(v)); 155 } 156 157 public Path outputDir() { 158 return getArgumentValue("--dest", () -> Test.defaultOutputDir(), Path::of); 159 } 160 161 public Path inputDir() { 162 return getArgumentValue("--input", () -> Test.defaultInputDir(),Path::of); 163 } 164 165 public String version() { 166 return getArgumentValue("--app-version", () -> "1.0"); 167 } 168 169 public String name() { 170 return getArgumentValue("--name", () -> getArgumentValue("--main-class")); 171 } 172 173 public boolean isRuntime() { 174 return hasArgument("--runtime-image") 175 && !hasArgument("--main-jar") 176 && !hasArgument("--module") 177 && !hasArgument("--app-image"); 178 } 179 180 public JPackageCommand setDefaultInputOutput() { 181 addArguments("--input", Test.defaultInputDir()); 182 addArguments("--dest", Test.defaultOutputDir()); 183 return this; 184 } 185 186 public JPackageCommand setFakeRuntime() { 187 verifyMutable(); 188 189 try { 190 Path fakeRuntimeDir = Test.workDir().resolve("fake_runtime"); 191 Files.createDirectories(fakeRuntimeDir); 192 193 if (Test.isWindows() || Test.isLinux()) { 194 // Needed to make WindowsAppBundler happy as it copies MSVC dlls 195 // from `bin` directory. 196 // Need to make the code in rpm spec happy as it assumes there is 197 // always something in application image. 198 fakeRuntimeDir.resolve("bin").toFile().mkdir(); 199 } 200 201 Path bulk = fakeRuntimeDir.resolve(Path.of("bin", "bulk")); 202 203 // Mak sure fake runtime takes some disk space. 204 // Package bundles with 0KB size are unexpected and considered 205 // an error by PackageTest. 206 Files.createDirectories(bulk.getParent()); 207 try (FileOutputStream out = new FileOutputStream(bulk.toFile())) { 208 byte[] bytes = new byte[4 * 1024]; 209 new SecureRandom().nextBytes(bytes); 210 out.write(bytes); 211 } 212 213 addArguments("--runtime-image", fakeRuntimeDir); 214 } catch (IOException ex) { 215 throw new RuntimeException(ex); 216 } 217 218 return this; 219 } 220 221 JPackageCommand addAction(Consumer<JPackageCommand> action) { 222 verifyMutable(); 223 actions.add(action); 224 return this; 225 } 226 227 public static JPackageCommand helloAppImage() { 228 JPackageCommand cmd = new JPackageCommand(); 229 cmd.setDefaultInputOutput().setDefaultAppName(); 230 PackageType.IMAGE.applyTo(cmd); 231 HelloApp.addTo(cmd); 232 return cmd; 233 } 234 235 public JPackageCommand setPackageType(PackageType type) { 236 verifyMutable(); 237 type.applyTo(this); 238 return this; 239 } 240 241 JPackageCommand setDefaultAppName() { 242 addArguments("--name", Test.enclosingMainMethodClass().getSimpleName()); 243 return this; 244 } 245 246 public Path outputBundle() { 247 final PackageType type = packageType(); 248 if (PackageType.IMAGE == type) { 249 return null; 250 } 251 252 String bundleName = null; 253 if (PackageType.LINUX.contains(type)) { 254 bundleName = LinuxHelper.getBundleName(this); 255 } else if (PackageType.WINDOWS.contains(type)) { 256 bundleName = WindowsHelper.getBundleName(this); 257 } else if (PackageType.MAC.contains(type)) { 258 bundleName = MacHelper.getBundleName(this); 259 } 260 261 return outputDir().resolve(bundleName); 262 } 263 264 /** 265 * Returns path to directory where application will be installed. 266 * 267 * E.g. on Linux for app named Foo default the function will return 268 * `/opt/foo` 269 */ 270 public Path appInstallationDirectory() { 271 final PackageType type = packageType(); 272 if (PackageType.IMAGE == type) { 273 return null; 274 } 275 276 if (PackageType.LINUX.contains(type)) { 277 if (isRuntime()) { 278 // Not fancy, but OK. 279 return Path.of(getArgumentValue("--install-dir", () -> "/opt"), 280 LinuxHelper.getPackageName(this)); 281 } 282 283 // Launcher is in "bin" subfolder of the installation directory. 284 return launcherInstallationPath().getParent().getParent(); 285 } 286 287 if (PackageType.WINDOWS.contains(type)) { 288 return WindowsHelper.getInstallationDirectory(this); 289 } 290 291 if (PackageType.MAC.contains(type)) { 292 return MacHelper.getInstallationDirectory(this); 293 } 294 295 throw new IllegalArgumentException("Unexpected package type"); 296 } 297 298 /** 299 * Returns path where application's Java runtime will be installed. 300 * If the command will package Java run-time only, still returns path to 301 * runtime subdirectory. 302 * 303 * E.g. on Linux for app named `Foo` the function will return 304 * `/opt/foo/runtime` 305 */ 306 public Path appRuntimeInstallationDirectory() { 307 if (PackageType.IMAGE == packageType()) { 308 return null; 309 } 310 return appInstallationDirectory().resolve(appRuntimePath(packageType())); 311 } 312 313 /** 314 * Returns path where application launcher will be installed. 315 * If the command will package Java run-time only, still returns path to 316 * application launcher. 317 * 318 * E.g. on Linux for app named Foo default the function will return 319 * `/opt/foo/bin/Foo` 320 */ 321 public Path launcherInstallationPath() { 322 final PackageType type = packageType(); 323 if (PackageType.IMAGE == type) { 324 return null; 325 } 326 327 if (PackageType.LINUX.contains(type)) { 328 return outputDir().resolve(LinuxHelper.getLauncherPath(this)); 329 } 330 331 if (PackageType.WINDOWS.contains(type)) { 332 return appInstallationDirectory().resolve(name() + ".exe"); 333 } 334 335 if (PackageType.MAC.contains(type)) { 336 return appInstallationDirectory().resolve(Path.of("Contents", "MacOS", name())); 337 } 338 339 throw new IllegalArgumentException("Unexpected package type"); 340 } 341 342 /** 343 * Returns path to application image directory. 344 * 345 * E.g. if --dest is set to `foo` and --name is set to `bar` the function 346 * will return `foo/bar` path. 347 * 348 * @throws IllegalArgumentException is command is doing platform packaging 349 */ 350 public Path appImage() { 351 final PackageType type = packageType(); 352 if (PackageType.IMAGE != type) { 353 throw new IllegalArgumentException("Unexpected package type"); 354 } 355 356 return outputDir().resolve(name()); 357 } 358 359 /** 360 * Returns path to application launcher relative to image directory. 361 * 362 * E.g. if --name is set to `Foo` the function will return `bin/Foo` path on 363 * Linux, and `Foo.exe` on Windows. 364 * 365 * @throws IllegalArgumentException is command is doing platform packaging 366 */ 367 public Path launcherPathInAppImage() { 368 final PackageType type = packageType(); 369 if (PackageType.IMAGE != type) { 370 throw new IllegalArgumentException("Unexpected package type"); 371 } 372 373 if (Test.isLinux()) { 374 return Path.of("bin", name()); 375 } 376 377 if (Test.isOSX()) { 378 return Path.of("Contents", "MacOS", name()); 379 } 380 381 if (Test.isWindows()) { 382 return Path.of(name() + ".exe"); 383 } 384 385 throw new IllegalArgumentException("Unexpected package type"); 386 } 387 388 /** 389 * Returns path to runtime directory relative to image directory. 390 * 391 * @throws IllegalArgumentException if command is configured for platform 392 * packaging 393 */ 394 public Path appRuntimeDirectoryInAppImage() { 395 final PackageType type = packageType(); 396 if (PackageType.IMAGE != type) { 397 throw new IllegalArgumentException("Unexpected package type"); 398 } 399 400 return appRuntimePath(type); 401 } 402 403 private static Path appRuntimePath(PackageType type) { 404 if (PackageType.LINUX.contains(type)) { 405 return Path.of("lib/runtime"); 406 } 407 return Path.of("runtime"); 408 } 409 410 public boolean isFakeRuntimeInAppImage(String msg) { 411 return isFakeRuntime(appImage().resolve( 412 appRuntimeDirectoryInAppImage()), msg); 413 } 414 415 public boolean isFakeRuntimeInstalled(String msg) { 416 return isFakeRuntime(appRuntimeInstallationDirectory(), msg); 417 } 418 419 private static boolean isFakeRuntime(Path runtimeDir, String msg) { 420 final List<Path> criticalRuntimeFiles; 421 if (Test.isWindows()) { 422 criticalRuntimeFiles = List.of(Path.of("bin\\server\\jvm.dll")); 423 } else if (Test.isLinux()) { 424 criticalRuntimeFiles = List.of(Path.of("lib/server/libjvm.so")); 425 } else if (Test.isOSX()) { 426 criticalRuntimeFiles = List.of(Path.of("lib/server/libjvm.dylib")); 427 } else { 428 throw new IllegalArgumentException("Unknwon platform"); 429 } 430 431 if (criticalRuntimeFiles.stream().filter( 432 v -> runtimeDir.resolve(v).toFile().exists()).findFirst().orElse( 433 null) == null) { 434 // Fake runtime 435 Test.trace(String.format( 436 "%s because application runtime directory [%s] is incomplete", 437 msg, runtimeDir)); 438 return true; 439 } 440 return false; 441 } 442 443 public Executor.Result execute() { 444 verifyMutable(); 445 if (actions != null) { 446 actions.stream().forEach(r -> r.accept(this)); 447 } 448 449 return new Executor() 450 .setExecutable(JavaTool.JPACKAGE) 451 .dumpOtput() 452 .addArguments(new JPackageCommand().addArguments( 453 args).adjustArgumentsBeforeExecution().args) 454 .execute(); 455 } 456 457 private JPackageCommand adjustArgumentsBeforeExecution() { 458 if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null) { 459 addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE); 460 } 461 462 if (!hasArgument("--verbose") && Test.VERBOSE_JPACKAGE) { 463 addArgument("--verbose"); 464 } 465 466 return this; 467 } 468 469 String getPrintableCommandLine() { 470 return new Executor() 471 .setExecutable(JavaTool.JPACKAGE) 472 .addArguments(args) 473 .getPrintableCommandLine(); 474 } 475 476 void verifyIsOfType(Collection<PackageType> types) { 477 verifyIsOfType(types.toArray(PackageType[]::new)); 478 } 479 480 void verifyIsOfType(PackageType ... types) { 481 if (!Arrays.asList(types).contains(packageType())) { 482 throw new IllegalArgumentException("Unexpected package type"); 483 } 484 } 485 486 @Override 487 protected boolean isMutable() { 488 return !immutable; 489 } 490 491 private final List<Consumer<JPackageCommand>> actions; 492 private boolean immutable; 493 494 private final static Map<String, PackageType> PACKAGE_TYPES 495 = new Supplier<Map<String, PackageType>>() { 496 @Override 497 public Map<String, PackageType> get() { 498 Map<String, PackageType> reply = new HashMap<>(); 499 for (PackageType type : PackageType.values()) { 500 reply.put(type.getName(), type); 501 } 502 return reply; 503 } 504 }.get(); 505 506 public final static Path DEFAULT_RUNTIME_IMAGE; 507 508 static { 509 // Set the property to the path of run-time image to speed up 510 // building app images and platform bundles by avoiding running jlink 511 // The value of the property will be automativcally appended to 512 // jpackage command line if the command line doesn't have 513 // `--runtime-image` parameter set. 514 String val = Test.getConfigProperty("runtime-image"); 515 if (val != null) { 516 DEFAULT_RUNTIME_IMAGE = Path.of(val); 517 } else { 518 DEFAULT_RUNTIME_IMAGE = null; 519 } 520 } 521 }