1 /*
2 * Copyright (c) 1999, 2024, 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.main;
27
28 import java.io.FileNotFoundException;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.PrintWriter;
32 import java.io.Writer;
33 import java.net.URL;
34 import java.nio.file.Files;
35 import java.nio.file.NoSuchFileException;
36 import java.nio.file.Path;
37 import java.nio.file.Paths;
38 import java.security.CodeSource;
39 import java.security.DigestInputStream;
40 import java.security.MessageDigest;
41 import java.security.NoSuchAlgorithmException;
42 import java.text.SimpleDateFormat;
43 import java.util.Calendar;
44 import java.util.Set;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47
48 import javax.tools.JavaFileManager;
49
50 import com.sun.tools.javac.api.BasicJavacTask;
51 import com.sun.tools.javac.code.Preview;
52 import com.sun.tools.javac.file.CacheFSInfo;
53 import com.sun.tools.javac.file.BaseFileManager;
54 import com.sun.tools.javac.file.JavacFileManager;
55 import com.sun.tools.javac.jvm.Target;
56 import com.sun.tools.javac.platform.PlatformDescription;
57 import com.sun.tools.javac.processing.AnnotationProcessingError;
58 import com.sun.tools.javac.resources.CompilerProperties.Errors;
59 import com.sun.tools.javac.util.*;
60 import com.sun.tools.javac.util.JCDiagnostic.DiagnosticInfo;
61 import com.sun.tools.javac.util.Log.PrefixKind;
62 import com.sun.tools.javac.util.Log.WriterKind;
63
64 import jdk.internal.opt.CommandLine;
65 import jdk.internal.opt.CommandLine.UnmatchedQuote;
66
67 /** This class provides a command line interface to the javac compiler.
68 *
69 * <p><b>This is NOT part of any supported API.
70 * If you write code that depends on this, you do so at your own risk.
71 * This code and its internal interfaces are subject to change or
72 * deletion without notice.</b>
73 */
74 public class Main {
75
76 /** The name of the compiler, for use in diagnostics.
77 */
78 String ownName;
79
80 /** The writer to use for normal output.
81 */
82 PrintWriter stdOut;
83
84 /** The writer to use for diagnostic output.
85 */
86 PrintWriter stdErr;
87
88 /** The log to use for diagnostic output.
89 */
90 public Log log;
91
92 /**
93 * If true, certain errors will cause an exception, such as command line
94 * arg errors, or exceptions in user provided code.
95 */
96 boolean apiMode;
97
98 private static final String ENV_OPT_NAME = "JDK_JAVAC_OPTIONS";
99
100 /** Result codes.
101 */
102 public enum Result {
103 OK(0), // Compilation completed with no errors.
104 ERROR(1), // Completed but reported errors.
105 CMDERR(2), // Bad command-line arguments
106 SYSERR(3), // System error or resource exhaustion.
107 ABNORMAL(4); // Compiler terminated abnormally
108
109 Result(int exitCode) {
110 this.exitCode = exitCode;
111 }
112
113 public boolean isOK() {
114 return (exitCode == 0);
115 }
116
117 public final int exitCode;
118 }
119
120 /**
121 * Construct a compiler instance.
122 * @param name the name of this tool
123 */
124 public Main(String name) {
125 this.ownName = name;
126 }
127
128 /**
129 * Construct a compiler instance.
130 * @param name the name of this tool
131 * @param out a stream to which to write messages
132 */
133 public Main(String name, PrintWriter out) {
134 this.ownName = name;
135 this.stdOut = this.stdErr = out;
136 }
137
138 /**
139 * Construct a compiler instance.
140 * @param name the name of this tool
141 * @param out a stream to which to write expected output
142 * @param err a stream to which to write diagnostic output
143 */
144 public Main(String name, PrintWriter out, PrintWriter err) {
145 this.ownName = name;
146 this.stdOut = out;
147 this.stdErr = err;
148 }
149
150 /** Report a usage error.
151 */
152 void reportDiag(DiagnosticInfo diag) {
153 if (apiMode) {
154 String msg = log.localize(diag);
155 throw new PropagatedException(new IllegalStateException(msg));
156 }
157 reportHelper(diag);
158 log.printLines(PrefixKind.JAVAC, "msg.usage", ownName);
159 }
160
161 /** Report helper.
162 */
163 void reportHelper(DiagnosticInfo diag) {
164 String msg = log.localize(diag);
165 String errorPrefix = log.localize(Errors.Error);
166 msg = msg.startsWith(errorPrefix) ? msg : errorPrefix + msg;
167 log.printRawLines(msg);
168 }
169
170
171 /**
172 * Programmatic interface for main function.
173 * @param args the command line parameters
174 * @return the result of the compilation
175 */
176 public Result compile(String[] args) {
177 Context context = new Context();
178 JavacFileManager.preRegister(context); // can't create it until Log has been set up
179 Result result = compile(args, context);
180 try {
181 // A fresh context was created above, so the file manager can be safely closed:
182 if (fileManager != null)
183 fileManager.close();
184 } catch (IOException ex) {
185 bugMessage(ex);
186 }
187 return result;
188 }
189
190 /**
191 * Internal version of compile, allowing context to be provided.
192 * Note that the context needs to have a file manager set up.
193 * @param argv the command line parameters
194 * @param context the context
195 * @return the result of the compilation
196 */
197 public Result compile(String[] argv, Context context) {
198 if (stdOut != null) {
199 context.put(Log.outKey, stdOut);
200 }
201
202 if (stdErr != null) {
203 context.put(Log.errKey, stdErr);
204 }
205
206 log = Log.instance(context);
207
208 if (argv.length == 0) {
209 OptionHelper h = new OptionHelper.GrumpyHelper(log) {
210 @Override
211 public String getOwnName() { return ownName; }
212 @Override
213 public void put(String name, String value) { }
214 };
215 try {
216 Option.HELP.process(h, "-help");
217 } catch (Option.InvalidValueException ignore) {
218 }
219 return Result.CMDERR;
220 }
221
222 // prefix argv with contents of environment variable and expand @-files
223 Iterable<String> allArgs;
224 try {
225 allArgs = CommandLine.parse(ENV_OPT_NAME, List.from(argv));
226 } catch (UnmatchedQuote ex) {
227 reportDiag(Errors.UnmatchedQuote(ex.variableName));
228 return Result.CMDERR;
229 } catch (FileNotFoundException | NoSuchFileException e) {
230 reportHelper(Errors.FileNotFound(e.getMessage()));
231 return Result.SYSERR;
232 } catch (IOException ex) {
233 log.printLines(PrefixKind.JAVAC, "msg.io");
234 ex.printStackTrace(log.getWriter(WriterKind.NOTICE));
235 return Result.SYSERR;
236 }
237
238 Arguments args = Arguments.instance(context);
239 args.init(ownName, allArgs);
240
241 if (log.nerrors > 0)
242 return Result.CMDERR;
243
244 Options options = Options.instance(context);
245
246 // init Log
247 boolean forceStdOut = options.isSet("stdout");
248 if (forceStdOut) {
249 log.flush();
250 log.setWriters(new PrintWriter(System.out, true));
251 }
252
253 // init CacheFSInfo
254 // allow System property in following line as a Mustang legacy
255 boolean batchMode = (options.isUnset("nonBatchMode")
256 && System.getProperty("nonBatchMode") == null);
257 if (batchMode)
258 CacheFSInfo.preRegister(context);
259
260 boolean ok = true;
261
262 // init file manager
263 fileManager = context.get(JavaFileManager.class);
264 JavaFileManager undel = fileManager instanceof DelegatingJavaFileManager delegatingJavaFileManager ?
265 delegatingJavaFileManager.getBaseFileManager() : fileManager;
266 if (undel instanceof BaseFileManager baseFileManager) {
267 baseFileManager.setContext(context); // reinit with options
268 ok &= baseFileManager.handleOptions(args.getDeferredFileManagerOptions());
269 }
270
271 // handle this here so it works even if no other options given
272 String showClass = options.get("showClass");
273 if (showClass != null) {
274 if (showClass.equals("showClass")) // no value given for option
275 showClass = "com.sun.tools.javac.Main";
276 showClass(showClass);
277 }
278
279 ok &= args.validate();
280 if (!ok || log.nerrors > 0)
281 return Result.CMDERR;
282
283 if (args.isEmpty())
284 return Result.OK;
285
286 // init Dependencies
287 if (options.isSet("debug.completionDeps")) {
288 Dependencies.GraphDependencies.preRegister(context);
289 }
290
291 BasicJavacTask t = (BasicJavacTask) BasicJavacTask.instance(context);
292
293 // init plugins
294 Set<List<String>> pluginOpts = args.getPluginOpts();
295 t.initPlugins(pluginOpts);
296
297 // init multi-release jar handling
298 if (fileManager.isSupportedOption(Option.MULTIRELEASE.primaryName) == 1) {
299 Target target = Target.instance(context);
300 List<String> list = List.of(target.multiReleaseValue());
301 fileManager.handleOption(Option.MULTIRELEASE.primaryName, list.iterator());
302 }
303
304 // pass preview mode to the file manager:
305 if (fileManager.isSupportedOption(Option.PREVIEWMODE.primaryName) == 1) {
306 Preview preview = Preview.instance(context);
307 fileManager.handleOption(Option.PREVIEWMODE.primaryName, List.of(String.valueOf(preview.isEnabled())).iterator());
308 }
309
310 // init JavaCompiler
311 JavaCompiler comp = JavaCompiler.instance(context);
312
313 // init doclint
314 List<String> docLintOpts = args.getDocLintOpts();
315 if (!docLintOpts.isEmpty()) {
316 t.initDocLint(docLintOpts);
317 }
318
319 if (options.get(Option.XSTDOUT) != null) {
320 // Stdout reassigned - ask compiler to close it when it is done
321 comp.closeables = comp.closeables.prepend(log.getWriter(WriterKind.NOTICE));
322 }
323
324 boolean printArgsToFile = options.isSet("printArgsToFile");
325 try {
326 comp.compile(args.getFileObjects(), args.getClassNames(), null, List.nil());
327
328 if (log.expectDiagKeys != null) {
329 if (log.expectDiagKeys.isEmpty()) {
330 log.printRawLines("all expected diagnostics found");
331 return Result.OK;
332 } else {
333 log.printRawLines("expected diagnostic keys not found: " + log.expectDiagKeys);
334 return Result.ERROR;
335 }
336 }
337
338 return (comp.errorCount() == 0) ? Result.OK : Result.ERROR;
339
340 } catch (OutOfMemoryError | StackOverflowError ex) {
341 resourceMessage(ex);
342 return Result.SYSERR;
343 } catch (FatalError ex) {
344 feMessage(ex, options);
345 return Result.SYSERR;
346 } catch (AnnotationProcessingError ex) {
347 apMessage(ex);
348 return Result.SYSERR;
349 } catch (PropagatedException ex) {
350 // TODO: what about errors from plugins? should not simply rethrow the error here
351 throw ex.getCause();
352 } catch (IllegalAccessError iae) {
353 if (twoClassLoadersInUse(iae)) {
354 bugMessage(iae);
355 }
356 printArgsToFile = true;
357 return Result.ABNORMAL;
358 } catch (Throwable ex) {
359 // Nasty. If we've already reported an error, compensate
360 // for buggy compiler error recovery by swallowing thrown
361 // exceptions.
362 if (comp == null || comp.errorCount() == 0 || options.isSet("dev"))
363 bugMessage(ex);
364 printArgsToFile = true;
365 return Result.ABNORMAL;
366 } finally {
367 if (printArgsToFile) {
368 printArgumentsToFile(argv);
369 }
370 if (comp != null) {
371 try {
372 comp.close();
373 } catch (ClientCodeException ex) {
374 throw new RuntimeException(ex.getCause());
375 }
376 }
377 }
378 }
379
380 void printArgumentsToFile(String... params) {
381 Path tmpDir = Paths.get(System.getProperty("java.io.tmpdir"));
382 Path out = tmpDir.resolve(String.format("javac.%s.args",
383 new SimpleDateFormat("yyyyMMdd_HHmmss").format(Calendar.getInstance().getTime())));
384 String strOut = "# javac crashed, this report includes the parameters passed to it in the @-file format\n";
385 try {
386 try (Writer w = Files.newBufferedWriter(out)) {
387 for (String param : params) {
388 param = param.replaceAll("\\\\", "\\\\\\\\");
389 if (param.matches(".*\\s+.*")) {
390 param = "\"" + param + "\"";
391 }
392 strOut += param + '\n';
393 }
394 w.write(strOut);
395 }
396 log.printLines(PrefixKind.JAVAC, "msg.parameters.output", out.toAbsolutePath());
397 } catch (IOException ioe) {
398 log.printLines(PrefixKind.JAVAC, "msg.parameters.output.error", out.toAbsolutePath());
399 System.err.println(strOut);
400 System.err.println();
401 }
402 }
403
404 private boolean twoClassLoadersInUse(IllegalAccessError iae) {
405 String msg = iae.getMessage();
406 Pattern pattern = Pattern.compile("(?i)(?<=tried to access class )([a-z_$][a-z\\d_$]*\\.)*[a-z_$][a-z\\d_$]*");
407 Matcher matcher = pattern.matcher(msg);
408 if (matcher.find()) {
409 try {
410 String otherClassName = matcher.group(0);
411 Class<?> otherClass = Class.forName(otherClassName);
412 ClassLoader otherClassLoader = otherClass.getClassLoader();
413 ClassLoader javacClassLoader = this.getClass().getClassLoader();
414 if (javacClassLoader != otherClassLoader) {
415 CodeSource otherClassCodeSource = otherClass.getProtectionDomain().getCodeSource();
416 CodeSource javacCodeSource = this.getClass().getProtectionDomain().getCodeSource();
417 if (otherClassCodeSource != null && javacCodeSource != null) {
418 log.printLines(Errors.TwoClassLoaders2(otherClassCodeSource.getLocation(),
419 javacCodeSource.getLocation()));
420 } else {
421 log.printLines(Errors.TwoClassLoaders1);
422 }
423 return true;
424 }
425 } catch (Throwable t) {
426 return false;
427 }
428 }
429 return false;
430 }
431
432 /** Print a message reporting an internal error.
433 */
434 void bugMessage(Throwable ex) {
435 log.printLines(PrefixKind.JAVAC, "msg.bug", JavaCompiler.version());
436 ex.printStackTrace(log.getWriter(WriterKind.NOTICE));
437 }
438
439 /** Print a message reporting a fatal error.
440 */
441 void feMessage(Throwable ex, Options options) {
442 log.printRawLines(ex.getMessage());
443 if (ex.getCause() != null && options.isSet("dev")) {
444 ex.getCause().printStackTrace(log.getWriter(WriterKind.NOTICE));
445 }
446 }
447
448 /** Print a message reporting an input/output error.
449 */
450 void ioMessage(Throwable ex) {
451 log.printLines(PrefixKind.JAVAC, "msg.io");
452 ex.printStackTrace(log.getWriter(WriterKind.NOTICE));
453 }
454
455 /** Print a message reporting an out-of-resources error.
456 */
457 void resourceMessage(Throwable ex) {
458 log.printLines(PrefixKind.JAVAC, "msg.resource");
459 ex.printStackTrace(log.getWriter(WriterKind.NOTICE));
460 }
461
462 /** Print a message reporting an uncaught exception from an
463 * annotation processor.
464 */
465 void apMessage(AnnotationProcessingError ex) {
466 log.printLines(PrefixKind.JAVAC, "msg.proc.annotation.uncaught.exception");
467 ex.getCause().printStackTrace(log.getWriter(WriterKind.NOTICE));
468 }
469
470 /** Print a message reporting an uncaught exception from an
471 * annotation processor.
472 */
473 void pluginMessage(Throwable ex) {
474 log.printLines(PrefixKind.JAVAC, "msg.plugin.uncaught.exception");
475 ex.printStackTrace(log.getWriter(WriterKind.NOTICE));
476 }
477
478 /** Display the location and checksum of a class. */
479 void showClass(String className) {
480 PrintWriter pw = log.getWriter(WriterKind.NOTICE);
481 pw.println("javac: show class: " + className);
482
483 URL url = getClass().getResource('/' + className.replace('.', '/') + ".class");
484 if (url != null) {
485 pw.println(" " + url);
486 }
487
488 try (InputStream in = getClass().getResourceAsStream('/' + className.replace('.', '/') + ".class")) {
489 final String algorithm = "SHA-256";
490 byte[] digest;
491 MessageDigest md = MessageDigest.getInstance(algorithm);
492 try (DigestInputStream din = new DigestInputStream(in, md)) {
493 byte[] buf = new byte[8192];
494 int n;
495 do { n = din.read(buf); } while (n > 0);
496 digest = md.digest();
497 }
498 StringBuilder sb = new StringBuilder();
499 for (byte b: digest)
500 sb.append(String.format("%02x", b));
501 pw.println(" " + algorithm + " checksum: " + sb);
502 } catch (NoSuchAlgorithmException | IOException e) {
503 pw.println(" cannot compute digest: " + e);
504 }
505 }
506
507 // TODO: update this to JavacFileManager
508 private JavaFileManager fileManager;
509
510 /* ************************************************************************
511 * Internationalization
512 *************************************************************************/
513
514 public static final String javacBundleName =
515 "com.sun.tools.javac.resources.javac";
516 }