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       * Creates a new forked compiler.
67       *
68       * @param  mojo  the MOJO from which to get the configuration
69       */
70      ForkedTool(final AbstractCompilerMojo mojo) {
71          basedir = mojo.basedir;
72          executable = Objects.requireNonNull(mojo.executable);
73          debugFilePath = mojo.getDebugFilePath();
74      }
75  
76      /**
77       * Returns the name of this tool.
78       */
79      @Override
80      public String name() {
81          return executable;
82      }
83  
84      /**
85       * Unconditionally returns -1, meaning that the given option is unsupported.
86       * This implementation actually knows nothing about which options are supported.
87       * Callers should ignore the return value.
88       *
89       * @param option ignored
90       * @return -1
91       */
92      @Override
93      public int isSupportedOption(String option) {
94          return -1;
95      }
96  
97      /**
98       * Returns the source versions of the Java programming language supported by this tool.
99       * This implementation arbitrarily returns the latest supported version of current JVM.
100      * Actually, this method does not know the supported versions.
101      */
102     @Override
103     public Set<SourceVersion> getSourceVersions() {
104         return Set.of(SourceVersion.latestSupported());
105     }
106 
107     /**
108      * Returns a new instance of the object holding a collection of files to compile.
109      */
110     public StandardJavaFileManager getStandardFileManager(
111             DiagnosticListener<? super JavaFileObject> diagnosticListener, Locale locale, Charset encoding) {
112         return new ForkedToolSources(encoding);
113     }
114 
115     /**
116      * Creates a process builder without starting the process.
117      * Callers can complete the builder configuration, then start the process.
118      */
119     private ProcessBuilder builder() {
120         var builder = new ProcessBuilder(executable);
121         if (basedir != null) {
122             builder.directory(basedir.toFile());
123         }
124         return builder;
125     }
126 
127     /**
128      * Executes the command and waits for its completion.
129      *
130      * @param out where to send additional compiler output
131      * @param fileManager the dependencies (JAR files)
132      * @param options the tool options
133      * @param compilationUnits the source files to process
134      * @return whether the operation succeeded
135      * @throws IOException if an I/O error occurred when starting the process
136      */
137     final boolean run(
138             Writer out,
139             ForkedToolSources fileManager,
140             Iterable<String> options,
141             Iterable<? extends JavaFileObject> compilationUnits)
142             throws IOException {
143         ProcessBuilder builder = builder();
144         List<String> command = builder.command();
145         for (String option : options) {
146             command.add(option);
147         }
148         fileManager.addAllLocations(command);
149         for (JavaFileObject source : compilationUnits) {
150             Path path = fileManager.asPath(source);
151             if (basedir != null) {
152                 try {
153                     path = basedir.relativize(path);
154                 } catch (IllegalArgumentException e) {
155                     // Ignore, keep the absolute path.
156                 }
157             }
158             command.add(path.toString());
159         }
160         File output = File.createTempFile("javac", null);
161         try {
162             var dest = ProcessBuilder.Redirect.appendTo(output);
163             builder.redirectError(dest);
164             builder.redirectOutput(dest);
165             return start(builder, out) == 0;
166         } finally {
167             out.append(Files.readString(output.toPath()));
168             output.delete();
169         }
170     }
171 
172     /**
173      * Runs the tool with the given arguments.
174      * This method is implemented as a matter of principle but should not be invoked.
175      */
176     @Override
177     public int run(InputStream in, OutputStream out, OutputStream err, String... arguments) {
178         ProcessBuilder builder = builder();
179         builder.command().addAll(Arrays.asList(arguments));
180         try {
181             return start(builder, System.err);
182         } catch (IOException e) {
183             throw new UncheckedIOException(e);
184         }
185     }
186 
187     /**
188      * Starts the process and wait for its completion.
189      * If a debug file has been specified, writes in that file the command which is about to be executed.
190      *
191      * @param builder builder of the process to start
192      * @param out where to send additional compiler output
193      */
194     private int start(ProcessBuilder builder, Appendable out) throws IOException {
195         if (debugFilePath != null) {
196             // Use the path separator as a way to identify the operating system.
197             final boolean windows = File.separatorChar == '\\';
198             String filename = debugFilePath.getFileName().toString();
199             filename = filename.substring(0, filename.lastIndexOf('.') + 1);
200             filename += windows ? "bat" : "sh";
201             boolean more = false;
202             try (BufferedWriter debugFile = Files.newBufferedWriter(debugFilePath.resolveSibling(filename))) {
203                 if (basedir != null) {
204                     debugFile.write(windows ? "chdir " : "cd ");
205                     debugFile.write(basedir.toString());
206                     debugFile.newLine();
207                 }
208                 for (String cmd : builder.command()) {
209                     if (more) {
210                         debugFile.append(' ');
211                     }
212                     debugFile.append(cmd);
213                     more = true;
214                 }
215                 debugFile.newLine();
216             }
217         }
218         Process process = builder.start();
219         try {
220             return process.waitFor();
221         } catch (InterruptedException e) {
222             out.append("Compilation has been interrupted by " + e).append(System.lineSeparator());
223             process.destroy();
224             return 1;
225         }
226     }
227 }