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