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     /**
 353      * chmod ugo+x file
 354      */
 355     private void setExecutable(Path file) {
 356         try {
 357             Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file);
 358             perms.add(PosixFilePermission.OWNER_EXECUTE);
 359             perms.add(PosixFilePermission.GROUP_EXECUTE);
 360             perms.add(PosixFilePermission.OTHERS_EXECUTE);
 361             Files.setPosixFilePermissions(file, perms);
 362         } catch (IOException ioe) {
 363             throw new UncheckedIOException(ioe);
 364         }
 365     }
 366 
 367     private void createImage() throws IOException {
 368         Collection<String> modules = resolve(options.mods);
 369         log.print(modules.stream().collect(Collectors.joining(" ")));
 370         ImageFileHelper imageHelper = new ImageFileHelper(modules);
 371         imageHelper.createModularImage(options.output);
 372 
 373         // jspawnhelper, might be in lib or lib/ARCH
 374         Path jspawnhelper = Paths.get("jspawnhelper");
 375         Path lib = options.output.resolve("lib");
 376         Optional<Path> helper = Files.walk(lib, 2)
 377                                      .filter(f -> f.getFileName().equals(jspawnhelper))
 378                                      .findFirst();
 379         if (helper.isPresent())
 380             setExecutable(helper.get());
 381     }
 382 
 383     private class ImageFileHelper {
 384         final Collection<String> modules;
 385         final Set<String> bootModules;
 386         final Set<String> extModules;
 387         final Set<String> appModules;
 388         final ImageModules imf;
 389 
 390         ImageFileHelper(Collection<String> modules) throws IOException {
 391             this.modules = modules;
 392             this.bootModules = modulesFor(BOOT_MODULES).stream()
 393                      .filter(modules::contains)
 394                      .collect(Collectors.toSet());
 395             this.extModules = modulesFor(EXT_MODULES).stream()
 396                     .filter(modules::contains)
 397                     .collect(Collectors.toSet());
 398             this.appModules = modules.stream()
 399                     .filter(m -> !bootModules.contains(m) && !extModules.contains(m))
 400                     .collect(Collectors.toSet());
 401 
 402             this.imf = new ImageModules(bootModules, extModules, appModules);
 403         }
 404 
 405         void createModularImage(Path output) throws IOException {
 406             Set<Archive> archives = modules.stream()
 407                                             .map(this::toModuleArchive)
 408                                             .collect(Collectors.toSet());
 409             ImageFile.create(output, archives, imf, options.endian);
 410         }
 411 
 412         ModuleArchive toModuleArchive(String mn) {
 413             return new ModuleArchive(mn,
 414                                      moduleToPath(mn, options.classes, false/*true*/),
 415                                      moduleToPath(mn, options.cmds, false),
 416                                      moduleToPath(mn, options.libs, false),
 417                                      moduleToPath(mn, options.configs, false));
 418         }
 419 
 420         private Path moduleToPath(String name, List<Path> paths, boolean expect) {
 421             Set<Path> foundPaths = new HashSet<>();
 422             if (paths != null) {
 423                 for (Path p : paths) {
 424                     Path rp = p.resolve(name);
 425                     if (Files.exists(rp))
 426                         foundPaths.add(rp);
 427                 }
 428             }
 429             if (foundPaths.size() > 1)
 430                 throw new RuntimeException("Found more that one path for " + name);
 431             if (expect && foundPaths.size() != 1)
 432                 throw new RuntimeException("Expected to find classes path for " + name);
 433             return foundPaths.size() == 0 ? null : foundPaths.iterator().next();
 434         }
 435 
 436         private List<String> modulesFor(String name) throws IOException {
 437             try (InputStream is = ImageBuilder.class.getResourceAsStream(name);
 438                  BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
 439                 return reader.lines().collect(Collectors.toList());
 440             }
 441         }
 442     }
 443 
 444     public void handleOptions(String[] args) throws BadArgs {
 445         // process options
 446         for (int i=0; i < args.length; i++) {
 447             if (args[i].charAt(0) == '-') {
 448                 String name = args[i];
 449                 Option option = getOption(name);
 450                 String param = null;
 451                 if (option.hasArg) {
 452                     if (name.startsWith("--") && name.indexOf('=') > 0) {
 453                         param = name.substring(name.indexOf('=') + 1, name.length());
 454                     } else if (i + 1 < args.length) {
 455                         param = args[++i];
 456                     }
 457                     if (param == null || param.isEmpty() || param.charAt(0) == '-') {
 458                         throw new BadArgs("Missing arg for %n", name).showUsage(true);
 459                     }
 460                 }
 461                 option.process(this, name, param);
 462                 if (option.ignoreRest()) {
 463                     i = args.length;
 464                 }
 465             } else {
 466                 // process rest of the input arguments
 467                 Path p = Paths.get(args[i]);
 468                 try {
 469                     moduleGraph.addAll(ModulesXmlReader.readModules(p)
 470                             .stream()
 471                             .collect(Collectors.toSet()));
 472                 } catch (Exception e) {
 473                     throw new RuntimeException(e);
 474                 }
 475             }
 476         }
 477     }
 478 
 479     private Option getOption(String name) throws BadArgs {
 480         for (Option o : recognizedOptions) {
 481             if (o.matches(name)) {
 482                 return o;
 483             }
 484         }
 485         throw new BadArgs("Unknown option %s", name).showUsage(true);
 486     }
 487 
 488     private void reportError(String format, Object... args) {
 489         log.format("Error: " + format + "%n", args);
 490     }
 491 
 492     private void warning(String format, Object... args) {
 493         log.format("Warning: " + format + "%n", args);
 494     }
 495 
 496     private static final String USAGE =
 497             "ImageBuilder <options> --output <path> path-to-modules-xml\n";
 498 
 499     private static final String USAGE_SUMMARY =
 500             USAGE + "Use --help for a list of possible options.";
 501 
 502     private void showHelp() {
 503         log.format(USAGE);
 504         log.format("Possible options are:%n");
 505         for (Option o : recognizedOptions) {
 506             String name = o.aliases[0].substring(1); // there must always be at least one name
 507             name = name.charAt(0) == '-' ? name.substring(1) : name;
 508             if (o.isHidden() || name.equals("h"))
 509                 continue;
 510 
 511             log.format("  --%s\t\t\t%s%n", name, o.description());
 512         }
 513     }
 514 }