1 /*
   2  * Copyright (c) 2014, 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 build.tools.module;
  27 
  28 import jdk.internal.jimage.Archive;
  29 
  30 import java.io.BufferedReader;
  31 import java.io.File;
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import java.io.InputStreamReader;
  35 import java.io.PrintWriter;
  36 import java.nio.ByteOrder;
  37 import java.nio.file.Files;
  38 import java.nio.file.InvalidPathException;
  39 import java.nio.file.Path;
  40 import java.nio.file.Paths;
  41 import java.util.ArrayList;
  42 import java.util.Collection;
  43 import java.util.HashMap;
  44 import java.util.HashSet;
  45 import java.util.LinkedList;
  46 import java.util.List;
  47 import java.util.Map;
  48 import java.util.Optional;
  49 import java.util.Set;
  50 import java.util.stream.Collectors;
  51 import jdk.internal.jimage.ImageFileCreator;
  52 
  53 /**
  54  * A tool for building a runtime image.
  55  *
  56  * java build.tools.module.ImageBuilder <options> --output <path> top/modules.xml,...
  57  *  Possible options are:
  58  *  --cmds                  Location of native commands
  59  *  --configs               Location of config files
  60  *  --help                  Print this usage message
  61  *  --classes               Location of module classes files
  62  *  --libs                  Location of native libraries
  63  *  --mods                  Comma separated list of module names
  64  *  --output                Location of the output path
  65  *  --endian                Byte order of the target runtime; {little,big}
  66  */
  67 class ImageBuilder {
  68     static class BadArgs extends Exception {
  69         private static final long serialVersionUID = 0L;
  70         BadArgs(String format, Object... args) {
  71             super(String.format(format, args));
  72             this.format = format;
  73             this.args = args;
  74         }
  75         BadArgs showUsage(boolean b) {
  76             showUsage = b;
  77             return this;
  78         }
  79         final String format;
  80         final Object[] args;
  81         boolean showUsage;
  82     }
  83 
  84     static class Option {
  85 
  86         interface Processing {
  87 
  88             void process(ImageBuilder task, String opt, String arg) throws BadArgs;
  89         }
  90 
  91         final boolean hasArg;
  92         final String[] aliases;
  93         final String description;
  94         final Processing processing;
  95 
  96         Option(boolean hasArg, String description, Processing processing,
  97                 String... aliases) {
  98             this.hasArg = hasArg;
  99             this.description = description;
 100             this.processing = processing;
 101             this.aliases = aliases;
 102         }
 103         boolean isHidden() {
 104             return false;
 105         }
 106         boolean matches(String opt) {
 107             for (String a : aliases) {
 108                 if (a.equals(opt)) {
 109                     return true;
 110                 } else if (opt.startsWith("--") && hasArg && opt.startsWith(a + "=")) {
 111                     return true;
 112                 }
 113             }
 114             return false;
 115         }
 116         boolean ignoreRest() {
 117             return false;
 118         }
 119         void process(ImageBuilder task, String opt, String arg) throws BadArgs {
 120             processing.process(task, opt, arg);
 121         }
 122         String description() {
 123             return description;
 124         }
 125     }
 126 
 127     private static Path CWD = Paths.get("");
 128 
 129     private static List<Path> splitPath(String arg, String separator)
 130         throws BadArgs
 131     {
 132         List<Path> paths = new ArrayList<>();
 133         for (String p: arg.split(separator)) {
 134             if (p.length() > 0) {
 135                 try {
 136                     Path path = CWD.resolve(p);
 137                     if (Files.notExists(path))
 138                         throw new BadArgs("path not found: %s", path);
 139                     paths.add(path);
 140                 } catch (InvalidPathException x) {
 141                     throw new BadArgs("path not valid: %s", p);
 142                 }
 143             }
 144         }
 145         return paths;
 146     }
 147 
 148     static Option[] recognizedOptions = {
 149         new Option(true, "Location of native commands", (task, opt, arg) -> {
 150             task.options.cmds = splitPath(arg, File.pathSeparator);
 151         }, "--cmds"),
 152         new Option(true, "Location of config files", (task, opt, arg) -> {
 153             task.options.configs = splitPath(arg, File.pathSeparator);
 154         }, "--configs"),
 155         new Option(false, "Print this usage message", (task, opt, arg) -> {
 156             task.options.help = true;
 157         }, "--help"),
 158         new Option(true, "Location of module classes files", (task, opt, arg) -> {
 159             task.options.classes = splitPath(arg, File.pathSeparator);
 160         }, "--classes"),
 161         new Option(true, "Location of native libraries", (task, opt, arg) -> {
 162             task.options.libs = splitPath(arg, File.pathSeparator);
 163         }, "--libs"),
 164         new Option(true, "Comma separated list of module names",
 165         (task, opt, arg) -> {
 166             for (String mn : arg.split(",")) {
 167                 if (mn.isEmpty()) {
 168                     throw new BadArgs("Module not found", mn);
 169                 }
 170                 task.options.mods.add(mn);
 171             }
 172         }, "--mods"),
 173         new Option(true, "Location of the output path", (task, opt, arg) -> {
 174             Path path = Paths.get(arg);
 175             task.options.output = path;
 176         }, "--output"),
 177         new Option(true, "Byte order of the target runtime; {little,big}",
 178         (task, opt, arg) -> {
 179             if (arg.equals("little")) {
 180                 task.options.endian = ByteOrder.LITTLE_ENDIAN;
 181             } else if (arg.equals("big")) {
 182                 task.options.endian = ByteOrder.BIG_ENDIAN;
 183             } else {
 184                 throw new BadArgs("Unknown byte order " + arg);
 185             }
 186         }, "--endian")
 187     };
 188 
 189     private final Options options = new Options();
 190 
 191     private PrintWriter log;
 192     void setLog(PrintWriter out) {
 193         log = out;
 194     }
 195 
 196     Set<Module> moduleGraph = new java.util.HashSet<>();
 197 
 198     /** Module list files */
 199     private static final String BOOT_MODULES = "boot.modules";
 200     private static final String EXT_MODULES = "ext.modules";
 201 
 202     /**
 203      * Result codes.
 204      */
 205     static final int EXIT_OK = 0,       // Completed with no errors.
 206                      EXIT_ERROR = 1,    // Completed but reported errors.
 207                      EXIT_CMDERR = 2,   // Bad command-line arguments
 208                      EXIT_SYSERR = 3,   // System error or resource exhaustion.
 209                      EXIT_ABNORMAL = 4; // terminated abnormally
 210 
 211 
 212     static class Options {
 213         boolean help;
 214         List<Path> classes;
 215         List<Path> cmds;
 216         List<Path> configs;
 217         List<Path> libs;
 218         Set<String> mods = new HashSet<>();
 219         Path output;
 220         ByteOrder endian = ByteOrder.nativeOrder(); // default, if not specified
 221     }
 222 
 223     public static void main(String[] args) throws Exception {
 224         ImageBuilder builder = new ImageBuilder();
 225         int rc = builder.run(args);
 226         System.exit(rc);
 227     }
 228 
 229     int run(String[] args) {
 230         if (log == null)
 231             log = new PrintWriter(System.out);
 232 
 233         try {
 234             handleOptions(args);
 235             if (options.help) {
 236                 showHelp();
 237                 return EXIT_OK;
 238             }
 239 
 240             if (options.classes == null)
 241                 throw new BadArgs("--classes must be specified").showUsage(true);
 242 
 243             Path output = options.output;
 244             if (output == null)
 245                 throw new BadArgs("--output must be specified").showUsage(true);
 246             Files.createDirectories(output);
 247             if (Files.list(output).findFirst().isPresent())
 248                 throw new BadArgs("dir not empty", output);
 249 
 250             if (options.mods.isEmpty())
 251                 throw new BadArgs("--mods must be specified").showUsage(true);
 252 
 253             if (moduleGraph.isEmpty())
 254                 throw new BadArgs("modules.xml must be specified").showUsage(true);
 255 
 256             if (options.cmds == null || options.cmds.isEmpty())
 257                 warning("--commands is not set");
 258             if (options.libs == null || options.libs.isEmpty())
 259                 warning("--libs is not set");
 260             //if (options.configs == null || options.configs.isEmpty())
 261             //    warning("--configs is not set");
 262 
 263             // additional option combination validation
 264 
 265             boolean ok = run();
 266             return ok ? EXIT_OK : EXIT_ERROR;
 267         } catch (BadArgs e) {
 268             reportError(e.format, e.args);
 269             if (e.showUsage)
 270                 log.println(USAGE_SUMMARY);
 271             return EXIT_CMDERR;
 272         } catch (Exception x) {
 273             x.printStackTrace();
 274             return EXIT_ABNORMAL;
 275         } finally {
 276             log.flush();
 277         }
 278     }
 279 
 280     private boolean run() throws IOException {
 281         createImage();
 282         return true;
 283     }
 284 
 285     class SimpleResolver {
 286         private final Set<Module> initialMods;
 287         private final Map<String,Module> nameToModule = new HashMap<>();
 288 
 289         SimpleResolver(Set<String> mods, Set<Module> graph) {
 290             graph.stream()
 291                  .forEach(m -> nameToModule.put(m.name(), m));
 292             initialMods = mods.stream()
 293                          .map(this::nameToModule)
 294                          .collect(Collectors.toSet());
 295         }
 296 
 297         /** Returns the transitive closure, in topological order */
 298         List<String> resolve() {
 299             List<Module> result = new LinkedList<>();
 300             Set<Module> visited = new HashSet<>();
 301             Set<Module> done = new HashSet<>();
 302             for (Module m : initialMods) {
 303                 if (!visited.contains(m))
 304                     visit(m, visited, result, done);
 305             }
 306             return result.stream()
 307                          .map(m -> m.name())
 308                          .collect(Collectors.toList());
 309         }
 310 
 311         private void visit(Module m, Set<Module> visited,
 312                            List<Module> result, Set<Module> done) {
 313             if (visited.contains(m)) {
 314                 if (!done.contains(m))
 315                     throw new IllegalArgumentException("Cyclic detected: " +
 316                             m + " " + getModuleDependences(m));
 317                 return;
 318             }
 319             visited.add(m);
 320             getModuleDependences(m).stream()
 321                                    .forEach(d -> visit(d, visited, result, done));
 322             done.add(m);
 323             result.add(m);
 324         }
 325 
 326         private Module nameToModule(String name) {
 327             Module m = nameToModule.get(name);
 328             if (m == null)
 329                 throw new RuntimeException("No module definition for " + name);
 330             return m;
 331         }
 332 
 333         private Set<Module> getModuleDependences(Module m) {
 334             return m.requires().stream()
 335                     .map(d -> d.name())
 336                     .map(this::nameToModule)
 337                     .collect(Collectors.toSet());
 338         }
 339     }
 340 
 341     private List<String> resolve(Set<String> mods ) {
 342         return (new SimpleResolver(mods, moduleGraph)).resolve();
 343     }
 344 
 345     private void createImage() throws IOException {
 346         Collection<String> modules = resolve(options.mods);
 347         log.print(modules.stream().collect(Collectors.joining(" ")));
 348         ImageFileHelper imageHelper = new ImageFileHelper(modules);
 349         imageHelper.createModularImage(options.output);
 350 
 351         // jspawnhelper, might be in lib or lib/ARCH
 352         Path jspawnhelper = Paths.get("jspawnhelper");
 353         Path lib = options.output.resolve("lib");
 354         Optional<Path> helper = Files.walk(lib, 2)
 355                                      .filter(f -> f.getFileName().equals(jspawnhelper))
 356                                      .findFirst();
 357         if (helper.isPresent())
 358             helper.get().toFile().setExecutable(true, false);
 359     }
 360 
 361     private class ImageFileHelper {
 362         final Collection<String> modules;
 363         final Set<String> bootModules;
 364         final Set<String> extModules;
 365         final Set<String> appModules;
 366 
 367         ImageFileHelper(Collection<String> modules) throws IOException {
 368             this.modules = modules;
 369             this.bootModules = modulesFor(BOOT_MODULES).stream()
 370                     .filter(modules::contains)
 371                     .collect(Collectors.toSet());
 372             this.extModules = modulesFor(EXT_MODULES).stream()
 373                     .filter(modules::contains)
 374                     .collect(Collectors.toSet());
 375             this.appModules = modules.stream()
 376                     .filter(m -> m.length() != 0 &&
 377                                  !bootModules.contains(m) &&
 378                                  !extModules.contains(m))
 379                     .collect(Collectors.toSet());
 380         }
 381 
 382         void createModularImage(Path output) throws IOException {
 383             Set<Archive> bootArchives = bootModules.stream()
 384                     .map(this::toModuleArchive)
 385                     .collect(Collectors.toSet());
 386             Set<Archive> extArchives = extModules.stream()
 387                     .map(this::toModuleArchive)
 388                     .collect(Collectors.toSet());
 389             Set<Archive> appArchives = appModules.stream()
 390                     .map(this::toModuleArchive)
 391                     .collect(Collectors.toSet());
 392             ImageFileCreator.create(output, "bootmodules", bootArchives, options.endian);
 393             ImageFileCreator.create(output, "extmodules", extArchives, options.endian);
 394             ImageFileCreator.create(output, "appmodules", appArchives, options.endian);
 395         }
 396 
 397         ModuleArchive toModuleArchive(String mn) {
 398             return new ModuleArchive(mn,
 399                                      moduleToPath(mn, options.classes, false/*true*/),
 400                                      moduleToPath(mn, options.cmds, false),
 401                                      moduleToPath(mn, options.libs, false),
 402                                      moduleToPath(mn, options.configs, false));
 403         }
 404 
 405         private Path moduleToPath(String name, List<Path> paths, boolean expect) {
 406             Set<Path> foundPaths = new HashSet<>();
 407             if (paths != null) {
 408                 for (Path p : paths) {
 409                     Path rp = p.resolve(name);
 410                     if (Files.exists(rp))
 411                         foundPaths.add(rp);
 412                 }
 413             }
 414             if (foundPaths.size() > 1)
 415                 throw new RuntimeException("Found more that one path for " + name);
 416             if (expect && foundPaths.size() != 1)
 417                 throw new RuntimeException("Expected to find classes path for " + name);
 418             return foundPaths.size() == 0 ? null : foundPaths.iterator().next();
 419         }
 420 
 421         private List<String> modulesFor(String name) throws IOException {
 422             try (InputStream is = ImageBuilder.class.getResourceAsStream(name);
 423                  BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
 424                 return reader.lines().collect(Collectors.toList());
 425             }
 426         }
 427     }
 428 
 429     public void handleOptions(String[] args) throws BadArgs {
 430         // process options
 431         for (int i=0; i < args.length; i++) {
 432             if (args[i].charAt(0) == '-') {
 433                 String name = args[i];
 434                 Option option = getOption(name);
 435                 String param = null;
 436                 if (option.hasArg) {
 437                     if (name.startsWith("--") && name.indexOf('=') > 0) {
 438                         param = name.substring(name.indexOf('=') + 1, name.length());
 439                     } else if (i + 1 < args.length) {
 440                         param = args[++i];
 441                     }
 442                     if (param == null || param.isEmpty() || param.charAt(0) == '-') {
 443                         throw new BadArgs("Missing arg for %n", name).showUsage(true);
 444                     }
 445                 }
 446                 option.process(this, name, param);
 447                 if (option.ignoreRest()) {
 448                     i = args.length;
 449                 }
 450             } else {
 451                 // process rest of the input arguments
 452                 Path p = Paths.get(args[i]);
 453                 try {
 454                     moduleGraph.addAll(ModulesXmlReader.readModules(p)
 455                             .stream()
 456                             .collect(Collectors.toSet()));
 457                 } catch (Exception e) {
 458                     throw new RuntimeException(e);
 459                 }
 460             }
 461         }
 462     }
 463 
 464     private Option getOption(String name) throws BadArgs {
 465         for (Option o : recognizedOptions) {
 466             if (o.matches(name)) {
 467                 return o;
 468             }
 469         }
 470         throw new BadArgs("Unknown option %s", name).showUsage(true);
 471     }
 472 
 473     private void reportError(String format, Object... args) {
 474         log.format("Error: " + format + "%n", args);
 475     }
 476 
 477     private void warning(String format, Object... args) {
 478         log.format("Warning: " + format + "%n", args);
 479     }
 480 
 481     private static final String USAGE =
 482             "ImageBuilder <options> --output <path> path-to-modules-xml\n";
 483 
 484     private static final String USAGE_SUMMARY =
 485             USAGE + "Use --help for a list of possible options.";
 486 
 487     private void showHelp() {
 488         log.format(USAGE);
 489         log.format("Possible options are:%n");
 490         for (Option o : recognizedOptions) {
 491             String name = o.aliases[0].substring(1); // there must always be at least one name
 492             name = name.charAt(0) == '-' ? name.substring(1) : name;
 493             if (o.isHidden() || name.equals("h"))
 494                 continue;
 495 
 496             log.format("  --%s\t\t\t%s%n", name, o.description());
 497         }
 498     }
 499 }