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