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 import jdk.internal.jimage.ImageFile;
  30 import jdk.internal.jimage.ImageModules;
  31 
  32 import java.io.BufferedReader;
  33 import java.io.File;
  34 import java.io.IOException;
  35 import java.io.InputStream;
  36 import java.io.InputStreamReader;
  37 import java.io.PrintWriter;
  38 import java.io.UncheckedIOException;
  39 import java.nio.ByteOrder;
  40 import java.nio.file.Files;
  41 import java.nio.file.InvalidPathException;
  42 import java.nio.file.Path;
  43 import java.nio.file.Paths;
  44 import java.nio.file.attribute.PosixFilePermission;
  45 import java.util.ArrayList;
  46 import java.util.Collection;
  47 import java.util.HashMap;
  48 import java.util.HashSet;
  49 import java.util.LinkedList;
  50 import java.util.List;
  51 import java.util.Map;
  52 import java.util.Optional;
  53 import java.util.Set;
  54 import java.util.stream.Collectors;
  55 
  56 /**
  57  * A tool for building a runtime image.
  58  *
  59  * java build.tools.module.ImageBuilder <options> --output <path> top/modules.xml,...
  60  *  Possible options are:
  61  *  --cmds                  Location of native commands
  62  *  --configs               Location of config files
  63  *  --help                  Print this usage message
  64  *  --classes               Location of module classes files
  65  *  --libs                  Location of native libraries
  66  *  --mods                  Comma separated list of module names
  67  *  --output                Location of the output path
  68  *  --endian                Byte order of the target runtime; {little,big}
  69  */
  70 class ImageBuilder {
  71     static class BadArgs extends Exception {
  72         private static final long serialVersionUID = 0L;
  73         BadArgs(String format, Object... args) {
  74             super(String.format(format, args));
  75             this.format = format;
  76             this.args = args;
  77         }
  78         BadArgs showUsage(boolean b) {
  79             showUsage = b;
  80             return this;
  81         }
  82         final String format;
  83         final Object[] args;
  84         boolean showUsage;
  85     }
  86 
  87     static abstract class Option {
  88         final boolean hasArg;
  89         final String[] aliases;
  90         Option(boolean hasArg, String... aliases) {
  91             this.hasArg = hasArg;
  92             this.aliases = aliases;
  93         }
  94         boolean isHidden() {
  95             return false;
  96         }
  97         boolean matches(String opt) {
  98             for (String a : aliases) {
  99                 if (a.equals(opt)) {
 100                     return true;
 101                 } else if (opt.startsWith("--") && hasArg && opt.startsWith(a + "=")) {
 102                     return true;
 103                 }
 104             }
 105             return false;
 106         }
 107         boolean ignoreRest() {
 108             return false;
 109         }
 110         abstract void process(ImageBuilder task, String opt, String arg) throws BadArgs;
 111         abstract String description();
 112     }
 113 
 114     private static Path CWD = Paths.get("");
 115 
 116     private static List<Path> splitPath(String arg, String separator)
 117         throws BadArgs
 118     {
 119         List<Path> paths = new ArrayList<>();
 120         for (String p: arg.split(separator)) {
 121             if (p.length() > 0) {
 122                 try {
 123                     Path path = CWD.resolve(p);
 124                     if (Files.notExists(path))
 125                         throw new BadArgs("path not found: %s", path);
 126                     paths.add(path);
 127                 } catch (InvalidPathException x) {
 128                     throw new BadArgs("path not valid: %s", p);
 129                 }
 130             }
 131         }
 132         return paths;
 133     }
 134 
 135     static Option[] recognizedOptions = {
 136         new Option(true, "--cmds") {
 137             void process(ImageBuilder task, String opt, String arg) throws BadArgs {
 138                 task.options.cmds = splitPath(arg, File.pathSeparator);
 139             }
 140             String description() { return "Location of native commands"; }
 141         },
 142         new Option(true, "--configs") {
 143             void process(ImageBuilder task, String opt, String arg) throws BadArgs {
 144                 task.options.configs = splitPath(arg, File.pathSeparator);
 145             }
 146             String description() { return "Location of config files"; }
 147         },
 148         new Option(false, "--help") {
 149             void process(ImageBuilder task, String opt, String arg) {
 150                 task.options.help = true;
 151             }
 152             String description() { return "Print this usage message"; }
 153         },
 154         new Option(true, "--classes") {
 155             void process(ImageBuilder task, String opt, String arg) throws BadArgs {
 156                 task.options.classes = splitPath(arg, File.pathSeparator);
 157             }
 158             String description() { return "Location of module classes files"; }
 159         },
 160         new Option(true, "--libs") {
 161             void process(ImageBuilder task, String opt, String arg) throws BadArgs {
 162                 task.options.libs = splitPath(arg, File.pathSeparator);
 163             }
 164             String description() { return "Location of native libraries"; }
 165         },
 166         new Option(true, "--mods") {
 167             void process(ImageBuilder task, String opt, String arg) throws BadArgs {
 168                 for (String mn : arg.split(",")) {
 169                     if (mn.isEmpty())
 170                         throw new BadArgs("Module not found", mn);
 171                     task.options.mods.add(mn);
 172                 }
 173             }
 174             String description() { return "Comma separated list of module names"; }
 175         },
 176         new Option(true, "--output") {
 177             void process(ImageBuilder task, String opt, String arg) throws BadArgs {
 178                 Path path = Paths.get(arg);
 179                 task.options.output = path;
 180             }
 181             String description() { return "Location of the output path"; }
 182         },
 183         new Option(true, "--endian") {
 184             void process(ImageBuilder task, String opt, String arg) throws BadArgs {
 185                 if (arg.equals("little"))
 186                     task.options.endian = ByteOrder.LITTLE_ENDIAN;
 187                 else if (arg.equals("big"))
 188                     task.options.endian = ByteOrder.BIG_ENDIAN;
 189                 else
 190                     throw new BadArgs("Unknown byte order " + arg);
 191             }
 192             String description() { return "Byte order of the target runtime; {little,big}"; }
 193         }
 194     };
 195 
 196     private final Options options = new Options();
 197 
 198     private PrintWriter log;
 199     void setLog(PrintWriter out) {
 200         log = out;
 201     }
 202 
 203     Set<Module> moduleGraph = new java.util.HashSet<>();
 204 
 205     /** Module list files */
 206     private static final String BOOT_MODULES = "boot.modules";
 207     private static final String EXT_MODULES = "ext.modules";
 208 
 209     /**
 210      * Result codes.
 211      */
 212     static final int EXIT_OK = 0,       // Completed with no errors.
 213                      EXIT_ERROR = 1,    // Completed but reported errors.
 214                      EXIT_CMDERR = 2,   // Bad command-line arguments
 215                      EXIT_SYSERR = 3,   // System error or resource exhaustion.
 216                      EXIT_ABNORMAL = 4; // terminated abnormally
 217 
 218 
 219     static class Options {
 220         boolean help;
 221         List<Path> classes;
 222         List<Path> cmds;
 223         List<Path> configs;
 224         List<Path> libs;
 225         Set<String> mods = new HashSet<>();
 226         Path output;
 227         ByteOrder endian = ByteOrder.nativeOrder(); // default, if not specified
 228     }
 229 
 230     public static void main(String[] args) throws Exception {
 231         ImageBuilder builder = new ImageBuilder();
 232         int rc = builder.run(args);
 233         System.exit(rc);
 234     }
 235 
 236     int run(String[] args) {
 237         if (log == null)
 238             log = new PrintWriter(System.out);
 239 
 240         try {
 241             handleOptions(args);
 242             if (options.help) {
 243                 showHelp();
 244                 return EXIT_OK;
 245             }
 246 
 247             if (options.classes == null)
 248                 throw new BadArgs("--classes must be specified").showUsage(true);
 249 
 250             Path output = options.output;
 251             if (output == null)
 252                 throw new BadArgs("--output must be specified").showUsage(true);
 253             Files.createDirectories(output);
 254             if (Files.list(output).findFirst().isPresent())
 255                 throw new BadArgs("dir not empty", output);
 256 
 257             if (options.mods.isEmpty())
 258                 throw new BadArgs("--mods must be specified").showUsage(true);
 259 
 260             if (moduleGraph.isEmpty())
 261                 throw new BadArgs("modules.xml must be specified").showUsage(true);
 262 
 263             if (options.cmds == null || options.cmds.isEmpty())
 264                 warning("--commands is not set");
 265             if (options.libs == null || options.libs.isEmpty())
 266                 warning("--libs is not set");
 267             //if (options.configs == null || options.configs.isEmpty())
 268             //    warning("--configs is not set");
 269 
 270             // additional option combination validation
 271 
 272             boolean ok = run();
 273             return ok ? EXIT_OK : EXIT_ERROR;
 274         } catch (BadArgs e) {
 275             reportError(e.format, e.args);
 276             if (e.showUsage)
 277                 log.println(USAGE_SUMMARY);
 278             return EXIT_CMDERR;
 279         } catch (Exception x) {
 280             x.printStackTrace();
 281             return EXIT_ABNORMAL;
 282         } finally {
 283             log.flush();
 284         }
 285     }
 286 
 287     private boolean run() throws IOException {
 288         createImage();
 289         return true;
 290     }
 291 
 292     class SimpleResolver {
 293         private final Set<Module> initialMods;
 294         private final Map<String,Module> nameToModule = new HashMap<>();
 295 
 296         SimpleResolver(Set<String> mods, Set<Module> graph) {
 297             graph.stream()
 298                  .forEach(m -> nameToModule.put(m.name(), m));
 299             initialMods = mods.stream()
 300                          .map(this::nameToModule)
 301                          .collect(Collectors.toSet());
 302         }
 303 
 304         /** Returns the transitive closure, in topological order */
 305         List<String> resolve() {
 306             List<Module> result = new LinkedList<>();
 307             Set<Module> visited = new HashSet<>();
 308             Set<Module> done = new HashSet<>();
 309             for (Module m : initialMods) {
 310                 if (!visited.contains(m))
 311                     visit(m, visited, result, done);
 312             }
 313             return result.stream()
 314                          .map(m -> m.name())
 315                          .collect(Collectors.toList());
 316         }
 317 
 318         private void visit(Module m, Set<Module> visited,
 319                            List<Module> result, Set<Module> done) {
 320             if (visited.contains(m)) {
 321                 if (!done.contains(m))
 322                     throw new IllegalArgumentException("Cyclic detected: " +
 323                             m + " " + getModuleDependences(m));
 324                 return;
 325             }
 326             visited.add(m);
 327             getModuleDependences(m).stream()
 328                                    .forEach(d -> visit(d, visited, result, done));
 329             done.add(m);
 330             result.add(m);
 331         }
 332 
 333         private Module nameToModule(String name) {
 334             Module m = nameToModule.get(name);
 335             if (m == null)
 336                 throw new RuntimeException("No module definition for " + name);
 337             return m;
 338         }
 339 
 340         private Set<Module> getModuleDependences(Module m) {
 341             return m.requires().stream()
 342                     .map(d -> d.name())
 343                     .map(this::nameToModule)
 344                     .collect(Collectors.toSet());
 345         }
 346     }
 347 
 348     private List<String> resolve(Set<String> mods ) {
 349         return (new SimpleResolver(mods, moduleGraph)).resolve();
 350     }
 351 
 352     private void createImage() throws IOException {
 353         Collection<String> modules = resolve(options.mods);
 354         log.print(modules.stream().collect(Collectors.joining(" ")));
 355         ImageFileHelper imageHelper = new ImageFileHelper(modules);
 356         imageHelper.createModularImage(options.output);
 357 
 358         // jspawnhelper, might be in lib or lib/ARCH
 359         Path jspawnhelper = Paths.get("jspawnhelper");
 360         Path lib = options.output.resolve("lib");
 361         Optional<Path> helper = Files.walk(lib, 2)
 362                                      .filter(f -> f.getFileName().equals(jspawnhelper))
 363                                      .findFirst();
 364         if (helper.isPresent())
 365             helper.get().toFile().setExecutable(true, false);
 366     }
 367 
 368     private class ImageFileHelper {
 369         final Collection<String> modules;
 370         final Set<String> bootModules;
 371         final Set<String> extModules;
 372         final Set<String> appModules;
 373         final ImageModules imf;
 374 
 375         ImageFileHelper(Collection<String> modules) throws IOException {
 376             this.modules = modules;
 377             this.bootModules = modulesFor(BOOT_MODULES).stream()
 378                      .filter(modules::contains)
 379                      .collect(Collectors.toSet());
 380             this.extModules = modulesFor(EXT_MODULES).stream()
 381                     .filter(modules::contains)
 382                     .collect(Collectors.toSet());
 383             this.appModules = modules.stream()
 384                     .filter(m -> !bootModules.contains(m) && !extModules.contains(m))
 385                     .collect(Collectors.toSet());
 386 
 387             this.imf = new ImageModules(bootModules, extModules, appModules);
 388         }
 389 
 390         void createModularImage(Path output) throws IOException {
 391             Set<Archive> archives = modules.stream()
 392                                             .map(this::toModuleArchive)
 393                                             .collect(Collectors.toSet());
 394             ImageFile.create(output, archives, imf, 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 }