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