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 }