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("runtime");
 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      * Function will always return "runtime".
 392      *
 393      * @throws IllegalArgumentException if command is configured for platform
 394      * packaging
 395      */
 396     public Path appRuntimeDirectoryInAppImage() {
 397         final PackageType type = packageType();
 398         if (PackageType.IMAGE != type) {
 399             throw new IllegalArgumentException("Unexpected package type");
 400         }
 401 
 402         return Path.of("runtime");
 403     }
 404 
 405     public boolean isFakeRuntimeInAppImage(String msg) {
 406         return isFakeRuntime(appImage().resolve(
 407                 appRuntimeDirectoryInAppImage()), msg);
 408     }
 409 
 410     public boolean isFakeRuntimeInstalled(String msg) {
 411         return isFakeRuntime(appRuntimeInstallationDirectory(), msg);
 412     }
 413 
 414     private static boolean isFakeRuntime(Path runtimeDir, String msg) {
 415         final List<Path> criticalRuntimeFiles;
 416         if (Test.isWindows()) {
 417             criticalRuntimeFiles = List.of(Path.of("server\\jvm.dll"));
 418         } else if (Test.isLinux()) {
 419             criticalRuntimeFiles = List.of(Path.of("server/libjvm.so"));
 420         } else if (Test.isOSX()) {
 421             criticalRuntimeFiles = List.of(Path.of("server/libjvm.dylib"));
 422         } else {
 423             throw new IllegalArgumentException("Unknwon platform");
 424         }
 425 
 426         if (criticalRuntimeFiles.stream().filter(v -> v.toFile().exists())
 427                 .findFirst().orElse(null) == null) {
 428             // Fake runtime
 429             Test.trace(String.format(
 430                     "%s because application runtime directory [%s] is incomplete",
 431                     msg, runtimeDir));
 432             return true;
 433         }
 434         return false;
 435     }
 436 
 437     public Executor.Result execute() {
 438         verifyMutable();
 439         if (actions != null) {
 440             actions.stream().forEach(r -> r.accept(this));
 441         }
 442 
 443         return new Executor()
 444                 .setExecutable(JavaTool.JPACKAGE)
 445                 .dumpOtput()
 446                 .addArguments(new JPackageCommand().addArguments(
 447                                 args).adjustArgumentsBeforeExecution().args)
 448                 .execute();
 449     }
 450 
 451     private JPackageCommand adjustArgumentsBeforeExecution() {
 452         if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null) {
 453             addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE);
 454         }
 455 
 456         if (!hasArgument("--verbose") && Test.VERBOSE_JPACKAGE) {
 457             addArgument("--verbose");
 458         }
 459 
 460         return this;
 461     }
 462 
 463     String getPrintableCommandLine() {
 464         return new Executor()
 465                 .setExecutable(JavaTool.JPACKAGE)
 466                 .addArguments(args)
 467                 .getPrintableCommandLine();
 468     }
 469 
 470     void verifyIsOfType(Collection<PackageType> types) {
 471         verifyIsOfType(types.toArray(PackageType[]::new));
 472     }
 473 
 474     void verifyIsOfType(PackageType ... types) {
 475         if (!Arrays.asList(types).contains(packageType())) {
 476             throw new IllegalArgumentException("Unexpected package type");
 477         }
 478     }
 479 
 480     @Override
 481     protected boolean isMutable() {
 482         return !immutable;
 483     }
 484 
 485     private final List<Consumer<JPackageCommand>> actions;
 486     private boolean immutable;
 487 
 488     private final static Map<String, PackageType> PACKAGE_TYPES
 489             = new Supplier<Map<String, PackageType>>() {
 490                 @Override
 491                 public Map<String, PackageType> get() {
 492                     Map<String, PackageType> reply = new HashMap<>();
 493                     for (PackageType type : PackageType.values()) {
 494                         reply.put(type.getName(), type);
 495                     }
 496                     return reply;
 497                 }
 498             }.get();
 499 
 500     public final static Path DEFAULT_RUNTIME_IMAGE;
 501 
 502     static {
 503         // Set the property to the path of run-time image to speed up
 504         // building app images and platform bundles by avoiding running jlink
 505         // The value of the property will be automativcally appended to
 506         // jpackage command line if the command line doesn't have
 507         // `--runtime-image` parameter set.
 508         String val = Test.getConfigProperty("runtime-image");
 509         if (val != null) {
 510             DEFAULT_RUNTIME_IMAGE = Path.of(val);
 511         } else {
 512             DEFAULT_RUNTIME_IMAGE = null;
 513         }
 514     }
 515 }