blob: 4cf4b2ff29d67600a0ddd0b53767df69f4cdfdb8 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2016 QNX Software Systems and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.cdt.cmake.core.internal;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import org.eclipse.cdt.cmake.core.ICMakeToolChainFile;
import org.eclipse.cdt.cmake.core.ICMakeToolChainManager;
import org.eclipse.cdt.cmake.core.internal.CommandDescriptorBuilder.CommandDescriptor;
import org.eclipse.cdt.cmake.core.properties.CMakeGenerator;
import org.eclipse.cdt.cmake.core.properties.ICMakeProperties;
import org.eclipse.cdt.cmake.core.properties.ICMakePropertiesController;
import org.eclipse.cdt.cmake.core.properties.IOsOverrides;
import org.eclipse.cdt.cmake.is.core.CompileCommandsJsonParser;
import org.eclipse.cdt.cmake.is.core.IIndexerInfoConsumer;
import org.eclipse.cdt.cmake.is.core.ParseRequest;
import org.eclipse.cdt.core.CommandLauncherManager;
import org.eclipse.cdt.core.ConsoleOutputStream;
import org.eclipse.cdt.core.ErrorParserManager;
import org.eclipse.cdt.core.IConsoleParser;
import org.eclipse.cdt.core.build.CBuildConfiguration;
import org.eclipse.cdt.core.build.IToolChain;
import org.eclipse.cdt.core.envvar.EnvironmentVariable;
import org.eclipse.cdt.core.envvar.IEnvironmentVariable;
import org.eclipse.cdt.core.model.ElementChangedEvent;
import org.eclipse.cdt.core.model.ICElementDelta;
import org.eclipse.cdt.core.model.ICModelMarker;
import org.eclipse.cdt.core.parser.ExtendedScannerInfo;
import org.eclipse.cdt.core.parser.IScannerInfo;
import org.eclipse.cdt.core.resources.IConsole;
import org.eclipse.core.resources.IBuildConfiguration;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.jobs.Job;
public class CMakeBuildConfiguration extends CBuildConfiguration {
public static final String CMAKE_GENERATOR = "cmake.generator"; //$NON-NLS-1$
public static final String CMAKE_ARGUMENTS = "cmake.arguments"; //$NON-NLS-1$
public static final String CMAKE_ENV = "cmake.environment"; //$NON-NLS-1$
public static final String BUILD_COMMAND = "cmake.command.build"; //$NON-NLS-1$
public static final String CLEAN_COMMAND = "cmake.command.clean"; //$NON-NLS-1$
private ICMakeToolChainFile toolChainFile;
// lazily instantiated..
private CMakePropertiesController pc;
private Map<IResource, IScannerInfo> infoPerResource;
/**
* whether one of the CMakeLists.txt files in the project has been modified and saved by the
* user since the last build.<br>
* Cmake-generated build scripts re-run cmake if one of the CMakeLists.txt files was modified,
* but that output goes through ErrorParserManager and is impossible to parse because cmake
* outputs to both stderr and stdout and ErrorParserManager intermixes these streams making it
* impossible to parse for errors.<br>
* To work around that, we run cmake in advance with its dedicated working error parser.
*/
private boolean cmakeListsModified;
/**
* whether we have to delete file CMakeCache.txt to avoid complaints by cmake
*/
private boolean deleteCMakeCache;
public CMakeBuildConfiguration(IBuildConfiguration config, String name) throws CoreException {
super(config, name);
ICMakeToolChainManager manager = Activator.getService(ICMakeToolChainManager.class);
toolChainFile = manager.getToolChainFileFor(getToolChain());
}
public CMakeBuildConfiguration(IBuildConfiguration config, String name, IToolChain toolChain) {
this(config, name, toolChain, null, "run"); //$NON-NLS-1$
}
public CMakeBuildConfiguration(IBuildConfiguration config, String name, IToolChain toolChain,
ICMakeToolChainFile toolChainFile, String launchMode) {
super(config, name, toolChain, launchMode);
this.toolChainFile = toolChainFile;
}
/**
* Gets the tool-chain description file to pass to the cmake command-line.
*
* @return the tool-chain file or <code>null</code> if cmake should take the native (i.e. the
* tools first found on the executable search path aka $path)
*/
public ICMakeToolChainFile getToolChainFile() {
return toolChainFile;
}
@SuppressWarnings("unused") // kept for reference of the property names
private boolean isLocal() throws CoreException {
IToolChain toolchain = getToolChain();
return (Platform.getOS().equals(toolchain.getProperty(IToolChain.ATTR_OS))
|| "linux-container".equals(toolchain.getProperty(IToolChain.ATTR_OS))) //$NON-NLS-1$
&& (Platform.getOSArch().equals(toolchain.getProperty(IToolChain.ATTR_ARCH)));
}
@Override
public IProject[] build(int kind, Map<String, String> args, IConsole console, IProgressMonitor monitor)
throws CoreException {
IProject project = getProject();
project.deleteMarkers(ICModelMarker.C_MODEL_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
try {
ConsoleOutputStream infoStream = console.getInfoStream();
Path buildDir = getBuildDirectory();
boolean runCMake = cmakeListsModified;
if (deleteCMakeCache) {
Files.deleteIfExists(buildDir.resolve("CMakeCache.txt")); //$NON-NLS-1$
deleteCMakeCache = false;
runCMake = true;
}
ICMakeProperties cmakeProperties = getPropertiesController().load();
runCMake |= !Files.exists(buildDir.resolve("CMakeCache.txt")); //$NON-NLS-1$
final SimpleOsOverridesSelector overridesSelector = new SimpleOsOverridesSelector();
if (!runCMake) {
CMakeGenerator generator = overridesSelector.getOsOverrides(cmakeProperties).getGenerator();
runCMake |= !Files.exists(buildDir.resolve(generator.getMakefileName()));
}
CommandDescriptorBuilder cmdBuilder = new CommandDescriptorBuilder(cmakeProperties, overridesSelector);
if (runCMake) {
CMakeBuildConfiguration.deleteCMakeErrorMarkers(project);
infoStream.write(String.format(Messages.CMakeBuildConfiguration_Configuring, buildDir));
CommandDescriptor command = cmdBuilder
.makeCMakeCommandline(toolChainFile != null ? toolChainFile.getPath() : null);
// tell cmake where its script is located..
IContainer srcFolder = project;
command.getArguments().add(new File(srcFolder.getLocationURI()).getAbsolutePath());
infoStream.write(String.join(" ", command.getArguments()) + '\n'); //$NON-NLS-1$
org.eclipse.core.runtime.Path workingDir = new org.eclipse.core.runtime.Path(
getBuildDirectory().toString());
// hook in cmake error parsing
try (CMakeErrorParser errorParser = new CMakeErrorParser(new CMakeExecutionMarkerFactory(srcFolder))) {
ParsingConsoleOutputStream errStream = new ParsingConsoleOutputStream(console.getErrorStream(),
errorParser);
IConsole errConsole = new CMakeConsoleWrapper(console, errStream);
// TODO startBuildProcess() calls java.lang.ProcessBuilder.
// Use org.eclipse.cdt.core.ICommandLauncher
// in order to run builds in a container.
Process p = startBuildProcess(command.getArguments(), new IEnvironmentVariable[0], workingDir,
errConsole, monitor);
if (p == null) {
console.getErrorStream().write(String.format(Messages.CMakeBuildConfiguration_Failure, "")); //$NON-NLS-1$
return null;
}
watchProcess(p, errConsole);
}
cmakeListsModified = false;
}
// parse compile_commands.json file
processCompileCommandsFile(console, monitor);
infoStream.write(String.format(Messages.CMakeBuildConfiguration_BuildingIn, buildDir.toString()));
// run the build tool...
try (ErrorParserManager epm = new ErrorParserManager(project, getBuildDirectoryURI(), this,
getToolChain().getErrorParserIds())) {
epm.setOutputStream(console.getOutputStream());
String envStr = getProperty(CMAKE_ENV);
List<IEnvironmentVariable> envVars = new ArrayList<>();
if (envStr != null) {
List<String> envList = CMakeUtils.stripEnvVars(envStr);
for (String s : envList) {
int index = s.indexOf("="); //$NON-NLS-1$
if (index == -1) {
envVars.add(new EnvironmentVariable(s));
} else {
envVars.add(new EnvironmentVariable(s.substring(0, index), s.substring(index + 1)));
}
}
}
CommandDescriptor commandDescr = cmdBuilder.makeCMakeBuildCommandline("all"); //$NON-NLS-1$
List<String> command = commandDescr.getArguments();
infoStream.write(String.join(" ", command) + '\n'); //$NON-NLS-1$
org.eclipse.core.runtime.Path workingDir = new org.eclipse.core.runtime.Path(
getBuildDirectory().toString());
// TODO startBuildProcess() calls java.lang.ProcessBuilder. Use org.eclipse.cdt.core.ICommandLauncher
// in order to run builds in a container.
// TODO pass envvars from CommandDescriptor once we use ICommandLauncher
Process p = startBuildProcess(command, envVars.toArray(new IEnvironmentVariable[0]), workingDir,
console, monitor);
if (p == null) {
console.getErrorStream().write(String.format(Messages.CMakeBuildConfiguration_Failure, "")); //$NON-NLS-1$
return null;
}
watchProcess(p, new IConsoleParser[] { epm });
project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
infoStream.write(String.format(Messages.CMakeBuildConfiguration_BuildingComplete, epm.getErrorCount(),
epm.getWarningCount(), buildDir.toString()));
}
return new IProject[] { project };
} catch (IOException e) {
throw new CoreException(Activator
.errorStatus(String.format(Messages.CMakeBuildConfiguration_Building, project.getName()), e));
}
}
@Override
public void clean(IConsole console, IProgressMonitor monitor) throws CoreException {
IProject project = getProject();
try {
project.deleteMarkers(ICModelMarker.C_MODEL_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
ICMakeProperties cmakeProperties = getPropertiesController().load();
CommandDescriptorBuilder cmdBuilder = new CommandDescriptorBuilder(cmakeProperties,
new SimpleOsOverridesSelector());
CommandDescriptor command = cmdBuilder.makeCMakeBuildCommandline("clean"); //$NON-NLS-1$
ConsoleOutputStream outStream = console.getOutputStream();
Path buildDir = getBuildDirectory();
if (!Files.exists(buildDir.resolve("CMakeFiles"))) { //$NON-NLS-1$
outStream.write(Messages.CMakeBuildConfiguration_NotFound);
return;
}
outStream.write(String.join(" ", command.getArguments()) + '\n'); //$NON-NLS-1$
org.eclipse.core.runtime.Path workingDir = new org.eclipse.core.runtime.Path(
getBuildDirectory().toString());
// TODO startBuildProcess() calls java.lang.ProcessBuilder. Use org.eclipse.cdt.core.ICommandLauncher
// in order to run builds in a container.
Process p = startBuildProcess(command.getArguments(), new IEnvironmentVariable[0], workingDir, console,
monitor);
if (p == null) {
console.getErrorStream().write(String.format(Messages.CMakeBuildConfiguration_Failure, "")); //$NON-NLS-1$
return;
}
watchProcess(p, console);
outStream.write(Messages.CMakeBuildConfiguration_BuildComplete);
project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
} catch (IOException e) {
throw new CoreException(Activator
.errorStatus(String.format(Messages.CMakeBuildConfiguration_Cleaning, project.getName()), e));
}
}
/**
* @param console the console to print the compiler output during built-ins detection to or
* <code>null</code> if no separate console is to be allocated. Ignored if
* workspace preferences indicate that no console output is wanted.
* @param monitor the job's progress monitor
*/
private void processCompileCommandsFile(IConsole console, IProgressMonitor monitor) throws CoreException {
IFile file = getBuildContainer().getFile(new org.eclipse.core.runtime.Path("compile_commands.json")); //$NON-NLS-1$
CompileCommandsJsonParser parser = new CompileCommandsJsonParser(
new ParseRequest(file, new CMakeIndexerInfoConsumer(this::setScannerInformation),
CommandLauncherManager.getInstance().getCommandLauncher(this), console));
parser.parse(monitor);
}
/**
* Recursively removes any files and directories found below the specified Path.
*/
private static void cleanDirectory(Path dir) throws IOException {
SimpleFileVisitor<Path> deltor = new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
super.postVisitDirectory(dir, exc);
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
};
Path[] files = Files.list(dir).toArray(Path[]::new);
for (Path file : files) {
Files.walkFileTree(file, deltor);
}
}
/** Lazily creates the CMakePropertiesController for the project.
*/
private CMakePropertiesController getPropertiesController() {
if (pc == null) {
final Path filePath = Path.of(getProject().getFile(".settings/CDT-cmake.yaml").getLocationURI()); //$NON-NLS-1$
pc = new CMakePropertiesController(filePath, () -> {
deleteCMakeCache = true;
// TODO delete cache file here for the case a user restarts the workbench
// prior to running a new build
});
}
return pc;
}
// interface IAdaptable
@Override
@SuppressWarnings("unchecked")
public <T> T getAdapter(Class<T> adapter) {
if (ICMakePropertiesController.class.equals(adapter)) {
return (T) pc;
}
return super.getAdapter(adapter);
}
/**
* Overridden since the ScannerInfoCache mechanism does not satisfy our needs.
*/
// interface IScannerInfoProvider
@Override
public IScannerInfo getScannerInformation(IResource resource) {
if (infoPerResource == null) {
// no build was run yet, nothing detected
try {
processCompileCommandsFile(null, new NullProgressMonitor());
} catch (CoreException e) {
Activator.log(e);
}
}
return infoPerResource == null ? null : infoPerResource.get(resource);
}
private void setScannerInformation(Map<IResource, IScannerInfo> infoPerResource) {
this.infoPerResource = infoPerResource;
}
/**
* Overwritten to detect whether one of the CMakeLists.txt files in the project was modified
* since the last build.
*/
@Override
public void elementChanged(ElementChangedEvent event) {
super.elementChanged(event);
// Only respond to post change events
if (event.getType() != ElementChangedEvent.POST_CHANGE)
return;
if (!cmakeListsModified) {
processElementDelta(event.getDelta());
}
}
/**
* Processes the delta in order to detect whether one of the CMakeLists.txt files in the project
* has been modified and saved by the user since the last build.
*
* @return <code>true</code> to continue with delta processing, otherwise <code>false</code>
*/
private boolean processElementDelta(ICElementDelta delta) {
if (delta == null) {
return true;
}
if (delta.getKind() == ICElementDelta.CHANGED) {
// check for modified CMakeLists.txt file
if (0 != (delta.getFlags() & ICElementDelta.F_CONTENT)) {
IResourceDelta[] resourceDeltas = delta.getResourceDeltas();
if (resourceDeltas != null) {
for (IResourceDelta resourceDelta : resourceDeltas) {
IResource resource = resourceDelta.getResource();
if (resource.getType() == IResource.FILE) {
String name = resource.getName();
if (!resource.isDerived(IResource.CHECK_ANCESTORS)
&& (name.equals("CMakeLists.txt") || name.endsWith(".cmake"))) { //$NON-NLS-1$ //$NON-NLS-2$
cmakeListsModified = true;
return false; // stop processing
}
}
}
}
}
}
// recurse...
for (ICElementDelta child : delta.getAffectedChildren()) {
if (!processElementDelta(child)) {
return false; // stop processing
}
}
return true;
}
/**
* Overwritten since we do not parse console output to get scanner information.
*/
// interface IConsoleParser2
@Override
public boolean processLine(String line) {
return true;
}
/**
* Overwritten since we do not parse console output to get scanner information.
*/
// interface IConsoleParser2
@Override
public boolean processLine(String line, List<Job> jobsArray) {
return true;
}
/**
* Overwritten since we do not parse console output to get scanner information.
*/
// interface IConsoleParser2
@Override
public void shutdown() {
}
/**
* Deletes all CMake error markers on the specified project.
*
* @param project the project where to remove the error markers.
* @throws CoreException
*/
private static void deleteCMakeErrorMarkers(IProject project) throws CoreException {
project.deleteMarkers(ICMakeExecutionMarkerFactory.CMAKE_PROBLEM_MARKER_ID, false, IResource.DEPTH_INFINITE);
}
private static class CMakeIndexerInfoConsumer implements IIndexerInfoConsumer {
/**
* gathered IScannerInfo objects or <code>null</code> if no new IScannerInfo was received
*/
private Map<IResource, IScannerInfo> infoPerResource = new HashMap<>();
private boolean haveUpdates;
private final Consumer<Map<IResource, IScannerInfo>> resultSetter;
/**
* @param resultSetter receives the all scanner information when processing is finished
*/
public CMakeIndexerInfoConsumer(Consumer<Map<IResource, IScannerInfo>> resultSetter) {
this.resultSetter = Objects.requireNonNull(resultSetter);
}
@Override
public void acceptSourceFileInfo(String sourceFileName, List<String> systemIncludePaths,
Map<String, String> definedSymbols, List<String> includePaths, List<String> macroFiles,
List<String> includeFiles) {
IFile file = getFileForCMakePath(sourceFileName);
if (file != null) {
ExtendedScannerInfo info = new ExtendedScannerInfo(definedSymbols,
systemIncludePaths.stream().toArray(String[]::new), macroFiles.stream().toArray(String[]::new),
includeFiles.stream().toArray(String[]::new), includePaths.stream().toArray(String[]::new));
infoPerResource.put(file, info);
haveUpdates = true;
}
}
/**
* Gets an IFile object that corresponds to the source file name given in CMake notation.
*
* @param sourceFileName the name of the source file, in CMake notation. Note that on
* windows, CMake writes filenames with forward slashes (/) such as
* {@code H://path//to//source.c}.
* @return a IFile object or <code>null</code>
*/
private IFile getFileForCMakePath(String sourceFileName) {
org.eclipse.core.runtime.Path path = new org.eclipse.core.runtime.Path(sourceFileName);
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(path);
// TODO maybe we need to introduce a strategy here to get the workbench resource
// Possible build scenarios:
// 1) linux native: should be OK as is
// 2) linux host, building in container: should be OK as is
// 3) windows native: Path.fromOSString()?
// 4) windows host, building in linux container: ??? needs testing on windows
return file;
}
@Override
public void shutdown() {
if (haveUpdates) {
// we received updates
resultSetter.accept(infoPerResource);
infoPerResource = null;
haveUpdates = false;
}
}
} // CMakeIndexerInfoConsumer
private static class SimpleOsOverridesSelector implements IOsOverridesSelector {
@Override
public IOsOverrides getOsOverrides(ICMakeProperties cmakeProperties) {
IOsOverrides overrides;
// get overrides. Simplistic approach ATM, probably a strategy might fit better.
// see comment in CMakeIndexerInfoConsumer#getFileForCMakePath()
final String os = Platform.getOS();
if (Platform.OS_WIN32.equals(os)) {
overrides = cmakeProperties.getWindowsOverrides();
} else {
// fall back to linux, if OS is unknown
overrides = cmakeProperties.getLinuxOverrides();
}
return overrides;
}
} // SimpleOsOverridesSelector
}