View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.compiler;
20  
21  import javax.lang.model.SourceVersion;
22  import javax.tools.DiagnosticListener;
23  import javax.tools.JavaFileObject;
24  import javax.tools.OptionChecker;
25  import javax.tools.StandardJavaFileManager;
26  import javax.tools.Tool;
27  
28  import java.io.BufferedWriter;
29  import java.io.File;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.OutputStream;
33  import java.io.UncheckedIOException;
34  import java.io.Writer;
35  import java.nio.charset.Charset;
36  import java.nio.file.Files;
37  import java.nio.file.Path;
38  import java.util.Arrays;
39  import java.util.List;
40  import java.util.Locale;
41  import java.util.Objects;
42  import java.util.Set;
43  
44  /**
45   * Base class of tool executed by invoking a command-line tool.
46   *
47   * @author Martin Desruisseaux
48   */
49  class ForkedTool implements Tool, OptionChecker {
50      /**
51       * The directory to run the compiler from, or {@code null} if none.
52       */
53      private final Path basedir;
54  
55      /**
56       * The executable of the compiler to use.
57       */
58      private final String executable;
59  
60      /**
61       * The file where to dump the command line, or {@code null} if none.
62       */
63      private final Path debugFilePath;
64  
65      /**
66       * Whether to write the {@code target/javac.sh} file even on successful compilation.
67       */
68      private final boolean verbose;
69  
70      /**
71       * Creates a new forked compiler.
72       *
73       * @param  mojo  the MOJO from which to get the configuration
74       */
75      ForkedTool(final AbstractCompilerMojo mojo) {
76          basedir = mojo.basedir;
77          executable = Objects.requireNonNull(mojo.executable);
78          debugFilePath = mojo.getDebugFilePath();
79          verbose = mojo.shouldWriteDebugFile();
80      }
81  
82      /**
83       * Returns the name of this tool.
84       */
85      @Override
86      public String name() {
87          return executable;
88      }
89  
90      /**
91       * Unconditionally returns -1, meaning that the given option is unsupported.
92       * This implementation actually knows nothing about which options are supported.
93       * Callers should ignore the return value.
94       *
95       * @param option ignored
96       * @return -1
97       */
98      @Override
99      public int isSupportedOption(String option) {
100         return -1;
101     }
102 
103     /**
104      * Returns the source versions of the Java programming language supported by this tool.
105      * This implementation arbitrarily returns the latest supported version of current JVM.
106      * Actually, this method does not know the supported versions.
107      */
108     @Override
109     public Set<SourceVersion> getSourceVersions() {
110         return Set.of(SourceVersion.latestSupported());
111     }
112 
113     /**
114      * Returns a new instance of the object holding a collection of files to compile.
115      */
116     public StandardJavaFileManager getStandardFileManager(
117             DiagnosticListener<? super JavaFileObject> diagnosticListener, Locale locale, Charset encoding) {
118         return new ForkedToolSources(encoding);
119     }
120 
121     /**
122      * Creates a process builder without starting the process.
123      * Callers can complete the builder configuration, then start the process.
124      */
125     private ProcessBuilder builder() {
126         var builder = new ProcessBuilder(executable);
127         if (basedir != null) {
128             builder.directory(basedir.toFile());
129         }
130         return builder;
131     }
132 
133     /**
134      * Executes the command and waits for its completion.
135      *
136      * @param out where to send additional compiler output
137      * @param fileManager the dependencies (JAR files)
138      * @param options the tool options
139      * @param compilationUnits the source files to process
140      * @return whether the operation succeeded
141      * @throws IOException if an I/O error occurred when starting the process
142      */
143     final boolean run(
144             Writer out,
145             ForkedToolSources fileManager,
146             Iterable<String> options,
147             Iterable<? extends JavaFileObject> compilationUnits)
148             throws IOException {
149         ProcessBuilder builder = builder();
150         List<String> command = builder.command();
151         for (String option : options) {
152             command.add(option);
153         }
154         fileManager.addAllLocations(command);
155         for (JavaFileObject source : compilationUnits) {
156             Path path = fileManager.asPath(source);
157             if (basedir != null) {
158                 try {
159                     path = basedir.relativize(path);
160                 } catch (IllegalArgumentException e) {
161                     // Ignore, keep the absolute path.
162                 }
163             }
164             command.add(path.toString());
165         }
166         File output = File.createTempFile("javac", null);
167         try {
168             var dest = ProcessBuilder.Redirect.appendTo(output);
169             builder.redirectError(dest);
170             builder.redirectOutput(dest);
171             return start(builder, out) == 0;
172         } finally {
173             /*
174              * Need to use the native encoding because it is the encoding used by the native process.
175              * This is not necessarily the default encoding of the JVM, which is "file.encoding".
176              * This property is available since Java 17.
177              */
178             String cs = System.getProperty("native.encoding");
179             out.append(Files.readString(output.toPath(), Charset.forName(cs)));
180             output.delete();
181         }
182     }
183 
184     /**
185      * Runs the tool with the given arguments.
186      * This method is implemented as a matter of principle but should not be invoked.
187      */
188     @Override
189     public int run(InputStream in, OutputStream out, OutputStream err, String... arguments) {
190         ProcessBuilder builder = builder();
191         builder.command().addAll(Arrays.asList(arguments));
192         try {
193             return start(builder, System.err);
194         } catch (IOException e) {
195             throw new UncheckedIOException(e);
196         }
197     }
198 
199     /**
200      * Starts the process and waits for its completion.
201      * If the process fails and a debug file has been specified, writes the command in that file.
202      *
203      * @param builder builder of the process to start
204      * @param out where to send additional compiler output
205      * @return the exit value of the process
206      */
207     private int start(ProcessBuilder builder, Appendable out) throws IOException {
208         Process process = builder.start();
209         int status;
210         try {
211             status = process.waitFor();
212         } catch (InterruptedException e) {
213             out.append("Compilation has been interrupted by " + e).append(System.lineSeparator());
214             process.destroy();
215             status = 1;
216         }
217         if ((status != 0 || verbose) && debugFilePath != null) {
218             // Use the path separator as a way to identify the operating system.
219             final boolean windows = File.separatorChar == '\\';
220             String filename = debugFilePath.getFileName().toString();
221             filename = filename.substring(0, filename.lastIndexOf('.') + 1);
222             filename += windows ? "bat" : "sh";
223             boolean more = false;
224             try (BufferedWriter debugFile = Files.newBufferedWriter(debugFilePath.resolveSibling(filename))) {
225                 if (basedir != null) {
226                     debugFile.write(windows ? "chdir " : "cd ");
227                     debugFile.write(basedir.toString());
228                     debugFile.newLine();
229                 }
230                 for (String cmd : builder.command()) {
231                     if (more) {
232                         debugFile.append(' ');
233                     }
234                     debugFile.append(cmd);
235                     more = true;
236                 }
237                 debugFile.newLine();
238             }
239         }
240         return status;
241     }
242 }