blob: a16f97d6bcab5bb08d10be5e194145957711dcaa [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018-2020 Martin Weber.
*
* Content is provided to you under the terms and conditions of the Eclipse Public License Version 2.0 "EPL".
* A copy of the EPL is available at http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.cdt.cmake.is.core.internal.builtins;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import org.eclipse.cdt.cmake.is.core.IParserPreferences;
import org.eclipse.cdt.cmake.is.core.IParserPreferencesAccess;
import org.eclipse.cdt.cmake.is.core.internal.Plugin;
import org.eclipse.cdt.cmake.is.core.participant.IRawIndexerInfo;
import org.eclipse.cdt.cmake.is.core.participant.builtins.IBuiltinsDetectionBehavior;
import org.eclipse.cdt.cmake.is.core.participant.builtins.IBuiltinsOutputProcessor;
import org.eclipse.cdt.cmake.is.core.participant.builtins.OutputSniffer;
import org.eclipse.cdt.core.ConsoleOutputStream;
import org.eclipse.cdt.core.ICommandLauncher;
import org.eclipse.cdt.core.resources.IConsole;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.osgi.framework.FrameworkUtil;
/**
* Detects preprocessor macros and include paths that are built-in to a
* compiler.
*
* @author Martin Weber
*/
public class CompilerBuiltinsDetector {
/** error marker ID */
private static final String MARKER_ID = Plugin.PLUGIN_ID + ".CompilerBuiltinsDetectorMarker"; //$NON-NLS-1$
private final String sourceFileExtension;
private final String command;
private final List<String> builtinsDetectionArgs;
private final IBuiltinsDetectionBehavior builtinsDetectionBehavior;
private IProject project;
private java.nio.file.Path buildDirectory;
/**
* @param builtinsDetectionBehavior how compiler built-ins are to be detected
* @param command the compiler command (argument # 0)
* @param builtinsDetectionArgs the compiler arguments from the command-line
* that affect built-in detection. For the GNU
* compilers, these are options like
* {@code --sysroot} and options that specify
* the language's standard (e.g.
* {@code -std=c++17}).
* @param sourceFileExtension the extension of the source file name
*/
public CompilerBuiltinsDetector(IBuiltinsDetectionBehavior builtinsDetectionBehavior, String command,
List<String> builtinsDetectionArgs, String sourceFileExtension) {
this.sourceFileExtension = Objects.requireNonNull(sourceFileExtension, "sourceFileExtension"); //$NON-NLS-1$
this.builtinsDetectionBehavior = Objects.requireNonNull(builtinsDetectionBehavior, "builtinsDetectionBehavior"); //$NON-NLS-1$
this.command = Objects.requireNonNull(command, "command"); //$NON-NLS-1$
this.builtinsDetectionArgs = Objects.requireNonNull(builtinsDetectionArgs, "builtinsDetectionArgs"); //$NON-NLS-1$
}
/**
* Runs built-in detection.
*
* @param project the project
* @param buildDirectory the build root directory. This is the working directory of the compiler process. Temporary,
* but cacheable input files for the compiler are generated her also
* @param launcher the launcher for the compiler process
* @param console the console to print the compiler output to or
* <code>null</code> if no console output is requested.
* @throws CoreException
*/
public IRawIndexerInfo detectBuiltins(IProject project, java.nio.file.Path buildDirectory,
ICommandLauncher launcher, IConsole console, IProgressMonitor monitor) throws CoreException {
this.project = Objects.requireNonNull(project, "project"); //$NON-NLS-1$
this.buildDirectory = Objects.requireNonNull(buildDirectory, "buildDirectory"); //$NON-NLS-1$
if (monitor == null) {
monitor = new NullProgressMonitor();
}
RawIndexerInfo result = new RawIndexerInfo();
final List<String> argList = getCompilerArguments();
argList.addAll(builtinsDetectionArgs);
console = startOutputConsole(console);
launcher.setProject(project);
launcher.showCommand(console != null);
final Process proc = launcher.execute(new Path(command), argList.toArray(new String[argList.size()]), getEnvp(),
new Path(this.buildDirectory.toString()), monitor);
if (proc != null) {
try {
// Close the input of the process since we will never write to it
proc.getOutputStream().close();
} catch (IOException e) {
}
// NOTE: we need 2 of these, since the output streams are not synchronized,
// causing loss of the output processors' internal state
final IBuiltinsOutputProcessor bopOut = builtinsDetectionBehavior.createCompilerOutputProcessor();
final IBuiltinsOutputProcessor bopErr = builtinsDetectionBehavior.createCompilerOutputProcessor();
long start = System.currentTimeMillis();
int state = launcher.waitAndRead(
new OutputSniffer(bopOut, console == null ? null : console.getOutputStream(), result),
new OutputSniffer(bopErr, console == null ? null : console.getErrorStream(), result), monitor);
if (console != null) {
final ConsoleOutputStream cis = console.getInfoStream();
try {
cis.write(String.format(Messages.CompilerBuiltinsDetector_msg_detection_finished,
System.currentTimeMillis() - start).getBytes());
cis.write("\n".getBytes()); //$NON-NLS-1$
} catch (IOException ignore) {
}
}
if (state != ICommandLauncher.COMMAND_CANCELED) {
// check exit status
final int exitValue = proc.exitValue();
if (exitValue != 0 && !builtinsDetectionBehavior.suppressErrormessage()) {
// compiler had errors...
String errMsg = String.format(Messages.CompilerBuiltinsDetector_errmsg_command_failed, command,
exitValue);
createMarker(errMsg);
}
}
} else {
// process start failed
createMarker(launcher.getErrorMessage());
}
return result;
}
/**
* Gets the compiler-arguments corresponding to the builtinDetection.
*/
private List<String> getCompilerArguments() {
List<String> args = new ArrayList<>();
args.addAll(builtinsDetectionBehavior.getBuiltinsOutputEnablingArgs());
String inputFile = getInputFile();
if (inputFile != null) {
args.add(inputFile);
}
return args;
}
/**
* Get environment variables from configuration as array of "var=value".
*
* @return String array of environment variables in format "var=value". Does not
* return {@code null}.
*/
private String[] getEnvp() {
// On POSIX (Linux, UNIX) systems reset language variables to default (English)
// with UTF-8 encoding since GNU compilers can handle only UTF-8 characters.
// Include paths with locale characters will be handled properly regardless
// of the language as long as the encoding is set to UTF-8.
// English language is set for parser because it relies on English messages
// in the output of the 'gcc -v'.
String[] strings = {
// override for GNU gettext
"LANGUAGE" + "=en", //$NON-NLS-1$//$NON-NLS-2$
// for other parts of the system libraries
"LC_ALL" + "=C.UTF-8" }; //$NON-NLS-1$ //$NON-NLS-2$
return strings;
}
/**
* Gets a path to the source file which is the input for the compiler. The file
* will be created with no content in the build directory.
*
* @return the full file system path of the source file
*/
private String getInputFile() {
String specFileName = "detect_compiler_builtins" + '.' + sourceFileExtension; //$NON-NLS-1$
java.nio.file.Path specFile = buildDirectory.resolve(specFileName);
if (!Files.exists(specFile)) {
try {
// In the typical case it is sufficient to have an empty file.
Files.createDirectories(specFile.getParent()); // no build ran yet, must create dirs
Files.createFile(specFile);
} catch (IOException e) {
Plugin.getDefault().getLog().log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, "getInputFile()", e)); //$NON-NLS-1$
}
}
return specFile.toString();
}
private void createMarker(String message) throws CoreException {
IMarker marker = project.createMarker(MARKER_ID);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_INFO);
marker.setAttribute(IMarker.MESSAGE, message);
}
/**
* Creates and starts the output console.
*
* @return CDT console or <code>null</code>
*
* @throws CoreException
*/
private IConsole startOutputConsole(IConsole console) throws CoreException {
IParserPreferences prefs = EclipseContextFactory
.getServiceContext(FrameworkUtil.getBundle(getClass()).getBundleContext())
.get(IParserPreferencesAccess.class).getWorkspacePreferences();
if (console != null && prefs.getAllocateConsole()) {
console.start(project);
try {
final ConsoleOutputStream cis = console.getInfoStream();
String msg;
msg = String.format(Messages.CompilerBuiltinsDetector_msg_detection_start,
SimpleDateFormat.getTimeInstance().format(new Date()), project.getName());
cis.write(msg.getBytes());
cis.write("\n".getBytes()); //$NON-NLS-1$
} catch (IOException ignore) {
}
return console;
}
return null; // no console to allocate
}
}