blob: c543ae5a19e6ac47c16842e955186a3c7614f749 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2019, 2020 Marc-Andre Laperle 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
*
* Contributors:
* Marc-Andre Laperle - Initial API and implementation
* Sergei Kovalchuk (NXP)
*******************************************************************************/
package org.eclipse.cdt.managedbuilder.internal.language.settings.providers;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.cdtvariables.ICdtVariableManager;
import org.eclipse.cdt.core.language.settings.providers.ICListenerAgent;
import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsEditableProvider;
import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsProvider;
import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsProvidersKeeper;
import org.eclipse.cdt.core.language.settings.providers.IWorkingDirectoryTracker;
import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsSerializableProvider;
import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsStorage;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICElementVisitor;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.core.settings.model.ICSourceEntry;
import org.eclipse.cdt.core.settings.model.util.CDataUtil;
import org.eclipse.cdt.managedbuilder.core.ManagedBuilderCorePlugin;
import org.eclipse.cdt.managedbuilder.language.settings.providers.AbstractBuildCommandParser;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceProxy;
import org.eclipse.core.resources.IResourceProxyVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import com.google.gson.Gson;
/**
* This language settings provider takes a compile_commands.json file as input (aka, Compilation Database or CDB) and parses the commands
* with a chosen build command parser. The command parser can be any implementation of AbstractBuildCommandParser like GCCBuildCommandParser,
* MSVCBuildCommandParser, etc.
*
* The file json file is re-parsed at startup through {@link #registerListener(ICConfigurationDescription)} but only if the timestamp changed.
* It it also parsed when the options are modified in the UI through {@link #processCompileCommandsFile(IProgressMonitor, ICConfigurationDescription)}
*/
public class CompilationDatabaseParser extends LanguageSettingsSerializableProvider
implements ICListenerAgent, ILanguageSettingsEditableProvider {
public static final String JOB_FAMILY_COMPILATION_DATABASE_PARSER = "org.eclipse.cdt.managedbuilder.internal.language.settings.providers.CompilationDatabaseParser"; //$NON-NLS-1$
private static final String ATTR_CDB_PATH = "cdb-path"; //$NON-NLS-1$
private static final String ATTR_BUILD_PARSER_ID = "build-parser-id"; //$NON-NLS-1$
private static final String ATTR_CDB_MODIFIED_TIME = "cdb-modified-time"; //$NON-NLS-1$
private static final String ATTR_EXCLUDE_FILES = "exclude-files"; //$NON-NLS-1$
public String getCompilationDataBasePathProperty() {
return getProperty(ATTR_CDB_PATH);
}
/**
* Resolve the compilation database path property by expanding variables (if any) and check that the file exists and is readable.
*
* @param cfgDescription the configuration description used to resolved variables that depend on it
* @return the resolved, readable path of the compilation database
* @throws CoreException On failure to resolve variables or non readable path
*/
public String resolveCompilationDataBasePath(ICConfigurationDescription cfgDescription) throws CoreException {
ICdtVariableManager varManager = CCorePlugin.getDefault().getCdtVariableManager();
String compilationDataBasePath = varManager.resolveValue(getCompilationDataBasePathProperty(), "", null, //$NON-NLS-1$
cfgDescription);
if (Files.isDirectory(Paths.get(compilationDataBasePath))
|| !Files.isReadable(Paths.get(compilationDataBasePath)))
throw new CoreException(new Status(Status.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID, MessageFormat
.format(Messages.CompilationDatabaseParser_CDBNotFound, getCompilationDataBasePathProperty())));
return compilationDataBasePath;
}
public void setCompilationDataBasePathProperty(String compilationDataBasePathProperty) {
setProperty(ATTR_CDB_PATH, compilationDataBasePathProperty);
}
public void setExcludeFiles(boolean selection) {
setPropertyBool(ATTR_EXCLUDE_FILES, selection);
}
public boolean getExcludeFiles() {
return getPropertyBool(ATTR_EXCLUDE_FILES);
}
public void setBuildParserId(String parserId) {
setProperty(ATTR_BUILD_PARSER_ID, parserId);
}
public String getBuildParserId() {
return getProperty(ATTR_BUILD_PARSER_ID);
}
public Long getCDBModifiedTime(String cdbPath) throws IOException {
FileTime lastModifiedTime = Files.getLastModifiedTime(Paths.get(cdbPath));
return lastModifiedTime.toMillis();
}
/**
* Visit all folders and exclude all files that do not have entries coming from the CDB.
* The algorithm detects when whole folders can be excluded to prevent a large number of
* individual file exclusions.
*/
private final class ExcludeSourceFilesVisitor implements ICElementVisitor {
private final ICConfigurationDescription cfgDescription;
ICSourceEntry[] entries = null;
private final IProgressMonitor monitor;
private final int sourceFilesCount;
private int nbChecked = 0;
// Keep track of excluded files and folders as we visit each folder in a depth-first manner.
private Stack<FolderExclusionInfo> folderExclusionInfos = new Stack<>();
private class FolderExclusionInfo {
// In case not all files are excluded in this folder, we need to keep track of which folders we'll exclude individually.
private ArrayList<IPath> childrenFoldersWithAllFilesExcluded = new ArrayList<>();
// In case not all files are excluded in this folder, we need to keep track of which files we'll exclude individually.
private ArrayList<IPath> excludedSourceFiles = new ArrayList<>();
// True if all children of the folder are excluded, recursively.
// Children folders can set this to false on their parent when non-excluded files are detected.
// Therefore, this value is reliable only when all children are visited.
private boolean allFilesExcluded = true;
}
//Note: monitor already has ticks allocated for number of source files (not considering exclusions though)
private ExcludeSourceFilesVisitor(IProgressMonitor monitor, int sourceFilesCount,
ICConfigurationDescription cfgDescription) {
this.monitor = monitor;
this.sourceFilesCount = sourceFilesCount;
this.cfgDescription = cfgDescription;
this.entries = cfgDescription.getSourceEntries();
}
public ICSourceEntry[] getSourceEntries() {
return entries;
}
@Override
public boolean visit(ICElement element) throws CoreException {
int elementType = element.getElementType();
if (elementType != ICElement.C_UNIT) {
boolean isSourceContainer = elementType == ICElement.C_CCONTAINER || elementType == ICElement.C_PROJECT;
if (isSourceContainer) {
folderExclusionInfos.push(new FolderExclusionInfo());
}
return isSourceContainer;
}
ITranslationUnit tu = (ITranslationUnit) element;
if (tu.isSourceUnit()) {
handleTranslationUnit(tu);
}
return false;
}
private void handleTranslationUnit(ITranslationUnit tu) throws CoreException {
FolderExclusionInfo folderInfo = folderExclusionInfos.peek();
List<ICLanguageSettingEntry> list = getSettingEntries(cfgDescription, tu.getResource(),
tu.getLanguage().getId());
if (list == null) {
folderInfo.excludedSourceFiles.add(tu.getResource().getFullPath());
} else {
folderInfo.allFilesExcluded = false;
}
monitor.worked(1);
if (nbChecked % 100 == 0) {
monitor.subTask(String.format(Messages.CompilationDatabaseParser_ProgressExcludingFiles, nbChecked,
sourceFilesCount));
}
nbChecked++;
}
@Override
public void leave(ICElement element) throws CoreException {
int elementType = element.getElementType();
if (elementType == ICElement.C_CCONTAINER || elementType == ICElement.C_PROJECT) {
FolderExclusionInfo folderInfo = folderExclusionInfos.pop();
if (folderInfo.allFilesExcluded && !folderExclusionInfos.isEmpty()) {
// Consider this folder for exclusion later, maybe the parent will also be excluded
folderExclusionInfos.peek().childrenFoldersWithAllFilesExcluded.add(element.getPath());
} else {
if (!folderExclusionInfos.isEmpty()) {
folderExclusionInfos.peek().allFilesExcluded = false;
}
// Exclude all children folders previously considered for exclusion, since the parent
// (the currently visited element) cannot be excluded, we have to exclude them individually.
for (IPath excludedFolder : folderInfo.childrenFoldersWithAllFilesExcluded) {
entries = CDataUtil.setExcluded(excludedFolder, true, true, entries);
}
// Exclude all direct children files that need to be excluded.
for (IPath excludedFile : folderInfo.excludedSourceFiles) {
entries = CDataUtil.setExcluded(excludedFile, false, true, entries);
}
}
}
}
}
private static class CDBWorkingDirectoryTracker implements IWorkingDirectoryTracker {
URI currentDirectory = null;
@Override
public URI getWorkingDirectoryURI() {
return currentDirectory;
}
public void setCurrentDirectory(URI currentDirectory) {
this.currentDirectory = currentDirectory;
}
}
@Override
public void registerListener(ICConfigurationDescription cfgDescription) {
unregisterListener();
try {
processCompileCommandsFile(null, cfgDescription);
} catch (CoreException e) {
ManagedBuilderCorePlugin.log(e);
}
}
@Override
public void unregisterListener() {
}
/**
* Processes the compilation database based on the attributes previously set.
* Parses the commands and sets the language setting entries. If cfgDescription is a writable configuration, it is assumed that the caller will call
* CoreModel#setProjectDescription. Otherwise if cfgDescription is read-only, the method will restart itself with a writable configuration description and call CoreModel#setProjectDescription.
*/
public boolean processCompileCommandsFile(IProgressMonitor monitor, ICConfigurationDescription cfgDescription)
throws CoreException {
if (cfgDescription.isReadOnly()) {
scheduleOnWritableCfgDescription(cfgDescription);
return false;
}
if (!cfgDescription.equals(cfgDescription.getProjectDescription().getDefaultSettingConfiguration()))
return false;
if (getCompilationDataBasePathProperty().isEmpty()) {
throw new CoreException(new Status(Status.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID,
Messages.CompilationDatabaseParser_CDBNotConfigured));
}
String cdbPath = resolveCompilationDataBasePath(cfgDescription);
try {
if (!getProperty(ATTR_CDB_MODIFIED_TIME).isEmpty()
&& getProperty(ATTR_CDB_MODIFIED_TIME).equals(getCDBModifiedTime(cdbPath).toString())) {
return false;
}
} catch (IOException e) {
throw new CoreException(new Status(Status.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID,
Messages.CompilationDatabaseParser_ErrorProcessingCompilationDatabase, e));
}
if (getBuildParserId().isEmpty()) {
throw new CoreException(new Status(Status.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID,
MessageFormat.format(Messages.CompilationDatabaseParser_BuildCommandParserNotConfigured, cdbPath)));
}
if (!isEmpty()) {
clear();
}
Long cdbModifiedTime;
try {
cdbModifiedTime = getCDBModifiedTime(cdbPath);
} catch (Exception e) {
//setProperty(ATTR_CDB_MODIFIED_TIME, Long.toString(0L));
throw new CoreException(new Status(Status.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID,
Messages.CompilationDatabaseParser_ErrorProcessingCompilationDatabase, e));
}
int totalTicks = getExcludeFiles() ? 100 : 60;
SubMonitor subMonitor = SubMonitor.convert(monitor, totalTicks);
subMonitor.subTask(Messages.CompilationDatabaseParser_ProgressParsingJSONFile);
subMonitor.split(5);
CompileCommand[] compileCommands = null;
try (FileReader reader = new FileReader(cdbPath)) {
Gson gson = new Gson();
compileCommands = gson.fromJson(reader, CompileCommand[].class);
} catch (Exception e) {
//setProperty(ATTR_CDB_MODIFIED_TIME, Long.toString(0L));
throw new CoreException(new Status(Status.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID,
Messages.CompilationDatabaseParser_ErrorProcessingCompilationDatabase, e));
}
if (compileCommands == null) {
throw new CoreException(new Status(Status.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID,
Messages.CompilationDatabaseParser_ErrorProcessingCompilationDatabase,
new NullPointerException(Messages.CompilationDatabaseParser_StillNull)));
}
AbstractBuildCommandParser outputParser;
try {
outputParser = getBuildCommandParser(cfgDescription, getBuildParserId());
} catch (Exception e) {
//setProperty(ATTR_CDB_MODIFIED_TIME, Long.toString(0L));
throw new CoreException(new Status(Status.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID,
Messages.CompilationDatabaseParser_ErrorProcessingCompilationDatabase, e));
}
CDBWorkingDirectoryTracker workingDirectoryTracker = new CDBWorkingDirectoryTracker();
SubMonitor parseCmdsMonitor = SubMonitor.convert(subMonitor.split(50), compileCommands.length);
outputParser.startup(cfgDescription, workingDirectoryTracker);
for (int i = 0; i < compileCommands.length; i++) {
CompileCommand c = compileCommands[i];
// Don't spam the progress view too much
if (i % 100 == 0) {
parseCmdsMonitor.subTask(String.format(Messages.CompilationDatabaseParser_ProgressParsingBuildCommands,
i, compileCommands.length));
}
String dir = c.getDirectory();
workingDirectoryTracker.setCurrentDirectory(null);
if (dir != null) {
File file = new File(dir);
if (file.exists()) {
workingDirectoryTracker.setCurrentDirectory(file.toURI());
}
}
String command = c.getCommand();
if (command != null) {
outputParser.processLine(command);
} else if (c.getArguments() != null) {
outputParser.processLine(String.join(" ", c.getArguments())); //$NON-NLS-1$
}
parseCmdsMonitor.worked(1);
}
LanguageSettingsStorage storage = outputParser.copyStorage();
SubMonitor entriesMonitor = SubMonitor.convert(subMonitor.split(5), storage.getLanguages().size());
entriesMonitor.subTask(Messages.CompilationDatabaseParser_ProgressApplyingEntries);
for (String language : storage.getLanguages()) {
SubMonitor langMonitor = entriesMonitor.split(1);
Set<String> resourcePaths = storage.getResourcePaths(language);
SubMonitor langEntriesMonitor = SubMonitor.convert(langMonitor, resourcePaths.size());
for (String resourcePath : resourcePaths) {
IFile file = cfgDescription.getProjectDescription().getProject().getFile(new Path(resourcePath));
if (file.exists()) {
List<ICLanguageSettingEntry> settingEntries = storage.getSettingEntries(resourcePath, language);
setSettingEntries(cfgDescription, file, language, settingEntries);
}
langEntriesMonitor.worked(1);
}
}
if (getExcludeFiles()) {
excludeFiles(cfgDescription, subMonitor);
}
setProperty(ATTR_CDB_MODIFIED_TIME, cdbModifiedTime.toString());
touchProjectDes(cfgDescription.getProjectDescription());
return true;
}
private void scheduleOnWritableCfgDescription(ICConfigurationDescription cfgDescription) {
WorkspaceJob job = new WorkspaceJob(Messages.CompilationDatabaseParser_Job) {
@Override
public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
// If the config description we have been given is read-only, we need to get a writable one instead in order to be able to set source entries (exclusions).
// The tricky thing is that in that situation, the CompilationDatabaseParser instance (this) came from the read-only project description so anything that is
// saved that is not stored in the project description (i.e. calls to setProperties) will be saved to the wrong instance so when we call setProjectDescription, our changes will be ignored.
// So instead, restart the whole thing with the corresponding CompilationDatabaseParser instance in the writable config.
IProject project = cfgDescription.getProjectDescription().getProject();
if (!project.isAccessible()) {
// Project was probably closed while the job was waiting to start.
return Status.CANCEL_STATUS;
}
ICProjectDescription projectDescription = CCorePlugin.getDefault().getCoreModel()
.getProjectDescription(project.getProject(), true);
ICConfigurationDescription writableCfg = projectDescription
.getConfigurationById(cfgDescription.getId());
if (!(writableCfg instanceof ILanguageSettingsProvidersKeeper)) {
return Status.CANCEL_STATUS;
}
CompilationDatabaseParser parser = null;
List<ILanguageSettingsProvider> settingProviders = ((ILanguageSettingsProvidersKeeper) writableCfg)
.getLanguageSettingProviders();
for (ILanguageSettingsProvider languageSettingsProvider : settingProviders) {
if (languageSettingsProvider.getId().equals(CompilationDatabaseParser.this.getId())
&& languageSettingsProvider instanceof CompilationDatabaseParser) {
parser = (CompilationDatabaseParser) languageSettingsProvider;
break;
}
}
if (parser == null) {
// Seems very unlikely to get here. This should mean that the provider was disabled before the job ran.
return Status.CANCEL_STATUS;
}
try {
if (parser.processCompileCommandsFile(monitor, writableCfg)) {
CoreModel.getDefault().setProjectDescription(project.getProject(), projectDescription);
}
} catch (CoreException e) {
// If we are running this in a WorkspaceJob it's because the CfgDescription was read-only so we are probably loading the project.
// We don't want to pop-up jarring error dialogs on start-up. Ideally, CDT would have problem markers for project setup issues.
ManagedBuilderCorePlugin.log(e);
}
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
return family == JOB_FAMILY_COMPILATION_DATABASE_PARSER;
}
};
// Using root rule because of call to setProjectDescription above
job.setRule(ResourcesPlugin.getWorkspace().getRoot());
job.schedule();
}
private void excludeFiles(ICConfigurationDescription cfgDescription, SubMonitor subMonitor) throws CoreException {
ICProject cProject = CCorePlugin.getDefault().getCoreModel()
.create(cfgDescription.getProjectDescription().getProject());
// Getting a approximation of the number of source files we will have to visit based on file names.
// Much faster than going through the CElements. Then do the real work and report progress.
// It's possible that the approximation will be pretty wrong if there are a lot of already excluded files
// then we won't visit them in the ExcludeSourceFilesVisitor and the progress monitor won't be ticked for those.
int sourceFilesCount[] = new int[1];
cProject.getProject().accept(new IResourceProxyVisitor() {
@Override
public boolean visit(IResourceProxy proxy) throws CoreException {
if (CoreModel.isValidSourceUnitName(cProject.getProject(), proxy.getName()))
sourceFilesCount[0]++;
return true;
}
}, IResource.DEPTH_INFINITE, IResource.NONE);
SubMonitor sourceMonitor = SubMonitor.convert(subMonitor.split(35), sourceFilesCount[0]);
ExcludeSourceFilesVisitor sourceFileVisitor = new ExcludeSourceFilesVisitor(sourceMonitor, sourceFilesCount[0],
cfgDescription);
cProject.accept(sourceFileVisitor);
ICSourceEntry[] sourceEntries = sourceFileVisitor.getSourceEntries();
subMonitor.split(5);
if (sourceEntries != null) {
cfgDescription.setSourceEntries(sourceEntries);
}
}
private void touchProjectDes(ICProjectDescription desc) {
// Make sure the project description is marked as modified so that language settings serialization kicks in.
// We need to let the setProjectDescription do the serialization because we cannot do it on a writable description
// and we need a writable description because we need to call setSourceEntries!
final QualifiedName TOUCH_PROPERTY = new QualifiedName(CCorePlugin.PLUGIN_ID, "touch-project"); //$NON-NLS-1$
desc.setSessionProperty(TOUCH_PROPERTY, ""); //$NON-NLS-1$
desc.setSessionProperty(TOUCH_PROPERTY, null);
}
private AbstractBuildCommandParser getBuildCommandParser(ICConfigurationDescription cfgDesc, String id)
throws CloneNotSupportedException {
ICConfigurationDescription configurationDescription = cfgDesc;
if (configurationDescription instanceof ILanguageSettingsProvidersKeeper) {
List<ILanguageSettingsProvider> settingProviders = ((ILanguageSettingsProvidersKeeper) configurationDescription)
.getLanguageSettingProviders();
for (ILanguageSettingsProvider languageSettingsProvider : settingProviders) {
if (languageSettingsProvider instanceof AbstractBuildCommandParser
&& languageSettingsProvider instanceof ILanguageSettingsEditableProvider) {
AbstractBuildCommandParser buildParser = (AbstractBuildCommandParser) languageSettingsProvider;
if (buildParser.getId().equals(id))
return (AbstractBuildCommandParser) ((ILanguageSettingsEditableProvider) buildParser).clone();
}
}
}
throw new IllegalArgumentException(MessageFormat
.format(Messages.CompilationDatabaseParser_BuildCommandParserNotFound, id, cfgDesc.getName()));
}
@Override
public boolean isEmpty() {
// treat provider that has been executed as not empty
// to let "Clear" button to restart the provider
return getProperty(ATTR_CDB_MODIFIED_TIME).isEmpty() && super.isEmpty();
}
@Override
public void clear() {
super.clear();
setProperty(ATTR_CDB_MODIFIED_TIME, null);
}
@Override
public CompilationDatabaseParser cloneShallow() throws CloneNotSupportedException {
CompilationDatabaseParser clone = (CompilationDatabaseParser) super.cloneShallow();
clone.setProperty(ATTR_CDB_MODIFIED_TIME, null);
return clone;
}
@Override
public CompilationDatabaseParser clone() throws CloneNotSupportedException {
return (CompilationDatabaseParser) super.clone();
}
}