1 /*
   2  * Copyright (c) 2009, 2011, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package org.openjdk.jigsaw.cli;
  27 
  28 import java.lang.module.*;
  29 import java.io.*;

  30 import java.util.*;
  31 
  32 import static java.lang.System.out;
  33 import static java.lang.System.err;
  34 
  35 import org.openjdk.jigsaw.*;
  36 import org.openjdk.jigsaw.SimpleLibrary.StorageOption;
  37 import org.openjdk.internal.joptsimple.*;
  38 
  39 /* Interface:
  40 
  41 jpkg [-v] [-L <library>] [-r <resource-dir>] [-i include-dir] \
  42      [-m <module_dir>] [-d <output_dir>] [-c <command>] [-n <name>] \
  43      [-e <e-mail@address>] [-s <short description>] [-l <long description>] \
  44      [-x <extra metadata>] [deb|jmod] <module_name>*
  45 
  46   -v           : verbose output
  47   -L           : library the modules are installed to
  48   -i           : directory with files to include as part of the package
  49   -m           : directory with modules to package
  50   -d           : destination directory to put the package in
  51   -c           : command name for launcher invoking main class
  52   -n           : maintainer name
  53   -e           : maintainer e-mail address
  54   -s           : short description
  55   -l           : long description
  56   -x           : additional metadata - for Debian packages, a file whose
  57                  contents gets appended to DEBIAN/control
  58   --fast       : use fastest, rather then best compression
  59 */
  60 
  61 public class Packager {
  62 
  63     private static final JigsawModuleSystem jms
  64         = JigsawModuleSystem.instance();
  65 
  66     private static final boolean jigsawDevMode
  67         = System.getenv("JIGSAW_DEV_MODE") != null;
  68 
  69     /** Temp dir for modules to be pre-installed into */
  70     private static File tmp_dst;
  71 
  72     /** Directory where the classes to package are stored on disk. */
  73     private File classes = new File(System.getProperty("user.dir"));
  74 
  75     /** Default destination dir is current directory */
  76     private File destination = new File(System.getProperty("user.dir"));
  77 
  78     /** The default location to install modules to */
  79     private File library = new File("/usr/lib/java/modules");
  80 
  81     /** The JRE to use in control scripts */
  82     private File javaHome = new File(System.getProperty("java.home"));
  83 
  84     private boolean verbose;
  85 
  86     private boolean fast;
  87 
  88     /** Command launcher for main class */
  89     private String bincmd;
  90 
  91     /** Directory with optional files to include in package */
  92     private File includes;
  93 
  94     /** Name of the maintainer or creator of the package */
  95     private String maintainer_name;
  96 
  97     /** Default name of the maintainer or creator of the package */
  98     private String default_maintainer_name = System.getProperty("user.name");
  99 
 100     /** E-mail address of the maintainer or creator of the package */
 101     private String maintainer_email;
 102 
 103     /** Default E-mail address of the maintainer or creator of the package */
 104     private String default_maintainer_email = "<generated@by.jpkg>";
 105 
 106     /** Short description of the package */
 107     private String short_description;
 108 
 109     /** Default short description of the package */
 110     private static String default_short_description = "Generated by jpkg";
 111 
 112     /** Long description of the package */
 113     private String long_description;
 114 
 115     /** Default long description of the package */
 116     private static String default_long_description
 117         = " This package was automatically generated from the corresponding Jigsaw module.\n"
 118         + " Information on Jigsaw is available at http://openjdk.java.net/projects/jigsaw.";
 119 
 120     /** Packaging-system dependent additional metadata */
 121     private File extra_metadata;
 122 
 123     private File natlibs;
 124     private File natcmds;
 125     private File config_dir;
 126 
 127     // Installed size
 128     private Integer installedSize = null;
 129 
 130     // Platform boot module
 131     private static final String BOOT_MODULE = "jdk.base";
 132 
 133     private static void createTempWorkDir()
 134         throws Command.Exception
 135     {
 136         try {
 137             tmp_dst = File.createTempFile("jigsaw", null);
 138             Files.delete(tmp_dst);
 139             Files.mkdirs(tmp_dst, "jigsaw temp directory");
 140         } catch (IOException x) {
 141             throw new Command.Exception(x);
 142         }
 143     }
 144 
 145     class Contents extends Command<SimpleLibrary> {
 146         protected void go(SimpleLibrary lib)
 147             throws Command.Exception
 148         {
 149             String jmodName = takeArg();
 150             finishArgs();
 151             try (FileInputStream fis = new FileInputStream(jmodName);
 152                   DataInputStream dis = new DataInputStream(fis);
 153                   ModuleFile.Reader reader = new ModuleFile.Reader(dis)) {
 154                 List<String> contents = reader.getContents();
 155                 for (String fn : contents)
 156                     out.format("  %s%n", fn);
 157             } catch (IOException x) {
 158                 throw new Command.Exception(x);
 159             }
 160         }
 161     }
 162 
 163     class Show extends Command<SimpleLibrary> {
 164         protected void go(SimpleLibrary lib)
 165             throws Command.Exception
 166         {
 167             String jmodName = takeArg();
 168             finishArgs();
 169             try (FileInputStream fis = new FileInputStream(jmodName);
 170                   DataInputStream dis = new DataInputStream(fis);
 171                   ModuleFile.Reader reader = new ModuleFile.Reader(dis)) {
 172                 ModuleInfo mi = jms.parseModuleInfo(reader.readStart());
 173                 Commands.formatModule(out, mi, verbose);
 174             } catch (IOException x) {
 175                 throw new Command.Exception(x);
 176             }
 177         }
 178     }
 179 
 180     class Jmod extends Command<SimpleLibrary> {
 181         private String getModuleVersion(String modulename)
 182             throws Command.Exception
 183         {
 184             Manifest mf = Manifest.create(modulename, classes);
 185             ModuleInfo info = getModuleInfo(mf);
 186             return info.id().version().toString();
 187         }
 188 
 189         protected void go(SimpleLibrary lib)
 190             throws Command.Exception
 191         {
 192             while (hasArg()) {
 193                 File outputfile = null;
 194                 try {
 195                     String modulename = takeArg();
 196                     String version = getModuleVersion(modulename);
 197                     String outputfilename = modulename + '@'+ version + ".jmod";
 198                     if (verbose)
 199                         System.out.println("Creating module file "
 200                                            + outputfilename + " for "
 201                                            + modulename);
 202                     outputfile = new File(destination, outputfilename);
 203                     ModuleFileWriter writer
 204                         = new ModuleFileWriter(outputfile, (fast || jigsawDevMode));
 205                     writer.writeModule(classes, natlibs, natcmds, config_dir);
 206                 }
 207                 catch (IOException x) {
 208                     if (outputfile != null && !outputfile.delete()) {
 209                         Throwable t
 210                             = new IOException(outputfile +
 211                                               ": Cannot delete").initCause(x);
 212                         throw new Command.Exception((IOException)t);
 213                     }
 214                     throw new Command.Exception(x);
 215                 }
 216             }
 217             finishArgs();
 218         }
 219     }
 220 
 221     static private ModuleInfo getModuleInfo(Manifest mf)
 222         throws Command.Exception
 223     {
 224         try {
 225             final String MINFO_CLASS = "module-info.class";
 226             File classdir = mf.classes().get(0);
 227             File mif = new File(classdir , MINFO_CLASS);
 228             if (!mif.exists())
 229                 mif = new File(classdir,
 230                                mf.module() + File.separator + MINFO_CLASS);
 231             byte[] bs = Files.load(mif);
 232             return jms.parseModuleInfo(bs);
 233         } catch (IOException x) {
 234             throw new Command.Exception(x);
 235         }
 236     }
 237 
 238     class Deb extends Command<SimpleLibrary> {
 239         private File tmp_module_dst;
 240         private File tmp_metadata_dst;
 241 
 242         private void createMetaDataDir()
 243             throws Command.Exception
 244         {
 245             tmp_metadata_dst = new File(tmp_dst, "DEBIAN");
 246             if (!tmp_metadata_dst.mkdirs())
 247                 throw new Command.Exception("Couldn't create meta data directory "
 248                                             + tmp_metadata_dst);
 249         }
 250 
 251         private void createModuleLibraryWorkDir()
 252             throws Command.Exception
 253         {
 254             tmp_module_dst = new File(tmp_dst, library.toString());
 255 
 256             if (!tmp_module_dst.mkdirs())
 257                 throw new Command.Exception("Couldn't create module destination directory "
 258                                             + tmp_module_dst);
 259 
 260             // Delete the modules dir to make SimpleLibrary happy,
 261             // it wants to create the jigsaw metadata and the directory
 262             // along with it.
 263             if (!tmp_module_dst.delete())
 264                 throw new Command.Exception("Can't delete " + tmp_module_dst);
 265         }
 266 
 267         private void preinstallModule(Manifest manifest)
 268             throws Command.Exception
 269         {
 270             try {
 271 
 272                 createModuleLibraryWorkDir();
 273                 Set<StorageOption> opts = Collections.emptySet();
 274                 if (BOOT_MODULE.equals(manifest.module())) {
 275                     // Create a module library to the boot module package
 276                     SimpleLibrary.create(tmp_module_dst, opts).installFromManifests(Collections.singleton(manifest));
 277                 } else {
 278                     // We need to create a throwaway SimpleLibrary to work with it,
 279                     // As there is no static preInstall method
 280                     File scratchlib_dst = new File(tmp_dst, "scratchlib");
 281                     SimpleLibrary.create(scratchlib_dst, opts).preInstall(manifest, tmp_module_dst);
 282                     Files.deleteTree(scratchlib_dst);
 283                 }
 284             } catch (IOException | ConfigurationException x) {
 285                 throw new Command.Exception(x);
 286             }
 287         }
 288 
 289         private String translateVersion(String v) {
 290             // Debian version format: [epoch:]upstream_version[-debian_revision]
 291             // upstream_version may contain only alphanumerics and '.', '+', '-', '~'
 292             // There is no epoch, ':' not allowed.
 293             //
 294             if (!v.matches("[A-Za-z0-9\\+-~\\.]+"))
 295                 throw new AssertionError("Invalid debian version format: " + v);
 296             return v;
 297         }
 298 
 299         private String computeDependencies(ModuleInfo info)
 300         {
 301             StringBuilder deps = new StringBuilder();
 302 
 303             for (ViewDependence d : info.requiresModules()) {
 304                 if (d.modifiers().contains(ViewDependence.Modifier.OPTIONAL))
 305                     continue; // skip optional dependency
 306 
 307                 deps.append(", ")
 308                     .append(d.query().name())
 309                     .append(' ')
 310                     .append(d.query().versionQuery() != null ?
 311                             "(" + translateVersion(d.query().versionQuery().toString()) + ")" :
 312                             "");
 313             }
 314 
 315             return deps.length() > 0 ?
 316                 deps.substring(2) :
 317                 "";
 318         }
 319 
 320         private String computeProvides(ModuleInfo info)
 321         {
 322             StringBuilder deps = new StringBuilder();
 323 
 324             for (ModuleId id : info.defaultView().aliases())
 325                 deps.append(", ")
 326                     .append(id.name());
 327 
 328             return deps.length() > 0 ?
 329                 deps.substring(2) :
 330                 "";
 331         }
 332 
 333         /** Long descriptions in Debian control files must start the line with a space. */
 334         private String formatDescription(String description)
 335         {
 336             return " " + description.replace("\n", "\n ");
 337         }
 338 
 339         /**
 340          * The ModuleInfo metadata gets translated into the package in the following way:
 341          *
 342          * package name is module's name
 343          * package version is module's version
 344          * package dependecies are module's required dependencies
 345          *
 346          * @param manifest The module's manifest
 347          */
 348         private void writeMetaData(Manifest manifest)
 349             throws Command.Exception
 350         {
 351             boolean bootmodule = BOOT_MODULE.equals(manifest.module());
 352             createMetaDataDir();
 353             ModuleInfo info = getModuleInfo(manifest);
 354 
 355             // Create the control file, and fill in dependency and provides info
 356             try (PrintStream control
 357                      = new PrintStream(new File(tmp_metadata_dst, "control"))) {
 358                 control.format("Package: %s%n"
 359                                + "Version: %s%n"
 360                                + "Section: misc%n"
 361                                + "Priority: optional%n"
 362                                + "Architecture: "
 363                                + System.getProperty("os.arch") + "%n",
 364                                info.id().name(),
 365                                translateVersion(info.id().version().toString()));
 366 
 367                 // If either maintainer name or e-mail is declared as parameter, use it
 368                 if (null != maintainer_name || null != maintainer_email) {
 369                     control.format("Maintainer: %s %s%n",
 370                                    maintainer_name == null? default_maintainer_name : maintainer_name,
 371                                    maintainer_email == null? default_maintainer_email : maintainer_email);
 372                 }
 373                 // Otherwise, if there is no extra metadata, use defaults
 374                 else if (null == extra_metadata) {
 375                     control.format("Maintainer: %s %s%n",
 376                                    default_maintainer_name,
 377                                    default_maintainer_email);
 378                 }
 379 
 380                 // If either short or long description is declared as parameter, use it
 381                 if (null != short_description || null != long_description) {
 382                     control.format("Description: %s%n"
 383                                    + "%s%n",
 384                                    short_description == null? default_short_description : short_description,
 385                                    long_description == null? default_long_description : formatDescription(long_description));
 386                 }
 387                 // Otherwise, if there is no extra metadata, use defaults
 388                 else if (null == extra_metadata) {
 389                     control.format("Description: %s%n"
 390                                    + "%s%n",
 391                                    default_short_description,
 392                                    default_long_description);
 393                 }
 394 
 395                 if (!bootmodule && !info.requiresModules().isEmpty())
 396                     control.format("Depends: %s\n", computeDependencies(info));
 397                 if (!info.defaultView().aliases().isEmpty())
 398                     control.format("Provides: %s\n", computeProvides(info));
 399                 if (null != extra_metadata)
 400                     control.format("%s\n", new String(Files.load(extra_metadata)));
 401                 if (installedSize != null)
 402                     control.format("Installed-Size: %d\n", installedSize);
 403 
 404                 // All jpkg generated packages (except boot) depend on a boot
 405                 // package being installed at time of installation to be able to run jmod.
 406                 if (!bootmodule)
 407                     control.format("Pre-Depends: %s\n", BOOT_MODULE);
 408 
 409 
 410                 // Generate the launcher script, if a main class exists
 411                 if (!bootmodule && info.defaultView().mainClass() != null) {
 412                     // If no command name is given, use module name
 413                     if (null == bincmd)
 414                         bincmd = info.id().name();
 415 
 416                     String BINDIR = "/usr/bin";
 417                     File bin = new File(tmp_dst + BINDIR);
 418                     if (!bin.mkdirs())
 419                         throw new IOException("Couldn't create " + tmp_dst + BINDIR);
 420 
 421                     File cmd = new File(bin, bincmd);
 422                     try (PrintStream launcher = new PrintStream(cmd)) {
 423                         String java_launcher = System.getProperty("java.home")
 424                                                + "/bin/java";
 425                         if (! (new File(java_launcher)).exists())
 426                             throw new IOException("Couldn't find java launcher "
 427                                                   + "at " + java_launcher);
 428 
 429                         launcher.format("#!/bin/sh\n" +
 430                                         "set -e\n" +
 431                                         "exec %s -ea -L %s -m %s \"$@\"\n",
 432                                         java_launcher, library,
 433                                         info.id().name());
 434                     }
 435                     cmd.setExecutable(true, false);
 436                 }
 437 
 438                 String installedJMod = javaHome.getPath() + "/bin/jmod";
 439 
 440                 // Before a package is installed,
 441                 //   check if the jigsaw module library needs to be created first
 442                 if (!bootmodule) {
 443                     File preinst = new File(tmp_metadata_dst, "preinst");
 444                     try (PrintStream pis = new PrintStream(preinst)) {
 445                         pis.format("#!/bin/sh\n" +
 446                                "set -e\n" +
 447                                "if [ ! -f %1$s/%%jigsaw-library ]\n" +
 448                                "then\n" +
 449                                "  %2$s  -L %1$s create\n" +
 450                                "fi\n",
 451                                library, installedJMod);
 452                     }
 453                     preinst.setExecutable(true, false);
 454 
 455                     // After a package is installed,
 456                     //  reconfigure the jigsaw module library for the module
 457                     File postinst = new File(tmp_metadata_dst, "postinst");
 458                     try (PrintStream pis = new PrintStream(postinst)) {
 459                         pis.format("#!/bin/sh\n" +
 460                                "set -e\n" +
 461                                "if [ -f %s/%s.pack ] ; then\n" +
 462                                " for i in %s/*.pack ; do\n" +
 463                                "   %s/bin/unpack200 -r $i %s/tmp.jar\n" +
 464                                "   unzip -o -q -d / %s/tmp.jar\n" +
 465                                "   rm %s/tmp.jar\n" +
 466                                " done\n" +
 467                                "fi\n" +
 468                                "%s -L %s config %s\n",
 469                                library, manifest.module(),
 470                                library,
 471                                javaHome, library,
 472                                library,
 473                                library,
 474                                installedJMod, library, info.id());
 475                     }
 476                     postinst.setExecutable(true, false);
 477                 }
 478 
 479                 // Before a package is removed,
 480                 //  remove the generated jigsaw module configuration
 481                 File prerm = new File(tmp_metadata_dst, "prerm");
 482                 try (PrintStream pis = new PrintStream(prerm)) {
 483                     pis.format("#!/bin/sh\n" +
 484                            "set -e\n" +
 485                            // Delete unpacked class files.
 486                            "if [ -e %1$s/%2$s/%3$s ]\n" +
 487                            "then\n" +
 488                            "  rm -rf %1$s/%2$s/%3$s\n" +
 489                            "fi\n" +
 490                            // Delete module library directory if it's empty.
 491                            "find %1$s/%2$s/ -maxdepth 0 -type d -empty -delete\n",
 492                            library, info.id().name(), info.id().version());
 493                 }
 494                 prerm.setExecutable(true, false);
 495             } catch (IOException x) {
 496                 throw new Command.Exception(x);
 497             }
 498         }
 499 
 500         private void buildPackage()
 501             throws Command.Exception
 502         {
 503             String dashz = "-z" + ((fast || jigsawDevMode) ? 1 : 9);
 504             try {
 505                 Process build
 506                     = (new ProcessBuilder("fakeroot", "dpkg-deb", dashz, "-Zlzma", "--build",
 507                                           tmp_dst.toString(), destination.toString())).start();
 508 
 509                 try (BufferedReader br = new BufferedReader(
 510                          new InputStreamReader(build.getErrorStream()))) {
 511 
 512                     if (0 != build.waitFor())
 513                         throw new Command.Exception("Failed to create package "
 514                                                     + br.readLine());
 515                 }
 516             } catch (IOException | InterruptedException x) {
 517                 throw new Command.Exception(x);
 518             }
 519         }
 520 
 521         private void cleanup()
 522             throws Command.Exception
 523         {
 524             try {
 525                 Files.deleteTree(tmp_dst);
 526             } catch (IOException x) {
 527                 throw new Command.Exception(x);
 528             }
 529         }
 530 
 531         private File getPackFile(Manifest manifest)
 532         {
 533             return new File(tmp_module_dst, manifest.module() + ".pack");
 534         }
 535 
 536         private void packModule(Manifest manifest)
 537             throws Command.Exception
 538         {
 539             // Can't pack the boot module since unpack200 tool may not exist
 540             if (BOOT_MODULE.equals(manifest.module()))
 541                 return;
 542 
 543             try {
 544                 File tmp_jar = new File(tmp_dst, "module.jar");
 545 
 546                 // Create temporary jar file with module classes and ressources
 547                 // Store entries, as we'll just need it for pack200 to read from
 548                 // and then delete it. No point in wasting time.
 549                 Process jar
 550                     = (new ProcessBuilder("jar", "c0f",  tmp_jar.toString(),
 551                                           "-C", tmp_dst.toString(),
 552                                           library.toString())).start();
 553                 try (BufferedReader br = new BufferedReader(
 554                         new InputStreamReader(jar.getErrorStream()))) {
 555                     if (0 != jar.waitFor())
 556                         throw new Command.Exception("Failed to jar module "
 557                                                     + br.readLine());
 558                 }
 559 
 560                 // Remove redundant META-INF directory from jar file,
 561                 // so that it doesn't pollute the filesystem hierarchy
 562                 // when we unpack the files again.
 563                 Process zip
 564                     = (new ProcessBuilder("zip",  tmp_jar.toString(),
 565                                           "-d", "META-INF/*")).start();
 566                 try (BufferedReader br = new BufferedReader(
 567                         new InputStreamReader(zip.getErrorStream()))) {
 568                     if (0 != zip.waitFor())
 569                         throw new Command.Exception("Failed to remove META-INF "
 570                                                     + "directory from jar "
 571                                                     + "module "
 572                                                     + br.readLine());
 573                 }
 574                 // Compress the jar file with pack200.
 575                 String dashE = "-E" + ((fast || jigsawDevMode) ? "0" : "9");
 576                 Process pack200
 577                     = (new ProcessBuilder("pack200", dashE, "-S-1", "--no-gzip",
 578                                           getPackFile(manifest).toString(),
 579                                           tmp_jar.toString())).start();
 580                 try (BufferedReader br = new BufferedReader(
 581                         new InputStreamReader(pack200.getErrorStream()))) {
 582                     if (0 != pack200.waitFor())
 583                         throw new Command.Exception("Failed to pack200 module "
 584                                                     + br.readLine());
 585                 }
 586 
 587                 if (! tmp_jar.delete())
 588                     throw new Command.Exception("Failed to delete temporary file " + tmp_jar);
 589                 Files.deleteTree(new File(tmp_module_dst, manifest.module().toString()));
 590             } catch (IOException | InterruptedException x) {
 591                 throw new Command.Exception(x);
 592             }
 593         }
 594 
 595         protected void go(SimpleLibrary lib)
 596             throws Command.Exception
 597         {
 598             List<Manifest> mfs = new ArrayList<>();
 599             while (hasArg()) {
 600                 mfs.add(Manifest.create(takeArg(), classes));
 601             }
 602             finishArgs();
 603 
 604             for (Manifest manifest : mfs) {
 605 
 606                 if (verbose)
 607                     System.out.println("Creating binary Debian package for " + manifest.module());
 608 
 609                 createTempWorkDir();
 610                 if (null != includes) {
 611                     try {
 612                         Files.copyTree(includes, tmp_dst);
 613                     } catch (IOException x) {
 614                         throw new Command.Exception(x);
 615                     }
 616                 }
 617                 preinstallModule(manifest);
 618                 packModule(manifest);
 619                 writeMetaData(manifest);
 620                 buildPackage();
 621                 cleanup();
 622             }
 623         }
 624     }
 625 
 626     private static final Map<String,Class<? extends Command<SimpleLibrary>>> commands
 627         = new HashMap<>();
 628 
 629     static {
 630         commands.put("contents", Contents.class);
 631         commands.put("deb", Deb.class);
 632         commands.put("jmod", Jmod.class);
 633         commands.put("show", Show.class);
 634     }
 635 
 636     private OptionParser parser;
 637 
 638     private static OptionSpec<File> resourcePath; // ##
 639 
 640     private void usage() {
 641         out.format("%n");
 642         out.format("usage: jpkg contents <module-file>%n");
 643         out.format("       jpkg show [-v] <module-file>%n");
 644         out.format("       jpkg [-v] [-L <library>] [-r <resource-dir>] ");
 645         out.format(            "[-i <include-dir>] [-m <module-dir>] ");
 646         out.format(            "[-d <output-dir>] [-c <command>] [-n <name>] ");
 647         out.format(            "[-e <e-mail@address>] [-s <short description>] ");
 648         out.format(            "[-l <long description>] [-x <extra metadata>] ");
 649         out.format(            "[deb|jmod] <module-name>%n");
 650         out.format("%n");
 651         try {
 652             parser.printHelpOn(out);
 653         } catch (IOException x) {
 654             throw new AssertionError(x);
 655         }
 656         out.format("%n");
 657     }
 658 
 659     public static void run(String[] args)
 660         throws OptionException, Command.Exception {
 661 
 662         new Packager().exec(args);
 663     }
 664 
 665     private void exec(String[] args)
 666         throws OptionException, Command.Exception {
 667 
 668         parser = new OptionParser();
 669 
 670         OptionSpec<File> libPath
 671             = (parser.acceptsAll(Arrays.asList("L", "library"),
 672                                  "Module-library location"
 673                                  + " (default $JAVA_MODULES)")
 674                .withRequiredArg()
 675                .describedAs("path")
 676                .ofType(File.class));
 677 
 678         OptionSpec<File> launcherPath
 679             = (parser.acceptsAll(Arrays.asList("c", "command"),
 680                                  "Launcher command name")
 681                .withRequiredArg()
 682                .describedAs("path")
 683                .ofType(File.class));
 684 
 685         parser.acceptsAll(Arrays.asList("v", "verbose"),
 686                           "Enable verbose output");
 687         parser.acceptsAll(Arrays.asList("h", "?", "help"),
 688                           "Show this help message");
 689         OptionSpec<File> destinationPath
 690             = (parser.acceptsAll(Arrays.asList("d", "dest-dir"),
 691                           "Destination directory for packages")
 692                .withRequiredArg()
 693                .describedAs("path")
 694                .ofType(File.class));
 695 
 696         OptionSpec<File> modulePath
 697             = (parser.acceptsAll(Arrays.asList("m", "module-dir"),
 698                           "Source directory for modules")
 699                .withRequiredArg()
 700                .describedAs("path")
 701                .ofType(File.class));
 702 
 703         OptionSpec<File> includePath
 704             = (parser.acceptsAll(Arrays.asList("i", "include"),
 705                                  "Directory of files to be included")
 706                .withRequiredArg()
 707                .describedAs("path")
 708                .ofType(File.class));
 709 
 710         OptionSpec<String> maintainerName
 711             = (parser.acceptsAll(Arrays.asList("n", "name"),
 712                                  "Package maintainer's name")
 713                .withRequiredArg()
 714                .describedAs("name")
 715                .ofType(String.class));
 716 
 717         OptionSpec<String> maintainerEmail
 718             = (parser.acceptsAll(Arrays.asList("e", "email"),
 719                                  "Package maintainer's e-mail address")
 720                .withRequiredArg()
 721                .describedAs("e-mail@address")
 722                .ofType(String.class));
 723 
 724         OptionSpec<String> shortDescription
 725             = (parser.acceptsAll(Arrays.asList("s", "short"),
 726                                  "Short description of the package")
 727                .withRequiredArg()
 728                .describedAs("description")
 729                .ofType(String.class));
 730 
 731         OptionSpec<String> longDescription
 732             = (parser.acceptsAll(Arrays.asList("l", "long"),
 733                                  "Long description of the package")
 734                .withRequiredArg()
 735                .describedAs("description")
 736                .ofType(String.class));
 737 
 738         parser.acceptsAll(Arrays.asList("fast"),
 739                           "Use fastest compression");
 740 
 741         OptionSpec<Integer> isize
 742             = (parser.acceptsAll(Arrays.asList("installed-size"),
 743                                  "Installed size in kilobytes")
 744                .withRequiredArg()
 745                .describedAs("size")
 746                .ofType(Integer.class));
 747 
 748         OptionSpec<File> javaHomePath
 749             = (parser.acceptsAll(Arrays.asList("java-home"),
 750                                  "Alternate $JAVA_HOME location")
 751                .withRequiredArg()
 752                .describedAs("dir")
 753                .ofType(File.class));
 754 
 755         OptionSpec<File> extraMetadata
 756             = (parser.acceptsAll(Arrays.asList("x", "extra"),
 757                                  "File or directory with additional metadata,"
 758                                  + " depending on the packaging system")
 759                .withRequiredArg()
 760                .describedAs("dir")
 761                .ofType(File.class));
 762 
 763         OptionSpec<File> nativeLibs
 764             = (parser.accepts("natlib", "Directory with native libs")
 765                .withRequiredArg()
 766                .describedAs("dir")
 767                .ofType(File.class));
 768 
 769         OptionSpec<File> nativeCmds
 770             = (parser.accepts("natcmd", "Directory with native launchers")
 771                .withRequiredArg()
 772                .describedAs("dir")
 773                .ofType(File.class));
 774 
 775         OptionSpec<File> config
 776             = (parser.accepts("config", "Directory with configuration")
 777                .withRequiredArg()
 778                .describedAs("dir")
 779                .ofType(File.class));
 780 
 781         if (args.length == 0) {
 782             usage();
 783             return;
 784         }
 785 
 786         OptionSet opts = parser.parse(args);
 787         if (opts.has("h")) {
 788             usage();
 789             return;
 790         }
 791         if (opts.has("v"))
 792             verbose = true;
 793         if (opts.has("fast"))
 794             fast = true;
 795         List<String> words = opts.nonOptionArguments();
 796         if (words.isEmpty()) {
 797             usage();
 798             return;
 799         }
 800         String verb = words.get(0);
 801         Class<? extends Command<SimpleLibrary>> cmd = commands.get(verb);
 802         if (cmd == null)
 803             throw new Command.Exception("%s: unknown command", verb);
 804         if (opts.has(launcherPath))
 805             bincmd = opts.valueOf(launcherPath).toString();
 806         if (opts.has(destinationPath))
 807             destination = opts.valueOf(destinationPath);
 808         if (opts.has(modulePath)) {
 809             classes = opts.valueOf(modulePath);
 810             checkPathArgument(classes, "Module");
 811         }
 812         if (opts.has(libPath)) {
 813             library = opts.valueOf(libPath);
 814         } else {
 815             String jm = System.getenv("JAVA_MODULES");
 816             if (jm != null)
 817                 library = new File(jm);
 818         }
 819         if (opts.has(includePath))
 820             includes = opts.valueOf(includePath);
 821         if (opts.has(javaHomePath))
 822             javaHome = opts.valueOf(javaHomePath);
 823         if (opts.has(maintainerName))
 824             maintainer_name = opts.valueOf(maintainerName);
 825         if (opts.has(maintainerEmail)) {
 826             maintainer_email = opts.valueOf(maintainerEmail);
 827             // Add missing e-mail quotes if necessary
 828             maintainer_email
 829                 = (maintainer_email.startsWith("<") ? "" : "<")
 830                 + maintainer_email
 831                 + (maintainer_email.endsWith(">") ? "" : ">") ;
 832         }
 833         if (opts.has(shortDescription))
 834             short_description = opts.valueOf(shortDescription);
 835         if (opts.has(longDescription))
 836             long_description = opts.valueOf(longDescription);
 837         if (opts.has(extraMetadata))
 838             extra_metadata = opts.valueOf(extraMetadata);
 839         if (opts.has(nativeLibs)) {
 840             natlibs = opts.valueOf(nativeLibs);
 841             checkPathArgument(natlibs, "Native library");
 842         }
 843         if (opts.has(nativeCmds)) {
 844             natcmds = opts.valueOf(nativeCmds);
 845             checkPathArgument(natcmds, "Native command");
 846         }
 847         if (opts.has(config)) {
 848             config_dir = opts.valueOf(config);
 849             checkPathArgument(config_dir, "Config");
 850         }
 851         if (opts.has(isize))
 852             installedSize = opts.valueOf(isize);
 853 
 854         if (cmd == Deb.class)
 855             (new Deb()).run(null, opts);
 856         else if (cmd == Jmod.class)
 857             (new Jmod()).run(null, opts);
 858         else if (cmd == Contents.class)
 859             (new Contents()).run(null, opts);
 860         else if (cmd == Show.class)
 861             (new Show()).run(null, opts);
 862     }
 863 
 864     /**
 865      * Helper method to check if a path exists before using it further.
 866      *
 867      * @param path to check
 868      * @param type of path being checked
 869      *
 870      * @throws Command.Exception if path doesn't exist
 871      */
 872     private static void checkIfPathExists(File path, String type)
 873         throws Command.Exception {
 874 
 875         if (!path.exists())
 876             throw new Command.Exception("%s path doesn't exist: %s",
 877                                         type, path);
 878     }
 879 
 880     /**
 881      * Helper method to check if a path is readable before using it further.
 882      *
 883      * @param path to check
 884      * @param type of path being checked
 885      *
 886      * @throws Command.Exception if path isn't readable
 887      */
 888     private static void checkIfPathIsReadable(File path, String type)
 889         throws Command.Exception {
 890 
 891         if (!path.canRead())
 892             throw new Command.Exception("%s path isn't readable: %s",
 893                                         type, path);
 894     }
 895 
 896     /**
 897      * Helper method to check if a path is a directory before using it further.
 898      *
 899      * @param path to check
 900      * @param type of path being checked
 901      *
 902      * @throws Command.Exception if path is not a directory
 903      */
 904     private static void checkIfPathIsDirectory(File path, String type)
 905         throws Command.Exception {
 906 
 907         if (!path.isDirectory())
 908             throw new Command.Exception("%s path is not a directory: %s",
 909                                         type, path);
 910     }
 911 
 912     /**
 913      * Helper method to check if a path argument is valid.
 914      *
 915      * @param path to check
 916      * @param type of path being checked
 917      *
 918      * @throws Command.Exception if path is not a directory
 919      */
 920     private static void checkPathArgument(File path, String type)
 921         throws Command.Exception {
 922 
 923         checkIfPathExists(path, type);
 924         checkIfPathIsReadable(path, type);
 925         checkIfPathIsDirectory(path, type);
 926     }
 927 
 928     private Packager() { }
 929 
 930     public static void main(String[] args) throws Exception {
 931         try {
 932             run(args);
 933         } catch (OptionException | Command.Exception x) {
 934             err.println(x.getMessage());
 935             System.exit(1);
 936         }
 937     }
 938 
 939 }
--- EOF ---