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 }