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 }