1 /* 2 * Copyright (c) 2018, 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 com.sun.tools.javac.launcher; 27 28 import java.io.BufferedInputStream; 29 import java.io.BufferedReader; 30 import java.io.ByteArrayOutputStream; 31 import java.io.FilterOutputStream; 32 import java.io.IOException; 33 import java.io.InputStreamReader; 34 import java.io.OutputStream; 35 import java.io.OutputStreamWriter; 36 import java.io.PrintStream; 37 import java.io.PrintWriter; 38 import java.lang.reflect.InvocationTargetException; 39 import java.lang.reflect.Method; 40 import java.lang.reflect.Modifier; 41 import java.net.URI; 42 import java.nio.charset.Charset; 43 import java.nio.file.Files; 44 import java.nio.file.InvalidPathException; 45 import java.nio.file.Path; 46 import java.nio.file.Paths; 47 import java.text.MessageFormat; 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.MissingResourceException; 54 import java.util.ResourceBundle; 55 56 import javax.lang.model.element.NestingKind; 57 import javax.lang.model.element.TypeElement; 58 import javax.tools.FileObject; 59 import javax.tools.ForwardingJavaFileManager; 60 import javax.tools.JavaFileManager; 61 import javax.tools.JavaFileManager.Location; 62 import javax.tools.JavaFileObject; 63 import javax.tools.SimpleJavaFileObject; 64 import javax.tools.StandardJavaFileManager; 65 import javax.tools.StandardLocation; 66 67 import com.sun.source.util.JavacTask; 68 import com.sun.source.util.TaskEvent; 69 import com.sun.source.util.TaskListener; 70 import com.sun.tools.javac.api.JavacTool; 71 import com.sun.tools.javac.code.Source; 72 import com.sun.tools.javac.resources.LauncherProperties.Errors; 73 import com.sun.tools.javac.util.JCDiagnostic.Error; 74 75 import jdk.internal.misc.VM; 76 77 import static javax.tools.JavaFileObject.Kind.SOURCE; 78 79 /** 80 * Compiles a source file, and executes the main method it contains. 81 * 82 * <p><b>This is NOT part of any supported API. 83 * If you write code that depends on this, you do so at your own 84 * risk. This code and its internal interfaces are subject to change 85 * or deletion without notice.</b></p> 86 */ 87 public class Main { 88 /** 89 * An exception used to report errors. 90 */ 91 public class Fault extends Exception { 92 private static final long serialVersionUID = 1L; 93 Fault(Error error) { 94 super(Main.this.getMessage(error)); 95 } 96 } 97 98 /** 99 * Compiles a source file, and executes the main method it contains. 100 * 101 * <p>This is normally invoked from the Java launcher, either when 102 * the {@code --source} option is used, or when the first argument 103 * that is not part of a runtime option ends in {@code .java}. 104 * 105 * <p>The first entry in the {@code args} array is the source file 106 * to be compiled and run; all subsequent entries are passed as 107 * arguments to the main method of the first class found in the file. 108 * 109 * <p>If any problem occurs before executing the main class, it will 110 * be reported to the standard error stream, and the the JVM will be 111 * terminated by calling {@code System.exit} with a non-zero return code. 112 * 113 * @param args the arguments 114 * @throws Throwable if the main method throws an exception 115 */ 116 public static void main(String... args) throws Throwable { 117 try { 118 new Main(System.err).run(VM.getRuntimeArguments(), args); 119 } catch (Fault f) { 120 System.err.println(f.getMessage()); 121 System.exit(1); 122 } catch (InvocationTargetException e) { 123 // leave VM to handle the stacktrace, in the standard manner 124 throw e.getTargetException(); 125 } 126 } 127 128 /** Stream for reporting errors, such as compilation errors. */ 129 private PrintWriter out; 130 131 /** 132 * Creates an instance of this class, providing a stream to which to report 133 * any errors. 134 * 135 * @param out the stream 136 */ 137 public Main(PrintStream out) { 138 this(new PrintWriter(new OutputStreamWriter(out), true)); 139 } 140 141 /** 142 * Creates an instance of this class, providing a stream to which to report 143 * any errors. 144 * 145 * @param out the stream 146 */ 147 public Main(PrintWriter out) { 148 this.out = out; 149 } 150 151 /** 152 * Compiles a source file, and executes the main method it contains. 153 * 154 * <p>The first entry in the {@code args} array is the source file 155 * to be compiled and run; all subsequent entries are passed as 156 * arguments to the main method of the first class found in the file. 157 * 158 * <p>Options for {@code javac} are obtained by filtering the runtime arguments. 159 * 160 * <p>If the main method throws an exception, it will be propagated in an 161 * {@code InvocationTargetException}. In that case, the stack trace of the 162 * target exception will be truncated such that the main method will be the 163 * last entry on the stack. In other words, the stack frames leading up to the 164 * invocation of the main method will be removed. 165 * 166 * @param runtimeArgs the runtime arguments 167 * @param args the arguments 168 * @throws Fault if a problem is detected before the main method can be executed 169 * @throws InvocationTargetException if the main method throws an exception 170 */ 171 public void run(String[] runtimeArgs, String[] args) throws Fault, InvocationTargetException { 172 Path file = getFile(args); 173 174 Map<String, byte[]> inMemoryClasses = new HashMap<>(); 175 String mainClassName = compile(file, getJavacOpts(runtimeArgs), inMemoryClasses); 176 177 String[] appArgs = Arrays.copyOfRange(args, 1, args.length); 178 execute(mainClassName, appArgs, inMemoryClasses); 179 } 180 181 /** 182 * Returns the path for the filename found in the first of an array of arguments. 183 * 184 * @param args the array 185 * @return the path 186 * @throws Fault if there is a problem determining the path, or if the file does not exist 187 */ 188 private Path getFile(String[] args) throws Fault { 189 if (args.length == 0) { 190 // should not happen when invoked from launcher 191 throw new Fault(Errors.NoArgs); 192 } 193 Path file; 194 try { 195 file = Paths.get(args[0]); 196 } catch (InvalidPathException e) { 197 throw new Fault(Errors.InvalidFilename(args[0])); 198 } 199 if (!Files.exists(file)) { 200 // should not happen when invoked from launcher 201 throw new Fault(Errors.FileNotFound(file)); 202 } 203 return file; 204 } 205 206 /** 207 * Reads a source file, ignoring the first line if it begins {@code #!}. 208 * 209 * <p>If the first two bytes are {@code #!}, indicating a "magic number" of an executable 210 * text file, the rest of the first line up to but not including the newline is ignored. 211 * All characters after the first two are read in the 212 * {@link Charset#defaultCharset default platform encoding}. 213 * 214 * @param file the file 215 * @return a file object containing the content of the file 216 * @throws Fault if an error occurs while reading the file 217 */ 218 private JavaFileObject readFile(Path file) throws Fault { 219 // use a BufferedInputStream to guarantee that we can use mark and reset. 220 try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file))) { 221 in.mark(2); 222 int magic = (in.read() << 8) + in.read(); 223 boolean shebang = magic == (('#' << 8) + '!'); 224 if (!shebang) { 225 in.reset(); 226 } 227 try (BufferedReader r = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) { 228 StringBuilder sb = new StringBuilder(); 229 if (shebang) { 230 r.readLine(); 231 sb.append("\n"); // preserve line numbers 232 } 233 char[] buf = new char[1024]; 234 int n; 235 while ((n = r.read(buf, 0, buf.length)) != -1) { 236 sb.append(buf, 0, n); 237 } 238 return new SimpleJavaFileObject(file.toUri(), SOURCE) { 239 @Override 240 public String getName() { 241 return file.toString(); 242 } 243 @Override 244 public CharSequence getCharContent(boolean ignoreEncodingErrors) { 245 return sb; 246 } 247 @Override 248 public boolean isNameCompatible(String simpleName, JavaFileObject.Kind kind) { 249 return (kind == JavaFileObject.Kind.SOURCE) 250 && !simpleName.equals("package-info"); 251 } 252 @Override 253 public String toString() { 254 return "JavacSourceLauncher[" + file + "]"; 255 } 256 }; 257 } 258 } catch (IOException e) { 259 throw new Fault(Errors.CantReadFile(file, e)); 260 } 261 } 262 263 /** 264 * Returns the subset of the runtime arguments that are relevant to {@code javac}. 265 * Generally, the relevant options are those for setting paths and for configuring the 266 * module system. 267 * 268 * @param runtimeArgs the runtime arguments 269 * @return the subset of the runtime arguments 270 **/ 271 private List<String> getJavacOpts(String... runtimeArgs) throws Fault { 272 List<String> javacOpts = new ArrayList<>(); 273 274 String sourceOpt = System.getProperty("jdk.internal.javac.source"); 275 if (sourceOpt != null) { 276 Source source = Source.lookup(sourceOpt); 277 if (source == null) { 278 throw new Fault(Errors.InvalidValueForSource(sourceOpt)); 279 } 280 javacOpts.addAll(List.of("--release", sourceOpt)); 281 } 282 283 for (int i = 0; i < runtimeArgs.length; i++) { 284 String arg = runtimeArgs[i]; 285 String opt = arg, value = null; 286 if (arg.startsWith("--")) { 287 int eq = arg.indexOf('='); 288 if (eq > 0) { 289 opt = arg.substring(0, eq); 290 value = arg.substring(eq + 1); 291 } 292 } 293 switch (opt) { 294 // The following options all expect a value, either in the following 295 // position, or after '=', for options beginning "--". 296 case "--class-path": case "-classpath": case "-cp": 297 case "--module-path": case "-p": 298 case "--add-exports": 299 case "--add-modules": 300 case "--limit-modules": 301 case "--patch-module": 302 case "--upgrade-module-path": 303 if (value == null) { 304 if (i== runtimeArgs.length - 1) { 305 // should not happen when invoked from launcher 306 throw new Fault(Errors.NoValueForOption(opt)); 307 } 308 value = runtimeArgs[++i]; 309 } 310 if (opt.equals("--add-modules") && value.equals("ALL-DEFAULT")) { 311 break; 312 } 313 javacOpts.add(opt); 314 javacOpts.add(value); 315 break; 316 default: 317 // ignore all other runtime args 318 } 319 } 320 return javacOpts; 321 } 322 323 /** 324 * Compiles a source file, placing the class files in a map in memory. 325 * Any messages generated during compilation will be written to the stream 326 * provided when this object was created. 327 * 328 * @param file the source file 329 * @param javacOptscompilation options for {@code javac} 330 * @param inMemoryClasses a map in which to store the generated classes 331 * @return the name of the first class found in the source file 332 * @throws Fault if any compilation errors occur, or if no class was found 333 */ 334 private String compile(Path file, List<String> javacOpts, Map<String, byte[]> inMemoryClasses) throws Fault { 335 JavaFileObject fo = readFile(file); 336 337 JavacTool javaCompiler = JavacTool.create(); 338 StandardJavaFileManager stdFileMgr = javaCompiler.getStandardFileManager(null, null, null); 339 JavaFileManager fm = new MemoryFileManager(inMemoryClasses, stdFileMgr); 340 JavacTask t = javaCompiler.getTask(out, fm, null, javacOpts, null, List.of(fo)); 341 MainClassListener l = new MainClassListener(t); 342 Boolean ok = t.call(); 343 if (!ok) { 344 throw new Fault(Errors.CompilationFailed); 345 } 346 if (l.mainClass == null) { 347 throw new Fault(Errors.NoClass); 348 } 349 String mainClassName = l.mainClass.getQualifiedName().toString(); 350 return mainClassName; 351 } 352 353 /** 354 * Invokes the {@code main} method of a specified class, using a class loader that 355 * will load recently compiled classes from memory. 356 * 357 * @param mainClassName the class to be executed 358 * @param appArgs the arguments for the {@code main} method 359 * @param inMemoryClasses the map of recently compiled classes 360 * @throws Fault if there is a problem finding or invoking the {@code main} method 361 * @throws InvocationTargetException if the {@code main} method throws an exception 362 */ 363 private void execute(String mainClassName, String[] appArgs, Map<String, byte[]> inMemoryClasses) 364 throws Fault, InvocationTargetException { 365 ClassLoader cl = new MemoryClassLoader(inMemoryClasses, ClassLoader.getSystemClassLoader()); 366 try { 367 Class<?> appClass = Class.forName(mainClassName, true, cl); 368 if (appClass.getClassLoader() != cl) { 369 throw new Fault(Errors.UnexpectedClass(mainClassName)); 370 } 371 Method main = appClass.getDeclaredMethod("main", String[].class); 372 int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC; 373 if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) { 374 throw new Fault(Errors.MainNotPublicStatic); 375 } 376 if (!main.getReturnType().equals(void.class)) { 377 throw new Fault(Errors.MainNotVoid); 378 } 379 main.setAccessible(true); 380 main.invoke(0, (Object) appArgs); 381 } catch (ClassNotFoundException e) { 382 throw new Fault(Errors.CantFindClass(mainClassName)); 383 } catch (NoSuchMethodException e) { 384 throw new Fault(Errors.CantFindMainMethod(mainClassName)); 385 } catch (IllegalAccessException e) { 386 throw new Fault(Errors.CantAccessMainMethod(mainClassName)); 387 } catch (InvocationTargetException e) { 388 // remove stack frames for source launcher 389 int invocationFrames = e.getStackTrace().length; 390 Throwable target = e.getTargetException(); 391 StackTraceElement[] targetTrace = target.getStackTrace(); 392 target.setStackTrace(Arrays.copyOfRange(targetTrace, 0, targetTrace.length - invocationFrames)); 393 throw e; 394 } 395 } 396 397 private static final String bundleName = "com.sun.tools.javac.resources.launcher"; 398 private ResourceBundle resourceBundle = null; 399 private String errorPrefix; 400 401 /** 402 * Returns a localized string from a resource bundle. 403 * 404 * @param key the key for the resource 405 * @param args values to be inserted into the resource 406 * @return the localized string 407 */ 408 private String getMessage(Error error) { 409 String key = error.key(); 410 Object[] args = error.getArgs(); 411 try { 412 if (resourceBundle == null) { 413 resourceBundle = ResourceBundle.getBundle(bundleName); 414 errorPrefix = resourceBundle.getString("launcher.error"); 415 } 416 String resource = resourceBundle.getString(key); 417 String message = MessageFormat.format(resource, args); 418 return errorPrefix + message; 419 } catch (MissingResourceException e) { 420 return "Cannot access resource; " + key + Arrays.toString(args); 421 } 422 } 423 424 /** 425 * A listener to detect the first class found in a compilation. 426 */ 427 static class MainClassListener implements TaskListener { 428 private final JavacTask task; 429 TypeElement mainClass; 430 431 MainClassListener(JavacTask t) { 432 task = t; 433 t.addTaskListener(this); 434 } 435 436 @Override 437 public void started(TaskEvent ev) { 438 if (ev.getKind() == TaskEvent.Kind.ANALYZE && mainClass == null) { 439 TypeElement te = ev.getTypeElement(); 440 if (te.getNestingKind() == NestingKind.TOP_LEVEL) { 441 mainClass = te; 442 } 443 } 444 } 445 } 446 447 /** 448 * An in-memory file manager. 449 * 450 * <p>Class files (of kind {@JavaFileObject.Kind.CLASS CLASS} written to 451 * {@link StandardLocation#CLASS_OUTPUT} will be written to an in-memory cache. 452 * All other file manager operations will be delegated to a specified file manager. 453 */ 454 static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> { 455 private final Map<String, byte[]> map; 456 457 MemoryFileManager(Map<String, byte[]> map, JavaFileManager delegate) { 458 super(delegate); 459 this.map = map; 460 } 461 462 @Override 463 public JavaFileObject getJavaFileForOutput(Location location, String className, 464 JavaFileObject.Kind kind, FileObject sibling) throws IOException { 465 if (location == StandardLocation.CLASS_OUTPUT && kind == JavaFileObject.Kind.CLASS) { 466 return createInMemoryClassFile(className); 467 } else { 468 return super.getJavaFileForOutput(location, className, kind, sibling); 469 } 470 } 471 472 private JavaFileObject createInMemoryClassFile(String className) { 473 URI uri = URI.create("memory:///" + className.replace('.', '/') + ".class"); 474 return new SimpleJavaFileObject(uri, JavaFileObject.Kind.CLASS) { 475 @Override 476 public OutputStream openOutputStream() { 477 return new FilterOutputStream(new ByteArrayOutputStream()) { 478 @Override 479 public void close() throws IOException { 480 out.close(); 481 byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); 482 map.put(className, bytes); 483 } 484 }; 485 } 486 }; 487 } 488 } 489 490 /** 491 * An in-memory classloader, that uses an in-memory cache written by {@link MemoryFileManager}. 492 * 493 * <p>The classloader uses the standard parent-delegation model, just providing 494 * {@code findClass} to find classes in the in-memory cache. 495 */ 496 static class MemoryClassLoader extends ClassLoader { 497 private final Map<String, byte[]> map; 498 499 MemoryClassLoader(Map<String, byte[]> map, ClassLoader parent) { 500 super(parent); 501 this.map = map; 502 } 503 504 @Override 505 protected Class<?> findClass(String name) throws ClassNotFoundException { 506 byte[] bytes = map.get(name); 507 if (bytes == null) { 508 throw new ClassNotFoundException(name); 509 } 510 return defineClass(name, bytes, 0, bytes.length); 511 } 512 } 513 }